重铸基础 | Java | 泛型,你真的掌握了嘛

870 阅读10分钟

A:今天来聊聊一些基础吧,简单说说泛型。

B:泛型不是一直在用嘛,很简单的啊,一对 <> 就能指定泛型,然后使用。

A:那你先讲讲什么叫泛型吧?

B:Emmm...(一时语塞,虽然一直在用,但是一时语塞,组织不起来语言)。

A:概念不清,意识模糊,只是停留在会用阶段,却不知道本质。

B:(假装一时忘记了而已)

A:既然经常使用泛型,那么应该知道泛型擦除机制吧。

B:这个我知道的,很简单嘛,为了兼容 JDK5 以前的版本。

A:既然在编译时,会有泛型擦除机制,那么在编译打包后,例如:Gson 又是怎么能正确解析成指定的泛型类呢?按照前面所说,泛型擦除了啊。

B:Emmm... 彻底蒙圈

泛型,作为一枚野生 Android 搬砖工,最常用的莫过于 泛型,在所有的架构中,泛型 是核心,所以说:泛型 是通往高阶的基础。哪怕你调用第三方框架再熟练,也只是个熟练的 API 调用者而已,出个问题就无法解决,只会甩锅给三方库,但是问题还是得不到解决。所以,在成长的路上,首先要彻底了解并掌握 泛型,只有这样,才能不至于在后期举步维艰。

1、先了解一下泛型是什么

先看一下 维基百科 的定义:

泛型的定义主要有以下两种:

  1. 在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表類,不能代表个别对象。(这是当今常见的定义)
  1. 在程序编码中一些包含参数的类。其参数可以代表类或对象等等。(现在人们大多把这称作模板)

在下定义之前,再了解一下参数分类,一般情况下,参数分为以下两类:

  1. 形式参数:输入是值;
  2. 类型参数:输入是类型;

接下来,结合代码进行简单介绍分析(已经了解的可跳过)。

1.1 形式参数

在工作中,经常编写的就是方法,例如:

// 其中,a、b 都是形式参数,传入的是已经构建好的对象(Java 一切皆对象)
public int add (int a, int b) { 
	return a + b;
}

根据方法本质来说,需要使用到传入的对象的某个值来进行相应的加工处理,这就是形式参数,简称 形参

1.2 类型参数

处理需要值以外,有时候还需要指定类型,例如:

List<String> list = new ArrayList<>();

<> 中,有一个类型 String,注意,是 类型 哟,而不是对象,至于原因吗,好好想想...再想想,这个 String 你实例化了吗,现在能理解 对象类型 的区别了吧,<String> 的目的就是为了告诉编译器,在这个 List 中,指定的是 String 类型的,只能对 String 类型的对象进行处理,不信的话你可以调用 list.add(0) 试试,看看会不会报错,至于原因,后续会讲。

由上可知,泛型是 类型化参数

2、为什么要使用泛型

  1. 在编译期进行更强的类型检查;
  2. 消除类型转换;
  3. 能够实现通用算法;

接下来,逐步进行解释:

2.1 在编译期进行更强的类型检查

Talk is cheap, show me the code. 先上一段毒代码:

// 一个用于存储 int 数据的集合
List list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add("4"); // 失误操作

for (int i = 0; i < list.size(); i++) {
	int num = (int) list.get(3);
	System.out.println("num is " + num);
}

由于没有类型约束,编译器不会报错,但是,在运行时,程序会直接崩溃,并报 ClassCastException,这要是发生在线上,就是生产事故了。那么如何在编译器就能检测出这个错误呢,这就该 泛型 登场了,修改代码如下:

// 一个用于存储 int 数据的集合
List list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add("4"); // 此次会直接报错,编译无法通过

int num4 = ist.get(3); // 同时,这里没有强转了

在约定类型后, list 只能添加对应的类型,也就是示例代码中的 Integer,如果传入 String,是无法通过编译的,这样就将错误由 运行时错误 提前为 编译时错误,并且在多人合作时,也不用担心其他小伙伴添加错误的类型对象。

