JAVA基礎之九-泛型(通用型別)

正在战斗中發表於2024-10-10

總體而言,泛型(通用型別)是一個好東西,它是一個工程上起到好處的東西,對於效能、安全等並沒有什麼幫助。

在java工程上,泛型屬於必須掌握的,理由如下:

1.各種原始碼中基本上都有泛型,無論是java基礎原始碼還是Spring或者阿帕奇的。不掌握這個,你讀不懂。你沒有方法饒過它

2.有了泛型,某種程度上會讓程式碼更清晰和簡潔

注意:本文中許多地方“泛型”和“通用型別”交叉使用,其中後者居多,二者都是表示java中的Generic Type。

通用型別,不是表示該型別通用,可以用用於任意地方,而是表示類的引數型別是不定的。

一、泛型定義

在oracle的官方文件中的描述:

A generic type is a generic class or interface that is parameterized over types

具體頁面地址:https://docs.oracle.com/javase/tutorial/java/generics/index.html

Generic有通用,一般的意思。

其實翻譯為通用型別也許更妥當一些,或者可引數化型別。

以上的一句話的意思:通用型別是允許引數化型別的類/介面

反過來的意思就是:如果一個類/介面不允許對其屬性型別進行引數化處理,那麼就不是通用的。

Java泛型是在Java 5(也稱為JDK 1.5)中首次引入的,這一版本在2004年釋出。泛型的引入是Java程式語言的一個重要里程碑

它允許程式設計師在編寫類、介面和方法時指定型別引數,使得編譯器可以在編譯時檢查型別安全,從而避免了型別轉換異常,提高了程式碼的可讀性和可維護性。
泛型的主要特性包括:
型別引數化:允許類或介面在定義時指定一個或多個型別引數,這些型別引數在例項化時會被具體的型別所替換。
型別檢查:在編譯時進行型別檢查,確保型別安全。
型別推斷:從Java 7開始,引入了菱形運算子<>,簡化了泛型例項化的語法,並且編譯器能夠自動推斷泛型型別。
泛型方法:可以在方法級別上定義泛型,而不僅僅是在類級別上。
有界型別引數:透過extends和super關鍵字對型別引數進行限制,確保型別引數是某個特定類或介面的子型別或超型別。

自Java 5引入泛型以來,Java的後續版本(如Java 6、Java 7、Java 8等)對泛型進行了進一步的完善和增強。

例如,Java 7引入了菱形運算子,簡化了泛型例項化的語法;Java 8增強了泛型的型別推斷能力。這些改進使得Java的泛型更加易用和強大。
總的來說,Java泛型的引入是Java程式語言發展的一個重要里程碑,它極大地提高了Java程式碼的型別安全性和可維護性。

以上內容是文心一言總結出來的,認真看了下,沒有什麼毛病,注意幾點:

1.重要里程碑-考慮到通用型別在java的原始碼中如此常見,可以肯定的這是名副其實

2.型別推斷、編譯器- 是的,實際幹活的主要是編譯器,不是執行時也不是編碼時候,工程師只要關注其優缺點和使用場景即可

通用型別的幾個關鍵詞/符號:T,?,<>,extends,super

其中T(也可以是任意合法的字母、詞彙,例如 GOGO,DODO,P,X,Y之類的)表示型別,表示特定型別的佔位符,在例項化物件的時候需要指定T的具體型別。

其中?是佔位符,表示任意型別(有一定限制),通常和extends/super組合使用,也可以單獨使用(多在方法程式碼區域)

它們的組合可以是:

  • <?> 表示不定型別,通常用於方法
  • <T> 表示不定型別,可用於類或者方法中
  • ? extends T ,表示T的任意子類,通常用於方法
  • ? super T,表示T的任意父類(祖類...),通常用於方法
  • T extends E,表示T是E的任意子類,可以用於方法和類
  • T super E,表示T是E的父祖類,主要用於方法
  • <T> T ,表示結果型別的強制轉換,統稱用於方法
  • T 通常用於方法引數或者表示返回型別

