Java 基礎 - 並不神奇的泛型

前言

前陣子給公司新人培訓Java 基礎相關的一些點,系統整理了一下泛型相關的知識點。特來分享一下。但願能讓一些對泛型不熟悉的同窗徹底掌握Java 泛型的相關知識點。java

開始以前,先給你們來一道測試題。程序員

List<String> strList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();
        
System.out.println(strList.getClass() == integerList.getClass());

請問,上面代碼最終結果輸出的是什麼?熟悉泛型的同窗應該可以答出來,而對泛型有所瞭解,可是瞭解不深刻的同窗可能會答錯。編程

content

  • 泛型概述數組

    • why 泛型
    • 泛型的做用
  • 泛型的定義和使用安全

    • 泛型類
    • 泛型方法
    • 泛型接口
  • 通配符 ?app

    • 無界通配符
    • 上限通配符
    • 下限通配符
  • 類型擦除

帶着問題

  1. Java中的泛型是什麼 ? 使用泛型的好處是什麼?
  2. 什麼是泛型中的限定通配符和無界通配符 ?
  3. 你能夠把List<String>傳遞給一個接受List<Object>參數的方法嗎?
  4. Java的泛型是如何工做的 ? 什麼是類型擦除 ?

1、泛型概述

最先的「泛型編程」的概念起源於C++的模板類(Template),Java 借鑑了這種模板理念,只是二者的實現方式不一樣。C++ 會根據模板類生成不一樣的類,Java 使用的是類型擦除的方式。

1.1 why 泛型?

Java1.5 發行版本中增長了泛型(Generic)。jvm

有不少緣由促成了泛型的出現,而最引人注意的一個緣由,就是爲了建立容器類。

-- 《Java 編程思想》ide

容器就是要存放要使用的對象的地方。數組也是如此,只是相比較的話,容器類更加的靈活,具備更多的功能。全部的程序,在運行的時候都要求你持有一大堆的對象,因此容器類算得上最須要具備重用性的類庫之一了。測試

看下面這個例子,ui

public class AutoMobile {
}

/**
 * 重用性很差的容器類
 */
public class Holder1 {

    private AutoMobile a;

    public Holder1(AutoMobile a) {
        this.a = a;
    }
    //~~
}

/**
 * 想要在java5 以前實現可重用性的容器類
 * @author Richard_yyf
 * @version 1.0 2019/8/29
 */
public class Holder2 {

    private Object a;

    public Holder2(Object a) {
        this.a = a;
    }

    public Object getA() {
        return a;
    }

    public void setA(Object a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Holder2 h2 = new Holder2(new AutoMobile());
        AutoMobile a = (AutoMobile) h2.getA();
        h2.setA("Not an AutoMobile");
        String s = (String) h2.getA();
        h2.setA(1);
        Integer x = (Integer) h2.getA();
    }
}



/**
 * 經過泛型來實現可重用性
 * 泛型的主要目的是指定容器要持有什麼類型的對象
 * 並且由編譯器來保證類型的正確性
 *
 * @author Richard_yyf
 * @version 1.0 2019/8/29
 */
public class Holder3WithGeneric<T> {

    private T a;

    public Holder3WithGeneric(T a) {
        this.a = a;
    }

    public T getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Holder3WithGeneric<AutoMobile> h3 = new Holder3WithGeneric<>(new AutoMobile());
        // No class cast needed
        AutoMobile a = h3.getA();
    }
}

經過上述對比,咱們應該能夠理解類型參數化具體是什麼個意思。

在沒有泛型以前,從集合中讀取到的每個對象都須要進行轉換。若是有人不當心插入了類型錯誤的對象,在運行時的轉換處理就會出錯。這顯然是不可忍受的。

泛型的出現,給Java帶來了不同的編程體驗。

1.2 泛型的做用

  1. 參數化類型。與普通的 Object 代替一切類型這樣簡單粗暴而言,泛型使得數據的類別能夠像參數同樣由外部傳遞進來。它提供了一種擴展能力。它更符合面向抽象開發的軟件編程宗旨。
  2. 類型檢測。當具體的類型肯定後,泛型又提供了一種類型檢測的機制,只有相匹配的數據才能正常的賦值,不然編譯器就不經過。因此說,它是一種類型安全檢測機制,必定程度上提升了軟件的安全性防止出現低級的失誤。
  3. 提升代碼可讀性。沒必要要等到運行的時候纔去強制轉換,在定義或者實例化階段,由於 Holder<AutoMobile>這個類型顯化的效果,程序員可以一目瞭然猜想出這個容器類持有的數據類型。
  4. 代碼重用。泛型合併了同類型對象的處理代碼,使得代碼重用度變高。

2、泛型的定義和使用

泛型按照使用狀況能夠分爲 3 種。

  1. 泛型類
  2. 泛型方法
  3. 泛型接口

2.1 泛型類

  • 概述:把泛型定義在類上
  • 定義格式:

    public class 類名 <泛型類型1,...> {
        ...
    }
  • 注意事項:泛型類型必須是引用類型(非基本數據類型)

