泛型是 JDK1.5 中引入的一个新特性,其本质是参数化类型,也就是说在泛型的使用过程中可以将操作的类型指定为一个参数,这种参数化行为可以发生在类、接口和方法中,分别称为泛型类、泛型接口和泛型方法。
为什么需要有泛型
拿 List 接口来说,在创建对应的 List 时可以指定对应的创建的集合类型,比如创建 Long 类型 List<Long> list = new ArrayList<>();
,或者 String 类型 List<String> list = new ArrayList<>();
但 在没有泛型之前,需要为不同的类型编写不同的 List 实现,所以泛型的出现减少了重复编码,使得编程变得更加的灵活。
泛型的使用方式
泛型的使用氛围三种,分别是泛型类,泛型接口和泛型方法
泛型类
泛型类的声明和非泛型类的声明不同之处在于,泛型类的声明需要在类型后添加类型声明部分,类型参数声明部门可以是一个或者多个类型参数,多个类型参数之前用逗号分隔,如 <T>
、<T,E>
;使用一个或多个类型作为其成员变量、方法参数或者返回值
package com.geekyous.demo.generics;
public class GenericsDemo<T> {
/**
* 声明参数类型变量
*/
private T t;
public GenericsDemo(T t) {
this.t = t;
}
/**
* 方法参数为参数类型的方法
*
* @param content 输出内容
*/
public void print(T content) {
System.out.println(content);
}
/**
* 返回值为参数类型的方法
*
* @return T
*/
public T getValue() {
return this.t;
}
public static void main(String[] args) {
GenericsDemo<String> stringGenerics = new GenericsDemo<>("this is string");
System.out.println(stringGenerics.getValue());
stringGenerics.print("hello word");
GenericsDemo<Integer> integerGenerics = new GenericsDemo<>(123);
System.out.println(integerGenerics.getValue());
integerGenerics.print(456);
}
}
泛型接口
泛型接口的使用和泛型类的使用相似,在泛型接口的声明中在接口名称后使用 <T>
添加类型参数,然后在接口方法中使用该类型参数
package com.geekyous.demo.generics;
/**
* 泛型接口
*
* @param <T>
*/
public interface IGenerics<T> {
void print(T content);
T get();
}
package com.geekyous.demo.generics;
/**
* 泛型接口实现
*
* @param <T>
*/
public class IGenericsImpl<T> implements IGenerics<T> {
private T t;
public IGenericsImpl() {
this.t = t;
}
@Override
public void print(T content) {
System.out.println(content);
}
@Override
public T get() {
return this.t;
}
}
泛型方法
泛型方法的使用是在方法的返回值之前添加类型参数声明
/**
* 定义一个泛型方法
*
* @param t 形参
* @param <T> 类型参数
*/
public <T> void print(T t) {
// do somethig
}
类型参数的命名
类型参数的命名可以为满足 java 命名规范的字符,但通常使用大写字母表示,常见的命名有:
- T :type 的首字母,一般表示非容器中的元素的类型参数
- E:Element 的首字母,一般表示容器中元素的类型参数
- K V:分别是 Key 和 value 的首字母,一般用于表示键值对中键和值的类型参数
- N :Number 的首字母,一般表示数字类型参数
泛型通配符
泛型通配符是泛型中的一种特殊语法,它用‘?’表示, 泛型允许创建类型参数,是的类、接口和方法可以处理相同逻辑的业务,而不需要编写重复的代码,简化了代码;而泛型通配符用在某些场景下不关心具体的泛型类型,只关心传入的参数和返回的参数范围,通配符是类型参数更加灵活使用的一种体现。
通配符使用
通配符通常用于方法参数中,当方法的入参中有泛型类或者泛型接口时,当我们无法确定具体的泛类参数类型时,可以使用通配符‘?’表示接收任意参数;通配符的使用方法有三种:
- 无界通配符
void add(GenericsDemo<?> e){}
,比如 add 方法作用添加一个 GenericsDemo 实例,此时无论是GenericsDemo 还是 GenericsDemo的实力都能正常添加。
- 上界限定通配符
上界限定通配符用 extends 关键字来限制传入的类型参数必须是相关类型的子类或者其本身,或者限定类型参数是某个接口的实现,例如void add(GenericsDemo<? extends Number> e){}
,限定传入的类型参数必须是 Number 的子类或者 Number 本身,此时无论是GenericsDemo 还是 GenericsDemo的实力都能正常添加,但无法添加GenericsDemo;注意这里 extends 既表示类的继承也表示接口的实现。如果有多重限定,可以使用‘&’,如 T 既继承 Number 又要实现 Runnable 接口,可以这么表示<T extends Runnable & Number>
,但是不能写成 <T extends Number & Runnable>
,类是要在接口之前的。
- 下届限定通配符
上界限定通配符用 super 关键字来限制传入的类型参数必须是相关类型的父类或者其本身,例如void add(GenericsDemo<? super Number> e){}
,限定传入的类型参数必须是 Number 的父类或者 Number 本身,此时无论是GenericsDemo 还是 GenericsDemo的实力都能无法添加,但GenericsDemo可以添加。
类型擦除
实际上泛型本质上是一种语法糖,在编译时编译器会进行泛型做类型检查,在编译成字节码后,泛型会被替换成相应的上界限定类型,如会替换成 Object,会替换成 Number,这种独特的机制称为类型擦除。如下编译后的 Anami.class 字节码文件中,成员变量、方法参数和返回值都是 Object 类型的了。
package com.geekyous.demo.generics;
public class Animal<T> {
private T t;
void print(T t) {
System.out.println(t);
}
T get() {
return t;
}
}
public class com.geekyous.demo.generics.Animal<T> {
public com.geekyous.demo.generics.Animal();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
void print(T);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
7: return
T get();
Code:
0: aload_0
1: getfield #19 // Field t:Ljava/lang/Object;
4: areturn
}
类型擦除的时期
综上,类型擦除的时期为编译期
类型擦除的意义
类型擦除的意义是为了向后兼容,也就是兼容不支持泛型的 JDK 版本。
类型擦除带来的限制
- 泛型只能是引用类型
正是由于有类型擦除的特性存在,所以 java 中的泛型只能支持引用类型,因为基本类型并不继承自 Object 类,所以无法进行类型擦除
- 不能使用 new T()
因为编译成字节码后,类型已经被擦除,因此在运行时 JVM 并不知道真正的 T 代表的是什么类型,也不能确定 T 中是否存在无参的构造方法,所以也就无法用 new T()来创建 T 对象。