1. 概述
Java类型系统由两种类型组成:基本类型和引用类型。
我们在这篇文章中已经讨论过基本类型转换,现在将聚焦引用类型转换,深入理解Java如何处理类型。
2. 基本类型 vs 引用类型
虽然基本类型转换和引用变量转换看起来相似,但它们是完全不同的概念。
两者都是将一种类型"转换"为另一种类型,但简单来说:
- 基本类型变量直接包含其值,转换意味着值的不可逆修改: ```java double myDouble = 1.1; int myInt = (int) myDouble;
assertNotEquals(myDouble, myInt);
转换后,`myInt`的值是`1`,无法从中恢复原始值`1.1`。
**引用类型则完全不同**:引用变量仅指向对象,并不包含对象本身。
引用类型转换不会修改对象本身,只是改变对象的"标签",从而扩展或限制操作该对象的能力。**向上转型会限制可用的方法和属性,向下转型则可能扩展这些能力**。
可以把引用想象成对象的遥控器。遥控器的按钮数量取决于其类型,而对象本身存储在堆中。当我们进行类型转换时,只是更换了遥控器的类型,并未改变对象本身。
## 3. 向上转型
**从子类到父类的转换称为向上转型**。通常由编译器隐式执行。
向上转型与继承——Java的另一核心概念密切相关。使用引用变量指向更具体的类型时,隐式向上转型就会发生。
为演示向上转型,先定义`Animal`类:
```java
public class Animal {
public void eat() {
// ...
}
}
扩展Animal
类:
public class Cat extends Animal {
public void eat() {
// ...
}
public void meow() {
// ...
}
}
创建Cat
实例并赋值给Cat
类型变量:
Cat cat = new Cat();
也可以赋值给Animal
类型变量:
Animal animal = cat;
这里发生了隐式向上转型。
显式转型也是可行的:
animal = (Animal) cat;
但沿着继承树向上转型时,显式转换是多余的。编译器知道cat
是Animal
,不会报错。
⚠️ 注意:引用变量可以指向声明类型的任何子类型。
通过向上转型,我们限制了Cat
实例可用的方法数量,但实例本身未改变。现在无法执行Cat
特有的操作——不能在animal
变量上调用meow()
。
虽然Cat
对象仍然是Cat
,但调用meow()
会导致编译错误:
// animal.meow(); The method meow() is undefined for the type Animal
要调用meow()
,需要向下转型animal
,稍后会讨论。
3.1. 多态
定义另一个Animal
子类Dog
:
public class Dog extends Animal {
public void eat() {
// ...
}
}
现在定义feed()
方法,将所有猫和狗都视为Animal
:
public class AnimalFeeder {
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
});
}
}
我们不希望AnimalFeeder
关心列表中的具体动物类型——是Cat
还是Dog
。在feed()
方法中,它们都是Animal
。
向animals
列表添加特定类型对象时,隐式向上转型发生:
List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
new AnimalFeeder().feed(animals);
添加猫和狗时,它们被隐式向上转型为Animal
。每个Cat
都是Animal
,每个Dog
也是Animal
。它们是多态的。
顺便说,所有Java对象都是多态的,因为每个对象至少是Object
。我们可以将Animal
实例赋值给Object
类型变量,编译器不会报错:
Object object = new Animal();
这就是为什么所有Java对象创建后都自带Object
特有方法(如toString()
)。
向接口转型也很常见。创建Mew
接口并让Cat
实现它:
public interface Mew {
public void meow();
}
public class Cat extends Animal implements Mew {
public void eat() {
// ...
}
public void meow() {
// ...
}
}
现在任何Cat
对象都可以向上转型为Mew
:
Mew mew = new Cat();
Cat
是Mew
;向上转型合法且隐式进行。
因此,Cat
同时是Mew
、Animal
、Object
和Cat
。它可以赋值给这四种类型的引用变量。
3.2. 方法重写
上例中,eat()
方法被重写。这意味着虽然eat()
是在Animal
类型变量上调用的,但实际执行的是真实对象(猫和狗)的方法:
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
});
}
如果在类中添加日志,会看到调用的是Cat
和Dog
的方法:
web - 2018-02-15 22:48:49,354 [main] INFO com.baeldung.casting.Cat - cat is eating
web - 2018-02-15 22:48:49,363 [main] INFO com.baeldung.casting.Dog - dog is eating
总结:
- 引用变量可以指向与其类型相同或子类型的对象
- 向上转型隐式进行
- 所有Java对象都是多态的,可通过向上转型视为父类型对象
4. 向下转型
如果想用Animal
类型变量调用Cat
类特有的方法呢?这时就需要向下转型。它是从父类到子类的转换。
看个例子:
Animal animal = new Cat();
我们知道animal
变量指向Cat
实例。想在animal
上调用Cat
的meow()
方法,但编译器会报错——Animal
类型没有meow()
方法。
要调用meow()
,需要将animal
向下转型为Cat
:
((Cat) animal).meow();
内层括号及其包含的类型有时称为转型操作符。注意外层括号也是编译必需的。
重写之前的AnimalFeeder
示例,加入meow()
调用:
public class AnimalFeeder {
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
if (animal instanceof Cat) {
((Cat) animal).meow();
}
});
}
}
现在可以访问Cat
类的所有方法。查看日志确认meow()
确实被调用:
web - 2018-02-16 18:13:45,445 [main] INFO com.baeldung.casting.Cat - cat is eating
web - 2018-02-16 18:13:45,454 [main] INFO com.baeldung.casting.Cat - meow
web - 2018-02-16 18:13:45,455 [main] INFO com.baeldung.casting.Dog - dog is eating
⚠️ 注意:上例中我们只对真正是Cat
实例的对象进行向下转型。为此使用了instanceof
操作符。
4.1. instanceof
操作符
向下转型前常用instanceof
检查对象是否属于特定类型:
if (animal instanceof Cat) {
((Cat) animal).meow();
}
4.2. ClassCastException
如果没用instanceof
检查类型,编译器不会报错,但运行时会抛出异常。
移除上例中的instanceof
验证:
public void uncheckedFeed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
((Cat) animal).meow();
});
}
这段代码能正常编译,但运行时会抛出异常:
java.lang.ClassCastException: com.baeldung.casting.Dog cannot be cast to com.baeldung.casting.Cat
这意味着我们试图将Dog
实例转换为Cat
实例。
❌ 如果向下转型的类型与实际对象类型不匹配,运行时总会抛出ClassCastException
。
注意:如果尝试向下转型到无关类型,编译器会直接阻止:
Animal animal;
String s = (String) animal;
编译器提示:"Cannot cast from Animal to String."
要使代码通过编译,两个类型必须在同一继承树中。
总结:
- 向下转型是访问子类特有成员的必要手段
- 向下转型通过转型操作符完成
- 安全向下转型需要
instanceof
操作符 - 如果实际对象类型与向下转型目标不匹配,运行时抛出
ClassCastException
5. cast()
方法
还有一种使用Class
方法转型的方式:
public void whenDowncastToCatWithCastMethod_thenMeowIsCalled() {
Animal animal = new Cat();
if (Cat.class.isInstance(animal)) {
Cat cat = Cat.class.cast(animal);
cat.meow();
}
}
上例中,用cast()
和isInstance()
方法替代了转型操作符和instanceof
。
cast()
和isInstance()
方法常与泛型配合使用。
创建泛型类AnimalFeederGeneric<T>
,其feed()
方法根据类型参数T
的值只"喂养"一种动物(猫或狗):
public class AnimalFeederGeneric<T> {
private Class<T> type;
public AnimalFeederGeneric(Class<T> type) {
this.type = type;
}
public List<T> feed(List<Animal> animals) {
List<T> list = new ArrayList<T>();
animals.forEach(animal -> {
if (type.isInstance(animal)) {
T objAsType = type.cast(animal);
list.add(objAsType);
}
});
return list;
}
}
feed()
方法检查每个动物,只返回T
类型的实例。
⚠️ 注意:Class
实例必须传给泛型类,因为无法从类型参数T
获取它。本例中我们通过构造函数传递。
设T
为Cat
,验证方法只返回猫:
@Test
public void whenParameterCat_thenOnlyCatsFed() {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
AnimalFeederGeneric<Cat> catFeeder
= new AnimalFeederGeneric<Cat>(Cat.class);
List<Cat> fedAnimals = catFeeder.feed(animals);
assertTrue(fedAnimals.size() == 1);
assertTrue(fedAnimals.get(0) instanceof Cat);
}
6. 总结
本基础教程探讨了向上转型、向下转型及其使用方法,以及这些概念如何帮助你利用多态特性。
本文代码可在GitHub获取。