2.2 消除类型转换

List list = new ArrayList<>();
list.add(1);
int num = (int) list.get(3); //由于 List 未指定类型,系统无法得知 get 后的类型,因此,需要进行强转

List list = new ArrayList<Integer>();
list.add(1);
int num = list.get(0); // 由于指定了类型 `Integer`,系统会推断出对应的类型,可以省去强转,也可避免强转成错误的类型,提高了代码的健壮性

2.3 能够实现通用算法

没有泛型以前的 add 世界:

public double add (int a, int b) {
	return a + b;
}
public double add (float a, float b) {
	return a + b;
}
public double add (double a, double b) {
	return a + b;
}
public double add (long a, long b) {
	return a + b;
}
public double add (byte a, byte b) {
	return a + b;
}
public double add (short a, short b) {
	return a + b;
}
// 如果根据排列组合来写,方法数量只会呈指数级增长

居然要写 6 个方法,才能实现兼容,使用泛型之后的世界:

public <N extends Number> double add(N a, N b) {
    return a.doubleValue() + b.doubleValue();
}

由于使用了泛型,方法数量减少为 1 个,实现了代码复用。因此,掌握好泛型,是通往高阶之路的基础。

3、泛型分类

  1. 泛型类
  2. 泛型接口
  3. 泛型方法
// 泛型类
public class Box<T> {
	public T t;
}
// 泛型接口
pubclic interface Box<T>{

}
// 泛型方法
public <T> void test(T t) {

}

4、常用的类型参数名称

  1. E --> Element
  2. K --> Key
  3. N --> Number
  4. T --> Type
  5. V --> Value
  6. S、U、V etc --> 2nd、3rd、4th types

只是一些共识,代表指定什么意思,跟着用就对了,如果你想特立独行,那...也不是不可以。

5、泛型扩展

在很多需求中,泛型 <> 中的类型可能不是一个确定的类型,那么能不能不指定具体类型,比如某一个类的父类,以便后续扩展呢接下来,答案是肯定的,接下来,将介绍一下 受限通配符非受限通配符,从而让泛型也具有可扩展性。

5.1 继承

5.1.1 接口的继承

interface GenericInterface<T>

/**
 * 接口继承方式一
 */
interface GenericChildInterface1<T> : GenericInterface<T>

/**
 * 接口继承方式二
 */
interface GenericChildInterface2 : GenericInterface<String>

比较简单,就不过多介绍了。

5.1.2 类的继承

open class GenericClass<T>

/**
 * 泛型继承方式一
 */
class GenericChildClass1<T> : GenericClass<T>()

/**
 * 泛型继承方式二
 */
class GenericChildClass2 : GenericClass<String>()

也比较简单,看一眼即可。

5.1.3 多继承

open class A
open class B

interface C
interface D

/**
 * 错误写法
 * 原因:注意单继承,这里 A、B 都是 class,同时继承多个 class,违背单继承原则,无法编译通过
 */
class MultipleGenericClass1<T : A, B, C, D>

/**
 * 错误写法
 * 原因:注意继承规则,如果存在 class,那么 class 一定要排在前面
 */
class MultipleGenericClass2<T : C, A, D>

/**
 * 正确写法
 */
class MultipleGenericClass3<T : A, C, D>

看注释即可。

5.2 通配符

5.2.1 受上限控制的通配符

语法格式:<? extends XXX>

优点:扩大兼容的范围。

List<Number> list1; // list1 只能匹配 Number 类型的数据
List<? exntends Number> list2; // list2 可以匹配 Number 类型的数据及其子类

5.2.2 受下限控制的通配符

语法格式:<? super XXX>

优点:扩大兼容的范围;可以建立泛型类、接口间的联系。

List<Integer> list1; // list1 只能匹配 Integer 类型的数据
List<? super Integer> list2; // list2 可以匹配 Integer 类型的数据及其祖类/接口

5.2.3 非受限的通配符

语法格式:<? >

