深入理解Java泛型:你對泛型的理解夠深入嗎?


泛型

泛型提供了一種將集合類型傳達給編譯器的方法,一旦編譯器知道了集合元素的類型,編譯器就可以對其類型進行檢查,做類型約束。

在沒有泛型之前:

/**
 * 迭代 Collection ,注意 Collection 里面只能是 String 類型
 */
public static void forEachStringCollection(Collection collection) {
    Iterator iterator = collection.iterator();
    while (iterator.hasNext()) {
        String next = (String) iterator.next();
        System.out.println("next string : " + next);
    }
}

這是使用泛型之后的程序:

public static void forEachCollection(Collection<String> collection) {
  Iterator<String> iterator = collection.iterator();
  while (iterator.hasNext()) {
    String next = iterator.next();
    System.out.println("next string : " + next);
  }
}

在沒有泛型之前,我們只能通過更直觀的方法命名和 doc 注釋來告知方法的調用者,forEachStringCollection方法只能接收元素類型為String的集合。然而這只是一種“約定”,如果使用方傳入了一個元素不為String類型的集合,在編譯期間代碼並不會報錯,只有在運行時,會拋出ClassCastException異常,這對調用方來說並不友好。

通過泛型,可以將方法的 doc 注釋轉移到了方法簽名上:forEachCollection(Collection<String> collection),方法調用者一看方法簽名便知道此處需要一個Collection<String>,編譯器也可以在編譯時檢查是否違反類型約束。需要說明的是,編譯器的檢查也是非常容易繞過的,如何繞過呢?請看下文哦~

畫外音:代碼就是最好的注釋。

泛型和類型轉化

思考,以下代碼是否合法:

List<String> strList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
objList.add("公眾號:Coder小黑"); // 代碼1
objList = strList; // 代碼2

廢話不多說,直接上答案。

image.png

代碼1很明顯是合法的。Object類型是String類型的父類。

那么代碼2為什么不合法呢?

在 Java 中,對象類型的賦值其實是引用地址的賦值,也就是說,假設代碼2賦值成功,objListstrList變量引用的是同一個地址。那會有什么問題呢?

如果此時,往objList中添加了一個非String類型的元素,也就相當於往strList中添加了一個非String類型的元素。很明顯,此處就破壞了List<String> strList。所以,Java 編譯器會認為代碼2是非法的,這是一種安全的做法。

畫外音:可能和大多數人的直覺不太一樣,那是我們考慮問題還不夠全面,此處的原因比結果更重要哦

泛型通配符

我們已經知道,上文的代碼2是不合法的。那么,接下來思考這樣兩個方法:

public static void printCollection1(Collection c) {}

public static void printCollection2(Collection<Object> c) {}

這兩個方法有什么區別呢?

printCollection1方法支持任意元素類型的Collection,而printCollection2方法只能接收Object類型的Collection。雖然StringObject的子類,但是Collection<String>並不是Collection<Object>的子類,和代碼2有異曲同工之妙。

再看一下下面這個方法:

public static void printCollection3(Collection<?> c) {}

printCollection3和上面的兩個方法又有什么區別呢?怎么理解printCollection3方法上的?呢?

?表示任意類型,表明printCollection3方法接收任意類型的集合。

好,那么問題又來了,請看如下代碼:

List<?> c = Lists.newArrayList(new Object());
Object o = c.get(0);
c.add("12"); // 編譯錯誤

為什么會編譯報錯呢?

我們可以將任意類型的集合賦值給List<?> c變量。但是,add方法的參數類型是,它表示未知類型,所以調用add方法時會編程錯誤,這是一種安全的做法。

get方法返回集合中的元素,雖然集合中的元素類型未知,但是無論是什么類型,其均為Object類型,所以使用Object類型來接收是安全的。

有界通配符

public static class Person extends Object {}

public static class Teacher extends Person {}

// 只知道這個泛型的類型是Person的子類,具體是哪一個不知道
public static void method1(List<? extends Person> c) {}

