写点什么

Java 核心技术之泛型详解

  • 2022 年 7 月 19 日
  • 本文字数:7781 字

    阅读完需:约 26 分钟


前言:📫 作者简介:小明java问道之路,专注于研究计算机底层,就职于金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的设计和架构📫

🏆 Java 领域优质创作者、阿里云专家博主、华为云享专家🏆

🔥 如果此文还不错的话,还请👍关注点赞收藏三连支持👍一下博主哦

本文导读

什么是泛型?我们在工程代码中一定看过 T,K,V 等等,这个就是泛型了,那我们看看官网是怎么说的这个 @泛型(Generic)

When you take an element out of a  Collection , you must cast it to the type of element that is stored in the collection. Besides being inconvenient, this is unsafe. The compiler does not check that your cast is the same as the collection's type, so the cast can fail at run time.

Generics provides a way for you to communicate the type of a collection to the compiler, so that it can be checked. Once the compiler knows the element type of the collection, the compiler can check that you have used the collection consistently and can insert the correct casts on values being taken out of the collection.

官方这话是什么意思呢:当你从集合中取出元素时,必须将其强制转换为存储在集合中的元素类型。除了不方便,这是不安全的。编译器不会检查强制转换是否与集合的类型相同,因此强制转换可能会在运行时失败。

泛型提供了一种将集合的类型传递给编译器的方法,以便可以对其进行检查。一旦编译器知道集合的元素类型,编译器就可以检查您是否一致地使用了集合,并且可以对从集合中取出的值插入正确的强制转换。

官方这段晦涩的语言什么意思呢?总之就是一句话:泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。


一、什么是泛型

Java 泛型(Generic)是 J2SE1.5 中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

了解泛型概念之后的学习的目标是什么呢?

一、了解泛型的规则与类型擦除

二、了解类型和限制两种泛型的通配符

三、了解在 API 设计时使用泛型的方式(自定义泛型类、泛型接口、泛型方法)

四、掌握泛型的使用及原理。

五、掌握泛型在中间件或者开源框架里的应用

下面我们对这几个问题一一探讨

二、泛型的规则

JDK5.0 之前是没有泛型这个概念的,那么当时是怎么写代码的

