Java泛型
1、什么是泛型
①定义
泛型可以适用很多很多不同的类型。泛型本质上是将类型参数化
。
泛型可以使用在类、方法、接口上面,分别叫做泛型类、泛型方法、泛型接口
②泛型优缺点对比
-
不使用泛型:
①类型不安全,不指定泛型默认的对象是Object,底层维护Object数组。并且错误只有在运行的时候才能发现异常。
②需要频繁的进行类型装换,甚至有时候不知道将对应的类型装换成什么类型。
-
使用泛型:
①类型是安全的,只能存指定指定泛型的对象。
②不需要进行强制装换,java编译器给进行了装换。
2、定义泛型
①泛型类
定义泛型类,在类名后面加一对尖括号并在尖括号中填写参数类型,参数可以有多个,多个参数使用逗号分隔。
常用约定泛型参数:
-
T:表示任意类型,type
-
E:集合中元素类型,element
-
K:键值对中的key
-
V:键值对中的value
通常地,泛型参数我们使用单个大写的字母表示泛型。
//泛型类
public class GenericClass<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
@Test
public void testGenericClass() {
GenericClass<String> g = new GenericClass<>();
g.setValue("张三");
//g.setValue(23); 错误的,不能指定string类型之外的类型
}
②泛型接口
//泛型接口
public interface GenericInterface<T> {
T show(T data);
void sayHello(T say);
}
//指定的泛型类型,替换了原先泛型参数T的位置
class impl implements GenericInterface<String> {
@Override
public String show(String data) {
return data;
}
@Override
public void sayHello(String say) {
System.out.println("say:" + say);
}
}
③泛型方法
泛型方法可以定义在泛型类之中也可以定义在普通类中。
泛型方法在返回值前面,修饰符后面添加尖括号并在其中添加参数类型。
泛型方法通常遵循的原则:如果可以定义泛型方法,那就定义泛型方法,那就不要把整个类定义成泛型类。
//普通方法中定义泛型方法
public class GenericMethod {
//需要注意的是,在泛型类中,静态方法中的泛型参数和泛型类没有一毛钱关系
public static <T> void fa(T value) {
System.out.println("泛型方法1");
}
public <T> void fb(T value) {
System.out.println("泛型方法2");
}
public <T> T fc(T value) {
System.out.println("泛型方法3");
return (T) value;
}
//限制T的类型,只能是继承自Number的类型
public <T extends Number> void fd(T value) {
System.out.println("泛型方法4");
}
//...
}
3、类型擦除
什么是类型擦除呢?类型擦除就是将指定泛型的类型替换到泛型参数的位置。没有指定泛型的类型默认使用Object替换。类型擦除对于JVM来说,根本就没有泛型的概念,只有具体的类。
编译时,对于泛型类:
第一步:去掉类名后的尖括号和尖括号中的参数,剩下的名字就是对应的类名。
第二步:对于类中使用了类型参数的变量类型,如果没有类型限定的情况下,使用Object替换,如果有类型的类型,使用限定的类型替换。
下面使用jdk提供的工具反编译一下生成的代码,查看类型擦除后是什么内容,就以上面的泛型类为例子查看:
//cmd 下
javap -help -- 查看帮助手册
javap -s -c 文件名.class -- 反编译并查看
javap -c -s 反编译的类文件名.class
没有指定泛型类型 的情况下,擦除类型后的默认类型为Object:
然后我再反编译泛型类中的那个测试方法:
@Test
public void testGenericClass() {
//这次指定了泛型的类型是String类型
GenericClass<String> g = new GenericClass<>();
g.setValue("张三");
//g.setValue(23); 错误的,不能指定string类型之外的类型
}
注!编译出来的信息,括号里面的是参数,括号后面的是返回值类型。
后面又写了一个方法,使用指定泛型类型的形参传入一个默认的泛型类,像下面这样:
public class TestGeneric {
public static void main(String[] args) {
GenericClass g = new GenericClass();
}
public static String test(GenericClass<String> g) {
return g.getValue();
}
}
查看反编译文件:
注!编译器自动为我们进行了强制类型转换。
结论:
泛型类的类型转换与强制类型转换的消除,是由编译器偷偷做了。
4、类型通配符
什么是类型通配符呢?类型通配符是用在方法的参数列表里面的,当传入的参数不确定的时候可以使用?
来表示,?
代表的就是类型通配符。并且还可以指定类型的上限和下限:
上限 <? extends Number>:所有number的子类包括其本身。
下限<? super Integer>:所有integer的父类或者接口,包括其本身。
//不确定泛型参数的类型
public void useGeneric(GenericClass<?> g) {
g.getValue();
}
//将传入的泛型参数限制在Number的子类或者是子接口
public void useGeneric2(GenericClass<? extends Number> g) {
g.getValue();
}
//传入的泛型参数只能是Integer的父类或者是父接口
public void useGeneric3(GenericClass<? super Integer> g) {
g.getValue();
}
5、使用泛型注意
-
不能使用基本数据类型去实例化类型参数:存在基本数据类型与类类型强制转换的问题,因为他们之间不能强制转换。
//不能使用基本数据类型去实例化类型参数 //GenericClass<int> g = new GenericClass<>(); //但是,可以使用包装类 GenericClass<Integer> g = new GenericClass<>();
-
运行时的类型检查不适用于泛型:判断继承关系。对于JVM讲,没有泛型的概念,也就是说不存在这种类型。
//不能在运行过程中去判断,泛型相关 //if ("字符串类型" instanceof GenericClass<String>){}
-
不能实例化泛型类型的数组。
泛型设计的原则:如果 一段代码在编译的时候没有给出“未经检查的转换”的警告,则程序在运行时不会出现ClasscastException异常。
//不能创建泛型数组 //GenericClass<Integer>[] g2 = new GenericClass<>[5]; //但是这样就可以,咋也不敢问,咋也不知道为啥 GenericClass<Integer>[] g3 = new GenericClass[5]; //ok
-
不能实例化类型参数。
//这都是不被允许的 new T(); //no T.class; //no T[] t = new T[5]; //no
-
静态方法不能使用类上下文中定义的类型参数。但是和类的上下文没有一毛钱关系的类型参数可以使用。
//在静态方法中使用类上下文的T是不被允许的 public static void test(T value) { return (T) value; } //但是可以将静态方法定义成泛型方法,只不过此时的T并非彼时的T,他和类上下文没有关系,只不过起名为T而已 public static <T> void test(T value) { }
-
泛型在异常中的使用:泛型可以被当作异常抛出,但是不能用来捕捉。
//这样使用是不正确的 try { } catch (T e) { } //这样可以使用,不过应该没人这么用把,注意T必须是继承自Throwable或者是Exception,不然异常还是不能被允许这样使用的 public void fa(T e) throws Throwable { throw e; }
-
类型擦除冲突。方法重写的时候可能会出现类型擦除冲突,如equals(T a);总之就是类型擦除之后也不能和其他类型重复。
//不能这么用,因为类的所有父类都是Object,并且当前类继承了父类的equals方法,如果参数指定T,当类型擦除的时候,T可能会被替换为Object就会出现重复 public boolean equals(T o) { return false; } //如: @Override public boolean equals(Object o) { return false; }
-
另外一个泛型原则:想要支持类型擦除的转换,就需要强行限制一个类或者类型参数,不能同时成为两个接口类型的子类,并且这两个接口是同一个接口的不同参数化。
//一个类,实现了同一个接口的不同参数化,这样是不被允许的,这样就需要限制一个类,或者是一个类型参数 public class TestClass implements GenericInterface<String>, GenericInterface<Integer>{ }