Java泛型使用及其技术演进
背景及概念
在没有泛型之前(Java5之前),Java中的集合类(如ArrayList、HashMap等)只能存储Object类型的对象,这使得在使用集合时需要进行强制类型转换,容易出现类型错误。
import java.util.ArrayList;
import java.util.Date;
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
//强制类型转换
String res = (String) list.get(0);
//十分不安全的行为
list.add(new Date());
}
}
为了解决这些情况,Java的设计者终于在Java5中引入泛型。具体的,使用类型参数(Type Parameter)来表示一个未知的类型。类型参数可以用任意标识符来表示,通常使用单个大写字母作为惯例,如T、E、K、U等。简单来说,引入泛型主要有以下两个好处:
-
类型安全:泛型使得在编译时就能够检测到类型错误,避免了在运行时出现类型转换异常。
-
代码重用:通过使用泛型,可以编写通用的代码,适用于多种不同类型的数据,提高了代码的灵活性和复用性。
基本用法
一般来说,使用泛型工具类很容易,但是自己编写会相对困难很多,设计者必须考虑的相当周全才能使自己的泛型类库比较完善,具备可拓展性(注:这就是抽象数据类型的过程)。
泛型类
泛型类是有一个或者多个类型变量的类,泛型类中的属性可以全都不是泛型,不过一般不会这样做,毕竟类型变量在整个类上定义就是用于指定方法的返回类型以及字段的类型。样例如下:
// 定义一个泛型类
public class GenericClass<T> {
// 类成员和方法定义
}
// 使用
GenericClass<Integer> singleInstance = new GenericClass<Integer>();
// 支持定义多个类型变量
public class MultiGenericClass<T, U> {
// 类成员和方法定义
}
// 使用
GenericClass<Integer, Long> multiInstance = new MultiGenericClass<Integer, Long>();
总结起来,定义泛型类的语法是在类名后面使用 <T> 或其他类型参数,并在类中使用这些类型参数。然后,可以通过指定实际类型来实例化泛型类,并可以使用泛型类中定义的成员和方法。
泛型方法
泛型方法是指具有泛型类型参数的方法。通过使用泛型方法,我们可以在方法级别上使用类型参数,使方法能够处理不同类型的数据,并提高代码的灵活性和复用性。
public class MultiGenericClass<T,U> {
private T value;
public static <T> T get(T... a){
return a[a.length-1];
}
public T getFirst(){
return value;
}
}
泛型方法可以在普通类,也可以在泛型类中定义。
通配符以及PECS原则
通配符是一种特殊的类型参数,用于在泛型类或方法中表示未知类型或不确定的类型。它提供了更大的灵活性,尤其是当你不关心具体类型时或需要对多个类型进行操作时。有两种通配符可以使用:
有限制通配符(Bounded wildcard)
使用 extends 关键字指定上界或者使用 super 关键字指定下界,限制了泛型方法接受的参数类型范围。
// Father.java
public class Father {
}
// Son.java
public class Son extends Father{
}
// extends 泛型方法接受 Father 及其子类的参数
void myGenericMethod(List<? extends Father> list) {
list.add(new Father()); //编译错误
list.add(new Son()); //编译错误
// 允许读取。假设list非空。
Father father = list.get(0);
}
// super 泛型方法接受 Father 及其父类的参数
void myGenericMethod(List<? super Father> list) {
// 允许
list.add(new Father());
// 编译错误
Father father = list.get(0);
// 允许,读取时只能是Object
Object object = list.get(0);
}
总结:
<? extends T>上界通配符:无法向其中设置值,但是可以进行正常的取出。所以偏向于内容的读取。
<? super T>下界通配符:可以设置 T 类型及其父类型的对象,但取出的时候只能赋值给 Object,所以更偏向于内容的存入。
无限定通配符(Unbounded Wildcard)
使用问号 ? 表示,表示可以匹配任何类型(等同于Object)。
public void myGenericMethod(List<?> myList) {
Object object = myList.get(0); // 只能是Object
myList.add(new Object()); //编译错误
}
<?>无限定通配符,不能写入数据,读取时返回值也只能赋给 Object,只能通过强转成指定类型,集合了上面说的两种通配符的缺点,不推荐使用。

PECS原则( Prodcuer Extends Consumer Super)
就是对上述两种限定通配符的具体说明:
-
Producer Extends 生产者角色需要获取资源以供生产时,建议使用 extends上界通配符,要支持协变( 从父类向子类转化),因为使用了 extends 通配符的类型更适合获取资源。
-
Consumer Super 消费者角色需要存入资源以供消费时,建议使用 super下界通配符,要支持逆变( 从子类向父类转化),因为使用 super 通配符的类型更适合存入资源。
Collections的copy方法就非常好的印证了这一点:

类型擦除
虚拟机没有泛型类型对象
也就是说,所有对象在虚拟机中都属于普通类,这意味着在程序编译并运行后我们的类型变量会被擦除(erased)并替换为限定类型,擦掉类型参数后的类型就叫做原始类型(raw type)。
这里的替换规则个人理解为:“替换最近上界”,也就是无限定符修饰,则为顶级父类Object。如果有,则会替换为其指定的类型。最直观的示例如下,这就是类型擦除的体现:

法桥接
子类重写父类方法时,必须和父类保持相同的方法名称,参数列表和返回类型。那么问题来了,如果按照之前的思路来讲,当泛型父类或接口的类型参数被擦除了,那么子类岂不是不构成重写条件?(参数类型很可能变化)
擦除前:

擦除后:

为了解决这个事情,Java引入了桥接方法,为每个继承/实现泛型类/接口的子类服务,以此保持多态性,字节码如下:

其实现原理,就是重写擦除后的父类方法,并在其内部委托了原始的子类方法,巧妙绕过了擦除带来的影响。不仅如此,就算不是泛型类,当子类方法重写父类方法的返回类型是父类返回类型的子类时,编译器也会生成桥接方法来满足重写的规则。
相关技术演进
上述讲的是当前Java 8所能支持的泛型特性,掌握他们也仅仅是满足了当前的技术需求。在低头编码的同时,我觉得也需要抬头望路,每每想到这里就激动人心。
接下来大概说下泛型在JDK新版本演进过程:
Java 10:
局部变量的类型推断。
当我们申明具体类型的时候,Java 8可以这样写,只需要在左边申明类型即可:
List<String> list = new ArrayList<>();
Map<String, String> map = new HashMap<>();
而在Java 10中,对类型的推断进一步优化,如下图所示,编译器根据分配给变量的值推断出类型:
var list = new ArrayList<>();
var list = List.of(1, 2, 3);
对于var的使用还有几点要注意的:
-
定义的时候必须初始化。
-
只能用于定义局部变量。
-
不能用于定义成员变量、方法参数、返回类型。
-
每次只能定义一个变量,不能复合声明变量。
Java 11(LTS):
Java 11于2018年9月发布,Java 8后第一个长期支持版本。
Lambda 参数的局部变量语法。
Java11 开始允许开发者在 Lambda 表达式中使用 var 进行参数声明。
// 下面两者是等价的
Consumer<String> consumer = (var i) -> System.out.println(i); Consumer<String> consumer = (String i) -> System.out.println(i);
Java 12:
instanceof 模式匹配。
instanceof 主要在类型强转前探测对象的具体类型。
之前的版本中,我们需要显示地对对象进行类型转换。
Object obj = "我是字符串";
if (obj instanceof String) {
String str = (String) obj;
System.out.println(str);
}
新版的 instanceof 可以在判断是否属于具体的类型同时完成转换。
Object obj = "我是字符串";
if (obj instanceof String str) {
System.out.println(str);
}
Java 17(LTS):
Java17于2021年9月发布,Spring 6.x 和 Spring Boot 3.x 最低支持的就是 Java 17。
Switch自动模式匹配(预览功能,--enable-preview)。
对于 null 值的判断也进行了优化。
// Old code
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
// New code
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
case null -> System.out.println("null");
default -> o.toString();
};
}
Java 21(LTS):
JDK 21 于 2023 年 9 月 发布,这是一个里程碑式、非常重要的版本。
1、Switch自动模式匹配(正式发布)。
2、未命名模式和变量(预览)。
未命名模式和变量使得我们可以使用下划线 _ 表示未命名的变量以及模式匹配时不使用的组件,旨在提高代码的可读性和可维护性。
未命名变量的典型场景是 try-with-resources 语句、 catch 子句中的异常变量和for循环。当变量不需要使用的时候就可以使用下划线 _代替,这样清晰标识未被使用的变量。
try (var _ = ScopedContext.acquire()) {
// No use of acquired resource
}
try { ... }
catch (Exception _) { ... }
catch (Throwable _) { ... }
for (int i = 0, _ = runOnce(); i < arr.length; i++) {
...
}
未命名模式是一个无条件的模式,并不绑定任何值。未命名模式变量出现在类型模式中。
// old code Color c is not used at all
if (r instanceof ColoredPoint(Point p, Color c)) { ... }
// new code, using _ to replace Color c
if (r instanceof ColoredPoint(Point p, _)) { ... p ... }
更多推荐




所有评论(0)