類型參數 規範(約定俗稱)

尖括號 <>中的 字母 被稱做是類型參數,用於指代任何類型。咱們常看到<T> 的寫法,事實上,T 只是一種習慣性寫法,若是你願意。你能夠這樣寫。

public class Test<Hello> {
    Hello field1;
}

但出於規範和可讀性的目的,Java 仍是建議咱們用單個大寫字母來表明類型參數。常見的如:

  • T 表明通常的任何類。
  • E 表明 Element 的意思,或者 Exception 異常的意思。
  • K 表明 Key 的意思。
  • V 表明 Value 的意思,一般與 K 一塊兒配合使用。
  • S 表明 Subtype 的意思

2.2 泛型方法

  • 概述:把泛型定義在方法上
  • 定義格式:

    public <泛型類型> 返回類型 方法名(泛型類型 變量名) {
        ...
    }
  • 注意事項:

    • 這裏的<T> 中的T被稱爲類型參數,而方法中的 T 被稱爲參數化類型,它不是運行時真正的參數。
    • 方法聲明中定義的形參只能在該方法裏使用,而接口、類聲明中定義的類型形參則能夠在整個接口、類中使用

泛型類和泛型方法共存的現象

/**
 * 泛型類與泛型方法的共存現象
 * @author Richard_yyf
 * @version 1.0 2019/8/29
 */
public class GenericDemo2<T> {

    public  void testMethod(T t){
        System.out.println(t.getClass().getName());
    }
    public  <T> T testMethod1(T t){
        return t;
    }

    public static void main(String[] args) {
        GenericDemo2<String> t = new GenericDemo2<>();
        t.testMethod("generic");
        Integer integer = 1;
        Integer i = t.testMethod1(integer);

    }
}

泛型方法始終以本身定義的類型參數爲準

固然,現實場景下千萬不要去做死寫出這麼難以閱讀的代碼。

2.3 泛型接口

泛型接口和泛型類差很少。

  • 泛型接口概述:把泛型定義在接口
  • 定義格式:

    public interface 接口名<泛型類型> {
        ...
    }

Demo

public interface GenericInterface<T> {

    void show(T t);
}

public class GenericInterfaceImpl<String> implements GenericInterface<String>{

    @Override
    public void show(String o) {

    }
}

3、 通配符 ?

除了用 <T>表示泛型外,還有 <?>這種形式。 被稱爲通配符。

爲何要引進這個概念呢?先來看下下面的Demo.

public class GenericDemo2 {

    class Base{}

    class Sub extends Base{}

    public void test() {
        // 繼承關係
        Sub sub = new Sub();
        Base base = sub;
        List<Sub> lsub = new ArrayList<>();
        // 編譯器是不會讓下面這行代碼經過的,
        // 由於 Sub 是 Base 的子類,不表明 List<Sub>和 List<Base>有繼承關係。
        List<Base> lbase = lsub;
    }
}

在現實編碼中,確實有這樣的需求,但願泛型可以處理某一範圍內的數據類型,好比某個類和它的子類,對此 Java 引入了通配符這個概念。

因此,通配符的出現是爲了指定泛型中的類型範圍

通配符有 3 種形式。

  1. <?>被稱做無限定的通配符
  2. <? extends T>被稱做有上限的通配符
  3. <? super T>被稱做有下限的通配符

3.1 無界通配符 <?>

無限定通配符常常與容器類配合使用,它其中的 ? 其實表明的是未知類型,因此涉及到 ? 時的操做,必定與具體類型無關。

// Collection.java
public interface Collection<E> extends Iterable<E> {
   
    boolean add(E e);
}

public class GenericDemo3 {
    /**
     * 測試 無限定通配符 <?>
     * @param collection c
     */
    public void testUnBoundedGeneric(Collection<?> collection) {
        collection.add(123);
        collection.add("123");
        collection.add(new Object());

        // 你只能調用 Collection 中與類型無關的方法
        collection.iterator().next();
        collection.size();
    }
}

無需關注 Collection 中的真實類型,由於它是未知的。因此,你只能調用 Collection 中與類型無關的方法。

有同窗可能會想,<?>既然做用這麼眇小,那麼爲何還要引用它呢? 

我的認爲,提升了代碼的可讀性,程序員看到這段代碼時,就可以迅速對此創建極簡潔的印象,可以快速推斷源碼做者的意圖。

(用的不多,可是要理解)

爲了接下去的說明方便,先定義一下幾個類。

class Food {}

    class Fruit extends Food {}

    class Apple extends Fruit {}

    class Banana extends Fruit {}

    // 容器類
    class Plate<T> {
        private T item;

        public Plate(T item) {
            this.item = item;
        }

        public T getItem() {
            return item;
        }

        public void setItem(T item) {
            this.item = item;
        }
    }

3.2 上限 通配符 <? extends T>

