0%

技术分享:Java 泛型

一、什么是泛型

Java泛型(generics)是JDK 5.0 引入的一个新特性,可以在编译时进行类型安全检查。泛型本质是将类型参数化,在输入不同类型的情况下复用代码,可以消除一部分强制类型转换。最常见的使用场景就是容器类,如集合类(java.util.Collection)、java.util.Optional等。

引入泛型的意义在于:

  • 适用于多种数据类型执行相同的代码(代码复用);

如果没有泛型,当我们要实现不同类型数字的加法的时候,就需要为每种类型重载一个add方法;有了泛型以后,就可以合并成一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 没有泛型

private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}

private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}

private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}

// 有泛型

private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
  • 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用泛型前,list中可以放任意类型(Object),使用时需要判断

List list = new ArrayList();
list.add("string");
list.add(100);
list.add(new User());

if (list.get(1) instanceof Integer) {
Integer i = (Integer)list.get(1); // 需要强制类型转换
......
}

// 使用泛型后,list中只能放String

List<String> list = new ArrayList<String>();
list.add(100); // 编译时报错

二、泛型的使用

泛型的使用主要有:泛型类、泛型接口、泛型方法三种方式。泛型类型参数一般用尖括号(<>)括起来,可以有多个,用逗号分割,如<T1, T2, ..., Tn>,常见的泛型参数名称有:

  • E - 元素 (常见于Java集合类)
  • K - 键
  • N - 数字
  • T - 任意类型
  • V - 值
  • S, U, V 等等

2.1 泛型类

泛型类的泛型参数部分放在类名后边。可以用如下格式定义一个泛型类:

1
class name<T1, T2, ..., Tn> { /* ... */ }

常见的泛型类如:java.util.Optionalorg.apache.commons.lang3.tuple.Pair。一般在创建实例的时候指定泛型,使用方式如下:

1
2
final Optional<User> optional = Optional.of(new User());
final Pair<Integer, User> pair = new Pair(1, new User());

2.2 泛型接口

泛型接口的定义与泛型类的定义类似。常见的泛型接口有:org.springframework.data.redis.core.RedisOperations<K, V>java.util.stream.Stream。使用方式有两种,一种是实现类依旧带泛型,在使用的时候指定;也可以在实现类中指定实际类,如:org.springframework.data.redis.core.RedisTemplate<K, V>org.springframework.data.redis.core.StringRedisTemplate

2.3 泛型方法

定义泛型方法时,需要在返回值的前边定义泛型,泛型可以用作返回值,也可以用作参数。格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 创建指定类型的实例
* @param c 类型
* @return 指定类型的实例
* @param <T> 泛型
*/
public static <T> T getInstance(Class<T> c) { /* ... */ }
| | | |
| | | * ------ 泛型 T 的类对象
| | * ------------ 泛型 T 的具体类型
| * ----------------------------- 方法返回值类型为 T
* -------------------------------- 声明次方法有一个泛型类型 T

泛型方法一般用于工具类,如com.fasterxml.jackson.databind.ObjectMapper中的public <T> T readValue(String content, Class<T> valueType) throws JsonProcessingException, JsonMappingExceptionjava.util.Optional中的静态方法等。在调用方法的时候将实际类型传入方法。

1
2
final Optional<User> optional = Optional.of(new User());
final User user = new ObjectMapper().readValue("{\"id\":1,\"name\",\"张三\"}", User.class);

2.4 泛型的范围

有时候我们会遇到一些泛型的隐式转换问题,这就涉及到了泛型的上下边界,可以将泛型类限定为一个范围而不是一个特定的类,如:

1
2
3
4
5
6
7
8
9
10
static class A {}
static class B extends A {}

private static void fun(final A a) {}
private static void funList(final List<A> a) {}

final B b = new B();
fun(b); // 可以正常调用
final List<B> bs = Arrays.asList(new B());
funList(bs); // 会报错

2.4.1 上下限

我们可以通过指定泛型的上下限来限制类型范围,如只能传入某个类的父类或子类。

1
2
3
4
5
6
7
8
// 上限
public static <T extends Number> T add(final T a, final T b) {}

// 下限
public static void fun(final List<? super String> list) {}
fun(new ArrayList<Integer>()); // 报错
fun(new ArrayList<>()); // 推断为String
fun(new ArrayList<Object>()); // Object是String的父类

小结[1]

1
2
3
4
5
6
7
8
9
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

2.4.2 多个限制

我们可以指定泛型同时满足多个类型,类型之间用&连接:

1
2
// 可序列化并且支持快速随机访问
public class TestList<T extends Serializable & RandomAccess> {}

三、 深入泛型

3.1 泛型擦除

Java为了兼容旧版本,通过"伪泛型"的策略来实现,也就是语法上支持泛型,但是在编译期间进行检查,之后会进行"类型擦除"(Type Erasure),将泛型替换为具体的类型。泛型擦除的原则有:

删除类型参数声明,即<>及其之间的内容;
根据泛型上下限推断并替换为实际类型,如果泛型为通配符?或者没有上限的,替换为Object,有上限的则根据子类替换原则取泛型最左边限定类型(即父类);
必要时插入强制类型转换来保证类型安全;
为保证擦除后的代码仍具有泛型的"多态性",可能会产生"桥接方法"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 擦除前
public class TypeErasure<T> {
private T value;
public T getValue() {
return value;
}
}
// 擦除后
public class TypeErasure {
private Object value;
public Object getValue() {
return value;
}
}

// 擦除前
public class TypeErasure<T extends Number> {
private T value;
public T getValue() {
return value;
}
}
// 擦除后
public class TypeErasure {
private Number value;
public Number getValue() {
return value;
}
}

3.1.1 如何证明泛型擦除了

通过查看字节码会发现,编译后的方法只有原始类型,而没有泛型。

1
2
3
4
5
6
7
public static <T extends Number> void fun(final T temp) { ...... }
// 编译后查看字节码方法表示为
public static fun(Ljava/lang/Number;)V

public static <T> void fun(final T temp) { ...... }
// 编译后查看字节码方法表示为
public static fun(Ljava/lang/Object;)V
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static <T extends Number> void funNN(final T temp) {
System.out.println(temp + "-Number");
}

public static <T> void funNN(final T temp) {
System.out.println(temp + "-Object");
}

Test.<Number>funNN(1);
Test.<Integer>funNN(2);
Test.<String>funNN("3");

// 输出:
1-Number
2-Number
3-Object

由于泛型在编译期间就擦除了,所以在运行时,可以通过反射绕过泛型检测。

1
2
3
4
5
6
7
8
9
final List<Integer> list = new ArrayList<Integer>();
list.add(1); // 这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 输出
1
asd

3.1.2 擦除后保留原始类型

泛型被擦除后,字节码中会使用对应的真实类型来表示,一般会使用限定的类型,如果没有限定,就用Object。

1
2
3
4
5
6
7
8
// 限定的类型是Number,则原始类型就是Number
public static <T extends Number> T add(final T x, final T y) {
return y;
}
// 没有限定,原始类型就是Object
public static <T> T add(final T x, final T y) {
return y;
}

要注意区分原始类型和泛型变量类型,在调用泛型方法的时候,既可以指定泛型变量类型,也可以不指定。在指定的情况下,对应的参数只能是指定类及其子类,不指定的情况下,泛型变量的类型为参数的共同父类的最小级,直到Object(类似于最小公倍数)。

1
2
3
4
5
6
7
8
9
10
11
12
public static <T> T add(final T x, final T y) {
return y;
}
// 不指定泛型
final int i = Test.add(1, 2); // 这两个参数都是Integer,所以T为Integer类型
final Number f = Test.add(1, 1.2); // 这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
final Object o = Test.add(1, "asd"); // 这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object

// 指定泛型
final int a = Test.<Integer>add(1, 2); // 指定了Integer,所以只能为Integer类型或者其子类
final int b = Test.<Integer>add(1, 2.2); // 编译错误,指定了Integer,不能为Float
final Number c = Test.<Number>add(1, 2.2); // 指定为Number,所以可以为Integer和Float

3.1.3 编译期检查

既然说泛型会在编译期擦除,那为什么当我们向一个String的List中添加Integer元素的时候会报错呢?泛型被擦除了,又是如何保证只能使用限定的类型的?

Java编译器在编译过程中,会先对泛型类型进行检查,然后将其擦除,最后再进行编译。这个检查只针对引用,不关注实际引用的对象。

1
2
3
4
5
6
7
8
9
List<String> list1 = new ArrayList();
list1.add("1"); // 编译通过
list1.add(1); // 编译错误
String str1 = list1.get(0); // 返回类型就是String

List list2 = new ArrayList<String>();
list2.add("1"); // 编译通过
list2.add(1); // 编译通过
Object object = list2.get(0); // 返回类型就是Object

上面第二个例子中List没有指定泛型,实际上相当于是Object,那么,我们可以指定为Object吗?答案是不可以。如果可以,泛型中的类型参数就都可以用Object表示,这样在用的时候就需要判断对象的真实类型是什么,并进行强制类型转换,但是,引入泛型的初衷不就是避免类型转换吗?

3.1.4 泛型的多态,桥接方法

在继承中指定泛型类型的时候,泛型擦除会造成多态冲突,Java的解决方案是自动生成桥接方法。比如说我们有如下两个类A和B,B在继承A的同时指定泛型为Date,并且重写了A类中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class A<T> {
private T value;

public T getValue() {
return this.value;
}

public void setValue(final T value) {
this.value = value;
}
}
public class B extends A<Date> {
@Override
public Date getValue() {
return super.getValue();
}

@Override
public void setValue(final Date value) {
super.setValue(value);
}
}

由于A类会在编译期间将泛型擦除为Object,所以A类编译后就会变成:

1
2
3
4
5
6
7
8
9
10
11
public class A {
private Object value;

public Object getValue() {
return this.value;
}

public void setValue(final Object value) {
this.value = value;
}
}

