Generics

Java泛型

简述:

理解的泛型,其实是这样的:假如没有泛型,出现在泛型类型位置的就会是Object类,而程序员在使用该类型的时候再手动将类型强转一下,不仅代码啰嗦、而且有类型不匹配的crash可能。

而java1.5提供泛型同时为了兼容旧版,本质上也是用了Object,只不过编译器将.java编译成.class时进行“泛型擦除”时会把泛型类型替代成Object上限类型(字节码中就不存在泛型了),再自动地使用处前加上原始类型的强转,之后再加载到JVM中。而在编译期存在的泛型可以借由IDE有效的进行类型检测可读的扩展,防止类型不匹配。

//java伪泛型的设计原因主要为了兼容老版本的java。真泛型并非做不到,而是因为如果用真泛型(即类型保留),老程序都需要修改。

//另外泛型类中基础数据类型需要使用Integer、Long等封装类的原因也是因为“擦除后会把泛型类型替代成Object上限类型

而kotlin的泛型是跟java一样的,在编译时会被擦除。但是kotlin提供了新的特性可以保留类型,就是**内联函数+reified(ˈriːɪfʌɪ/)**,泛型实化,可以说是真泛型:内联函数(inline)会把方法体copy到调用处(即不会创建新的虚拟机栈帧),

1
2
3
inline fun <reified C : Activity> Context.startActivityKtx() {
startActivity(Intent(this, C::class.java))
}

协变:①协变父子关系一致子类也可以作为参数传进来→java<? extends Entity>上界通配符→kotlin<out Entity>

逆变:②逆变父子关系颠倒父类也可以作为参数传进来→java<? super Article>下界通配符→kotlin<in Entiry>

不变:③不变:只能

无限通配符<?> == java <? extend Object> == kotlin<*> == kotlin<out Any>

不变比如:

1
2
3
4
5
/**
* 支持添加和删除元素的通用有序元素集合。
* 参数:
* E - 列表中包含的元素的类型。可变列表的元素类型是不变的。 */
public interface MutableList<E> : List<E>, MutableCollection<E>

协变比如:

1
2
3
4
5
6
/**
* 通用的有序元素集合。该接口中的方法仅支持对列表的只读访问;通过MutableList接口支持读/写访问。
* 参数:
* E - 列表中包含的元素的类型。该列表的元素类型是协变的。
*/
public interface Collection<out E> : Iterable<E>

与普通的 Object 代替一切类型这样简单粗暴而言,

简述:泛型提供了可以使类型像参数一样由外部传递进来的扩展能力,同时还提供了编译时的类型检测机制,即当传入数据类型为泛型 时才能编译通过,提高了可读性。

但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

T 代表一般的任何类。
E 代表 Element 的意思,或者 Exception 异常的意思。
K 代表 Key 的意思。
V 代表 Value 的意思,通常与 K 一起配合使用。
S 代表 Subtype 的意思,文章后面部分会讲解示意。

泛型可以为类和方法分别定义泛型参数,

泛型类:

1
2
3
4
//尖括号 <>中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。
public class Test<T> {
T field1;
}

泛型方法始终以自己定义的类型参数为准

通配符的出现是为了指定泛型中的类型范围

通配符有 3 种形式。

  1. <?>被称作无限定的通配符。
  2. <? extends T>被称作有上限的通配符。
  3. <? super T>被称作有下限的通配符。

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除

在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限。

Java获取泛型的class方法:

一:在构造类时传递泛型类型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass<T> {
private Class<T> type;

public MyClass(Class<T> type) {
this.type = type;
}
}

public class Main {
public static void main(String[] args) {
MyClass<String> myObj = new MyClass<>(String.class);
myObj.printType(); // Output: Type: java.lang.String
}
}

二:反射获取

1
2
3
4
5
6
7
8
9
class MyClass<T> {
public void printType() {
Type superclass = getClass().getGenericSuperclass();
if (superclass instanceof ParameterizedType) {
Type[] typeArguments = ((ParameterizedType) superclass).getActualTypeArguments();
typeArguments[0]//泛型类型可能多个
}
}
}