关键使用场合:

  1. 一个方法,而这方法的实现可以利用 Object 类中提供的功能时泛型类中的方法不依赖类型参数时如 List.size() 方法,它并不关心 List 中元素的具体类型;
  2. List<XXX> 是 List<?> 的一个子类型理解 List<Object> 和 List<?> 的不同:差在 NULL 处理,前者不支持,而后者却可接受一个 null 入表;

泛型类/接口之间,没有任何关联,例如 List<Integer>List<Number> 没有任何关联,不过可以使用通配符来建立联系。

通配符只能在泛型方法中使用,泛型类、泛型接口无法使用。

通配符只能在泛型方法中使用,泛型类、泛型接口无法使用。

通配符只能在泛型方法中使用,泛型类、泛型接口无法使用。

重要的情况说 3 遍。

6、PESC 原则

PESC 原则的好处:提升了 API 的灵活性。

6.1 PE(Producer extends)

如果你只需要从集合中获得类型 T , 使用 <? extends T> 通配符。

public void test(List<? extends Number> list) {
    list.add(0); // 错误, 此处 list 作为一个生产者,后续操作只能消费,不能继续生产
}

6.2 SC(Consumer super)

如果你只需要将类型T放到集合中, 使用 <? super T> 通配符。

public void test(List<? super Number> list) {
    list.add(0); // 错误, 此处 list 作为一个消费者,后续操作只能生产,不能继续消费
}

当然,如果你既想当 生产者,又想当 消费者,那么不使用通配符即可。

7、类型擦除

在讲 类型擦除 之前,先来了解一下 泛型 历史。泛型是在 JDK5 才出现的,为了保证旧的 JDK 版本能正常运行,那么只能对 泛型 操刀了,将 泛型 处理成低版本能识别的信息,由此可见,Java/Kotlin 中的 泛型伪泛型

那么,如何兼容呢?类型擦除 就是在该场景下诞生的。

擦除规则:

编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,如果没有对类型参数做出限制,那么就替换为 Object 类型。(因此,编译出的字节码仅仅包含了常规类,接口和方法。)

在必要时插入类型转换以保持类型安全。

生成桥方法以在扩展泛型时保持多态性。

字节码如下,有兴趣的话可以研究研究:

// 在子类,泛型设置为 Integer,那么 setT 的参数就变为 Integer, 但是基类却不知道这个情况
public getT()Ljava/lang/Integer;
@Lorg/jetbrains/annotations/Nullable;() // invisible
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL com/wd/kt/generic/Box.getT ()Ljava/lang/Object;
CHECKCAST java/lang/Integer
ARETURN
L1
LOCALVARIABLE this Lcom/wd/kt/generic/IntegerBox; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// 由于父类不知道子类的情况,为了不出现父类编译错误,因此有 bridge 方法,将子类与父类类型关联起来
// access flags 0x1041
public synthetic bridge getT()Ljava/lang/Object;
L0
LINENUMBER 3 L0
ALOAD 0
INVOKEVIRTUAL com/wd/kt/generic/IntegerBox.getT ()Ljava/lang/Integer;
ARETURN
MAXSTACK = 1
MAXLOCALS = 1

8、泛型中的约束和局限性

8.1 不能实例化类型变量

/**
 * 错误
 * 原因:泛型中不能实例化类型变量
 */
private var t:T = T()

8.2 静态域中不能引用类型变量

/**
 * 错误
 * 原因:静态域中不能引用类型变量
 */
 private var t = T()

8.3 静态方法中不能引用类型变量

/**
 * 错误
 * 原因:静态方法中不能引用类型变量
 */
fun test(t: T) {

}

/**
 * 静态方法如果是泛型方法,那么可以正常使用
 */
fun <T> test(t: T) {
    
}

8.4 基础类型不能作为泛型实例化类型,包装类型可以

val list = arrayListOf<double>() // 错误写法,基础类型不能作为泛型实例化类型
val list = arrayListOf<Double>() // 正确写法,包装类型可以作为泛型实例化类型

8.5 泛型不能使用 instanceof 的

