咱們知道Java類型系統由兩種類型組成:基礎類型和封裝類型。java
從子類到超類的轉換稱爲向上轉型。一般,向上是由編譯器隱式執行的。安全
向上轉型與繼承密切相關 - 這是Java中的另外一個核心概念。使用引用變量來引用更具體的類型是很常見的。每次咱們這樣作時,都會發生隱式的向上轉型。bash
咱們定義一個Animal類:函數
public class Animal {
public void eat() {
// ...
}
}
複製代碼
如今咱們來擴展Animal:測試
public class Cat extends Animal {
public void eat() {
// ...
}
public void meow() {
// ...
}
}
複製代碼
如今,咱們能夠建立一個對象Cat類,並把它分配給類型的引用變量cat:ui
Cat cat = new Cat();
this
咱們還能夠將它分配給Animal類型的引用變量:spa
Animal animal = cat;
日誌
在上面的分配中,發生了隱式的向上轉換。咱們能夠明確地作到:code
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
複製代碼
總結一下:
若是咱們想使用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運算符來檢查對象是否屬於特定類型:
if (animal instanceof Cat) {
((Cat) animal).meow();
}
複製代碼
若是咱們沒有使用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」。
對於要編譯的代碼,兩種類型都應該在同一繼承樹中。
咱們總結一下:
還有另外一種使用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 類,它只「喂」一種類型的動物 - 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,過濾特定類型的全部元素,並以正確的類型返回這些元素。這是用法的一個例子:
我遇到的一種技術不常使用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類型轉換的向上轉換、向下轉換、靜態轉換、動態轉換。但願這些知識點能夠對你有所幫助。