1.什么是泛型
泛型程序设计(generic programming)是[程序设计语言]的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。
1.1 Java的泛型
Java 泛型的参数只可以代表类,不能代表个别对象。由于Java泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型,而且无法直接使用基本值类型作为泛型类型参数。Java编译程序在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快。
由于运行时会消除泛型的对象实例类型信息等缺陷经常被人诟病,Java及JVM的开发方面也尝试解决这个问题,例如Java通过在生成字节码时添加类型推导辅助信息,从而可以通过反射接口获得部分泛型信息。通过改进泛型在JVM的实现,使其支持基本值类型泛型和直接获得泛型信息等。
泛型,又叫参数化类型,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
泛型中我们常引入一个类型变量T(其他大写字母也行,不过常用的是T, E, K, V等),并且用<>括起来,并放在类名的后面,泛型类是允许有从个类型变量的。
按照约定,类型参数名称命名为单个大小写字母,以便可以在使用普通类或接口名称时能够容易地区分类型参数。以下是常用的类型参数列表。
E-元素,主要由Java集合(Collections)框架使用。
K-键,主要用于表示映射中的键的参数类型。
V-值,主要用于表示映射中的值的参数类型。
N-数字,主要表示数字。
T-类型, 主要用于表示第一个通用参数类型。
S-类型,主要用于表示第二个通用参数类型。
U-类型,主要用于表示第三个通用参数类型。
V-类型,主要用于表示第四个通用参数类型。
1.2 泛型类和泛型接口
可以为任何类、接口增加泛型声明
package sandwich.test8;
/**
* @author 公众号:IT三明治
* @date 2022/1/16
* 泛型类, 引入一个类型变量T(其他大写字母也行)
*/
public class Generic <T> {
private T data;
public Generic() {}
public Generic(T data) {
this();
this.data = data;
}
}
泛型接口与泛型类的定义基本相同
package sandwich.test8;
/**
* @author 公众号:IT三明治
* @date 2022/1/16
* 泛型接口
*/
public interface GenericInterface<T> {
public T data();
}
1.3泛型类和接口的使用
实现泛型接口的类,有两种实现方法
public interface Generator<T> {
public T getInfo();
}
对以上接口,分别用两种方法去实现
1、在new实例的时候指定类型
/**
* @author 公众号:IT三明治
* @date 2022/1/16
*/
public class GeneratorImpl<T> implements Generator<T> {
private T data;
public GeneratorImpl(T data) {
this.data = data;
}
@Override
public T getInfo() {
return data;
}
public static void main(String[] args) {
Generator<String> generator = new GeneratorImpl<>("IT Sandwich");
System.out.println(generator.getInfo());
}
}
2.在实现类中指定具体类型
/**
* @author 公众号:IT三明治
* @date 2022/1/16
*/
public class GeneratorImpl2 implements Generator<String>{
@Override
public String getInfo() {
return "IT Sandwich";
}
public static void main(String[] args) {
Generator generator = new GeneratorImpl2();
System.out.println(generator.getInfo());
}
}
这种方式new出类实例时,和普通类没有什么区别
不过有些代码检测工具的代码规范,还是建议你指定类型
改成以下
/**
* @author 公众号:IT三明治
* @date 2022/1/16
*/
public class GeneratorImpl2 implements Generator<String>{
@Override
public String getInfo() {
return "IT Sandwich";
}
public static void main(String[] args) {
Generator<String> generator = new GeneratorImpl2();
System.out.println(generator.getInfo());
}
}
1.4 泛型方法
/**
* @author 公众号:IT三明治
* @date 2022/1/16
*/
public class GenericMethodTest {
/***
* 泛型方法
* @param t
* @param <T>
* @return
*/
public <T> T getGenericValue(T t) {
return t;
}
public void sum(int x, int y) {
System.out.println(x+y);
}
public static void main(String[] args) {
GenericMethodTest genericMethod = new GenericMethodTest();
genericMethod.sum(1,2);
System.out.println(genericMethod.getGenericValue("IT Sandwich"));
System.out.println(genericMethod.getGenericValue(100));
}
}
泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。
2.为什么我们需要泛型
通过下面代码的使用我们就可以知道为什么我们需要泛型
/**
* @author 公众号:IT三明治
* @date 2022/1/16
*/
public class SumUtil {
public int sumInt(int x, int y) {
return x + y;
}
public float sumFloat(float x, float y) {
return x + y;
}
public <T extends Number> double sum(T x, T y) {
return x.doubleValue() + y.doubleValue();
}
/**
* 多个不同泛型的情况
* @param x
* @param y
* @param <T>
* @param <S>
* @return double
*/
public <T extends Number, S extends Number> double sum2(T x, S y) {
return x.doubleValue() + y.doubleValue();
}
public static void main(String[] args) {
SumUtil sumUtil = new SumUtil();
//不使用泛型
System.out.println(sumUtil.sumInt(1,2));
System.out.println(sumUtil.sumFloat(1.11f, 1.22f));
//使用泛型
System.out.println(sumUtil.sum(1.111d, 1.222f));
System.out.println(sumUtil.sum2(1.111d, 1.222f));
System.out.println(sumUtil.sum(1, 2));
}
}
泛型的好处是
- 适用于多种数据类型执行相同的代码
- 泛型中的类型在使用时指定,不需要强制类型转换
3.虚拟机是如何实现泛型的
3.1泛型擦除
Java语言中的泛型,它只是在程序源码中存在,在编译后的字节码文件中,就已经替换为原生类型(Raw Type),并且在相应的地方插入了强制类型转型代码。因此,对于运行期的Java语言,ArrayList和ArrayList就是同一个类,所以泛型技术实际上是Java语言的一个语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
将一段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,会发现泛型不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型。
/**
* @author 公众号:IT三明治
* @date 2022/1/16
*/
public class GenericClean {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("Author", "IT Sandwich");
System.out.println(map.get("Author"));
}
}
再看反编译后的class
上面的泛型类型变成了原生类型
3.2 泛型使用注意事项
上面这段代码是不能被编译的,因为参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两种方法的特征签名变得一模一样(在IDEA中是不行的,但是jdk的编译器却可以,因为jdk是根据方法返回值+方法名+参数来区分方法是否重复的)
3.3弱记忆
JVM版本兼容问题:JDK1.5以前,为了确保泛型的兼容性,JVM除了擦除,其实还是保留了泛型信息(Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息):弱记忆。
从Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。