这样的话,子类中的getValue是一种普遍的写法,即协变;setValue方法类型是Date,而父类的类型是Object,就不是重写,而是重载了。Java通过一种特殊的方式来实现这种情况——桥接方法。可以查看B类的字节码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// class version 52.0 (52)
// access flags 0x21
// signature LA<Ljava/util/Date;>;
// declaration: B extends A<java.util.Date>
public class B extends A {

// compiled from: B.java

// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL A.<init> ()V
RETURN
L1
LOCALVARIABLE this LB; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x1
public getValue()Ljava/util/Date;
L0
LINENUMBER 6 L0
ALOAD 0
INVOKESPECIAL A.getValue ()Ljava/lang/Object;
CHECKCAST java/util/Date
ARETURN
L1
LOCALVARIABLE this LB; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x1
public setValue(Ljava/util/Date;)V
L0
LINENUMBER 11 L0
ALOAD 0
ALOAD 1
INVOKESPECIAL A.setValue (Ljava/lang/Object;)V
L1
LINENUMBER 12 L1
RETURN
L2
LOCALVARIABLE this LB; L0 L2 0
LOCALVARIABLE value Ljava/util/Date; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2

// access flags 0x1041
public synthetic bridge setValue(Ljava/lang/Object;)V
L0
LINENUMBER 3 L0
ALOAD 0
ALOAD 1
CHECKCAST java/util/Date
INVOKEVIRTUAL B.setValue (Ljava/util/Date;)V
RETURN
L1
LOCALVARIABLE this LB; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2

// access flags 0x1041
public synthetic bridge getValue()Ljava/lang/Object;
L0
LINENUMBER 3 L0
ALOAD 0
INVOKEVIRTUAL B.getValue ()Ljava/util/Date;
ARETURN
L1
LOCALVARIABLE this LB; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}

这里可能会有一点疑问,子类中同时存在Object getValue()Date getValue(),如果是我们自己写的Java代码,肯定是无法编译通过的,但是JVM字节码允许同时存在方法名相同、参数相同、返回值不同的多个方法。需要注意的是,Class.getDeclaredMethods()会返回类中所有的方法,包括桥接方法;而Class.getDeclaredMethod(String, Class...)只会返回一个实际的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (final Method method : B.class.getDeclaredMethods()) {
System.out.println(method.toGenericString());
}
System.out.println("========");
System.out.println(B.class.getDeclaredMethod("getValue").toGenericString());

// 输出
public static void B.main(java.lang.String[]) throws java.lang.NoSuchMethodException
public java.lang.Object B.getValue()
public java.util.Date B.getValue()
public void B.setValue(java.util.Date)
public void B.setValue(java.lang.Object)
========
public java.util.Date B.getValue()

3.1.5 基本类型不能作为泛型类型

因为泛型擦除后原始类型会变成Object,因此只能是Integer而不能是int。我们可以list.add(1)是因为Java基本类型的自动装箱拆箱。

3.2 泛型中的其他特性

3.2.1 泛型不能被直接实例化

泛型类型不能被直接实例化,是因为真实类型只有在运行时才能确定,在编译期无法确定,也就无法找到对应的字节码。

1
2
3
4
5
T test = new T(); // 编译报错
// 通过反射获取泛型实例
public <T> T getInstance(Class<T> clazz) throws InstantiationException, IllegalAccessException {
return clazz.newInstance();
}

3.2.1 泛型类中的静态方法和静态变量

泛型类中的静态方法和静态变量是不能直接使用泛型类中声明的泛型参数的。因为泛型类中的类型参数是在定义对象的时候指定的,而静态方法和静态变量是不需要创建对象就可以使用的。对象都没创建,泛型参数也就无法指定。静态方法如果需要用到泛型,就需要在定义方法的时候定义泛型参数。

1
2
3
4
5
6
7
8
9
10
11
public class Test<T> {    
public static T one; // 编译错误

public static T show(T one) { // 编译错误
return null;
}

public static <T> T look(T one) { // 这里使用的是方法前边定义的T,而不是类上边定义的T,因此是正确的。
return null;
}
}

3.2.2 泛型与异常

  • 不能抛出,也不能捕获泛型类对象。不允许用泛型类扩展Throwable。
  • 不能在cache子句中使用泛型变量。
  • 可以在声明中使用泛型。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BusinessException<T> extends RuntimeException { ...... } // 无法通过编译

try {
} cache (T t) { // 擦除之后会变成原始类型,和之后的重复
} cache (RuntimeException e) {}

public <X extends Throwable> T orElseThrow(final Supplier<? extends X> exceptionSupplier) throws X { // 这种是可以的
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}

3.2.3 如何获取泛型参数类型

只有在类实现的时候(包括匿名内部类等)指定的泛型才可以通过Class获取,其他情况只能通过增加一个Class<T>的参数来得到。

1
2
3
4
5
6
7
8
9
10
public class GenericClass<T> {
private T value;
}
final GenericClass<String> genericClass = new GenericClass<String>() {
};
final Type superclass = genericClass.getClass().getGenericSuperclass();
final Type type = ((ParameterizedType)superclass).getActualTypeArguments()[0];
System.out.println(type.getTypeName());
// 输出
java.lang.String

  1. Java基础 - 泛型机制详解

旺旺小学酥 微信

微信

旺旺小学酥 支付宝

支付宝