匿名内部类在初始化的时候就会绑定父类或者父接口的信息,这样就能通过获取父类或父接口的泛型类型信息,来实现我们的需求

如Gson中的TypeToken或FastJson中的TypeReference,使用创建继承于特定抽象类的匿名内部类对象获得image-20240428144654122

kotlin 泛型

Kotlin 泛型系统继承于 Java泛型,依然是一种语法糖的伪泛型,会在编译时发生类型擦除。但可以通过inline+reified实现泛型实化的真泛型

Inline+reified 泛型实化的原理:

我们都知道内联函数的原理,编译器把实现内联函数的字节码动态插入到每次的调用点。那么实化的原理正是基于这个机制,每次调用带实化类型参数的函数时,编译器都知道此次调用中作为泛型类型实参的具体类型。所以编译器只要在每次调用时生成对应不同类型实参调用的字节码插入到调用点即可。 总之一句话很简单,就是带实化参数的函数每次调用都生成不同类型实参的字节码,动态插入到调用点。由于生成的字节码的类型实参引用了具体的类型,而不是类型参数所以不会存在擦除问题。

简述:由于inline的存在,泛型类型实参的调用处在同一个方法体内,相当于直接知道了类型,所以只要编译时直接生成对应的不同类型实参即可

实化类型参数函数的使用限制

这里说的使用限制主要有两点:

1、Java调用Kotlin中的实化类型参数函数限制

明确回答Kotlin中的实化类型参数函数不能在Java中的调用,我们可以简单的分析下,首先Kotlin的实化类型参数函数主要得益于inline函数的内联功能,但是Java可以调用普通的内联函数但是失去了内联功能,失去内联功能也就意味实化操作也就化为泡影。故重申一次Kotlin中的实化类型参数函数不能在Java中的调用

2、Kotlin实化类型参数函数的使用限制

  • 不能使用非实化类型形参作为类型实参调用带实化类型参数的函数
  • 不能使用实化类型参数创建该类型参数的实例对象
  • 不能调用实化类型参数的伴生对象方法
  • reified关键字只能标记实化类型参数的内联函数,不能作用与类和属性。

泛型类型获取方式

在Kotlin中可以通过下述方法获取泛型的类型

通过匿名内部类获得泛型参数类型

具体示例:

1
2
3
4
5
6
7
// Java 
ArrayList arrayList = new ArrayList<String>(){};
System.out.println(arrayList.getClass().getGenericSuperclass());

// Kotlin
val clazz = object :ArrayList<String>(){}// Kotlin 的匿名内部类
println(clazz.javaClass.genericSuperclass)

为什么可以通过匿名内部类可以在运行期获得泛型参数的类型呢?这是因为 泛型的类型擦除并不是完全的将所有信息擦除,而会 将类型信息放在所属 class 的常量池中,这样我们就可以通过相应的方式获得类型信息,而匿名内部类就可以实现这个功能。

Java 将泛型信息存储在何处:类信息的签名中。

匿名内部类在初始化的时候就会绑定父类或者父接口的信息,这样就能通过获取父类或父接口的泛型类型信息,来实现我们的需求,可以通过利用此来设计一个获得所有类型信息的泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
open class GenericsToken<T>{
var type : Type = Any::class.java
init {
val superClass = this.javaClass.genericSuperclass
type = (superClass as ParameterizedType).actualTypeArguments[0]
}
}

fun main(args: Array<String>) {
// 创建一个匿名内部类
val oneKt = object:GenericsToken<Map<String,String>>(){}
println(oneKt.type)
}

打印日志:

1
java.util.Map<java.lang.String, ? extends java.lang.String>

至于如果获得参数化类型,可参见此博客:ParameterizedType应用,java反射,获取参数化类型的class实例

其实正是因为类型擦除的原因,在使用 Gson 反序列化对象的时候除了制定泛型参数,还需要传入一个 class :

1
2
3
public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { 
...
}

因为 Gson 没有办法根据 T 直接去反序列化,所以 Gson 也是使用了相同的设计,通过匿名内部类获得相应的类型参数,然后传到 fromJson 中进行反序列化。

