面试题五:Java泛型的实现机制是怎样的?

Java泛型的实现机制是怎样的?

一、面试官视角:这道题想考察什么?

  • 对Java泛型是否仅停留在集合框架的使用
  • 对Java泛型的实现机制的认知和理解
  • 是否有足够的项目开发实战和“踩坑”经验
  • 对泛型(或模版)编程是否有深入的对比研究
  • 对常见的框架原理是否有过深入剖析

二、题目剖析:

1、类型擦除从编译角度的细节

2、类型擦除对运行时的影响

3、类型擦除对反射的影响

4、对比类型不擦除的语言(C#)

5、为什么Java选择类型擦除

  • 类型擦除的好处:

    1、运行时内存负担小(因为编译之后都是一个类型,如List<String>类型,编译之后就是List类型)

    2、兼容性好(Jdk1.5才推出泛型,因此类型擦除是为了顾及Jdk1.0到1.4的版本)

  • 类型擦除的问题:

    1、基本类型无法作为泛型实参(就有了装箱和拆箱的开销问题)

    2、泛型类型无法用作方法重载(因为类型擦除了,不管形参是List<String> 还是 List<Integer>类型,编译之后都是List类型)

    3、泛型类型无法当做真实类型使用(因为类型擦除了,编译之后,所有T类型都会转换成Object类型,那么直接new 一个 Object类型的话,其实这不是我们想要的类型,而我们想要的是T类型的实际类型,但Java并不能new出来,因为Java根本不知道这个T类型到底是个啥)

    4、静态方法无法引用类泛型参数(因为类泛型参数只有在类实例化之后才知道,而静态方法根本不需要有类的实例,但静态方法可以声明泛型参数)

    1
    2
    3
    class GenericClass<T> {
    public static <R> R max(R a, R b) {...}
    }

    5、类型强转的运行时开销(因为类型擦除,在运行时,字节码会将类型进行强转,这样你才能使用具体类型的方法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    在Jdk < 1.5

    List strings = new ArrayList();
    strings.add("Hello");
    String value = (String)strings.get(0); // 在代码中需要手动将类型进行强转,同时在字节码中也会将类型进行强转

    在Jdk >= 1.5

    List<String> strings = new ArrayList<>();
    strings.add("Hello");
    String value = strings.get(0); // 在代码中我们使用了泛型,所以不需要手动将类型进行强转,但会在运行时的字节码中将类型进行强转,这是因为编译完成之后,类型被擦除了。

6、知识迁移:Gson.fromJson为什么需要传入Class?

1
2
3
4
public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
Object object = fromJson(json, (Type)classOfT);
return Primitives.wrap(classOfT).cast(object);
}

还是那句话,在Java中,泛型在编译之后,都会被擦除,你不传具体的类型,我怎么给你转成你想要的类型。

7、附加的签名信息(Signatures)

1
2
3
4
5
6
7
8
9
// SuperCalss有个泛型参数:T
class SupperClass<T> {}

// SubClass继承SuperClass,并将泛型参数确定为String
class SubClass extends SuperClass<String> {
public List<Map<String, Integer>> getValue() {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
通过反射获取类泛型信息:
ParameterizedType superType = (ParameterizedType)SubClass.class.getGenericSuperClass();
for(Type actualTypeArgument : superType.getActrualTypeArguments()) {
System.out.println(actualTypeArgument);
}

通过发射获取方法泛型信息:
ParameterizedType methodType = (ParameterizedType)SubClass.class.getMethod("getValue").getGenericReturnType();
for(Type type : methodType.getActrualTypeArguments()) {
System.out.println(type);
}

Tip:混淆时要保留签名信息
- keepattributes Signature

8、知识迁移:使用泛型签名的两个实例

1
2
3
4
5
6
Gson:

// TypeToken,本身是用protected声明的类,不能直接new,但是可以用来构造匿名内部类,匿名内部类为什么可以直接被new出来,是因为子类可以访问父类的构造方法
// getType,里面调用的就是getGenericReturnType(),从而可以获取实际的泛型类型
Type collectionType = new TypeToken<Collection<Integer>>(){}.getType();
Collection<Integer> ints = gson.fromJson(json, collectionType);
1
2
3
4
5
6
7
Retrofit:

interface GithubServiceApi {
// 通过接口返回的数据都是序列化后的信息,比如json、pb以及xml等,这些信息并没有携带类型的信息,而且我们知道Call<User>编译后,都会擦除类型,也就是说Call<User>会变成Call<Object>,那我们如何将其解析成对应的JavaBean呢?其实还是通过getGenericReturnType()进行发射,从而获取实际的泛型类型。
@Get("users/{login}")
Call<User> getUserCallback(@Path("login")String login);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Kotlin反射的实现原理:

class HelloWorld

public final class com/zane/demo/HelloWorld {
//access flags 0x1
public <init>()V

...

@LKotlin/Metadata;(mv={1,1,13}, bv={1,0,3}, k=1, d1={"\u0000\u000c\n\u0002 ...."}, d2={"Lcom/zane/demo/HelloWorld;", "", "()V", "Chapter1_JavaBasics_main"})
//compiled from: HelloWorld.kt
}

Kotlin是通过Metadata注解来实现反射的。

Tip:混淆时要保留签名信息(如果没有用到反射,就不需要注意了)
- keep class kotlin.Metadata {*;}

三、题目结论:

  • Java泛型采用类型擦除实现(Java的实现机制就是类型擦除)
  • 类型编译时被擦除为Object,所以不兼容基本类型
  • 类型擦除的实现方案主要考虑后向兼容
  • 泛型类型签名信息特定场景下反射可获取