Java中的類型轉(zhuǎn)換-高級進階

java-scripts.jpg

概述

我們知道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)換。希望這些知識點可以對你有所幫助。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容