理解方法调用
- 首先
- 什么是隐式参数 --->隐式参数是调用该方法的对象本身。
- 接下来
- 方法的名称和参数列表被称为方法的签名(signature)。
- 在Java中,方法的签名由方法的名称和参数列表组成,用于唯一标识一个方法。
- 返回类型不是签名的一部分。不过在覆盖一个方法时,需要保证返回类型的兼容性:--->允许子类将覆盖方法的返回类型改为原返回类型的子类型。
- 具体的例子:
- 详细:
- "协变"(covariant)是一种类型关系 --->子类型的某些属性或方法可以比父类型更具体(特化)"
- 协变(covariance)是指子类型的返回类型可以是父类型的子类型,而逆变(contravariance)是指子类型的返回类型可以是父类型的超类型。
- 注意
- 1.具体说明是怎么实现_协变
- 2.具体说明是怎么实现_逆变
- 静态绑定(Static Binding)和动态绑定(Dynamic Binding)
- 1. **静态绑定(Static Binding)**:编译时期确定的方法或函数【可共享的】
- 静态绑定适用于以下情况:
- 举例代码:
- 静态常量和静态绑定是两个不同的概念。
- 在Java中,`static` 和 `final` 都是关键字,用于定义变量的特性。它们有不同的含义和用途。
- 需要注意的是,尝试修改 `final` 和 `static final` 变量的值会导致编译错误,因为它们被声明为不可修改的常量。
- 总结:
- - Math类中的定义:
- 2. **动态绑定(Dynamic Binding)**:
- 动态绑定适用于以下情况:
- 代码举例:
- 总结:
假设要针对x.f(args),其中 隐式参数 x 是一个声明为类 C 对象的隐式参数。以下是详细描述方法调用的过程:
首先
编译器会检查对象在声明过程中具备的声明类型以及所调用的方法名 f。由于可能存在重名的情况,可能存在多个名为 f 但参数类型不同的方法。例如,可能同时存在 f(int) 和 f(String) 这样的方法。编译器会逐一列举在类 C 中所有名为 f 的方法,还会查找超类中所有可访问的名为 f 的方法 [ 注意:超类的私有方法除外 ]。这是编译器获得了所有可能被调用的备用的方法。【在此,形成方法表1或候选方法列表,接下来会进一步筛选出最终应该调用的方法。】
什么是隐式参数 —>隐式参数是调用该方法的对象本身。
在Java中,隐式参数是指在方法调用过程中自动传递的参数,而不需要显式地在方法调用中提供。隐式参数通常是通过对象的方法调用来使用的。
示例代码:
public class Example {
private int value;
public Example(int value) {
this.value = value;
}
public void printValue() {
System.out.println(value);
}
public static void main(String[] args) {
Example example = new Example(10);
example.printValue();
}
}
在上面的代码中,printValue()
方法是一个实例方法,它没有显式的参数。然而,它可以访问隐式参数 value
,该参数是通过对象 example
的方法调用来传递的。在 example.printValue()
这行代码中,example
就是隐式参数。
与隐式参数相对应的是显示参数。显示参数是在方法调用时显式地提供的参数。例如,以下是一个使用显示参数的示例:
public class Example {
public static void printValue(int value) {
System.out.println(value);
}
public static void main(String[] args) {
int value = 10;
printValue(value);
}
}
在上面的代码中,printValue()
方法接受一个显式参数 value
。在 printValue(value)
这行代码中,value
就是显示参数。
总结一下,隐式参数是在方法调用中自动传递的参数,而不需要显式提供,通常是通过对象的方法调用来使用的。相比之下,显示参数是在方法调用时显式地提供的参数。
接下来
编译器会分析方法调用中提供的参数类型。它会匹配这些参数类型与所有候选的方法参数类型,寻找一个与提供参数类型完全匹配的方法。这一步骤被称为重载解析(overloading resolution)。
假设我们调用 x.f(“Hello_world_work0806”),编译器会选择参数类型为 String 的 f 方法,而不是参数类型为 int 的。
需要注意的是,由于允许进行类型转换(例如,int 可以转换成 double,Manager 可以转换成 Employee 等),这个过程可能会变得相当复杂。
【如果编译器无法找到与提供参数类型完全匹配的方法,或者在进行类型转换后有多个方法与之匹配,编译器将会报告错误。】到这一步,编译器已经成功 确定了需要调用的方法名和参数类型.这意味着在方法调用的过程中,编译器通过上述步骤精确地确定了应该调用的方法。这个过程确保了方法调用的准确性和可靠性。
public class Mains {
public static void main(String[] args) {
printNumber(5); // 输出 "Printing int: 5"
printNumber(3.14); // 输出 "Printing double: 3.14"
}
static void printNumber(int num) {
System.out.println("Printing int: " + num);
}
static void printNumber(double num) {
System.out.println("Printing double: " + num);
}
}
如果存在多个名为f的方法,它们的参数类型都可以接受你提供的参数,那么编译器会选择参数类型与你提供的参数类型最接近的那个方法。例如,如果你调用f(5),并且存在一个参数类型为int的f方法和一个参数类型为double的f方法,那么编译器会选择参数类型为int的f方法,因为int类型比double类型更接近你提供的参数类型。
这个过程可能会变得复杂,因为Java允许进行类型转换。例如,int可以转换成double,Manager可以转换成Employee等。所以,如果你提供的参数类型和任何一个候选方法的参数类型都不完全匹配,编译器会尝试进行类型转换,以找到一个可以接受你提供的参数的方法。
方法的名称和参数列表被称为方法的签名(signature)。
方法的参数列表包括参数的数量、类型和顺序。方法的签名不包括方法的返回类型和访问修饰符。
在Java中,方法的签名由方法的名称和参数列表组成,用于唯一标识一个方法。
public void calculateSum(int a, int b)
在上面的示例中,方法的名称是calculateSum,参数列表是int a和int b,因此方法的签名是calculateSum(int, int)。
f(int) 和 f(String) 是两个有着相同名字但参数不同的方法,则他们的签名是不同的。
在子类里,如果你定义了一个和超类有着相同签名的方法,那这个子类方法会“覆盖”(override)超类中同签名的方法。
返回类型不是签名的一部分。不过在覆盖一个方法时,需要保证返回类型的兼容性:—>允许子类将覆盖方法的返回类型改为原返回类型的子类型。
允许子类在覆盖(重写)父类方法时,将方法的返回类型修改为父类方法返回类型的子类型。
换句话说,子类可以返回更具体的子类型,而不必仅仅返回与父类方法完全相同的类型。
在Java中,方法的签名由方法的名称和参数列表组成,而不包括方法的返回类型。这意味着,如果两个方法具有相同的名称和参数列表,但返回类型不同,它们的方法签名是相同的。这种情况下,编译器无法区分这两个方法。
然而,当涉及到子类覆盖(重写)父类的方法时,确实需要考虑返回类型的兼容性。尽管方法的签名相同,但返回类型必须满足协变性的原则,即子类方法的返回类型必须是父类方法返回类型的子类型。这样做是为了确保子类对象可以被正确地视为父类对象,并且在使用父类方法时能够处理返回值2。如果不满足这个条件,编译器会报错,因为这可能会导致类型不匹配的错误。
例如,假设有一个父类A和一个子类B,它们分别定义了一个同名方法foo(),父类方法的返回类型是A,子类方法的返回类型是B。子类B可以重写父类A的方法,并将返回类型改为B,(1)因为B是A的子类,所以B对象可以被视为A对象。(2)那么当我们通过子类对象调用foo()方法时,返回的是B类型的对象。
子类型返回的是父类型的子类型。
也就是说,如果在子类中重写父类的方法,并将返回类型改为父类返回类型的子类型,那么子类方法的返回值将是子类型的对象。
class A {
public A foo() {
return new A();
}
}
class B extends A {
@Override
public B foo() {
return new B();
}
}
这样,当我们通过父类引用调用子类对象的foo()方法时,返回的是子类对象B。这种方式称为协变【后面有解释】返回类型,它允许我们在子类中返回更具体的类型,提供了更好的灵活性和可读性。
因为 B 是 A 的子类,所以 B 继承了 A 的方法。这包括方法名和参数列表。
当你在子类 B 中重写父类 A 的方法时,你可以将返回类型更改为 B。这是合法的,因为 B 是 A 的子类,所以 B 的对象可以被视为 A 的对象。
当你创建一个子类 B 的对象并调用重写后的方法时,实际上你可以将返回的 B 对象视为 A 对象的一种扩展。这是多态性的体现,你可以通过父类引用调用子类方法,而不必了解具体的子类类型。
具体的例子:
class Animal {
public Animal reproduce() {
return new Animal();
}
}
class Dog extends Animal {
@Override
public Dog reproduce() {
return new Dog();
}
}
在上面的代码中,有一个父类Animal和一个子类Dog。父类Animal定义了一个方法reproduce()
,返回类型为Animal。子类Dog重写了父类Animal的方法,并将返回类型改为Dog。
当我们使用子类Dog的对象调用reproduce()
方法时:
Dog dog = new Dog(); // 创建一个新的 Dog 对象,赋值给 dog 变量
Dog newDog = dog.reproduce(); // 调用 dog 对象的 reproduce() 方法,返回一个新的 Dog 对象,赋值给 newDog 变量
通过子类重写父类方法,我们可以更具体地表达子类对象的行为。在这个例子中,子类Dog重写了父类Animal的reproduce()
方法,并确保返回的是Dog类型的对象。这样,在使用子类Dog对象调用reproduce()
方法时,我们可以直接获得一个新的Dog对象。
详细:
当一个父类有一个方法返回某种类型,子类可以覆盖这个方法并返回该类型的子类型。
假设有一个父类 Animal
和一个子类 Dog
,并且在 Animal
类中有一个方法 makeSound()
,返回类型是 String
,表示动物发出的声音。在子类 Dog
中,可以覆盖 makeSound()
方法并返回 Bark
类的实例,表示狗的吠声。
class Animal {
public String makeSound() {
return "Some generic animal sound";
}
}
class Dog extends Animal {
public Bark makeSound() {
return new Bark();
}
}
class Bark {
public String sound() {
return "Woof woof!";
}
}
在这个例子中,子类 Dog
覆盖了父类 Animal
的 makeSound()
方法,并且返回类型从 String
改变为了 Bark
类的实例,这是父类方法返回类型的子类型。这样,你可以调用 Dog
类的 makeSound()
方法得到一个更具体的结果,而不仅仅是通用的动物声音。
“协变”(covariant)是一种类型关系 —>子类型的某些属性或方法可以比父类型更具体(特化)"
“协变”(covariant)是一种类型关系,指的是子类型与父类型之间的关系,在这种关系下,子类型的某些属性或方法可以比父类型更具体(特化)。在你提到的情况中,"协变的返回类型"指的是子类方法的返回类型可以是原始方法返回类型的子类型。这意味着,子类可以返回更具体的类型,而不违反方法签名的规则。
协变返回类型主要体现在子类覆盖(override)了父类方法并改变了方法的返回类型,使其返回比父类更具体的类型。具体来说,以下部分展示了协变返回类型的使用:
class Publication {
private String title;
public Publication(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
class Book extends Publication {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
public String getAuthor() {
return author;
}
}
class Magazine extends Publication {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
public int getIssueNumber() {
return issueNumber;
}
}
class Library {
public Publication getRecommendation() {
// 返回一个 Publication 对象作为推荐读物
return new Publication("Generic Recommendation");
}
}
class BookLibrary extends Library {
@Override
public Book getRecommendation() {
// 返回一个 Book 对象作为推荐读物
return new Book("The Catcher in the Rye", "J.D. Salinger");
}
}
class MagazineLibrary extends Library {
@Override
public Magazine getRecommendation() {
// 返回一个 Magazine 对象作为推荐读物
return new Magazine("National Geographic", 123);
}
}
在这个例子中,Library
类的 getRecommendation
方法返回类型是 Publication
,而BookLibrary
和 MagazineLibrary
分别覆盖了这个方法,并将返回类型改为更具体的 Book
和 Magazine
。这种方式允许子类方法返回比父类更具体的类型,而仍然保持方法签名的一致性。
所以,协变返回类型在这里的体现就是子类方法覆盖了父类方法并返回更具体的类型,这符合"子类型与父类型之间的关系,在这种关系下,子类型的某些属性或方法可以比父类型更具体(特化)"的解释。
协变(covariance)是指子类型的返回类型可以是父类型的子类型,而逆变(contravariance)是指子类型的返回类型可以是父类型的超类型。
逆变表示一个泛型类型的参数类型在继承关系中变得更加具体(即变窄)。
逆变通常在方法参数中使用,允许传递更通用的类型。
协变表示一个泛型类型的返回类型在继承关系中变得更加通用(即变宽)。
协变通常在方法返回值中使用,允许返回更具体的类型。
-
协变:如果类型B是类型A的子类型,那么在某些上下文中,我们可以使用类型B的对象替代类型A的对象。这通常应用于方法的返回类型。例如,如果一个方法返回一个Animal类型的对象,那么它也可以返回一个Dog类型的对象(假设Dog是Animal的子类)。
-
逆变:如果类型B是类型A的子类型,那么在某些上下文中,我们可以使用类型A的对象替代类型B的对象。这通常应用于方法的参数类型。例如,如果一个方法接受一个Dog类型的对象作为参数,那么它也可以接受一个Animal类型的对象(假设Dog是Animal的子类)。
-
协变和逆变的主要目的是提供更大的灵活性和类型安全。它们允许我们在保持类型安全的同时,编写更通用和可重用的代码。例如,如果我们有一个处理Animal对象的方法,通过逆变,我们可以将这个方法应用于Dog对象,而无需编写专门处理Dog对象的方法。
class Animal {
public void eat() {
System.out.println("Animal eats");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog eats");
}
}
class AnimalHelper {
// 协变:返回类型是协变的
public Dog getAnimal() {
return new Dog();
}
// 逆变:参数类型是逆变的
public void feedAnimal(Animal animal) {
animal.eat();
}
}
public class Mains {
public static void main(String[] args) {
AnimalHelper helper = new AnimalHelper();
// 协变:我们可以将Dog赋值给Animal
Animal animal = helper.getAnimal();
// 逆变:我们可以将Dog传递给接受Animal的方法
helper.feedAnimal(new Dog());
}
}
在这个例子中,AnimalHelper 类中的两个方法展示了协变和逆变的概念:
getAnimal() 方法使用协变:返回类型 Dog 是协变的。这意味着你可以将一个返回类型为 Dog 的方法赋值给类型为 Animal 的引用变量。因为 Dog 是 Animal 的子类,所以这个协变操作是安全的。
feedAnimal(Animal animal) 方法使用逆变:参数类型 Animal 是逆变的。这意味着你可以将一个类型为 Dog 的对象传递给接受 Animal 类型参数的方法。由于 Dog 是 Animal 的子类,所以逆变操作也是安全的。
在 main 方法中,你展示了如何使用这些协变和逆变的特性:
协变:你调用 helper.getAnimal() 方法并将返回值赋给一个类型为 Animal 的引用变量 animal。这是因为你可以将 Dog 类型赋值给 Animal 类型,利用了返回类型的协变特性。
逆变:你调用 helper.feedAnimal(new Dog()) 方法,将 Dog 对象传递给接受 Animal 类型参数的方法。这是因为你可以将 Dog 类型传递给接受 Animal 类型参数的方法,利用了参数类型的逆变特性。
注意
协变:
在协变中,我们可以将派生类(如 Dog)的实例分配给基类(如 Animal)的引用。这是因为派生类是基类的一个特殊类型,具有基类的所有属性和方法,但可能还有额外的特性。
协变涉及返回类型。例如,在协变中,你可以在方法返回类型为 Dog 的情况下,将返回值赋给类型为 Animal 的引用变量。
逆变
在逆变中,我们可以将基类(如 Animal)的实例传递给接受派生类(如 Dog)类型参数的方法。这是因为基类是派生类的通用类型,基类对象中包含了派生类对象的通用行为。
逆变涉及方法参数类型。例如,在逆变中,你可以将类型为 Dog 的参数传递给接受 Animal 类型参数的方法。
区别
在协变中,“赋值” 涉及将一个具体类型(如 Dog)赋值给更通用的类型(如 Animal)的引用变量。这是由于返回类型的协变特性,可以保证派生类对象的特性在基类引用中仍然有效。
在逆变中,“传递” 涉及将一个基类类型的对象传递给接受派生类类型参数的方法。这是由于参数类型的逆变特性,允许基类对象的通用属性在派生类方法中得到正确的处理。
通过你提供的代码,你确实展示了这两种情况的应用。通过 AnimalHelper 类的 getAnimal() 方法,你展示了协变,因为你可以将 Dog 类型的实例分配给类型为 Animal 的引用变量。通过 feedAnimal(Animal animal) 方法,你展示了逆变,因为你可以将 Dog 类型的对象传递给接受 Animal 类型参数的方法。这两种特性一起允许你在泛型方法中更灵活地操作不同类型的对象。
1.具体说明是怎么实现_协变
具体来说,getAnimal方法的返回类型是Dog,但在main方法中,我们将这个Dog对象赋值给了一个Animal类型的变量。这是因为Dog是Animal的子类型,所以我们可以使用Dog对象替代Animal对象。这就是协变的概念。
class AnimalHelper {
// 协变:返回类型是协变的
public Dog getAnimal() {
return new Dog();
}
}
public class Contravariance {
public static void main(String[] args) {
AnimalHelper helper = new AnimalHelper();
// 协变:我们可以将Dog赋值给Animal
Animal animal = helper.getAnimal();
animal.eat();
}
}
在这段代码中,Dog对象被赋值给了Animal类型的变量,这是协变的一个例子。
2.具体说明是怎么实现_逆变
在AnimalHelper类的feedAnimal方法中,参数类型是Animal。这意味着你可以传递任何Animal对象或其子类的对象给这个方法。因此,你可以将Dog对象传递给这个方法,即使它期望的是一个Animal对象。这就是逆变的概念。
在main方法中,你创建了一个Dog对象,并将其传递给了feedAnimal方法。尽管feedAnimal方法期望的是一个Animal对象,但由于Dog是Animal的子类,因此你可以将Dog对象传递给它。这就是逆变的一个具体应用。
这段代码的运行结果将是输出"Dog eats",因为feedAnimal方法调用了传入对象的eat方法,而传入的对象是一个Dog对象,所以调用的是Dog类中覆写的eat方法。
静态绑定(Static Binding)和动态绑定(Dynamic Binding)
总的来说,静态绑定在编译时就确定方法调用,速度快但不够灵活,而动态绑定在运行时根据实际对象类型确定方法调用,更加灵活但速度相对较慢。在Java中,大部分方法调用使用的是动态绑定,因为它能够支持多态性和继承等特性。
1. 静态绑定(Static Binding):编译时期确定的方法或函数【可共享的】
静态绑定是在**编译时期确定方法或函数的调用**,不需要等到运行时期才决定。
它适用于private方法、static方法、final方法和构造器的调用。
静态绑定(static binding)是指在编译时期(compile time)就能确定方法或函数的调用,不需要等到运行时期才决定。在静态绑定中,方法或函数的调用是根据引用类型(也称为编译时类型)来决定的。当编译器在编译代码时,会根据引用类型确定调用该类型定义的方法或函数。因此,静态绑定的方法或函数调用是在编译时期就确定的,不会受到运行时期对象的实际类型的影响。
- 发生在编译时(compile time)。
- 适用于private、static、final方法以及构造器等情况。
- 在编译时就能确定要调用的方法,因为方法的选择是基于引用变量的声明类型。
- 编译器可以在编译阶段直接解析方法调用,因此速度较快。
静态绑定适用于以下情况:
- Private 方法:由于private方法在同一类中是不可继承的,编译器可以直接确定要调用的方法。
private方法是不能被子类重写的,所以调用private方法时,编译器可以准确地知道应该调用哪个方法。
- Static 方法:静态方法属于类而不是实例,因此不需要实例化对象就可以调用。编译器可以根据声明的类来确定要调用的静态方法。
static方法是属于类而不是对象的,所以调用static方法时,编译器可以准确地知道应该调用哪个方法。
- Final 方法:final修饰的方法不能被子类重写,因此编译器可以准确知道要调用的是声明的类中的final方法。
final方法是不能被子类重写的,所以调用final方法时,编译器可以准确地知道应该调用哪个方法。
- 构造器:在创建对象时,编译器根据构造器的参数列表来准确地选择合适的构造器。
构造器是用于创建对象的特殊方法,调用构造器时,编译器可以准确地知道应该调用哪个构造器。
静态绑定的一个特点是,它在编译时就能够解析方法调用,因此执行速度较快。但是,静态绑定缺乏动态继承和多态性的特性,因为它不会考虑对象的实际类型。相比之下,动态绑定会在运行时根据实际对象类型来确定方法调用,提供了更大的灵活性和多态性。
举例代码:
class Animal {
public static void printType() {
System.out.println("This is an animal.");
}
}
class Dog extends Animal {
public static void printType() {
System.out.println("This is a dog.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
Animal dog = new Dog();
Dog realUnderDogClassDog = new Dog();
animal.printType(); // 静态绑定,输出:"This is an animal."
dog.printType(); // 静态绑定,输出:"This is an animal.",因为静态方法不会被子类重写
realUnderDogClassDog.printType();// 静态绑定,输出:"This is a dog."
}
}
由于printType()方法是静态方法,静态方法的调用是根据引用类型来确定的,不会受到运行时期对象的实际类型的影响。所以无论animal变量引用的是Animal对象还是dog变量引用的是Dog对象,调用printType()方法时都会直接调用Animal类中定义的printType()方法。因此,输出结果都是"This is an animal."。
这就是静态绑定的特点,方法调用在编译时期就已经确定,不会根据对象的实际类型来决定。
静态常量和静态绑定是两个不同的概念。
静态常量是指在编译时期就确定并且不能被修改的常量。它通常使用关键字final
来声明,并且在声明时就必须初始化。静态常量在类加载时被初始化,并且可以通过类名直接访问。静态常量是类级别的,意味着它在整个类中都是共享的,所有对象共享同一个静态常量的值。
静态绑定是指在编译时期就确定方法或函数的调用。它发生在静态方法、静态变量、静态常量的访问以及使用类名访问静态成员时。静态绑定是根据引用类型来决定方法或函数的调用,不会受到对象的实际类型的影响。这是因为静态成员和静态方法是与类关联的,不依赖于对象的创建。因此,无论使用哪个对象引用去调用静态成员或静态方法,都会调用到类中定义的静态成员或静态方法。
总结起来,静态常量是在编译时期确定并且不能被修改的常量,而静态绑定是在编译时期确定方法或函数的调用。静态常量是类级别的,可以通过类名直接访问,而静态绑定是根据引用类型来决定方法或函数的调用,并且不受对象的实际类型的影响。
在Java中,static
和 final
都是关键字,用于定义变量的特性。它们有不同的含义和用途。
首先,我们分别来看看 static
、final
和 static final
的含义:
-
static
:用于表示变量或方法是类级别的,不依赖于对象的创建,可以通过类名直接访问。但是static
并不代表不可修改,静态变量在程序运行期间是可以被修改的。 -
final
:用于表示常量,一旦赋值后就不能再修改。可以用于变量、方法、类等地方。 -
static final
:将static
和final
结合在一起,表示一个类级别的不可修改的常量。
现在,我们将上述概念应用于代码,并逐步思考输出结果:
public class StaticFinalExample {
public static String staticVariable = "Static Variable";
public final String finalVariable = "Final Variable";
public static final String staticFinalVariable = "Static Final Variable";
public static void main(String[] args) {
StaticFinalExample obj1 = new StaticFinalExample();
// 输出1:访问静态变量
System.out.println("Static Variable (obj1): " + obj1.staticVariable);
obj1.staticVariable = "hello"; // 修改静态变量的值
System.out.println("Static Variable (obj1): " + obj1.staticVariable);
// 输出2:访问 final 变量
System.out.println("Final Variable (obj1): " + obj1.finalVariable);
// 尝试修改 final 变量的值不会导致编译错误,但运行时会报错
// obj1.finalVariable = "Modified Final Variable";
// 输出3:访问 static final 变量
System.out.println("Static Final Variable (obj1): " + obj1.staticFinalVariable);
// 尝试修改 static final 变量的值会导致编译错误
// obj1.staticFinalVariable = "Modified Static Final Variable";
// 输出4:输出修改后的值
System.out.println("Static Variable (obj1): " + obj1.staticVariable);
}
}
需要注意的是,尝试修改 final
和 static final
变量的值会导致编译错误,因为它们被声明为不可修改的常量。
总结:
static
表示类级别的,可以通过类名直接访问的变量或方法。final
表示常量,一旦赋值后不能再修改。static final
表示类级别的不可修改的常量。static
和final
可以独立使用,也可以结合在一起使用。
- Math类中的定义:
/**
* The {@code double} value that is closer than any other to
* <i>e</i>, the base of the natural logarithms.
*/
public static final double E = 2.7182818284590452354;
/**
* The {@code double} value that is closer than any other to
* <i>pi</i>, the ratio of the circumference of a circle to its
* diameter.
*/
public static final double PI = 3.14159265358979323846;
/**
* Constant by which to multiply an angular value in degrees to obtain an
* angular value in radians.
*/
private static final double DEGREES_TO_RADIANS = 0.017453292519943295;
/**
* Constant by which to multiply an angular value in radians to obtain an
* angular value in degrees.
*/
private static final double RADIANS_TO_DEGREES = 57.29577951308232;
2. 动态绑定(Dynamic Binding):
动态绑定(Dynamic Binding)是一种方法调用的绑定方式,它发生在运行时(runtime)。在动态绑定中,方法的选择是基于对象的实际类型,而不仅仅是引用变量的声明类型。
它适用于非私有、非静态、非final的实例方法和接口方法的调用,实现了多态性。
动态绑定(dynamic binding)是指在运行时期(runtime)根据对象的实际类型来决定方法或函数的调用。在动态绑定中,方法或函数的调用是根据实际类型(也称为运行时类型)来决定的。当程序在运行时期调用方法或函数时,会根据对象的实际类型来确定调用该类型定义的方法或函数。因此,动态绑定的方法或函数调用是在运行时期根据对象的实际类型决定的。
- 发生在运行时(runtime)。
- 适用于方法调用依赖于隐式参数的实际类型的情况。
- 在运行时根据对象的实际类型来确定要调用的方法,方法选择是基于对象的实际类型。
- 需要在每次方法调用时进行实际的查找,因此速度相对较慢,但提供了更大的灵活性和多态性。
动态绑定适用于以下情况:
- 调用非私有、非静态、非final的实例方法:这些方法可以被子类重写,所以在调用这些方法时,会根据对象的实际类型来确定调用哪个方法。
- 调用接口方法:接口方法是抽象的方法定义,实现类需要实现接口方法,因此在调用接口方法时,会根据实现类的实际类型来确定调用哪个方法。
代码举例:
动态绑定是指在运行时期根据对象的实际类型来确定方法或函数的调用。
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
public void makeSound() {
System.out.println("狗发出汪汪声");
}
}
class Cat extends Animal {
public void makeSound() {
System.out.println("猫发出喵喵声");
}
}
public class DynamicBindingExample {
public static void main(String[] args) {
Animal animal1 = new Animal();
Animal animal2 = new Dog();
Animal animal3 = new Cat();
animal1.makeSound(); // 输出:"动物发出声音"
animal2.makeSound(); // 输出:"狗发出汪汪声"
animal3.makeSound(); // 输出:"猫发出喵喵声"
}
}
我们定义了用 Animal
类和它的两个子类 Dog
和 Cat
。
Animal
类中有一个 makeSound()
方法,而子类 Dog
和 Cat
分别覆盖了该方法并提供了它们自己的实现。
在 main
方法中,我们创建了一个 Animal
对象 animal1
,以及两个子类对象 animal2
和 animal3
。
当我们调用 `makeSound()` 方法时,
由于动态绑定的作用,实际调用的方法是根据对象的实际类型来确定的。因此,
animal1.makeSound()
调用的是 Animal
类的 makeSound()
方法,
animal2.makeSound()
调用的是 Dog
类的 makeSound()
方法,
animal3.makeSound()
调用的是 Cat
类的 makeSound()
方法。
这就是动态绑定的特性,它允许程序在运行时根据对象的实际类型来确定调用的方法,而不是根据引用类型来确定。
总结:
动态绑定的特性允许子类对象在运行时根据实际类型来调用适当的方法,而不是根据编译时类型。这使得代码可以更具适应性,当引入新的子类时,无需修改现有的代码就可以调用新子类特有的方法。
为确保在子类中覆盖(重写)超类方法时,子类方法的访问权限不能低于超类方法的访问权限。如果超类方法是 public,那么子类方法也必须声明为 public。如果违反了这一规则,编译器将会报错,因为这可能导致在某些情况下无法正常访问继承的方法。
Para_1:Java虚拟机(JVM)负责在运行时解析和执行Java程序【根据编译后的字节码文件生成和维护的。】。在Java程序启动时,JVM会加载字节码文件,并将类的方法信息存储在方法区(Method Area)中。包括创建方法表【也称为虚方法表__vtable】。
方法表用于存储类的实例方法信息和调用地址,以便在运行时进行动态绑定和多态调用。因此,形成方法表是由JVM在程序运行时完成的。【方法表的结构和存储方式是由JVM定义的,因此不同的JVM实现可能会有一些细节上的差异。】
Para_2:方法表(method table)是虚拟机为每个类预先计算【存储方法信息】的数据结构,用于记录类中所有方法的签名和要调用的实际方法【它包含了类中定义的所有方法的信息,包括方法的名称、参数类型、返回类型、访问修饰符等。】。
当程序运行并且采用动态绑定调用方法时,虚拟机会根据对象的实际类型在方法表中查找对应的方法。如果找到了与实际类型对应的方法,则调用该方法;如果没有找到,则会在父类中寻找,直到找到对应的方法或者到达顶层父类为止。【方法表的主要作用是在运行时支持动态绑定和多态性。】
Para_3:在程序运行时,当调用一个方法时,JVM会根据方法表中的信息来确定要调用的具体方法。由于方法表记录了方法的实际地址或偏移量,因此可以实现多态特性,即在运行时根据对象的实际类型来确定调用哪个具体的方法。
需要注意的是:方法表是在编译时生成的,而不是在运行时动态生成的。因此,对于动态添加的方法或者通过反射机制生成的方法,方法表中是不会包含这些方法的信息的。 ↩︎Java类的返回值类型可以是任何有效的数据类型,包括原始数据类型 (如int、double.char等)、对象类型(如自定义类、String等)、数组类型等。具体的返回值类型取决于方法的定义和实现。 ↩︎