语法层面规定的,究其原因就是泛型擦除机制,在编译器泛型就不存在了,所以 instanceof 无效。

8.6 两个泛型类相同

val boxInt = Box<Int>();
val boxString = Box<String>();
System.out.println(boxInt.class == boxString.class); // true

因为泛型擦除机制,在泛型被擦除后,boxInt 与 boxString 的类型是相同的,都是 Box 类。

8.7 泛型可以声明数组,但是不能初始化泛型数组

Box<Double>[] boxs; // 没有异常
boxs = new Box<Double>[10]; // 错误,泛型数组不能初始化

8.8 泛型不能 extends Exception/Throwable*/

public <T extends Throwable> void test(T t) {
	try {
	
	} catch (T t) { // 错误,不能捕捉异常对象
	
	}
}

// 编写可以捕获异常代码
public <T extends Throwable> void test(T t) throws T {
	try {
	
	} catch (Throwable e) {
		throw t;
	}
}

8.9 Java 中 Arrays 不能使用泛型

/**
 * 错误示例
 * 在 Java 中,数组不支持泛型
 */
Arrays arrays = new Arrays<String>();

8.10 Java 中 List 与 List <?> 区别在哪

List:原始类型,不是泛型,所以不涉及到类型检查;

List <?>: 泛型,涉及到类型检查;

8.11 Java 中 List<Object> 与 List<?> 的区别在哪

List<?>:是一个未知类型的 List,可以把 List<String>、List<Integer> 等赋值给 List<?>;

List<Object>:是任意类型的 List,却不能把 List<String>、List<Integer> 等赋值给 List<Object>;

9、泛型区分

先定义一个泛型类。

public class TestBox<T> {

    public T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

}
public <T> void test() {
    // 原始类型
    TestBox box1 = new TestBox();
    // signature Lcom/wd/generic/TestBox<Ljava/lang/Object;>;
    // declaration: box2 extends com.wd.generic.TestBox<java.lang.Object>
    TestBox<Object> box2 = new TestBox<>();
    // signature Lcom/wd/generic/TestBox<*>;
    // declaration: box3 extends com.wd.generic.TestBox<?>
    TestBox<?> box3 = new TestBox<>();
    // signature Lcom/wd/generic/TestBox<TT;>;
    // declaration: box4 extends com.wd.generic.TestBox<T>
    TestBox<T> box4 = new TestBox<>();
    // signature Lcom/wd/generic/TestBox<+TT;>;
    // declaration: box5 extends com.wd.generic.TestBox<? extends T>
    TestBox<? extends T> box5 = new TestBox<>();
    // signature Lcom/wd/generic/TestBox<-TT;>;
    // declaration: box6 extends com.wd.generic.TestBox<? super T>
    TestBox<? super T> box6 = new TestBox<>();
}

10、泛型与反射

前面我们讲到了泛型擦除,那么我们常用的 Gson、FastJson 等框架,又怎么能正确转换成我们需要的泛型呢?

其实,泛型虽然擦除了,从字节码我们可以看出还是保留了关键信息:

signature: 指定了泛型对应的类型;

declaration:声明了原始代码信息;

既然保留的有信息,那么在使用时,通过特殊手段获取到该信息,那么就能继续正常使用泛型了,加下来结合代码看看是如何实现的。

class Test {

    val map = HashMap<String, Number>()

}

fun main() {
    val field = Test::class.java.getDeclaredField("map") // 获取 field
    val paramType = field.genericType as ParameterizedType // 获取 ParameterizedType,ParameterizedType 中存储的有泛型信息
    for (item in paramType.actualTypeArguments) { // 获取实际泛型信息,并打印
        println(item.typeName)
        // 打印结果为:
        // java.lang.String
        // java.lang.Number
    }

}

由此可见,虽然泛型擦除了,但是并不妨碍运行期获取泛型实际类型信息,方法总是有的,研究一下或许就会有收获。

最后,示例项目地址信息 示例代码,欢迎交流学习,如果错误,请指正。