1. 概述

Java类型系统由两种组成:基本类型和引用类型。

这篇文章将会介绍一下引用类型,以便更好地理解Java如何处理类型类型转换。

2. 基本类型 VS 引用类型

尽管基本类型的转换和引用类型转换看起来很相似,但它们的概念却截然不同。

在这两种情况下,我们都将一种类型“转变”为另一种类型。

  • 基本类型转换

但是,基本类型变量包含其值,基本类型的转换意味着其值是不可逆的转换。

double myPie = 3.14;
int myIntPie = (int) myPie;
         
assertNotEquals(myPie, myIntPie);

在上面的例子中转换后,myIntPie变量为1,我们无法从中恢复前一个值1.1。

  • 引用类型转换

引用类型变量不同,引用类型变量仅引用一个对象,但不包含该对象本身。并且转换引用类型变量不会触及它引用的对象,而只是以另一种方式标记此对象,扩展或缩小使用它的机会。向上转换会缩小此对象可用的方法和属性列表,而向下转换则可以扩展它。

引用就像对象的远程控制。遥控器根据其类型具有更多或更少的按钮,并且对象本身存储在堆中。

当我们进行转换时,我们会更改遥控器的类型,但不会更改对象本身。

3.向上转型

从子类到超类的转换称为向上转型(upcasting)。 通常,向上转型是由编译器隐式执行的。

向上转型与继承密切相关。这是Java中的另一个核心概念。

使用引用类型变量来引用更具体的类型是很常见的。 每次我们这样做时,都会发生隐式的向上转换。

为了演示向上转型,我们定义一个Animal类:

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的事情 - 我们不能在动物变量上调用meow()

虽然Cat对象仍然是Cat对象,但调用meow()会导致编译器错误:

// animal.meow(); 提示说Animal类中没有定义meow()方法。

如果要调用meow(),我们需要将animal向下转型,我们稍后会这样讲到。

因为有了向上转型,我们可以使用面向对象的多态特性。

3.1 多态

让我们定义Animal的另一个子类,一个Dog类:

public class Dog extends Animal {
 
    public void eat() {
         // ... 
    }
}

现在我们可以定义feed()方法来处理像所有的动物:

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

我们不希望AnimalFeeder关心列表中有哪些动物(猫或狗)。 在

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

我们不希望AnimalFeeder关心列表中有哪些动物(猫或狗)。 在feed()方法中,它们都是动物。

当我们将特定类型的对象添加到动物列表(animals)时,会发生隐式向上转型。

List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
new AnimalFeeder().feed(animals);

我们添加了Cat和Dog,它们被隐含地向上转型成了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,向上转型是合法的并且一般都是隐式转型的。

3.2 Overriding

在上面的示例中,我们重写了eat()方法。 这意味着尽管在Animal类型的变量上调用了eat(),但是工作是在调用真实对象上方法来完成的。

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

如果我们在我们的类中添加一些日志记录,我们将看到调用Cat和Dog的方法:

23:39:57.541 [main] INFO  com.ripjava.java.core.casting.Cat - cat is eating
23:39:57.543 [main] INFO  com.ripjava.java.core.casting.Dog - dog is eating
总结一下:
  • 如果对象与变量的类型相同或者它是子类型,则引用类型变量可以隐式的向上转型。
  • 所有Java对象都是多态的,并且由于向上转型可以被视为父类型的对象。

4. 向下转型

如果我们想使用Animal类型的变量来调用仅适用于Cat类的方法,该怎么办?

这是一个向下转型。 它是从超类到子类的转换。

我们来举个例子:

Animal animal = new Cat();

我们知道动物变量是指Cat的实例。 我们想在动物身上调用Cat的meow()方法。 但编译器提示我们类型为Animal的meow()方法不存在。

要调用 meow() ,我们应该将Animal向下转型为Cat:

((Cat) animal).meow();

最里面内括号和它们包含的类型称为强制转换运算符。 请注意,外部括号也是必须的。

让我们用meow()方法重写之前的AnimalFeeder示例:

public class AnimalFeeder {
 
    public void feed(List<Animal> animals) {
        animals.forEach(animal -> {
            animal.eat();
            if (animal instanceof Cat) {
                ((Cat) animal).meow();
            }
        });
    }
}

现在我们可以访问Cat类可用的所有方法。 查看日志以确保实际调用了meow():

23:39:57.546 [main] INFO  com.ripjava.java.core.casting.Cat - meow

请注意,在上面的示例中,我们尝试仅向下转型的对象实际上是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:

这意味着我们正在尝试将作为Dog实例转换为Cat实例。

如果我们向下转型的类型与真实对象的类型不匹配,则ClassCastException总是在运行时抛出。

注意,如果我们尝试向下转型为不相关的类型,编译器是不允许我们这样做的:

Animal animal;
String s = (String) animal;

编译器会 提示我 “无法从Animal转换为String”。对于要编译的代码,两种类型都应该在同一继承树中。

总结一下:
  • 为了获得特定于子类的成员的访问权,必须进行向下转型。
  • 使用强制转换运算符完成向下转型, 要安全地向下转换对象,我们需要instanceof运算符
  • 如果真实对象与我们向下转换的类型不匹配,则将在运行时抛出ClassCastException

5. Cast() Method

还有另一种使用Class方法强制转换对象的方法:

public void whenDowncastToCatWithCastMethod_thenMeowIsCalled() {
    Animal animal = new Cat();
    if (Cat.class.isInstance(animal)) {
        Cat cat = Cat.class.cast(animal);
        cat.meow();
    }
}

在上面的示例中,使用了cast()isInstance()方法,而不是相应的castinstanceof运算符。

通常使用具有泛型类型的cast()isInstance()方法。

让我们用feed()方法创建AnimalFeederGeneric <T>类,它只“喂”一种类型的Animal - Dog或Cat,取决于类型参数的值:

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并确保该方法仅返回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上获得。