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的设计方式算是精通。

目录
打赏
0
0
0
0
85
分享
相关文章
单位网络监控软件:Java 技术驱动的高效网络监管体系构建
在数字化办公时代,构建基于Java技术的单位网络监控软件至关重要。该软件能精准监管单位网络活动,保障信息安全,提升工作效率。通过网络流量监测、访问控制及连接状态监控等模块,实现高效网络监管,确保网络稳定、安全、高效运行。
91 11
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
93 7
智慧产科一体化管理平台源码,基于Java,Vue,ElementUI技术开发,二开快捷
智慧产科一体化管理平台覆盖从备孕到产后42天的全流程管理,构建科室协同、医患沟通及智能设备互联平台。通过移动端扫码建卡、自助报道、智能采集数据等手段优化就诊流程,提升孕妇就诊体验,并实现高危孕产妇五色管理和孕妇学校三位一体化管理,全面提升妇幼健康宣教质量。
47 12
SaaS云计算技术的智慧工地源码,基于Java+Spring Cloud框架开发
智慧工地源码基于微服务+Java+Spring Cloud +UniApp +MySql架构,利用传感器、监控摄像头、AI、大数据等技术,实现施工现场的实时监测、数据分析与智能决策。平台涵盖人员、车辆、视频监控、施工质量、设备、环境和能耗管理七大维度,提供可视化管理、智能化报警、移动智能办公及分布计算存储等功能,全面提升工地的安全性、效率和质量。
CRaC技术助力ACS上的Java应用启动加速
容器计算服务借助ACS的柔性算力特性并搭配CRaC技术极致地提升Java类应用的启动速度。
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
2496 2
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
113 7
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
192 1
【技术开发】接口管理平台要用什么技术栈?推荐:Java+Vue3+Docker+MySQL
该文档介绍了基于Java后端和Vue3前端构建的管理系统的技术栈及功能模块,涵盖管理后台的访问、登录、首页概览、API接口管理、接口权限设置、接口监控、计费管理、账号管理、应用管理、数据库配置、站点配置及管理员个人设置等内容,并提供了访问地址及操作指南。
java-ajax技术详解!!!
本文介绍了Ajax技术及其工作原理,包括其核心XMLHttpRequest对象的属性和方法。Ajax通过异步通信技术,实现在不重新加载整个页面的情况下更新部分网页内容。文章还详细描述了使用原生JavaScript实现Ajax的基本步骤,以及利用jQuery简化Ajax操作的方法。最后,介绍了JSON作为轻量级数据交换格式在Ajax应用中的使用,包括Java中JSON与对象的相互转换。
89 1

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等