看一下在 Kotlin 中我们使用 Gson 来进行泛型类的反序列化:

1
2
3
val  json = "...."
val rType = object: TypeToken<List<String>>(){}.type// 获得反序列化的数据类型
val stringList = Gson().fromJson<List<String>>(json,rType)

当然可以直接传输数据类型:

1
2
// 存在局限,比如不能传入 List<String> 的数据类型
val stringList = Gson().fromJson<String::class.java>(json,rType)

在 Kotlin 中除了使用匿名内部类获得泛型参数外,还可以使用内联函数来获取。

使用内联函数获取泛型的参数类型

内联函数的特征:

内联函数(inline)在编译时会将具体的函数字节码插入调用的地方,类型插入相应的字节码中,这就意味着泛型参数类型也会被插入到字节码中,那么就可以实现在运行时就可以获得对应的参数类型了。

使用内联函数获取泛型的参数类型十分的简单,只要加上 reified 关键字,意思是:在编译时会将 具体的类型 插入到相应的字节码中,那么就可以获得对应参数的类型,与 Java 中的泛型在编译器进行类型擦除不同,Kotlin 中使用 reified 修饰泛型,该泛型类型信息不会被抹去,所以 Kotiln 中的该泛型为 真泛型

reified 为 Kotlin 中的一个关键字,还有一个叫做 inline,后者可以将函数定义为内联函数,前者可以将内联函数的泛型参数当做真实类型使用.

可以借此来为 Gson 定义一个扩展函数:

1
2
3
4
inline fun <reified T
: Any> Gson.fromJson(json: String): T{
return fromJson(json, T::class.java)
}

有了此扩展方法,就无须在 Kotlin 当中显式的传入一个 class 对象就可以直接反序列化 json 了:

1
2
3
4
5
class Person(var id: Int, var name: String) 
 
fun test(){
val person: Person = Gson().fromJson<User>("""{"id": 0, "name": "Jack" }""")
}

由于 Gson.fromJson 是内联函数,方法调用时插入调用位置,T 的类型在编译时就可以确定了,反编译之后的代码:

1
2
3
4
5
public static final void test() { 
Gson $receiver$iv = new Gson();
String json$iv = "{\"id\": 0, \"name\": \"Jack\" }";
Person person = (Person)$receiver$iv.fromJson(json$iv, Person.class);
}

这就是 Kotin 的泛型被称为 真泛型 的原因。

但是 refied 存在一个问题:reified 只能修饰方法,而当定义一个泛型类时,reified 是无法通过类似以上的方式获得泛型参数的,但是仍然可以通过其他方式获得泛型类中的泛型参数类型,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class View<T>(val clazz:Class<T>){
val presenter by lazy { clazz.newInstance() }
companion object{
// 在构造函数执行之前,执行了此处,真泛型的重载函数。
inline operator fun <reified T> invoke() = View(T::class.java)
}
}

class Presenter

fun main(args: Array<String>) {
// 两者等效,具体实现如下
val p = View<Presenter>().presenter
val a = View.Companion.invoke<Presenter>().presenter
}

这种写法特别适合在 android 中的 MVP,不用再在 Activity 中显式的显示 Presenter 的类名。

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

//接受多个类型参数。
public class MultiType <E,T>{
E value1;
T value2;

public E getValue1(){
return value1;
}

public T getValue2(){
return value2;
}
}

//泛型类与泛型方法的共存
public class Test1<T>{

public void testMethod(T t){
System.out.println(t.getClass().getName());
}
public <T> T testMethod1(T t){
return t;
}
}

//上述泛型类与泛型方法共存的testMethod1实际上其类型参数是自己在函数申明的pulic <T> 中,与 整个类的T没关系,
//为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。应该像下面这样写。
public class Test1<T>{

public void testMethod(T t){
System.out.println(t.getClass().getName());
}
public <E> E testMethod1(E e){
return e;
}
}
Author

white crow

Posted on

2021-12-31

Updated on

2024-05-06

Licensed under