写点什么

Java 泛型机制详解;这些你都知道吗?

  • 2022 年 4 月 23 日
  • 本文字数:3486 字

    阅读完需:约 11 分钟

public U getFirst() {return first;}public V getSecond() {return second;}}


改进后的 Pair 类可以这么使用:


Pair<String,Integer> pair = new Pair<String,Integer>("张三",100);

泛型的基本原理

看到上面的案例我们大概知道了一个简单的泛型如何定义,那么不禁会有一个疑惑,那就是泛型类型到底是什么呢?我们为什么一定要定义一个类型参数呢?熟悉 Java 多态特性的我们都知道,我们完全可以定义一个通用的父类类型,然后传递具体的子类型不也能实现这样的操作吗?同样的 Java 中也存在所有的类的基类--Object,如果我们直接使用 Object 不也可以吗?如下:


public class Pair {Object first;Object second;public Pair(Object first, Object second){this.first = first;this.second = second;}public Object getFirst() {return first;}public Object getSecond() {return second;}}


使用的时候的代码只要这么改动:


Pair minmax = new Pair(1,100);Integer min = (Integer)minmax.getFirst();//字段强制转换 Integer max = (Integer)minmax.getSecond();//字段强制转换


这样使用其实是可以的,事实上 Java 提供的泛型机制其实底层就是如此实现的。之所以这么设计,与 Java 当初设计的时候的 jvm 虚拟机编译机制有关系,要知道泛型设计的时候 Java 才到 Jdk1.4 版本,而我们都知道 Java 有编译器和 Java 虚拟机,编译器会帮我们把 Java 代码转换为**.Class**,虚拟机则是负责加载**.Class**,对于泛型类,Java 编译器会把泛型部分的代码转换为普通的代码,即和上面的 Object 类型接管一样,将类型的 T 进行擦除,替换为 Object,并且进行必要的类型的强制转换操作,所以在 Java 虚拟机执行 Java 字节码的过程中,其实和 Object 操作是一样的,并不知道泛型,也不存在泛型。那么既然泛型还是会转换为 Object,进行泛型擦除,Java 为什么要在 1.5 开始支持并设计出泛型机制呢?

泛型的好处

其实想要理解这点,我们不妨考虑一下,泛型的好处在哪?同时也去思考一下如果我们使用 Object 编程,缺陷会存在在哪?熟悉泛型的都知道,泛型有两个好处:


1.更好的安全性


2.更好的可读性


我们也知道 Java 语言在我们开发编译的阶段,ide 就会进行代码检查,当我们的语法出现问题的时候,ide 会在编译阶段就把错误标识出来,减少程序的潜在 Bug 数。但是我们不妨看下 Object 操作的代码:


Pair pair = new Pair("张三",1);Integer id = (Integer)pair.getFirst();String name = (String)pair.getSecond();


可以看出来,无论 id 是否为 Integer 类型,或者 name 是否为 String 类型,我们在编译阶段,由于类型为 Object,我们都会进行强制转换操作,在编译期这些操作都是语法合理的,并不会报错,但是如果这些字段中存在类型错误,也必须等到程序运行到这里才会提示 ClassCastException 异常,但是如果我们使用的是泛型机制,并且使用的时候标明了类型为 String 和 Integer,那么如果我们使用的类型不一致,在编译时已经报错,必须修改后才可以成功运行,如下:


Pair<String,Integer> pair = new Pair<>("张三",1);Integer id = pair.getFirst(); //编译错误 String name = pair.getSecond(); //编译错误


所以很明显的可以看出来,如果使用了泛型后,类的后缀添加对应的泛型类型,我们很明确的知道具体的类型是什么,提高开发的可读性,并且因为 ide 会做类型检查,所以安全性也会更高

泛型方法

当然泛型的作用域范围比较广,我们不仅可以定义在类/接口的申明上,我们也可以将泛型作用在方法上,与类的泛型相互隔离,实现更精细粒度的泛型操作。并且需要注意的是,一个类的泛型定义和方法的泛型定义并无直接关系,两者是相互独立的,即类的泛型可以定义为 T,而方法也可以定义为泛型 T,但是这两个 T 并不属于同一个。首先我们先看一个泛型方法的案例:


public static <T> int indexOf(T[] arr, T elm){for(int i=0; i<arr.length; i++){if(arr[i].equals(elm)){return i;}}return -1;}


可以看出来,indexOf 方法就是一个泛型方法,使用的时候,我们可以如下:


indexOf(new Integer[]{1,3,5}, 10)


同样的泛型方法拥有和泛型类一样的所有特性,也可以定义多个泛型参数在方法上,比如:


public static <U,V> Pair<U,V> createPair(U first, V second){Pair<U,V> pair = new Pair<>(first, second);return pair;}


但是与泛型类不同的是,使用的时候只需要传入确定类型的值即可,并不需要申明泛型类型后缀,如下:


createPair("张三",1);

泛型的上限界定

在前面的学习中我们都知道泛型擦除会转化为 Object 类型,但是我们能不能给 Object 的范围缩小呢?即限制泛型的父类类型上限是多少,在 Java 中其实是支持的,而泛型中支持这个上限界定是使用了 extends 关键字来表示的,当然这里的父类类型可以是接口、类或者类型参数,我们分别介绍下:

接口作为父类类型

比如我们开发中遇到一个场景,我们必须实现 Comparable 接口来实现动态的类型的比较,这个时候代码如下:


public static <T extends Comparable> T max(T[] arr){T max = arr[0];for(int i=1; i<arr.length; i++){if(arr[i].compareTo(max)>0){max = arr[i];}}return max;}


max 是泛型类型 T 的数组的对应下标的值,不过这么编写代码的话,会被编译器警告,因为 Comparable 接口本身也是个泛型接口,所以我们写的时候建议也去指定 Comparable 接口的泛型上界,修改如下:


public static <T extends Comparable<T>> T max(T[] arr){...................}


此种方式可以实现泛型类型的递归类型限制传递

上界为具体类

还记得我们上面的实例 Pair 类使用的泛型类型,我们可以实现一个子类:


public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {public NumberPair(U first, V second) {super(first, second);}}


当我们限制了对应的类型范围后,我们就可以把 first 和 second 变量作为 Number 类型进行处理了,比如我们内部有一个求和的方法:


public double plus(){return getFirst().doubleValue() + getSecond().doubleValue();}


所以当我们定义完后,我们的使用即为如下这样:


NumberPair<Integer, Double> pair = new NumberPair<>(10, 12.34);double sum = pair.plus();


可以看出来,限制了泛型类型范围后,编译器检查的会更严格,如果类型不对直接会报错,并且泛型擦除的时候转换的类型则为指定的范围上界的类型

泛型的通配符

上面我们提到了一些例子,就是使用了参数类型作为范围上界,但是这种写法比较繁琐,有木有更简化的写法呢?当然有,泛型支持通配符形式,可以简化范围上界的泛型写法,一个简单的通配符泛型如下:


public void addAll(DynamicArray<? extends E> c) {for(int i=0; i<c.size; i 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 ++){add(c.get(i));}}


可以看到当前的写法中 c 的类型是 DynamicArray<? extends E>类型,?表示通配符,<?extends E>表示有限定通配符,具体需要匹配泛型 E 或者 E 的子类型即可,至于具体是什么类型,完全可以未知,当我们使用的时候,代码如下:


DynamicArray<Number> numbers = new DynamicArray<>();DynamicArray<Integer> ints = new DynamicArray<>();ints.add(100);ints.add(34);numbers.addAll(ints);


这里 E 是 Number 类型的时候,?可以匹配为 DynamicArray<Integer>,那么通配符和范围上界指定的效果一样,这两者有什么区别呢?


1.<T extends E>写法仅限于用于定义类型参数,申明了一个类型参数 T(使用的时候必须指定泛型类型)


2.<? extends E>用于实例化类型参数,可以用于实例化泛型变量中的类型参数,只是当前类型可以是未知的,只需要知道范围上限,即属于泛型 E 的子类即可(使用的时候可以不指定泛型类型,或者直接传递子类类型即可)


那么我们什么时候使用通配符,什么时候需要定义类型参数范围呢?首先我们先来认知下通配符分类以及各类通配符的用法

无限定通配符

在泛型中,除了上述的有限定通配符以外,还有无限定通配符超类型通配符,我们首先来了解无限定通配符,使用无限定通配符实现一个简单的 DynamicArray 中查找元素,代码如下:


public static int indexOf(DynamicArray<?> arr, Object elm){for(int i=0; i<arr.size(); i++){if(arr.get(i).equals(elm)){return i;}}return -1;}


可以看到上述的泛型即使用了无限定通配符,当然此通配符也可以使用泛型类型 T 来代替,效果是相同的,不过无限定通配符使用起来更简洁,当然无论是上述的哪一种通配符,都有一个限制--只能读,不可以写入,我们先看例子:


DynamicArray<Integer> ints = new DynamicArray<>();DynamicArray<? extends Number> numbers = ints;Integer a = 200;numbers.add(a); //代码错误,不允许添加

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Java泛型机制详解;这些你都知道吗?_Java_爱好编程进阶_InfoQ写作社区