泛型是什么
- 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理 解呢?
- 顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形 参),然后在使用/调用时传入具体的类型(类型实参)。
- 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说 在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、 泛型方法。
- 引入一个类型变量 T(其他大写字母都可以,不过常用的就是 T,E,K,V 等等),并且用<>括起来,并放在类名的后面。泛型类 是允许有多个类型变量的。
泛型类
/**
* 泛型类
* 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
*/
public class NormalGeneric<T> {
private T data;
public NormalGeneric() {
}
public NormalGeneric(T data) {
this();
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
NormalGeneric<String> normalGeneric = new NormalGeneric<>();
normalGeneric.setData("Test");
System.out.println(normalGeneric.getData());
}
}
泛型接口
- 泛型接口与泛型类的定义基本相同
/**
*泛型接口
* 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
*/
public interface Generator<T> {
public T next();
}
- 而实现泛型接口的类,有两种实现方法:
- 未传入泛型实参时:
/**
* 实现泛型类,方式1
* 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
*/
public class ImplGenerator<T> implements Generator<T> {
private T data;
public ImplGenerator(T data) {
this.data = data;
}
@Override
public T next() {
return data;
}
}
在 new 出类的实例时,需要指定具体类型:
public static void main(String[] args) {
ImplGenerator<String> implGenerator = new ImplGenerator<>("Test");
System.out.println(implGenerator.next());
}
- 传入泛型实参
/**
* 实现泛型类,方式2
*/
public class ImplGenerator2 implements Generator<String> {
@Override
public String next() {
return "King";
}
}
在 new 出类的实例时,和普通的类没区别。
public static void main(String[] args) {
ImplGenerator2 implGenerator2 = new ImplGenerator2();
System.out.println(implGenerator2.next());
}
泛型方法
/**
* 泛型方法
* 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public class GenericMethod {
//泛型方法
public <T> T genericMethod(T t) {
return t;
}
//普通方法
public void test(int x, int y) {
System.out.println(x + y);
}
public static void main(String[] args) {
GenericMethod genericMethod = new GenericMethod();
genericMethod.test(13, 7);
System.out.println(genericMethod.<String>genericMethod("Test"));
System.out.println(genericMethod.genericMethod(180));
}
}
泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类
为什么我们需要泛型?
通过两段代码我们就可以知道为何我们需要泛型
/**
* 为什么需要泛型
*/
public class NeedGeneric {
public int addInt(int x,int y){
return x+y;
}
public float addFloat(float x,float y){
return x+y;
}
public static void main(String[] args) {
//不使用泛型
NeedGeneric needGeneric = new NeedGeneric();
System.out.println(needGeneric.addInt(1,2));
System.out.println(needGeneric.addFloat(1.2f,2.4f));
//使用泛型
System.out.println(needGeneric.add(3.2d,4.5d));
System.out.println(needGeneric.add(1,2));
}
//泛型方法
public <T extends Number> double add(T x,T y){
return x.doubleValue()+y.doubleValue();
}
}
实际开发中,经常有数值类型求和的需求,例如实现 int 类型的加法, 有时候还需要实现 long 类型的求和, 如果还需要 double 类型 的求和,需要重新在重载一个输入是 double 类型的 add 方法。
所以泛型的好处就是:
- 适用于多种数据类型执行相同的代码
- 泛型中的类型在使用时指定,不需要强制类型转换
虚拟机是如何实现泛型的?
-
Java 语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(RawType,也称为裸类 型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一 个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛 型。
-
将一段 Java 代码编译成 Class 文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了 Java 泛型 出现之前的写法,因为泛型类型都变回了原生类型
/**
* 泛型擦除
*/
public class Theory {
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
map.put("King","18");
System.out.println(map.get("King"));
}
}
使用泛型注意事项(作为了解)
上面这段代码是不能被编译的,因为参数 List<Integer>和 List<String>编译之后都被擦除了,变成了一样的原生类型 List<E>, 擦除动作导致这两种方法的特征签名变得一模一样(注意在 IDEA 中是不行的,但是 jdk 的编译器是可以,因为 jdk 是根据方法返回值+ 方法名+参数)。
- JVM 版本兼容性问题:JDK1.5 以前,为了确保泛型的兼容性,JVM 除了擦除,其实还是保留了泛型信息(Signature 是其中最重要的 一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类 型的信息)----弱记忆
- 另外,从 Signature 属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际 上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。