最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java 泛型 Generic機制實例詳解

 更新時間:2022年11月08日 16:36:17   作者:Airsaid  
這篇文章主要為大家介紹了Java 泛型 Generic機制實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

泛型是什么?

使用泛型可以指定類型變量,從而讓代碼可以對不同類型的對象進行重用。以及,還可以讓編譯器更好的了解類型,從而避免強制類型轉換,提升代碼的安全性。

類型變量就是尖括號 <>中的變量,類型變量的命名規(guī)范是使用大寫字母,例如 E 表示元素類型,K、V 分別表示鍵和值類型,T 和相鄰的 U、S 表示任意類型。當然你也可以起其他的名字,編譯器對此并沒有強制限制,但是還是按照規(guī)范來。

泛型類

泛型類(generic class)就是有一個或多個類型變量的類。例如下面的 Pair 類,就有兩個類型變量,分別是 T 和 U:

public class Pair<T, U> {
  public T first;
  public U second;
  public Pair(T first, U second) {
    this.first = first;
    this.second = second;
  }
}

在使用時,就可以傳入任意的類型。例如:

Pair<Integer, String> pair = new Pair<>(1, "A");

思考一下,如果沒有泛型的存在,那么當我們要操作不同類型的對象時,要么為每種類型都創(chuàng)建一個類,但是這樣就會存在大量的重復代碼,不容易維護。要么就直接使用對象的頂級父類 Object 類,但是這樣的話在使用時就需要進行強制轉換,存在安全問題,即使每次強轉前進行判斷,也會存在重復代碼和遺漏的風險。

說回泛型類,泛型類的類型變量是定義在類名后面,在整個類中都可以使用定義的類型變量。所以在選擇使用泛型類時,泛型變量應該是和類所關聯(lián)的。如果僅與方法關聯(lián),那么可以使用泛型方法。

泛型方法

泛型方法的類型參數是定義在方法返回類型的前面的,只在當前方法中可以使用。例如:

public static <T> T getMiddle(T... a) {
  return a[a.length / 2];
}

泛型方法可以在普通類中定義,也可以在泛型類中定義。如果泛型變量僅在方法內會用到,就可以考慮使用泛型方法。

類型變量的限制

默認情況下,類型變量可以是任何類型。但是有時候,我們需要限制類型變量的類型。此時可以通過 extends關鍵字來限制:

  • <T extends UpperBoundType> 限制泛型類型為特定類型或者特定類型的子類

例如,假設我們有一個 Printer 類,我們只需要這個類打印動物的信息,那么就可以使用 <T extends Animal> 來限制只能接收 Animal 或其實現(xiàn)類:

public interface Animal {
  String getName();
}
public static class Printer<T extends Animal> {
  public void print(T t) {
    System.out.println(t.getName());
  }
}
public static class Dog implements Animal {
  @Override
  public String getName() {
    return "Woof";
  }
}
public static class Cat implements Animal {
  @Override
  public String getName() {
    return "Meow";
  }
}
public static void main(String[] args) {
  Printer<Animal> animalPrinter = new Printer<>();
  animalPrinter.print(new Dog());
  animalPrinter.print(new Cat());
  animalPrinter.print(new People("Abc")); // 如果試圖傳入一個其他類型,則會編譯失敗
}

一個類型變量,可以指定多個限制類型。例如:

public class Printer<T extends Dog & Animal> {
  ...
}

上面的限制的意思是,類型變量必須是 Dog 或其子類,并且實現(xiàn)了 Animal 接口。需要注意的是,在指定多個限制的類型時,除了第一個限制類型可以是類以外,其他的限制類型必須是接口類型。這是因為 Java 是不支持多重繼承的。

類型擦除

由于泛型是在 Java 1.5 才推出的,所以 Java 為了保證在之前版本上的兼容性,泛型實際上在 JVM 中是沒有的!

也就是說,編譯器在編譯時會擦除掉泛型的參數類型,并提供一個相應的原始類型(raw type),這個原始類型的名字就是去掉類型參數后的泛型類型名。類型變量會被擦除(erased),并替換為其限定類型,沒有限定類型則替換為 Object 類型。

舉個例子,如果我們定義了一個泛型類:

public class Printer<T> {
  ...
}

