Java核心技术之泛型详解

简介: Java核心技术之泛型详解,没看过官网,不知道类型擦除会产生的问题还敢说自己了解泛型,原理、源码、实战有了解吗

image.png

前言:📫 作者简介:小明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);
}

image.gif

在 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);
}

image.gif

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

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;
}

image.gif

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

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

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

泛型中的通配符

无界通配符

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

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

public void addAll(Collection<?> col){
    ...
}

image.gif

上界通配符

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

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

image.gif

image.png

// 在泛型中指定上边界的叫上界通配符<? 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<>();
}

image.gif

下界通配符

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

image.png

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

image.gif

通用方法与类型推断

通用方法

通用方法是指方法参数的类型是泛型,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
    }

image.gif

类型推断

类型推断是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]

image.gif

那么泛型的概念原理和使用都了解了,泛型在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();
    }
}

image.gif

我们将其分别编译后查看.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

image.gif

可以看到泛型就是在使用泛型代码的时候,将类型信息传递给具体的泛型代码。而经过编译后,生成的 .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); }
}

image.gif

堆污染

堆污染在编译时并不会报错,只会在编译时提示有可能导致堆污染的警告.,在运行时,如果发生了堆污染,那么就会抛出类型转换异常。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);
}

image.gif

类型的限制

Java泛型转换的事实:

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

jdk定义了7种泛型的使用限制:
1、不能用简单类型来实例化泛型实例
2、不能直接创建类型参数实例
3、不能声明静态属性为泛型的类型参数
4、不能对参数化类型使用cast或instanceof
5、不能创建数组泛型
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");
    }
}

image.gif

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);
    }

image.gif

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;
}

image.gif

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<?>) { }
    }

image.gif

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>();

image.gif

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

// 泛型类不能直接或间接的扩展 Throwable 类,以下情况会报编译错
// Extends Throwable indirectly
class MathException<T> extends Exception { } // compile-time error
// Extends Throwable directly
class 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 { }
}

image.gif

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

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

image.gif

总结

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

相关文章
|
19天前
|
JSON 前端开发 JavaScript
java-ajax技术详解!!!
本文介绍了Ajax技术及其工作原理,包括其核心XMLHttpRequest对象的属性和方法。Ajax通过异步通信技术,实现在不重新加载整个页面的情况下更新部分网页内容。文章还详细描述了使用原生JavaScript实现Ajax的基本步骤,以及利用jQuery简化Ajax操作的方法。最后,介绍了JSON作为轻量级数据交换格式在Ajax应用中的使用,包括Java中JSON与对象的相互转换。
33 1
|
26天前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
31 7
|
28天前
|
移动开发 前端开发 Java
过时的Java技术盘点:避免在这些领域浪费时间
【10月更文挑战第14天】 在快速发展的Java生态系统中,新技术层出不穷,而一些旧技术则逐渐被淘汰。对于Java开发者来说,了解哪些技术已经过时是至关重要的,这可以帮助他们避免在这些领域浪费时间,并将精力集中在更有前景的技术上。本文将盘点一些已经或即将被淘汰的Java技术,为开发者提供指导。
59 7
|
24天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
40 3
|
24天前
|
SQL 监控 Java
Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面
本文探讨了Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,以实现高效稳定的数据库访问。示例代码展示了如何使用HikariCP连接池。
14 2
|
26天前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
27 4
|
24天前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
22 1
|
24天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
37 1
|
26天前
|
SQL Java 数据库连接
打破瓶颈:利用Java连接池技术提升数据库访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,避免了频繁的连接建立和断开,显著提升了数据库访问效率。常见的连接池库包括HikariCP、C3P0和DBCP,它们提供了丰富的配置选项和强大的功能,帮助优化应用性能。
44 2
|
29天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。
下一篇
无影云桌面