Java中的類型轉換-高級進階

java-scripts.jpg

概述

咱們知道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
複製代碼

總結一下:

  • 若是對象與變量的類型相同或者它是子類型,則引用變量能夠引用對象
  • 向上發生隱含的上行
  • 全部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 類,它只「喂」一種類型的動物 - 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類型轉換的向上轉換、向下轉換、靜態轉換、動態轉換。但願這些知識點能夠對你有所幫助。

相關文章
相關標籤/搜索