以上幾種都必須牢記,建議透過java自有程式碼和自己編寫例子來強化記憶。

需要特別注意的是,組合雖然多,但是還是有許多限制的。 通用型別的實現全靠JCP解釋,所以為什麼這個行,那個

不行,並沒有太多的理由,就好像為什麼有引力。但隨著java版本的迭代,通用型別有望得到更好的支援。

通用型別本質上在於:

1.限定/泛化方法的引數型別 -用在方法上,通常是限定引數型別

2.限定/泛化類成員的型別 -用在類上,通常是為了限定成員型別

二、通用型別經典例子(list)

2.1 ArrayList

以下是ArrayList的幾個典型定義,包含了類的定義,其中5個是方法上的。

1.public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable;
2.public E get(int index);
3.public ArrayList(Collection<? extends E> c);
4.public void forEachRemaining(Consumer<? super E> action);
5.public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    };
6.public boolean removeAll(Collection<?> c);

這裡都是經典的用法:限定類的特定型別/型別範圍。

有一個稍微特殊的例clone()(上文第5):

ArrayList<?> v = (ArrayList<?>) super.clone();

考慮到返回的是Object型別,v定義為任意型別都是可以的,例如 ArrayList<E> v。

那麼JCP這裡為什麼要寫成<?>而不是<E>?

2.2、其它

-- jdbcTemplate
1.public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
        return queryForObject(sql, getSingleColumnRowMapper(requiredType));
}
2.public <T> List<T> queryForList(String sql, Class<T> elementType) throws DataAccessException {
        return query(sql, getSingleColumnRowMapper(elementType));
}

-- Stream
<R, A> R collect(Collector<? super T, A, R> collector);
public final Stream<P_OUT> limit(long maxSize) {
        if (maxSize < 0)
            throw new IllegalArgumentException(Long.toString(maxSize));
        return SliceOps.makeRef(this, 0, maxSize);
    }
-- jdk.internal.vm.vector.VectorSupport

@IntrinsicCandidate

public static

<VM ,Sh extends VectorShuffle<E>, E>

VM shuffleToVector(Class<?> VM, Class<?>E , Class<?> ShuffleClass, Sh s, int length,

ShuffleToVectorOperation<VM,Sh,E> defaultImpl) {

assert isNonCapturingLambda(defaultImpl) : defaultImpl;

return defaultImpl.apply(s);

}

在java的核心程式碼中,類似VectorShuffle這樣讓人頭痛的程式碼還是不少的。

JdbcTemplate對於通用型別的使用都是相對標準和剋制的。

但是在Stream中,通用型別得到了大量的使用,某種程度上可以說,沒有通用型別,java這個Stream是編不出來的。

三、作用

注意:以下所闡述的內容都是侷限於JAVA17版本為止,不能在21等可用版本上確認同樣的情況。

3.1、主要作用

關於用途,官方文件給出的是三大點:

https://docs.oracle.com/javase/tutorial/java/generics/index.html

    Stronger type checks at compile time.
    A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. 
Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find. Elimination of casts. The following code snippet without generics requires casting: List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); When re-written to use generics, the code does not require casting: List
<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast Enabling programmers to implement generic algorithms. By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type safe and easier to read.

注:為了節省篇幅,對原文做了一些裁剪。

1.編譯時候的型別檢查

2.消除型別轉換 -也就是有些人說的型別擦除,型別替換(為實際型別)

3.編寫通用演算法

這些是經典的作用,也是最主要的作用。

3.2、侷限性

它有眾多侷限性,這些侷限性可以歸納為一點:不能任意地在類定義、方法和變數中隨意地使用通用型別相關的符號

1.不能在方法的引數中這樣定義--方法中的通用型別符號只能跟在具體的型別之後用於限定某個型別

do(? extends T xx)

do(<? extends T> xx)

換言之,方法中引數,通用型別符號只能跟在具體型別之後,T除外。