那么當類型擦除后,實際上就變?yōu)榱?Printer<Object> 類型。

如果指定了限定類型,例如:

public class Printer<T extends Animal> {
  ...
}

那么擦除后,就變?yōu)榱?Printer<Animal> 類型。

如果指定了多個限定的類型,那么擦除后就會是第一個限定的類型。例如:

public class Printer<T extends Dog & Animal> {
  ...
}

擦除后就變?yōu)榱?Printer<Dog>類型。

Java 泛型的很多限制都是由于泛型擦除導致的,下面會詳細介紹下泛型的局限性。

通配符

通配符(Wildcard)就是 Java 泛型中的問號 ? 。要想知道通配符是要解決什么樣的問題,我覺得首先需要知道什么是 Variance(不知道這個正確的譯名是什么,如有知道請留言)?

提到 Variance 必須先提到子類型(subtyping),子類型是面向對象中類型多態(tài)的其中一種表現(xiàn)形式,主要用于描述 is-a 這樣的關系,例如 S 是 T 的子類型,那么 S is a subtype of T。

Variance 指的是如何根據組成類型之間的子類型關系,來確定更復雜的類型之間的子類型關系。

上面這段話比較繞,舉個實際的例子來說,Variance 指的就是當子類型在更復雜的場景下,例如 If S is a subtype of T,那 Generic<S> is subtype of Generic<T> 這種關系是否還能成立。

Variance 分為下面幾種形式:

  • 不變(Invariance):如果 B 是 A 的子類型,那么 Generic<B> 不是 Generic<A> 的子類型。
  • 協(xié)變(Covariance):如果 B 是 A 的子類型,那么 Generic<B>Generic<A> 的子類型。
  • 逆變(Contravariance):如果 B 是 A 的子類型,那么 Generic<A>Generic<B> 的子類型(逆變就是逆轉了子類型關系)。
  • 雙變(Bi-variance):Java 中不存在雙變,因此不討論。

在 Java 當中,泛型是不變的,這也就意味著下面這段看似沒問題的代碼會報錯:

List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // 編譯報錯

因為按照不變的說明,List<Dog> 根本就不是 List<Animal> 的子類型,所以自然就不能賦值了。

Java 中的泛型采用不變性是為了保證類型安全。不過不變性雖然保證了類型安全,但是也讓靈活性大大降低。為此,Java 提供了通配符,可以解除部分這種限制,也不損失安全性。

上界通配符

上界通配符的形式為:? extends Type,使用上限通配符可以將類型參數變?yōu)閰f(xié)變的,這就可以讓原本無法賦值的類型可以正常賦值了。但是這在解除不變性限制的同時,也帶來了新的限制,就是參數類型只能作為返回值類型,無法作為入參的類型。例如:

List<? extends Animal> animals = new ArrayList<Dog>(); // 可以正確賦值了
animals.add(new Dog()); // 編譯錯誤,類型不匹配,無法寫入,除非寫入 null(也就是參數類型無法作為入參的類型)
Animal animal = animals.get(0); // 可以獲取(也就是參數類型可作為返回值類型)

這就叫當上帝給你關了一扇門,同時會幫你打開一扇窗。盡管窗戶有個洞,但總比沒有好吧?你說是不是?

下界通配符

下界通配符的形式為:? super Type,使用下界通配符可以將類型參數變?yōu)槟孀兊?,也就是說顛倒了類型關系,父類型可以賦值給子類型了!例如:

List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals; // 可以正確賦值!

看起來挺神奇的吧!這種方式同樣也帶來了新的限制,在使用這種變量的時候,不可以調用返回值包含類型參數的方法,也不能獲得包含類型參數的字段值。簡單來說,就是只能作為輸入,但是不能輸出。例如:

dogs.add(new Dog()); // 可以寫入
Dog dog = dogs.get(0); // 編譯報錯,類型不匹配: Required type: Dog  Provided: capture of ? super Dog

對于獲取時編譯報類型不匹配的問題,其實可以通過類型強制轉換來解決。

無界通配符

無界通配符的形式就是一個單獨的問號 ?。當我們對類型參數一無所知,但仍然希望以安全的方式使用它時,則可以考慮使用無界通配符。例如:

public static boolean hasNull(Pair<?> p) {
  return p.first != null && p.second != null;
}

反射和泛型

因為泛型擦除,所以無法在反射時獲取泛型類型。但是擦除的類依然保留原先泛型的微弱記憶。例如,如果定義了以下泛型方法:

public static <T extends Comparable<? super T>> T min(T[] a)

那么,可以通過反射可以獲取到以下信息:

public static Comparable min(Comparable[] a)

也就是說,我們可以重新構造實現(xiàn)者聲明的泛型類和方法的有關內容。但是,不會知道對于特定的對象或方法調用會如何解析類型參數。

為了表述泛型類型聲明,可以使用 java.lang.reflect包中的接口 Type。這個接口包含以下子類型:

  • Class 類:描述具體類型
  • TypeVariable 接口:描述類型變量(如 T extends Comparable<? super T>
  • WildcardType 接口:描述通配符(如 ? super T
  • ParameterizedType 接口:描述泛型類或接口類型(如 Comparable<? super T>
  • GenericArrrayType 接口:描述泛型數組(如 T[]

下面的這個示例程序,就是通過反射來打印指定類的信息:

public class GenericReflectionTest {
  public static void main(String[] args) {
    String className;
    try (Scanner scanner = new Scanner(System.in)) {
      System.out.println("Enter class name (e.g., java.util.Collections): ");
      className = scanner.next();
    }
    try {
      Class<?> cl = Class.forName(className);
      ClassPrinter.print(cl);
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
  public static class ClassPrinter {
    public static void print(Class<?> cl) {
      System.out.print(cl);
      printTypes(cl.getTypeParameters(), "<", ", ", ">", true);
      final Type sc = cl.getGenericSuperclass();
      if (sc != null) {
        System.out.print(" extends ");
        printType(sc, false);
      }
      printTypes(cl.getGenericInterfaces(), " implements ", ", ", "", false);
      System.out.print(" {");
      System.out.println();
      final Method[] methods = cl.getDeclaredMethods();
      for (int i = 0; i < methods.length; i++) {
        final Method method = methods[i];
        printMethod(method);
        if (i < methods.length - 1) {
          System.out.println();
        }
      }
      System.out.println();
      System.out.print("}");
    }
    private static void printTypes(Type[] types, String pre, String sep, String suf, boolean isDefinition) {
      if (pre.equals(" extends ") && Arrays.equals(types, new Type[]{Object.class})) {
        return;
      }
      if (types.length > 0) {
        System.out.print(pre);
      }
      for (int i = 0; i < types.length; i++) {
        if (i > 0) {
          System.out.print(sep);
        }
        printType(types[i], isDefinition);
      }
      if (types.length > 0) {
        System.out.print(suf);
      }
    }
    private static void printType(Type type, boolean isDefinition) {
      if (type instanceof Class) { // 具體類型
        Class<?> cl = (Class<?>) type;
        System.out.print(cl.getName());
      } else if (type instanceof TypeVariable) { // 類型變量
        TypeVariable<?> tv = (TypeVariable<?>) type;
        System.out.print(tv.getName());
        if (isDefinition) {
          printTypes(tv.getBounds(), " extends ", " & ", "", false);
        }
      } else if (type instanceof WildcardType) { // 通配符
        WildcardType wt = (WildcardType) type;
        System.out.print("?");
        printTypes(wt.getUpperBounds(), " extends ", " & ", "", false);
        printTypes(wt.getLowerBounds(), " super ", " & ", "", false);
      } else if (type instanceof ParameterizedType) { // 泛型類或接口類型
        ParameterizedType pt = (ParameterizedType) type;
        Type ownerType = pt.getOwnerType();
        if (ownerType != null) {
          printType(ownerType, false);
        }
        printType(pt.getRawType(), false);
        printTypes(pt.getActualTypeArguments(), "<", ", ", ">", false);
      } else if (type instanceof GenericArrayType) { // 泛型數組
        GenericArrayType gat = (GenericArrayType) type;
        printType(gat.getGenericComponentType(), isDefinition);
        System.out.print("[]");
      } else {
        System.out.print("unknown type: " + type);
      }
    }
    private static void printMethod(Method m) {
      final String name = m.getName();
      System.out.print("  ");
      System.out.print(Modifier.toString(m.getModifiers()));
      System.out.print(" ");
      printTypes(m.getTypeParameters(), "<", ", ", ">", true);
      printType(m.getGenericReturnType(), false);
      System.out.print(" ");
      System.out.print(name);
      System.out.print("(");
      printTypes(m.getGenericParameterTypes(), "", ", ", "", false);
      System.out.print(")");
    }
  }
}

以下面的這個測試類為例:

public class TestClass<T extends Comparable<? super T>> implements Cloneable {
  public void test(T t) {
    System.out.println(t);
  }
}

運行上面的程序的輸出結果是:

class com.airsaid.java.samples.generic.TestClass<T extends java.lang.Comparable<? super T>> extends java.lang.Object implements java.lang.Cloneable {
  public void test(T)
}

可以看到,可以獲取到該泛型類所定義的泛型信息。

類型字面量

有時候,我們確實需要在運行時獲取泛型的參數類型,那么真的就沒有辦法了嗎?

答案是有的,在上面反射的例子中我們可以看到,字節(jié)碼當中其實保存了部分的泛型信息(簽名信息)。

舉一個實際場景的例子,Gson 這個框架相信大家都用過。在進行反序列化時,有多個重載方法:

fromJson(String json, Class cl)

fromJson(String json, TypeToken type)

當使用第一個方法時,如果第二個參數是一個泛型類的 class 對象,那么就會出現(xiàn)問題。例如下面的這段示例代碼:

Foo<DataBean> foo = new Foo<DataBean>();
gson.fromJson(json, foo.getClass());

這是因為無法獲取到 Foo 類的泛型參數 DataBean。

因此,Gson 提供了 TypeToken 類來處理這種問題。使用起來是這樣的:

Foo<DataBean> foo = new Foo<DataBean>();
gson.fromJson(json, new TypeToken<Foo<DataBean>>(){});

下面是我簡化過后的 TypeToken 源碼:

public class TypeToken<T> {
  private final Type type;
  protected TypeToken() {
    this.type = getTypeTokenTypeArgument();
  }
  public Type getType() {
    return type;
  }
  private Type getTypeTokenTypeArgument() {
    final Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof ParameterizedType) {
      ParameterizedType parameterized = (ParameterizedType) superclass;
      if (parameterized.getRawType() == TypeToken.class) {
        return parameterized.getActualTypeArguments()[0];
      }
    } else if (superclass == TypeToken.class) {
      throw new IllegalStateException("TypeToken must be created with a type argument: new TypeToken<...>() {}; "
                                      + "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved.");
    }
    throw new IllegalStateException("Must only create direct subclasses of TypeToken");
  }
}

TypeToken 的原理就是創(chuàng)建了一個匿名子類對象,然后通過 getGenericSuperclass()方法獲取其泛型父類,再獲取其中定義的參數化類型的類型參數。通過這種方式,就可以獲取到 TypeToken 所定義的泛型類型了:

public class TypeTokenTest {
  public static void main(String[] args) {
    TypeToken<?> typeToken = new TypeToken<List<String>>() {};
    System.out.println(typeToken.getType()); // java.util.List<java.lang.String>
  }
}

限制和局限性

不能使用基本類型實例化類型參數

泛型的類型參數不可以使用基本類型,例如 Pair<int>,這是因為類型擦除后類型變?yōu)榱?Object,而 Object 無法存儲基本類型數值。對于這種情況,我們需要使用基本類型對應的包裝類型。

運行時類型查詢只適用于原始類型

同樣也是由于類型擦除導致的問題,如果要在代碼中進行判斷是否是某個泛型類型,這是無法做到的。例如:

if (a instanceof Pair<String>) // 錯誤

不能創(chuàng)建參數化類型的數組

還是由于類型擦除導致的問題,無法實例化參數化類型的數組。例如:

Pair<String>[] table = new Pair<String>[10]; // 錯誤

不能創(chuàng)建 Throwable 的子類為泛型類

如果希望創(chuàng)建一個 Throwable 的子類,那么子類就不可以是泛型類。例如:

class Generic<T> extends Exception {
}

此時,編譯器會直接報錯,提示:Subclass of 'Throwable' may not have type parameters。

這還是因為泛型擦除,這就導致在 catch 時不知道該執(zhí)行哪個代碼分支:

try {
    ...
} catch (Generic<String> e) {
} catch (Generic<Integer> e) {
}

以上就是Java 泛型 Generic機制實例詳解的詳細內容,更多關于Java 泛型 Generic的資料請關注腳本之家其它相關文章!

相關文章

  • Java中父類和子類之間的轉換操作示例

    Java中父類和子類之間的轉換操作示例

    這篇文章主要介紹了Java中父類和子類之間的轉換操作,結合實例形式分析了Java中父類和子類之間的轉換相關原理、操作技巧與使用注意事項,需要的朋友可以參考下
    2020-05-05
  • flatten-maven-plugin使用教程

    flatten-maven-plugin使用教程

    這篇文章主要介紹了flatten-maven-plugin使用,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-07-07
  • Java中靜態(tài)代碼塊、構造代碼塊、構造函數和普通代碼塊的區(qū)別

    Java中靜態(tài)代碼塊、構造代碼塊、構造函數和普通代碼塊的區(qū)別

    在Java中,靜態(tài)代碼塊、構造代碼塊、構造函數、普通代碼塊的執(zhí)行順序是一個筆試的考點,通過這篇文章希望大家能徹底了解它們之間的執(zhí)行順序,需要的朋友可以參考下
    2023-05-05
  • 在java中如何將inputStream對象轉換為File對象(不生成本地文件)

    在java中如何將inputStream對象轉換為File對象(不生成本地文件)

    這篇文章主要介紹了在java中如何將inputStream對象轉換為File對象(不生成本地文件),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2025-03-03
  • Java中如何正確遍歷刪除List中的元素

    Java中如何正確遍歷刪除List中的元素

    刪除List中元素這個場景很場景,很多人可能直接在循環(huán)中直接去刪除元素,這樣做對嗎?下面小編就來和大家一起討論如何正確遍歷刪除List中的元素,文中有詳細的代碼示例供大家參考,需要的朋友可以參考下
    2023-11-11
  • Spring中@ExceptionHandler注解的工作原理詳解

    Spring中@ExceptionHandler注解的工作原理詳解

    這篇文章主要介紹了Spring中@ExceptionHandler注解的工作原理詳解,Spring Web注解@ExceptionHandler可以用來指定處理某類異常的控制器方法,從而在這些異常發(fā)生時,會有相應的控制器方法來處理此類異常,需要的朋友可以參考下
    2024-01-01
  • 詳解Java編程中Annotation注解對象的使用方法

    詳解Java編程中Annotation注解對象的使用方法

    這篇文章主要介紹了Java編程中Annotation注解對象的使用方法,注解以"@注解名"的方式被編寫,與類、接口、枚舉是在同一個層次,需要的朋友可以參考下
    2016-03-03
  • Java中final與繼承操作實例分析

    Java中final與繼承操作實例分析

    這篇文章主要介紹了Java中final與繼承操作,結合實例形式分析了Java中使用final阻止繼承的相關原理與操作注意事項,需要的朋友可以參考下
    2019-09-09
  • SpringBoot整合Security權限控制登錄首頁

    SpringBoot整合Security權限控制登錄首頁

    這篇文章主要為大家介紹了SpringBoot整合Security權限控制登錄首頁示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11
  • java并發(fā)編程之進程和線程調度基礎詳解

    java并發(fā)編程之進程和線程調度基礎詳解

    這篇文章主要介紹了java并發(fā)編程之進程和線程調度基礎,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06

最新評論

中卫市| 郧西县| 诸暨市| 绍兴县| 巨野县| 隆德县| 宁城县| 邹城市| 昌吉市| 健康| 鱼台县| 双鸭山市| 柳河县| 邹城市| 涞源县| 安泽县| 曲沃县| 洛隆县| 嘉祥县| 嘉峪关市| 察隅县| 金乡县| 仙居县| 靖西县| 都匀市| 长葛市| 密山市| 无极县| 新竹县| 衡东县| 乾安县| 富川| 襄汾县| 平湖市| 乐安县| 承德市| 西乡县| 林甸县| 溧水县| 石城县| 定西市|