// 只知道這個泛型的類型是Teacher的父類,具體是哪一個不知道
public static void method2(List<? super Teacher> c) {}

思考如下代碼運行結果:

public static void test3() {
  List<Teacher> teachers = Lists.newArrayList(new Teacher(), new Teacher());
  // method1 處理的是 Person 的 子類,Teacher 是 Person 的子類
  method1(teachers);
}


// 只知道這個泛型的類型是Person的子類,具體是哪一個不知道
public static void method1(List<? extends Person> c) {
  // Person 的子類,轉Person, 安全
  Person person = c.get(0);
  c.add(new Person()); //代碼3,編譯錯誤
}

代碼3為什么會編譯錯誤呢?

method1只知道這個泛型的類型是Person的子類,具體是哪一個不知道。如果代碼3編譯成功,那么上述的代碼中,就是往List<Teacher> teachers中添加了一個Person元素。此時,后續在操作List<Teacher> teachers時,大概率會拋出ClassCastException異常。

再來看如下代碼:

public static void test4() {
  List<Person> teachers = Lists.newArrayList(new Teacher(), new Person());
  // method1 處理的是 Person 的 子類,Teacher 是 Person 的子類
  method2(teachers);
}

// 只知道這個泛型的類型是Teacher的父類,具體是哪一個不知道
public static void method2(List<? super Teacher> c) {
  // 具體是哪一個不知道, 只能用Object接收
  Object object = c.get(0); // 代碼4
  c.add(new Teacher()); // 代碼5,不報錯
}

method2泛型類型是Teacher的父類,而Teacher的父類有很多,所以代碼4只能使用Object來接收。子類繼承父類,所以往集合中添加一個Teacher對象是安全的操作。

最佳實踐:PECS 原則

PECS:producer extends, consumer super

  • 生產者,生產數據的, 使用<? extends T>
  • 消費者,消費數據的,使用<? super T>

怎么理解呢?我們直接上代碼:

/**
 * producer - extends, consumer- super
 */
public static void addAll(Collection<? extends Object> producer,
                          Collection<? super Object> consumer) {
    consumer.addAll(producer);
}

有同學可能會說,這個原則記不住怎么辦?

沒關系,筆者有時候也記不清。不過幸運的是,在 JDK 中有這個一個方法:java.util.Collections#copy,該方法很好的闡述了 PECS 原則。每次想用又記不清的時候,看一眼該方法就明白了~

// java.util.Collections#copy
public static <T> void copy(List<? super T> dest, List<? extends T> src){}

畫外音:知識很多、很雜,我們應該在大腦中建立索引,遇到問題,通過索引來快速查找解決方法

更安全的泛型檢查

上述的一些檢查都是編譯時的檢查,而想要騙過編譯器的檢查也很簡單:

public static void test5() {
  List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
  List copy = list;
  copy.add("a");
  List<Integer> list2 = copy;
}

test5方法就騙過了編譯器,而且能成功運行。

那什么時候會報錯呢?當程序去讀取list2中的元素時,才會拋出ClassCastException異常。

Java 給我們提供了java.util.Collections#checkedList方法,在調用add時就會檢查類型是否匹配。

public static void test6() {
  List<Integer> list = Collections.checkedList(Arrays.asList(1, 2, 3, 4, 5), Integer.class);
  List copy = list;
  // Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.String element into collection with element type class java.lang.Integer
  copy.add("a");
}

畫外音:這是一種 fail-fast 的思想,在 add 時發現類型不一致立刻報錯,而不是繼續運行可能存在問題的程序

類型擦除(Type Erasure)

我們知道,編譯器會將泛型擦除,那怎么理解泛型擦除呢?是統一改成Object嗎?

泛型擦除遵循以下規則:

  • 如果泛型參數無界,則編譯器會將其替換為Object
  • 如果泛型參數有界,則編譯器會將其替換為邊界類型。
public class TypeErasureDemo {
    public <T> void forEach(Collection<T> collection) {}

    public <E extends String> void iter(Collection<E> collection) {}
}

使用javap命令查看 Class 文件信息:

