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;

但沿着继承树向上转型时,显式转换是多余的。编译器知道catAnimal,不会报错。

⚠️ 注意:引用变量可以指向声明类型的任何子类型。

通过向上转型,我们限制了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();

CatMew;向上转型合法且隐式进行。

因此,Cat同时是MewAnimalObjectCat。它可以赋值给这四种类型的引用变量。

3.2. 方法重写

上例中,eat()方法被重写。这意味着虽然eat()是在Animal类型变量上调用的,但实际执行的是真实对象(猫和狗)的方法:

public void feed(List<Animal> animals) {
    animals.forEach(animal -> {
        animal.eat();
    });
}

如果在类中添加日志,会看到调用的是CatDog的方法:

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上调用Catmeow()方法,但编译器会报错——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获取它。本例中我们通过构造函数传递。

TCat,验证方法只返回猫:

@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获取。


原始标题:Object Type Casting in Java