2.在例項化變數的時候,使用通用型別語法,可能不會編譯錯誤,但是可能會產生其它錯誤

List<? extends Cup> bookList=new ArrayList<>();

這樣雖然不會報錯,但是add方法無法使用,因為編譯器無法確認 add方法例項的型別。

編譯器老是提示add的引數必須是 ? extends Cup型別,但是即使你用一個Cup的子孫(WaterCup是Cup的兒子)也無法達成目的。

3.其他

例如如下示例是現階段不支援的:

public class blade<T>{
    public void split( ? extends T){
    }
}

split的方法希望只支援特定的T子類,但是現階段不允許這麼寫。


四、泛型的編譯時和執行時

4.1保證編譯時候不會出現問題

當我們完成一個通用型別後,就需要使用它。所謂的使用,就是在某個程式碼段中定義一個類的例項,或者使用例項方法(或者類的靜態方法)

4.1.1定義通用型別的變數

工程師通常必須在變數中確定變數的引數型別,上文的型別是String。

以ArrayList為例子:

List<String> list=new ArrayList<>();

但是,在極少數的情況下,引數型別可以使用萬用字元,例如ArrayList.clone方法就是。

如果使用萬用字元,那麼限制有很多,具體自行體會。

4.1.2使用通用型別的方法

為了便於說明,舉例:

例1.public <T> T doThing(xxx)

例2.publicT doSomeThing(xxx)

這二者主要區別在於:

a.例1中<T> 會把返回值強制轉為變數型別,這個<T>指的是變數的型別

如果不小心使用,則常常會發生編譯不報錯,但是執行時報錯的情況(通常是形如"...cannot be cast to class....")

b.而例2則相反,它會要求變數必須是T型別的

如果不是T型別則會在編譯時候報錯。

4.2編譯後的程式碼

如前,我們知道如何在編譯的時候,避免通用型別的編譯錯誤。

考慮到通用型別是為了支援各種型別,那麼當一個通用型別被例項化後,其例項執行的程式碼其實還是類的程式碼,那麼又如何保證能夠使用正確的型別?

猜想:

a.在編譯後的程式碼中,jcp在類程式碼中插入變數p用於儲存外部傳遞進來的變數v的實際型別T,並且在執行時,把V==T,之後再進行型別轉換

b.在編譯後的變數中,型別被定義為Object,然後在使用的時候再進行強制轉換

為了驗證這個想法,寫了一個最簡單的例子:

public class GenericSimpleTest<T> {
    T value;
    public GenericSimpleTest(T value) {
        this.value = value;

    }
    public T getValue() {
        return value;
    }

    public static void main(String[] args) {
        GenericSimpleTest<String> test = new GenericSimpleTest<>("Hello World");
        System.out.println(test.getValue());
    }
}

使用javap檢視

generictype>javap -l -c GenericSimpleTest.class

也可以使用Asm Bytecode viewer、Jadx或者 Bytecode viewer(eclipse),前二者是在idea中。雖然jcp不夠方便,但是不用額外佔用ide的資源,也不用額外安裝,使用的頻率也極低。

當然這些ide外掛的優點是方便。

先說結論:就測試類GenericSimpleTest而言,是基於第二中方案實現(內部用Object儲存,使用的時候進行強制轉換)

注意看下上圖示出的5點,以下逐一解釋:

1.建構函式賦值,對應this.value=value,可以看到是Object型別

2.本地變數表中有一個變數指向類的變數value,型別是Object

3.方法getValue取回的值是Object型別

4.靜態測試主類main中,在列印前會先檢驗getValue的的值是否為String型別,並嘗試進行轉換

5.main呼叫println列印字串

指令checkcast的意思:嘗試進行轉換,否則返回null。

關於位元組碼中的指令,有幾個可以參考的地址:

https://www.cnblogs.com/tsvico/p/12708417.html

https://www.cnblogs.com/chanshuyi/p/jvm_serial_05_jvm_bytecode_analysis.html (此人的jvm系列寫得不錯

