深刻理解Java泛型:你对泛型的理解够深刻吗?

泛型

泛型提供了一种将集合类型传达给编译器的方法,一旦编译器知道了集合元素的类型,编译器就能够对其类型进行检查,作类型约束。java

在没有泛型以前:spring

/**
 * 迭代 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异常,这对调用方来讲并不友好。json

经过泛型,能够将方法的 doc 注释转移到了方法签名上:forEachCollection(Collection<String> collection),方法调用者一看方法签名便知道此处须要一个Collection<String>,编译器也能够在编译时检查是否违反类型约束。须要说明的是,编译器的检查也是很是容易绕过的,如何绕过呢?请看下文哦~安全

画外音:代码就是最好的注释。微信

泛型和类型转化

思考,如下代码是否合法:app

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

废话很少说,直接上答案。框架

image.png

代码1很明显是合法的。Object类型是String类型的父类。debug

那么代码2为何不合法呢?code

在 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小黑

相关文章
相关标签/搜索