概述
我们知道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 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);
}
动态转换
在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类型转换的向上转换、向下转换、静态转换、动态转换。希望这些知识点可以对你有所帮助。