class文件信息1

class文件信息2

通過 Class 文件信息可以看到:編譯器將forEach方法的泛型替換為了Object,將iter方法的泛型替換為了String

泛型和方法重載(overload)

了解完泛型擦除規則之后,我們來看一下當泛型遇到方法重載,會遇到什么樣的問題呢?

閱讀如下代碼:

// 第一組
public static void printArray(Object[] objs) {}

public static <T> void printArray(T[] objs) {}
// 第二組
public static void printArray(Object[] objs) {}

public static <T extends Person> void printArray(T[] objs) {}

上面兩組方法是否都構成了重載呢?

  • 第一組:泛型會被擦除,也就是說,在運行時期,T[]其實就是Object[],因此第一組不構成重載。

  • 第二組:<T extends Person>表明接收的方法是Person的子類,構成重載。

使用 ResolvableType 解析泛型

Spring 框架中提供了org.springframework.core.ResolvableType來優雅解析泛型。

一個簡單的使用示例如下:

public class ResolveTypeDemo {

    private static final List<String> strList = Lists.newArrayList("a");

    public <T extends CharSequence> void exchange(T obj) {}

    public static void resolveFieldType() throws Exception {
        Field field = ReflectionUtils.findField(ResolveTypeDemo.class, "strList");
        ResolvableType resolvableType = ResolvableType.forField(field);
        // class java.lang.String
        System.out.println(resolvableType.getGeneric(0).resolve());
    }

    public static void resolveMethodParameterType() throws Exception {
        Parameter[] parameters = ReflectionUtils.findMethod(ResolveTypeDemo.class, "exchange", CharSequence.class).getParameters();
        ResolvableType resolvableType = ResolvableType.forMethodParameter(MethodParameter.forParameter(parameters[0]));
        // interface java.lang.CharSequence
        System.out.println(resolvableType.resolve());
    }

    public static void resolveInstanceType() throws Exception {
        PayloadApplicationEvent<String> instance = new PayloadApplicationEvent<>(new Object(), "hi");
        ResolvableType resolvableTypeForInstance = ResolvableType.forInstance(instance);
        // class java.lang.String
        System.out.println(resolvableTypeForInstance.as(PayloadApplicationEvent.class).getGeneric().resolve());
    }
}

泛型和 JSON 反序列化

最近看到這樣一個代碼,使用 Jackson 將 JSON 轉化為 Map。

public class JsonToMapDemo {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public static <K, V> Map<K, V> toMap(String json) throws JsonProcessingException {
        return (Map) OBJECT_MAPPER.readValue(json, new TypeReference<Map<K, V>>() {
        });
    }

    public static void main(String[] args) throws JsonProcessingException {
        // {"1":{"id":1}}
        String json = "{\"1\":{\"id\":1}}";
        Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
        });
   
    }

    @Data
    public static class User implements Serializable {
        private static final long serialVersionUID = 8817514749356118922L;
        private int id;
    }
}

運行 main 方法,代碼雖然正常結束。但是這個代碼其實是有問題的,有什么問題呢?一起來看如下代碼:

public static void main(String[] args) {
  // {"1":{"id":1}}
  String json = "{\"1\":{\"id\":1}}";
  Map<Integer, User> userIdMap = toMap(json);
  userIdMap.forEach((integer, user) -> {
    // 出處代碼會報錯
    // Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    System.out.println(user.getId());
  });
}

為什么會報ClassCastException呢?讓我們來 Debug 一探究竟。

debug

通過 Debug 可以發現:Map<Integer, User> userIdMap對象的 key 其實是String類型,而 value 是一個LinkedHashMap。這很好理解,上述代碼這個寫法,根本不知道 K,V 是什么。正確寫法如下:

public static void main(String[] args) throws JsonProcessingException {
  // {"1":{"id":1}}
  String json = "{\"1\":{\"id\":1}}";
  Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
  });
  userIdMap.forEach((integer, user) -> {
    System.out.println(user.getId());
  });
}

歡迎關注微信公眾號:Coder小黑

Coder小黑


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM