本文由 简悦 SimpRead 转码, 原文地址 ricstudio.top

前言

前阵子给公司新人培训 Java 基础相关的一些点,系统整理了一下泛型相关的知识点。特来分享一下。希望能让一些对泛型不熟悉的同学完全掌握 Java 泛型的相关知识点。

开始之前,先给大家来一道测试题。

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

请问,上面代码最终结果输出的是什么?熟悉泛型的同学应该能够答出来,而对泛型有所了解,但是了解不深入的同学可能会答错。

content

  • 泛型概述
    • why 泛型
    • 泛型的作用
  • 泛型的定义和使用
    • 泛型类
    • 泛型方法
    • 泛型接口
  • 通配符 ?
    • 无界通配符
    • 上限通配符
    • 下限通配符
  • 类型擦除

带着问题

  1. Java 中的泛型是什么 ? 使用泛型的好处是什么?
  2. 什么是泛型中的限定通配符和无界通配符 ?
  3. 你可以把List<String>传递给一个接受List<Object>参数的方法吗?
  4. Java 的泛型是如何工作的 ? 什么是类型擦除 ?

一、泛型概述

最早的 “泛型编程” 的概念起源于 C 的模板类 (Template),Java 借鉴了这种模板理念,只是两者的实现方式不同。C 会根据模板类生成不同的类,Java 使用的是类型擦除的方式。

1.1 why 泛型?

Java1.5 发行版本中增加了泛型(Generic)。

有很多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。 — 《Java 编程思想》

容器就是要存放要使用的对象的地方。数组也是如此,只是相比较的话,容器类更加的灵活,具有更多的功能。所有的程序,在运行的时候都要求你持有一大堆的对象,所以容器类算得上最需要具有重用性的类库之一了。

看下面这个例子,

  1. public class AutoMobile {
  2. }
  3. /*
  4. * 重用性不好的容器类
  5. */
  6. public class Holder1 {
  7. private AutoMobile a;
  8. public Holder1(AutoMobile a) {
  9. this.a = a;
  10. }
  11. }
  12. /*
  13. * 想要在java5 之前实现可重用性的容器类
  14. * @author Richard_yyf
  15. * @version 1.0 2019/8/29
  16. */
  17. public class Holder2 {
  18. private Object a;
  19. public Holder2(Object a) {
  20. this.a = a;
  21. }
  22. public Object getA() {
  23. return a;
  24. }
  25. public void setA(Object a) {
  26. this.a = a;
  27. }
  28. public static void main(String[] args) {
  29. Holder2 h2 = new Holder2(new AutoMobile());
  30. AutoMobile a = (AutoMobile) h2.getA();
  31. h2.setA("Not an AutoMobile");
  32. String s = (String) h2.getA();
  33. h2.setA(1);
  34. Integer x = (Integer) h2.getA();
  35. }
  36. }
  37. /*
  38. * 通过泛型来实现可重用性
  39. * 泛型的主要目的是指定容器要持有什么类型的对象
  40. * 而且由编译器来保证类型的正确性
  41. *
  42. * @author Richard_yyf
  43. * @version 1.0 2019/8/29
  44. */
  45. public class Holder3WithGeneric<T> {
  46. private T a;
  47. public Holder3WithGeneric(T a) {
  48. this.a = a;
  49. }
  50. public T getA() {
  51. return a;
  52. }
  53. public void setA(T a) {
  54. this.a = a;
  55. }
  56. public static void main(String[] args) {
  57. Holder3WithGeneric<AutoMobile> h3 = new Holder3WithGeneric<>(new AutoMobile());
  58. AutoMobile a = h3.getA();
  59. }
  60. }

通过上述对比,我们应该可以理解类型参数化具体是什么个意思。

在没有泛型之前,从集合中读取到的每一个对象都需要进行转换。如果有人不小心插入了类型错误的对象,在运行时的转换处理就会出错。这显然是不可忍受的。

泛型的出现,给 Java 带来了不一样的编程体验。

1.2 泛型的作用

  1. 参数化类型。与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
  2. 类型检测。 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
  3. 提高代码可读性。 不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Holder<AutoMobile>这个类型显化的效果,程序员能够一目了然猜测出这个容器类持有的数据类型。
  4. 代码重用。 泛型合并了同类型对象的处理代码,使得代码重用度变高。

二、泛型的定义和使用

泛型按照使用情况可以分为 3 种。

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

2.1 泛型类

  • 概述:把泛型定义在类上
  • 定义格式:

    1. public class 类名 <泛型类型1,...> {
    2. ...
    3. }
  • 注意事项:泛型类型必须是引用类型(非基本数据类型)

类型参数 规范(约定俗称)

尖括号 <>中的 字母 被称作是类型参数,用于指代任何类型。我们常看到<T> 的写法,事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。

  1. public class Test<Hello> {
  2. Hello field1;
  3. }

但出于规范和可读性的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

  • T 代表一般的任何类。
  • E 代表 Element 的意思,或者 Exception 异常的意思。
  • K 代表 Key 的意思。
  • V 代表 Value 的意思,通常与 K 一起配合使用。
  • S 代表 Subtype 的意思

2.2 泛型方法

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

    1. public <泛型类型> 返回类型 方法名(泛型类型 变量名) {
    2. ...
    3. }
  • 注意事项:

    • 这里的<T> 中的T被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。
    • 方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用

泛型类和泛型方法共存的现象

  1. * 泛型类与泛型方法的共存现象
  2. * @author Richard_yyf
  3. * @version 1.0 2019/8/29
  4. */
  5. public class GenericDemo2<T> {
  6. public void testMethod(T t){
  7. System.out.println(t.getClass().getName());
  8. }
  9. public <T> T testMethod1(T t){
  10. return t;
  11. }
  12. public static void main(String[] args) {
  13. GenericDemo2<String> t = new GenericDemo2<>();
  14. t.testMethod("generic");
  15. Integer integer = 1;
  16. Integer i = t.testMethod1(integer);
  17. }
  18. }

泛型方法始终以自己定义的类型参数为准

当然,现实场景下千万不要去作死写出这么难以阅读的代码。

2.3 泛型接口

泛型接口和泛型类差不多。

  • 泛型接口概述:把泛型定义在接口
  • 定义格式:
    1. public interface 接口名<泛型类型> {
    2. ...
    3. }

Demo

  1. public interface GenericInterface<T> {
  2. void show(T t);
  3. }
  4. public class GenericInterfaceImpl<String> implements GenericInterface<String>{
  5. @Override
  6. public void show(String o) {
  7. }
  8. }

三、 通配符 ?

除了用 <T>表示泛型外,还有 <?>这种形式。 被称为通配符。

为什么要引进这个概念呢?先来看下下面的 Demo.

  1. public class GenericDemo2 {
  2. class Base{}
  3. class Sub extends Base{}
  4. public void test() {
  5. Sub sub = new Sub();
  6. Base base = sub;
  7. List<Sub> lsub = new ArrayList<>();
  8. List<Base> lbase = lsub;
  9. }
  10. }

在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。

所以,通配符的出现是为了指定泛型中的类型范围

通配符有 3 种形式。

  1. <?>被称作无限定的通配符
  2. <? extends T>被称作有上限的通配符
  3. <? super T>被称作有下限的通配符

3.1 无界通配符 <?>

无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。

  1. public interface Collection<E> extends Iterable<E> {
  2. boolean add(E e);
  3. }
  4. public class GenericDemo3 {
  5. * 测试 无限定通配符 <?>
  6. * @param collection c
  7. */
  8. public void testUnBoundedGeneric(Collection<?> collection) {
  9. collection.add(123);
  10. collection.add("123");
  11. collection.add(new Object());
  12. collection.iterator().next();
  13. collection.size();
  14. }
  15. }

无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。

有同学可能会想,<?>既然作用这么渺小,那么为什么还要引用它呢? 

个人认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。

(用的很少,但是要理解)

为了接下去的说明方便,先定义一下几个类。

  1. class Food {}
  2. class Fruit extends Food {}
  3. class Apple extends Fruit {}
  4. class Banana extends Fruit {}
  5. class Plate<T> {
  6. private T item;
  7. public Plate(T item) {
  8. this.item = item;
  9. }
  10. public T getItem() {
  11. return item;
  12. }
  13. public void setItem(T item) {
  14. this.item = item;
  15. }
  16. }

3.2 上限 通配符 <? extends T>

<?>代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 T 及 类型 T 的子类都可以放入这个容器中。

什么是上界

在这个体系中,上限通配符 Plate<? extends Fruit> 覆盖下图中蓝色的区域。

image.png

副作用

边界让 Java 不同泛型之间的转换更容易了。但不要忘记,这样的转换也有一定的副作用。那就是容器的部分功能可能失效。

  1. public void testUpperBoundedBoundedGeneric() {
  2. Plate<? extends Fruit> p = new Plate<>(new Apple());
  3. // 会报错,因为编译器不知道具体容器内是 Fruit 还是它的派生类
  4. p.setItem(new Fruit());
  5. p.setItem(new Apple());
  6. Fruit fruit = p.getItem();
  7. Food food = p.getItem();
  8. }

<? 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>覆盖下图中红色的区域。
image.png

副作用

  1. public void testLowerBoundedBoundedGeneric() {
  2. Plate<? super Fruit> p = new Plate<>(new Fruit());
  3. p.setItem(new Fruit());
  4. p.setItem(new Apple());
  5. // 会报错,只有所有类的基类 Object 可以装下
  6. Apple apple = p.getItem();
  7. Object o = p.getItem();
  8. }

因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是 Fruit 的基类,往里面存比 Fruit 粒度小的类都可以。但是往外读取的话就费劲了,只有所有类的基类 Object 可以装下。但这样一来元素类型信息就都丢失了。

3.4 PECS 原则

PECS - Producer Extends Consumer Super

  • “Producer Extends” – 如果你需要一个只读容器,用它来 produce T,那么使用 <? extends T> 。
  • “Consumer Super” – 如果你需要一个只写容器,用它来 consume T,那么使用 <? super T>。
  • 如果需要同时读取以及写入,那么我们就不能使用通配符了。

四、 类型擦除

泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。

这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉

专业术语叫做 **类型擦除**

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

打印的结果为 true 是因为 List<String>List<Integer>在 jvm 中的 Class 都是 List.class

泛型信息被擦除了。

  1. /* 类型擦除 相关类
  2. *
  3. * @author Richard_yyf
  4. * @version 1.0 2019/8/29
  5. */
  6. public class EraseHolder<T> {
  7. T data;
  8. public EraseHolder(T data) {
  9. this.data = data;
  10. }
  11. public static void main(String[] args) {
  12. EraseHolder<String> holder = new EraseHolder<>("hello");
  13. Class clazz = holder.getClass();
  14. System.out.println("erasure class is:" + clazz.getName());
  15. Field[] fs = clazz.getDeclaredFields();
  16. for ( Field f:fs) {
  17. System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
  18. }
  19. EraseHolder2<String> holder2 = new EraseHolder2<>("hello");
  20. clazz = holder2.getClass();
  21. fs = clazz.getDeclaredFields();
  22. for ( Field f:fs) {
  23. System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
  24. }
  25. }
  26. /**
  27. * 指定通配符上线
  28. * @param <T>
  29. */
  30. static class EraseHolder2<T extends String> {
  31. T data;
  32. public EraseHolder2(T data) {
  33. this.data = data;
  34. }
  35. }
  36. }

局限性

利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。

  1. public class EraseReflectDemo {
  2. public static void main(String[] args) {
  3. List<Integer> list = new ArrayList<>();
  4. list.add(23);
  5. // 编译不通过
  6. list.add("123");
  7. try {
  8. Method method = list.getClass().getDeclaredMethod("add", Object.class);
  9. // 因为泛型的类型擦除机制,通过反射可以设置非 Integer 的值
  10. method.invoke(list, "test");
  11. method.invoke(list, 42.9f);
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. }
  15. for (Object o : list) {
  16. System.out.println(o);
  17. }
  18. }
  19. }