
概述
我們知道Java類型系統(tǒng)由兩種類型組成:基礎(chǔ)類型和封裝類型。
向上轉(zhuǎn)型
從子類到超類的轉(zhuǎn)換稱為向上轉(zhuǎn)型。通常,向上是由編譯器隱式執(zhí)行的。
向上轉(zhuǎn)型與繼承密切相關(guān) - 這是Java中的另一個核心概念。使用引用變量來引用更具體的類型是很常見的。每次我們這樣做時,都會發(fā)生隱式的向上轉(zhuǎn)型。
我們定義一個Animal類:
public class Animal {
public void eat() {
// ...
}
}
現(xiàn)在我們來擴展Animal:
public class Cat extends Animal {
public void eat() {
// ...
}
public void meow() {
// ...
}
}
現(xiàn)在,我們可以創(chuàng)建一個對象Cat類,并把它分配給類型的引用變量cat:
Cat cat = new Cat();
我們還可以將它分配給Animal類型的引用變量:
Animal animal = cat;
在上面的分配中,發(fā)生了隱式的向上轉(zhuǎn)換。我們可以明確地做到:
animal = (Animal) cat;
但是沒有必要顯式地繼承繼承樹。編譯器知道cat是Animal并且不顯示任何錯誤。
注意,該引用可以引用聲明類型的任何子類型。
使用向上轉(zhuǎn)型,我們限制了Cat實例可用的方法數(shù)量,但沒有更改實例本身?,F(xiàn)在我們不能做任何特定于Cat的事情-我們不能在animal變量上調(diào)用meow()。
雖然Cat對象仍然是Cat對象,但調(diào)用meow()會導(dǎo)致編譯器錯誤:
// animal.meow(); The method meow() is undefined for the type Animal
要調(diào)用meow(),我們需要向下轉(zhuǎn)型animal,我們稍后會這樣做。
但現(xiàn)在我們將描述是什么讓我們向上轉(zhuǎn)型,我們可以利用多態(tài)性。
多態(tài)性
讓我們定義Animal的另一個子類,一個Dog類:
public class Dog extends Animal {
public void eat() {
// ...
}
}
現(xiàn)在我們可以定義feed()方法來處理像動物一樣的所有貓狗:
public class AnimalFeeder {
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
});
}
}
我們不希望AnimalFeeder關(guān)注列表中的哪種動物 - 貓或狗。在feed()方法中,它們都是動物。
當(dāng)我們將特定類型的對象添加到動物列表時,會發(fā)生隱式向上轉(zhuǎn)型:
List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
new AnimalFeeder().feed(animals);
我們添加了貓和狗,它們被隱含地轉(zhuǎn)向了Animal類型。每只貓都是動物,每只狗都是動物。他們是多態(tài)的。
順便說一句,所有Java對象都是多態(tài)的,因為每個對象至少是一個Object。我們可以將一個Animal實例分配給Object類型的引用變量,編譯器不會報錯:
Object object = new Animal();
這就是為什么我們創(chuàng)建的所有Java對象都已經(jīng)具有Object特定的方法,例如toString()。
向上轉(zhuǎn)型到接口也很常見。
我們可以創(chuàng)建Mew接口并讓Cat實現(xiàn)它:
public interface Mew {
public void meow();
}
public class Cat extends Animal implements Mew {
public void eat() {
// ...
}
public void meow() {
// ...
}
}
現(xiàn)在任何Cat對象也可以向上轉(zhuǎn)換為Mew:
Mew mew = new Cat();
Cat是Mew,向上轉(zhuǎn)型是合法的并且是隱含的。
因此,Cat是Mew,Animal,Object和Cat。在我們的示例中,它可以分配給所有四種類型的引用變量。
重寫
在上面的示例中,覆蓋了eat()方法。這意味著盡管在Animal類型的變量上調(diào)用了eat(),但是工作是通過在真實對象上調(diào)用的方法完成的 - Cat和Dog:
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
});
}
如果我們在我們的類中添加一些日志記錄,我們會看到Cat和Dog的方法被調(diào)用:
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
總結(jié)一下:
- 如果對象與變量的類型相同或者它是子類型,則引用變量可以引用對象
- 向上發(fā)生隱含的上行
- 所有Java對象都是多態(tài)的,并且由于向上轉(zhuǎn)型可以被視為超類型的對象
向下轉(zhuǎn)型
如果我們想使用Animal類型的變量來調(diào)用僅適用于Cat類的方法,該怎么辦?這是一個向下轉(zhuǎn)型。它是從超類到子類的轉(zhuǎn)換。
我們來舉個例子:
Animal animal = new Cat();
我們知道動物變量是指Cat的實例。我們想在動物身上調(diào)用Cat的meow()方法。但編譯器提示類型為Animal的meow()方法不存在。
應(yīng)該將Animal轉(zhuǎn)向Cat:
((Cat) animal).meow();
內(nèi)括號和它們包含的類型有時稱為強制轉(zhuǎn)換運算符。請注意,編譯代碼也需要外部括號。
讓我們用meow()方法重寫之前的AnimalFeeder示例:
public class AnimalFeeder {
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
if (animal instanceof Cat) {
((Cat) animal).meow();
}
});
}
}
現(xiàn)在我們可以訪問Cat類可用的所有方法。查看日志以確保實際調(diào)用了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
請注意,在上面的示例中,我們嘗試僅向下轉(zhuǎn)換那些實際上是Cat實例的對象。為此,我們使用運算符instanceof。
instanceof操作
我們經(jīng)常在向下轉(zhuǎn)換之前使用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無法強制轉(zhuǎn)換為com.william.casting.Cat
這意味著我們正在嘗試將作為Dog實例的對象轉(zhuǎn)換為Cat實例。
如果我們向下轉(zhuǎn)型的類型與真實對象的類型不匹配,則ClassCastException總是在運行時拋出。
注意,如果我們嘗試向下轉(zhuǎn)型為不相關(guān)的類型,編譯器將不允許這樣
Animal animal;
String s = (String) animal;
編譯器說“無法從Animal轉(zhuǎn)換為String”。
對于要編譯的代碼,兩種類型都應(yīng)該在同一繼承樹中。
我們總結(jié)一下:
- 為了獲得特定于子類的成員的訪問權(quán),必須進行向下轉(zhuǎn)換
- 使用強制轉(zhuǎn)換運算符完成向下轉(zhuǎn)換
- 要安全地向下轉(zhuǎn)換對象,我們需要instanceof運算符
- 如果真實對象與我們向下轉(zhuǎn)換的類型不匹配,則將在運行時拋出ClassCastException
Cast()方法
還有另一種使用Class方法強制轉(zhuǎn)換對象的方法:
public void test() {
Animal animal = new Cat();
if (Cat.class.isInstance(animal)) {
Cat cat = Cat.class.cast(animal);
cat.meow();
}
}
在上面的示例中,使用了cast()和isInstance()方法,而不是相應(yīng)的cast和instanceof運算符。
通常使用具有泛型類型的cast()和isInstance()方法。
讓我們用feed()方法創(chuàng)建AnimalFeederGeneric <T>類,它只“喂”一種類型的動物 - Cat或Dog,取決于類型參數(shù)的值:
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實例也應(yīng)該傳遞給泛型類,因為我們無法從類型參數(shù)T中獲取它。在我們的示例中,我們在構(gòu)造函數(shù)中傳遞它。
讓我們使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);
}
動態(tài)轉(zhuǎn)換
在Java 5之前,以下代碼將是常態(tài):
List dates = new ArrayList();
dates.add(new Date());
Object object = dates.get(0);
Date date = (Date) object;
需要轉(zhuǎn)換。雖然運行時類型是Date,但編譯器無法知道它。
使用泛型,可以重寫上面的代碼:
List<Date> dates = new ArrayList<>();
dates.add(new Date());
Date date = dates.get(0);
沒有轉(zhuǎn)換:由于泛型,編譯器有足夠的信息。
強轉(zhuǎn)換
一個這樣的用例是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;
** 靜態(tài)轉(zhuǎn)換**
使用Java進行強制轉(zhuǎn)換的最常用方法如下:
Object obj; // may be an integer
if (obj instanceof Integer) {
Integer objAsInt = (Integer) obj;
// do something with 'objAsInt'
}
這使用了 instanceof和cast運算符。實例轉(zhuǎn)換的類型(在本例中為 Integer)必須在編譯時靜態(tài)知道,所以讓我們調(diào)用這個靜態(tài)轉(zhuǎn)換。
如果 obj不是 Integer,則上述測試將失敗。如果我們試圖拋出它,我們會得到一個 ClassCastException。如果 obj為 null,則它會使instanceof測試失敗 但可以被強制轉(zhuǎn)換,因為 null可以是任何類型的引用。
動態(tài)轉(zhuǎn)換
最初可用的唯一轉(zhuǎn)換形式是靜態(tài)轉(zhuǎn)換。這意味著需要在編譯時知道轉(zhuǎn)換類型。但是,讓我們設(shè)想一個接受a的方法Stream<Object>,過濾特定類型的所有元素,并以正確的類型返回這些元素。這是用法的一個例子:
我遇到的一種技術(shù)不常使用Class上與運算符對應(yīng)的方法 :
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'
}
因為類型在編譯類型是未知的,我們將稱之為動態(tài)轉(zhuǎn)換。
對于錯誤類型和空引用的實例,測試和強制轉(zhuǎn)換的結(jié)果與靜態(tài)強制轉(zhuǎn)換的結(jié)果完全相同。
現(xiàn)在
轉(zhuǎn)換Optional或Stream元素的值是一個兩步過程:首先我們必須過濾掉錯誤類型的實例,然后我們可以轉(zhuǎn)換為所需的類型。
使用Class上的方法 ,我們使用方法引用來完成此操作。使用Optional的示例 :
Optional<?> obj; // may contain an Integer
Optional<Integer> objAsInt = obj
.filter(Integer.class::isInstance)
.map(Integer.class::cast);
通過上面的寫法,我們可以實現(xiàn)動態(tài)轉(zhuǎn)換。
再舉一個案例
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());
}
以上為動態(tài)轉(zhuǎn)換的demo案例,用這個寫法可以實現(xiàn)動態(tài)轉(zhuǎn)換。
總結(jié)
本篇章介紹了Java類型轉(zhuǎn)換的向上轉(zhuǎn)換、向下轉(zhuǎn)換、靜態(tài)轉(zhuǎn)換、動態(tài)轉(zhuǎn)換。希望這些知識點可以對你有所幫助。