import java.io.File;import java.util.ArrayList;/** * @author mac * @date 2020/10/31-11:05 */public static void main(String[] args) {   ArrayList arrayList = new ArrayList();   arrayList.add(1);   arrayList.add("a");   // 这里没有错误检查。可以向数组列表中添加任何类的对象   arrayList.add(new File("/"));   // 对于这个调用,如果将get的结果强制类型转换为String类型,就会产生一个错误   // Exception in thread "main" java.lang.ClassCastException: java.io.File cannot be cast to java.lang.String   String file = (String) arrayList.get(2);   System.out.println(file);}
复制代码


在 JDK5.0 以前,如果一个方法返回值是 Object,一个集合里装的是 Object,那么获取返回值或元素只能强转,如果有类型转换错误,在编译器无法觉察,这就大大加大程序的错误几率!

public static void main(String[] args) {    ArrayList<String> arrayList = new ArrayList<String>();    arrayList.add("a");    String s = (String) arrayList.get(0);    // 6、7行代码编译不通过,不会导致运行后才发生错误    arrayList.add(1);    arrayList.add(new File("/"));    String file = (String) arrayList.get(2);}
复制代码


从泛型的使用可以看出,泛型是一种类型约束,简而言之,泛型在定义类,接口和方法时使类型(类和接口)成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种使用不同输入重复使用相同代码的方法。区别在于形式参数的输入是值,而类型参数的输入是类型。

JDK 是在编译期对类型进行检查,提供了编译时类型的安全性。它为集合框架增加了编译时类型的安全性,并消除了繁重的类型转换工作。

public class Person {    int gender;}public class Driver extends Person {    String name;    int skilllevel;}public static void main(String[] ars) {   List<Person> ls = new Arraylist<>();   //这里会不会编译报错?   List<Driver> list = ls;}
复制代码


然而泛型的应用也不是没有坑,比如上述代码,可以看出编译报错,这是不允许子类型化的泛型规则——假设允许,那么是不是可以改成以下的情况,在 JDK 里所有的类都是 Object 的子类,如果允许子类

型化,那么 ls 里不就可以存放任意类型的元素了吗,这就和泛型的类型约束完全相悖,所以 JDK 在泛型的校验上有很严格的约束。

为了防止子类型化混乱,泛型有了通配符的概念

泛型中的通配符

无界通配符

在上述的泛型示例中,我们都是指定了特定的类型,至少也是 Object,假设有一种场景,你不知道这个类型是啥,它可以是 Object,也可以是 Person 那咋办?这种场景就需要用到通配符,如

下所示,通常采用一个?来表示。

public void addAll(Collection<?> col){    ...}
复制代码

上界通配符

基于上述的场景,加入我想限制这个类型为 Person 的子类,只要是 Person 的子类就都可以,如果泛型写成<Person> 那么只能强转如下所示,那么就失去了泛型的意义,又回到了最初的起点。这时候怎么办?

List<Person> list = new ArrayList<>();list.add(new Driver());Person person = list.get(0);Driver driver = (Driver) person; // 针对这种情况于是有了有界通配符的推出。
复制代码



// 在泛型中指定上边界的叫上界通配符<? extends XXX>public void count(Collection<? extends Person> persons) {}public void count2(Collection<Person> persons) {}public void testCount() {    List<Driver> drivers = new ArrayList<>();    // 符合上界通配符规则,编译不报错    count(drivers);    // 违反子类型化原则,编译报错    count2(drivers);    // 符合下界通配符原则,编译不报错    List<Person> persons = new ArrayList<>();}
复制代码

下界通配符

原理同上界通配符, 下界通配符将未知类型限制为特定类型或该类型的超类型,下限通配符使用通配符(' ? ')表示,后跟 super 关键字,后跟下限:<?super A>。



public void count3(Collection<? super Driver> drivers) {}public void testCount() {    //符合下界通配符原则,编译不报错    List<Person> persons = new ArrayList<>();    count3(persons);}
复制代码

通用方法与类型推断

通用方法

通用方法是指方法参数的类型是泛型,static 和非 static 的方法都可以使用,还有就是构造方法也可以使用。我们看具体的使用

/** * @author mac * @date 2020/10/31-12:24 * 定义一个bean类 */public class Pair<K, V> {    private K key; private V value;    public Pair(K key, V value) {        this.key = key; this.value = value;    }    public void setKey(K key) { this.key = key; }    public void setValue(V value) { this.value = value; }    public K getKey() { return key; }    public V getValue() { return value; }}public class Util {    // <K, V>通用方法入参类型    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {        return p1.getKey().equals(p2.getKey()) &&                p1.getValue().equals(p2.getValue()); // 使用Object中equals判断是否相等    }}    public static void main(String[] args) {        Pair<Integer, String> p1 = new Pair<>(1, "apple");        Pair<Integer, String> p2 = new Pair<>(2, "pear");        // JDK8之后可以这么写boolean same = Util.compare(p1, p2);        boolean same = Util.<Integer, String>compare(p1, p2);         System.out.println(same); // false    }
复制代码

类型推断

类型推断是 Java 编译器查看每个方法调用和相应声明以确定使调用适用的类型参数的能力。推理算法确定参数的类型,以及确定结果是否已分配或返回的类型(如果有)。最后,推理算法尝试找到与所有参数一起使用的最具体的类型。

/** * @author macfmc * @date 2020/10/31-12:39 */public class Box<U> {    U u;    public U get() { return u; }    public void set(U u) { this.u = u; }}public class BoxDemo {    public static <U> void addBox(U u, List<Box<U>> boxes) {        Box<U> box = new Box<U>();        box.set(u);        boxes.add(box);    }    public static <U> void outputBoxes(List<Box<U>> boxes) {        int counter = 0;        for (Box<U> box : boxes) {            U boxContents = box.get();            System.out.println("Box #" + counter + " contains [" + boxContents.toString() + "]");            counter++;        }    }    public static void main(String[] args) {        ArrayList<Box<Integer>> listOfIntegerBoxes = new ArrayList<>();        // JDK8可以使用 BoxDemo.addBox(Integer.valueOf(10), listOfIntegerBoxes);        BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);        BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);        BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);        BoxDemo.outputBoxes(listOfIntegerBoxes);    }}// 结果// Box #0 contains [10]// Box #1 contains [20]// Box #2 contains [30]
复制代码

那么泛型的概念原理和使用都了解了,泛型在 JVM 中是如何去解析的呢?

泛型擦除

我们看下面两段代码

public class Node {    private Object obj;    public Object get() { return obj; }    public void set(Object obj) { this.obj = obj; }    public static void main(String[] argv) {        Student stu = new Student();        Node node = new Node();        node.set(stu);        Student stu2 = (Student) node.get();    }}public class Node<T> {    private T obj;    public T get() { return obj; }    public void set(T obj) { this.obj = obj; }    public static void main(String[] argv) {        Student stu = new Student();        Node<Student> node = new Node<>();        node.set(stu);        Student stu2 = node.get();    }}
复制代码

我们将其分别编译后查看.class 字节码文件

public Node();        Code:           0: aload_0           1: invokespecial #1       // Method java/lang/Object."<init>": ()V           4: return    public java.lang.Object get();        Code:           0: aload_0           1: getfield    #2        // Field obj:Ljava/lang/Object;           4: areturn    public void set(java.lang.Object);        Code:           0: aload_0           1: aload_1           2: putfield    #2        // Field obj:Ljava/lang/Object;           5: return    public Node();        Code:           0: aload_0           1: invokespecial #1       // Method java/lang/Object."<init>": ()V           4: return    public java.lang.Object get();        Code:           0: aload_0           1: getfield    #2        // Field obj:Ljava/lang/Object;           4: areturn    public void set(java.lang.Object);        Code:           0: aload_0           1: aload_1           2: putfield    #2        // Field obj:Ljava/lang/Object;           5: return
复制代码

可以看到泛型就是在使用泛型代码的时候,将类型信息传递给具体的泛型代码。而经过编译后,生成的 .class 文件和原始的代码一模一样,就好像传递过来的类型信息又被擦除了一样。

类型擦除主要包括:一、通用类型的檫除:在类型擦除过程中,Java 编译器将擦除所有类型参数,如果类型参数是有界的,则将每个参数替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object。二、通用方法的擦除:java 编译器还会檫除通用方法参数中的类型参数

类型檫除的问题

桥接方法

类型檫除在有一些情况下会产生意想不到的问题,为了解决这个问题,java 编译器采用桥接方法的方式。先看个官方案例

// 泛型擦除前public class Node<T> {    public T data;    public Node(T data) { this.data = data; }    public void setData(T data) { this.data = data; }}public class MyNode extends Node<Integer> {    public MyNode(Integer data) { super(data); }    public void setData(Integer data) { super.setData(data); }}// 泛型檫除后public class Node {    public Object data;    public Node(Object data) { this.data = data; }    public void setData(Object data) { this.data = data; }}public class MyNode extends Node {    public MyNode(Integer data) { super(data); }    public void setData(Integer data) { super.setData(data); }}// 但是编译器会产生桥接方法public class MyNode extends Node {    public MyNode(Object data) { super(data); }    // Bridge method generated by the compiler    // 编译器产生的桥接方法    public void setData(Object data) { setData((Integer) data); }    public void setData(Integer data) { super.setData(data); }}
复制代码

堆污染

堆污染在编译时并不会报错,只会在编译时提示有可能导致堆污染的警告.,在运行时,如果发生了堆污染,那么就会抛出类型转换异常。Heap pollution(堆污染),,指的是当把一个不带泛型的对象赋值给一个带泛型的变量时,就有可能发生堆污染。

public static void main(String[] args) {    List lists = new ArrayList<Integer>();    lists.add(1);    List<String> list = lists;    // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String    String str = list.get(0);    System.out.println(str);}
复制代码

类型的限制

Java 泛型转换的事实:

虚拟机中没有泛型,只有普通的类和方法。所有的类型参数都用它们的限定类型替换。桥接方法被合成来保持多态。为保持类型安全性,必要时插入强制类型转换。

jdk 定义了 7 种泛型的使用限制:1、不能用简单类型来实例化泛型实例 2、不能直接创建类型参数实例 3、不能声明静态属性为泛型的类型参数 4、不能对参数化类型使用 cast 或 instanceof5、不能创建数组泛型 6、不能 create、catch、throw 参数化类型对象 7、重载的方法里不能有两个相同的原始类型的方法

1、不能用简单类型来实例化泛型实例

class Pair<K, V> {    private K key;    private V value;    public Pair(K key, V value) { this.key = key; this.value = value; }    public static void main(String[] args) {        // 编译时会报错,因为 int、char 属于基础类型,不能用于实例化泛型对象        Pair<int, char> p = new Pair(8, 'a');        // 编译不会报错        Pair<Integer, String> p2 = new Pair<>(8, "a");    }}
复制代码

2、不能直接创建类型参数实例

public static <E> void append(List<E> list) {        E elem = new E(); // compile-time error 编译报错        list.add(elem);    }    //作为解决办法,可以通过反射来创建    public static <E> void append(List<E> list, Class<E> cls) throws Exception {        E elem = cls.newInstance(); // OK        list.add(elem);    }
复制代码

3、不能声明静态属性为泛型的类型参数

/** *  类的静态字段是该类所有非静态对象所共享的,如果可以,那么在有多种类型的情况下,os到底应该是哪种类型呢? *  下面这种情况,os到底应该是Smartphone还是Pager还是TablePC呢 *  MobileDevice<Smartphone> phone = new MobileDevice<>(); *  MobileDevice<Pager> pager = new MobileDevice<>(); *  MobileDevice<TabletPC> pc = new MobileDevice<>(); */public class MobileDevice<T> {    //非法    private static T os;}
复制代码

4、不能对参数化类型使用 cast 或 instanceof

public static <E> void rtti(List<E> list) {        // 编译期会提示异常——因为 java 编译器在编译器会做类型檫除,于是在运行期就无法校验参数的类型        if (list instanceof ArrayList<Integer>) { }    }    // 解决方法可以通过无界通配符来进行参数化    public static void rtti(List<?> list) {        // 编译不会报错        if (list instanceof ArrayList<?>) { }    }
复制代码

5、不能创建数组泛型

// 编译器报错        List<Integer>[] arrayOfLists = new List<Integer>[2];        // 用一个通用列表尝试同样的事情,会出现一个问题        Object[] strings = new String[2];        strings[0] = "hi"; // OK        strings[1] = 100; // An ArrayStoreException is thrown.        Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed 缺少数组维        stringLists[0] = new ArrayList<String>(); // OK        // java.lang.ArrayStoreException: java.util.ArrayList but the runtime can't detect it.        stringLists[1] = new ArrayList<Integer>();
复制代码

6、不能 create、catch、throw 参数化类型对象

// 泛型类不能直接或间接的扩展 Throwable 类,以下情况会报编译错// Extends Throwable indirectlyclass MathException<T> extends Exception { } // compile-time error// Extends Throwable directlyclass QueueFullException<T> extends Throwable { } // compile-time error// 捕捉泛型异常也是不允许的    public static <T extends Exception, J> void execute(List<J> jobs) {        try {            for (J job : jobs) { }        } catch (T e) { // compile-time error        }    }// 但是可以在字句中使用类型参数class Parser<T extends Exception> {    public void parse(File file) throws T { }}
复制代码

7、重载的方法里不能有两个相同的原始类型的方法

// 因为类型檫除后,两个方法将具有相同的签名,重载将共享相同的类文件表示形式,并且将生成编译时错误。public class Example {    public void print(Set<String> strSet) { }    public void print(Set<Integer> intSet) { }}
复制代码

总结

代码中泛型的演变过程泛型的使用及为什么使用是基础算是会用,泛型的三种通配符的使用及使用规则通用方法的使用及类型推断是进阶算是了解,类型擦除及类型擦除的问题类型的使用限制是补充算是熟悉,能了解泛型在 JDK 源码中的常用 API 的设计方式算是精通。

发布于: 刚刚阅读数: 3
用户头像

物有本末,事有终始。知所先后,则近道矣 2020.03.20 加入

🏆CSDNJava领域优质创作者/阿里云专家博主/华为云享专家 📫就职某大型金融互联网公司后端高级工程师 👍专注于研究计算机底层/Java/架构/设计模式/算法

评论

发布
暂无评论
Java核心技术之泛型详解_Java_小明Java问道之路_InfoQ写作社区