<?>表明着類型未知,可是咱們的確須要對於類型的描述再精確一點,咱們但願在一個範圍內肯定類別,好比類型 T 及 類型 T 的子類均可以放入這個容器中。

什麼是上界

在這個體系中,上限通配符 Plate<? extends Fruit> 覆蓋下圖中藍色的區域。

img

反作用

邊界讓Java不一樣泛型之間的轉換更容易了。但不要忘記,這樣的轉換也有必定的反作用。那就是容器的部分功能可能失效。

public void testUpperBoundedBoundedGeneric() {
       Plate<? extends Fruit> p = new Plate<>(new Apple());

       // 不能存入任何元素
        p.setItem(new Fruit()); // error
        p.setItem(new Apple()); // error

        // 讀出來的元素須要是 Fruit或者Fruit的基類
        Fruit fruit = p.getItem();
        Food food = p.getItem();
//        Apple apple = p.getItem();
    }

<? extends Fruit>會使往盤子裏放東西的set( )方法失效。但取東西get( )方法還有效。好比下面例子裏兩個set()方法,插入Apple和Fruit都報錯。

緣由是編譯器只知道容器內是Fruit或者它的派生類,但具體是什麼類型不知道。多是Fruit?多是Apple?也多是Banana,RedApple,GreenApple?

若是你須要一個只讀容器,用它來produce T,那麼使用<? extends T> 。

3.3 下限通配符 <? super T>

相對應的,還有下限通配符 <? super T>

什麼是下界

對應剛纔那個例子,Plate<? super Fruit>覆蓋下圖中紅色的區域。

img

反作用

public void testLowerBoundedBoundedGeneric() {
//        Plate<? super Fruit> p = new Plate<>(new Food());
        Plate<? super Fruit> p = new Plate<>(new Fruit());

        // 存入元素正常
        p.setItem(new Fruit());
        p.setItem(new Apple());

        // 讀取出來的東西,只能放在Object中
        Apple apple = p.getItem(); // error
        Object o = p.getItem();
    }

由於下界規定了元素的最小粒度的下限,其實是放鬆了容器元素的類型控制。既然元素是Fruit的基類,往裏面存比Fruit粒度小的類均可以。可是往外讀取的話就費勁了,只有全部類的基類Object能夠裝下。但這樣一來元素類型信息就都丟失了。

3.4 PECS 原則

PECS - Producer Extends Consumer Super

  • 「Producer Extends」 – 若是你須要一個只讀容器,用它來produce T,那麼使用<? extends T> 。
  • 「Consumer Super」 – 若是你須要一個只寫容器,用它來consume T,那麼使用<? super T>。
  • 若是須要同時讀取以及寫入,那麼咱們就不能使用通配符了。

4、 類型擦除

泛型是 Java 1.5 版本才引進的概念,在這以前是沒有泛型的概念的,但顯然,泛型代碼可以很好地和以前版本的代碼很好地兼容。

這是由於,泛型信息只存在於代碼編譯階段,在進入 JVM 以前,與泛型相關的信息會被擦除掉

專業術語叫作 類型擦除

List<String> strList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();
        
System.out.println(strList.getClass() == integerList.getClass());
==== output =====
true
=================

打印的結果爲 true 是由於 List<String>List<Integer>在 jvm 中的 Class 都是 List.class。

泛型信息被擦除了。

/**
 * 類型擦除 相關類
 *
 * @author Richard_yyf
 * @version 1.0 2019/8/29
 */
public class EraseHolder<T> {

    T data;

    public EraseHolder(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
        EraseHolder<String> holder = new EraseHolder<>("hello");
        Class clazz = holder.getClass();
        System.out.println("erasure class is:" + clazz.getName());

        Field[] fs = clazz.getDeclaredFields();
        for ( Field f:fs) {
            // 那咱們可不能夠說,泛型類被類型擦除後,相應的類型就被替換成 Object 類型呢?
            System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
        }

        EraseHolder2<String> holder2 = new EraseHolder2<>("hello");
        clazz = holder2.getClass();
        fs = clazz.getDeclaredFields();
        for ( Field f:fs) {
            System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
        }
    }

    static class EraseHolder2<T extends String> {
        T data;

        public EraseHolder2(T data) {
            this.data = data;
        }
    }
}

侷限性

利用類型擦除的原理,用反射的手段就繞過了正常開發中編譯器不容許的操做限制。

public class EraseReflectDemo {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(23);
        // can't add here
        // 由於泛型的限制 boolean add(E e);
        list.add("123"); // error

        // 利用反射能夠繞過編譯器去調用add方法
        // 又由於類型擦除時 boolean add(E e); 等同於 boolean add(Object e);

        try {
            Method method = list.getClass().getDeclaredMethod("add", Object.class);

            method.invoke(list, "test");
            method.invoke(list, 42.9f);
        } catch (Exception e) {
            e.printStackTrace();
        }

        for (Object o : list) {
            System.out.println(o);
        }


    }
}
==== output =====
23
test
42.9
=================
相關文章
相關標籤/搜索