在写码的过程中,经常出现比如
// 声明一个父类引用,实际上将它赋给了一个子类对象
List<String> myList = new ArrayList<>();
这样的情况,这时就会好奇,这样的多态声明具体规则是怎样的?这次总结一下:
在Java中,引用可以进行类型转换: 我们可以将一个子类引用转换为其父类引用,这叫做向上转换(upcast)或者宽松转换。所以我们可以声明一个父类引用,并将其赋给一个子类的实例。这是多态性的一个重要概念,允许我们以一种通用的方式操作不同子类的对象,而不需要知道具体的子类类型,比如:
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog(); // 子类实例赋给父类引用
Animal animal2 = new Cat(); // 子类实例赋给父类引用
animal1.makeSound(); // 调用Dog类的makeSound方法
animal2.makeSound(); // 调用Cat类的makeSound方法
}
}
在上面的示例中,Dog
和 Cat
类都是 Animal
类的子类。然后,我们声明了两个 Animal
类型的引用变量 animal1
和 animal2
,并将它们分别赋值为 Dog
和 Cat
类的实例,然后分别调用两个animal的makeSound,运行正常,没有问题。
这个时候会想,如果我在子类比如dog中新加一个方法叫bark,新加一个成员变量叫breed,那么我声明的父类引用animal1, 可以正确使用这两个成员方法和变量吗?
答案是不可以,父类引用 animal1
不能直接访问这两个新添加的成员,因为父类 Animal
没有定义这些成员。只有子类 Dog
才能访问它们:
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
void bark() {
System.out.println("Dog is barking");
}
String breed; // 新的成员变量 breed
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
// 下面两行是不合法的,因为父类引用不能访问子类特有的方法和成员变量
// animal1.bark(); // 错误,父类引用不能调用子类特有的方法
// animal1.breed = "Labrador"; // 错误,父类引用不能访问子类特有的成员变量
// 但是可以调用从父类继承的方法
animal1.makeSound(); // 合法,会调用子类 Dog 的 makeSound 方法
}
}
如上所示,尝试使用 animal1
来调用 bark
方法和访问 breed
变量将会导致编译错误,因为这些是子类 Dog
特有的成员。只有在将 animal1
强制转换为 Dog
类型之后,才能访问这些子类特有的成员。
if (animal1 instanceof Dog) {
Dog dog = (Dog) animal1; // 强制类型转换为 Dog 类型
dog.bark(); // 现在可以访问 bark 方法
dog.breed = "Labrador"; // 现在可以访问 breed 成员变量
}
所以,同理
当我们声明一个 List<String>
引用并将其实际赋值为 ArrayList<String>
时,我们可以调用 List
接口中定义的方法,但不能直接调用 ArrayList
特有的方法。这是因为引用类型确定了我们可以调用的方法,而不是实际对象的类型。
public class Main {
public static void main(String[] args) {
List<String> myList = new ArrayList<>();
// 添加元素 - 这是List接口的方法
myList.add("Apple");
myList.add("Banana");
myList.add("Cherry");
// 获取元素 - 这是List接口的方法
String fruit = myList.get(0);
System.out.println("First fruit: " + fruit);
// 不能调用ArrayList特有的方法,比如ensureCapacity
// myList.ensureCapacity(20); // 这将导致编译错误
// 若要调用ArrayList特有的方法,需要进行强制类型转换
if (myList instanceof ArrayList) {
ArrayList<String> arrayList = (ArrayList<String>) myList;
arrayList.ensureCapacity(20); // 这是ArrayList的方法
}
}
}
那么,声明List而不是ArrayList的好处是什么呢?
1. 更通用的代码:声明 List
可以编写更通用的代码,不会依赖于特定的 List
实现。这使得代码更具灵活性,这样当我们以后想换到其他 List
实现,如 LinkedList
,只需要更改最上面一行ArrayList换成LinkedList就行了,而不需要更改大部分代码。
2. 接口隔离原则:使用 List
接口符合接口隔离原则,这是面向对象设计原则之一。它强调了客户端代码(使用 List
的代码)不应该依赖于不需要的方法。通过声明 List
,只公开了 List
接口定义的方法,而不是 ArrayList
特有的方法,有助于保持代码的干净和可维护。