https://blog.csdn.net/feiqipengcheng/article/details/109958051

常見的指令還是需要背誦下,否則看jcp比較吃力,好訊息是指令的名稱還是比較通俗易懂,哪怕不看專業參考也大概猜測出來。

位元組碼指令,如果只是做一些業務開發(非工具開發),通常沒有必要精通,絕大部分工程師只要讀原始碼的時候,知道指令的大概意思即可。

不過以我本人的經驗而言,作為一個高階工程師還是有必要知道這些,至少理解java原理和閱讀原始碼不會那麼吃力。

五、我的例子

限於時間,自己直接編寫一個例子,而不是檢視有關程式的原始碼。

以下原始碼定義了一個ArrayList的子類,主要演示了定義方法的第三種情況:當使用了不在類上定義的泛型

public class GTypeTest2 {
    public static class MyArrayList<T> extends ArrayList<T> {
        public MyArrayList clone() {
            MyArrayList<T> list = (MyArrayList<T>) super.clone();
            return list;
        }

        @Override
        public T get(int index) {
            return super.get(index);
        }

        public <T> T getT(int index) {
            return (T) this.get(index);
        }

        public void addHuman(Human c) {
            add((T) c);
        }

        /**
         * 新增一個泛型方法,返回值是k. k不是在類上定義的泛型,所以必須在方法名前使用<K>
         * 這樣可以表示它是一個泛型,否則會產生編譯錯誤
         */
        public <K> K  addSp(K k) {
            add((T) k);
            return k;
        }

    }

    public static void main(String[] args) {
        MyArrayList<Object> list = new MyArrayList<>();
        list.add("hello");
        Human h = new Human("lzf");
        list.addHuman(h);
        //演示 “T” ,“<T> T”的截然想法的效果.
        //前者作用與編譯時;後者作用於執行時

        //<T> T 測試-方法getT會在執行時試圖把結果轉為Human型別
        Human h2 = list.getT(1);
        System.out.println(h2);
        //這個是必然報錯的測試
        try{
            House h3 = list.getT(1);
            System.out.println(h3);
        }
        catch (Exception e){
            System.out.println("異常類名稱:"+e.getClass().getName());
            System.out.println("異常類資訊:"+e.getMessage());
        }
        //T 測試
        Object o = list.get(0);
        System.out.println(o);

        //<K> K 測試  -- 注意k不是在類中定義的泛型,所以必須在方法名前使用<K>
        Woman w=new Woman();
        w.setName("mgy");
        
        Woman g=list.addSp(w);
        System.out.println(g.getName());
    }
}

特別注意下上文中紅色字型部分。

執行main後,輸出如下:

Human [gender=lzf, birthDay=null, power=null]
異常類名稱:java.lang.ClassCastException
異常類資訊:class study.base.oop.classes.Human cannot be cast to class study.base.oop.classes.House (study.base.oop.classes.Human and study.base.oop.classes.House are in unnamed module of loader 'app')
hello
mgy

六、小結

a.通用型別屬於java工程師必須掌握的,如果希望寫複雜一些內容。此外也有助於閱讀原始碼

b.通用型別不是萬能的,定義的時候存在各種限制。一些限制也許會在將來的版本中消除掉,規則是人定的,只要願意,可以隨心而定。

現在不支援的,將來就可能支援;反之,現在支援的,將來可以過時了。 如何解釋,全看JCP的意思。

c.萬用字元?和T是不一樣的,前者基本需要結合extends ,super使用,也可以單獨使用,但是隻能用於方法,典型的可以參考ArrayList.clone

d.通用型別可以出現在類、方法、方法體中。在做型別強轉的時候,目標型別可以帶通用型別符號。

e.如果一個表示萬用字元的符號K,在類中沒有定義,但是希望在方法中使用,則必須在方法名前新增<K>,否則編譯器不會當做不知道的型別,從而引發編譯錯誤

f.如果想透徹理解泛型,有必要掌握一些javap和位元組碼的知識

相關文章