深入理解 Java 中的类型转换

 2022-09-26

202209262243326491.png

概述

我们知道Java类型系统由两种类型组成:基础类型和封装类型。

向上转型

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

向上转型与继承密切相关 - 这是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的事情-我们不能在animal变量上调用meow()。

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

// animal.meow(); The method meow() is undefined for the type Animal

要调用meow(),我们需要向下转型animal,我们稍后会这样做。

但现在我们将描述是什么让我们向上转型,我们可以利用多态性。

多态性

让我们定义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关注列表中的哪种动物 - 猫或狗。在feed()方法中,它们都是动物。

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

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

我们添加了猫和狗,它们被隐含地转向了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。在我们的示例中,它可以分配给所有四种类型的引用变量。

重写

在上面的示例中,覆盖了eat()方法。这意味着尽管在Animal类型的变量上调用了eat(),但是工作是通过在真实对象上调用的方法完成的 - Cat和Dog:

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

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

    2019-05-29 17:48:49,354 [main] INFO com.william.casting.Cat - cat is eating
    2019-05-29 17:48:49,363 [main] INFO com.william.casting.Dog - dog is eating

总结一下:

  • 如果对象与变量的类型相同或者它是子类型,则引用变量可以引用对象
  • 向上发生隐含的上行
  • 所有Java对象都是多态的,并且由于向上转型可以被视为超类型的对象

向下转型

如果我们想使用Animal类型的变量来调用仅适用于Cat类的方法,该怎么办?这是一个向下转型。它是从超类到子类的转换。

我们来举个例子:

    Animal animal = new Cat();

我们知道动物变量是指Cat的实例。我们想在动物身上调用Cat的meow()方法。但编译器提示类型为Animal的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():

    2019-05-29 18:28:19,445 [main] INFO com.william.casting.Cat - cat is eating
    2019-05-29 18:28:19,454 [main] INFO com.william.casting.Cat - meow
    2019-05-29 18:28:19,455 [main] INFO com.william.casting.Dog - dog is eating

请注意,在上面的示例中,我们尝试仅向下转换那些实际上是Cat实例的对象。为此,我们使用运算符instanceof。

instanceof操作

我们经常在向下转换之前使用instanceof运算符来检查对象是否属于特定类型:

    if (animal instanceof Cat) {
     ((Cat) animal).meow();
    }

ClassCastException异常

如果我们没有使用instanceof运算符检查类型,编译器就不会报错。但在运行时,会有一个异常。

为了演示这个,让我们从上面的代码中删除instanceof运算符:

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

此代码编译没有问题。但如果我们尝试运行它,我们会看到一个异常:

    java.lang.ClassCastException:com.william.casting.Dog无法强制转换为com.william.casting.Cat

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

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

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

    Animal animal;
    String s = (String) animal;

编译器说“无法从Animal转换为String”。

对于要编译的代码,两种类型都应该在同一继承树中。

我们总结一下:

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

Cast()方法

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

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

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

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

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

    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()方法检查每个Animal,并返回仅那些的实例Ť。

注意,Class实例也应该传递给泛型类,因为我们无法从类型参数T中获取它。在我们的示例中,我们在构造函数中传递它。

让我们使T等于Cat并确保该方法仅返回cat:

    @Test
    public void test() {
     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);
    }

动态转换

在Java 5之前,以下代码将是常态:

    List dates = new ArrayList();
    dates.add(new Date());
    Object object = dates.get(0);
    Date date = (Date) object;

需要转换。虽然运行时类型是Date,但编译器无法知道它。

使用泛型,可以重写上面的代码:

    List<Date> dates = new ArrayList<>();
    dates.add(new Date());
    Date date = dates.get(0);

没有转换:由于泛型,编译器有足够的信息。

强转换

一个这样的用例是Servlet API。在servlet上下文/请求/会话中存储对象的映射不使用泛型。他们也会使用Object

    // In a servlet
    ServletContext context = getServletContext();
    context.put("date", new Date());
    // Somewhere else
    ServletContext context = getServletContext();
    Object object = context.get("date");
    Date date = (Date) object;

** 静态转换**

使用Java进行强制转换的最常用方法如下:

    Object obj; // may be an integer
    if (obj instanceof Integer) {
     Integer objAsInt = (Integer) obj;
     // do something with 'objAsInt'
    }

这使用了 instanceof和cast运算符。实例转换的类型(在本例中为 Integer)必须在编译时静态知道,所以让我们调用这个静态转换。

如果 obj不是 Integer,则上述测试将失败。如果我们试图抛出它,我们会得到一个 ClassCastException。如果 obj为 null,则它会使instanceof测试失败 但可以被强制转换,因为 null可以是任何类型的引用。

动态转换

最初可用的唯一转换形式是静态转换。这意味着需要在编译时知道转换类型。但是,让我们设想一个接受a的方法Stream<Object>,过滤特定类型的所有元素,并以正确的类型返回这些元素。这是用法的一个例子:

我遇到的一种技术不常使用Class上与运算符对应的方法 :

    Object obj; // may be an integer
    if (Integer.class.isInstance(obj)) {
     Integer objAsInt = Integer.class.cast(obj);
     // do something with 'objAsInt'
    }

请注意,虽然在此示例中,要编译的类在编译时也是已知的,但不一定如此:

    Object obj; // may be an integer
    Class<T> type = // may be Integer.class
    if (type.isInstance(obj)) {
     T objAsType = type.cast(obj);
     // do something with 'objAsType'
    }

因为类型在编译类型是未知的,我们将称之为动态转换。

对于错误类型和空引用的实例,测试和强制转换的结果与静态强制转换的结果完全相同。

现在

转换Optional或Stream元素的值是一个两步过程:首先我们必须过滤掉错误类型的实例,然后我们可以转换为所需的类型。

使用Class上的方法 ,我们使用方法引用来完成此操作。使用Optional的示例 :

    Optional<?> obj; // may contain an Integer
    Optional<Integer> objAsInt = obj
     .filter(Integer.class::isInstance)
     .map(Integer.class::cast);

通过上面的写法,我们可以实现动态转换。

再举一个案例

    List<?> items = ...
    List<Date> dates = filter(Date.class, items);

改造

    static <T> List<T> filter(Class<T> clazz, List<?> items) {
     return items.stream()
     .filter(clazz::isInstance)
     .map(clazz::cast)
     .collect(Collectors.toList());
    }

以上为动态转换的demo案例,用这个写法可以实现动态转换。

总结

本篇章介绍了Java类型转换的向上转换、向下转换、静态转换、动态转换。希望这些知识点可以对你有所帮助。