泛型
什么是泛型
在强类型语言中,可以先不设置参数类型,用某个符号作为占位符.最后在运行时指定参数类型来替换.
为什么要使用泛型
- 动态化参数,代码编写可以更加灵活、复用性高
- 类型安全,避免手动的类型转换.保证在运行时出现的错误能提早放到编译时检查
- 解耦类或方法所使用的类型之间的约束
java的泛型
要求
java的泛型是从jdk1.5之后引入的.所以使用泛型
的最低要求是jdk1.5
为什么会在jdk1.5之后引入泛型
最主要的原因就是为了重写容器相关的类(Collection).如果没有泛型,都使用Object来代替,那么就会出现大量的类型转换与模板代码,复用性低.
使用场景
- 暂时不指定类型,而是稍后再决定具体使用什么类型
- 限制其类型,使类型需要保持一直
- 大量的样板重复代码,只是类型不同
泛型语法
在类上编写泛型
用<>表示泛型,T表示占位符类型,可以自定义不一定叫T.这样就可以先不指定类型,最后在调用时指定.
- 示例
public class Holder<T> {
// 先不指定类型
private T value;
public void set(T val) {
value = val;
}
public T get() {
return value;
}
public static void main(String[] args) {
// 在调用时指定类型
Holder<String> holder = new Holder<>();
holder.set("test");
System.out.println(holder.get());
}
}
在方法上编写泛型
摘自
Thinking in java
泛型设计的基本原则,如果泛型方法能够代替泛型类,应该尽量优先使用泛型方法.因为在类上定义泛型是全局的,在方法上定义作用在方法上,作用范围更小.这样就为类上预留了泛型参数,适合以后扩展.
- 示例
与类上定义泛型的语法不同的是,只要在返回值之前定义<T>,就能在方法上定义泛型
public class HolderUtils {
public static <T> T getHolder(Holder<T> holder) {
return holder.get();
}
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.set("test");
System.out.println(HolderUtils.getHolder(holder));
}
}
泛型的工作原理
从上面的例子中看出,感觉java的泛型就是在运行时指定类型就像 Holder<String> holder = new Holder<>();把指定的类型String 动态的替换成占位符T.实际本非如此.在java中泛型参数类型都会转成Object,擦除实际的类型.
- 下面是刚才例子的反编译的结果
javap -c Holder.class
Compiled from "Holder.java"
public class generics.Holder<T> {
public generics.Holder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void set(T);
Code:
0: aload_0
1: aload_1
// 传入的T转换成Object,类型被擦除
2: putfield #2 // Field value:Ljava/lang/Object;
5: return
public T get();
Code:
0: aload_0
// 返回的T也为Object,类型被擦除
1: getfield #2 // Field value:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class generics/Holder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String test
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
17: aload_1
18: invokevirtual #8 // Method get:()Ljava/lang/Object;
// 需要时类型检查,确保类型安全,然后强制类型转换
21: checkcast #9 // class java/lang/String
24: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
}
从上面的结果可以看出泛型的工作原理为
- 编译期检查类型
- 编译之后把实际类型替换成Object类型,参数实际类型
- 在需要时,编译器自动先做类型检查然后强制转成需要的类型
java 泛型的局限性
java的泛型不是纯粹的.在使用泛型时,因为擦除无法获取具体的类型.不能使用泛型来执行类型相关的例如
new instanceof
反射等操作.
// 因为获取不到真实的类型,转成Object类型了,则会出现以下几个问题
// 1. 泛型是不能new对象的
T t = new T(); // error
// 2. 泛型不能用instanceof
t instanceof // error
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.set("test");
// 3.使用getClass().getTypeParameters() 只能获取占位符 [T], 而不是实际的类型
System.out.println(Arrays.toString(
holder.getClass().getTypeParameters()));
}
什么是擦除
java的泛型是通过擦除来实现的.在使用泛型时,任何类型信息都会被擦除.在泛型代码内部,无法获得有关泛型参数类型的信息.只能获取到定义泛型的占位标识符
使用擦除的原因
java的泛型不是从jdk1.0就已经存在的,泛型是从1.5开始的,需要向下兼容
老类库需要升级泛型的兼容性.例如有x、y类库, x 依赖于 y.
这时y升级使用了泛型,x没有使用泛型.那么势必不能对调用y的x产生影响.所以x应该不具备感知y使用泛型的能力.所以当依赖的类库使用了泛型,则不能对现有类库造成影响.所以泛型不是强制的,则类型信息必须被擦除.
如何解决擦除的问题
1. 定义泛型边界
因为泛型的擦除,我们获取不到实际的类型,可以通过泛型边界来获取实际的类型.如果定义边界,泛型将会擦除到第一个边界
语法
使用extends关键字,来定义边界,表示传入的类型必须是其边界的类型或者子类
例子:
public class HolderUtils {
public static <T extends Person> T getHolder(Holder<T> holder) {
T t = holder.get();
t.run();
return t;
}
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.set("test");
// System.out.println(HolderUtils.getHolder(holder));
// error 类型不匹配,已经限定了只能是Person以及Person的子类
Holder<Person> holderPerson = new Holder<>();
holderPerson.set(new Person());
System.out.println(HolderUtils.getHolder(holderPerson));
}
}
class Person {
public void run() {
System.out.println("run ...");
}
}
因为定义了边界所以擦除到第一个边界类型Person.
这样就不会使用Object来代替,就能够使用边界类型的方法与属性.
解决了一部分的擦除问题
2. 传入类型标识
通过传入类型标识Class<T> tClass来确保泛型类型,
使用 class.newInstance()来创建对象,t就能确定其类型信息,
能够执行类型相关的操作例如instanceof等.这样就彻底解决了擦除的带来的类型问题.
public class HolderUtils {
public static <T extends Person> T getHolder(Holder<T> holder) {
T t = holder.get();
t.run();
return t;
}
public static <T> T newHolder(Class<T> tClass) throws IllegalAccessException, InstantiationException {
T t = tClass.newInstance();
if (t instanceof Person) {
System.out.println("type is Person...");
}
return t;
}
public static void main(String[] args) throws Exception{
Person person = newHolder(Person.class);
}
}
class Person {
public void run() {
System.out.println("run ...");
}
}
控制台输出:type is Person...
通配符
在讲通配符之前,先要弄清2个知识点
- 什么是泛型容器类型
- 什么是泛型持有类型
泛型容器类型是可以自动向上转型的
// 泛型容器类型
List list = new ArrayList();
ArrayList -> List OK
表示ArrayList是List的某一种类型
泛型持有类型是不支持向上转型的
class Fruit {}
class Apple extends Fruit {}
// 泛型持有类型
List<Fruit> fruit = new ArrayList<Apple>();
ArrayList -> List error 编译不通过
虽然Apple能够自动向上转型为Fruit,但是装有苹果的篮子不代表就能装水果.当泛型容器类型定义持有对象类型时,容器类型就不能向上转型.
为什么容器类型定义持有对象时就不能自动向上转型了
主要原因是会出现类型转换错误的问题,泛型的目的就是为了让运行期出现的问题,放到了编译器处理.增加了代码的健壮性,避免了类型转换错误.
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
// 数组可以编译通过,但是在运行期出现异常
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
try {
fruit[0] = new Orange();// ArrayStoreException
} catch(Exception e) { System.out.println(e); }
控制台输出: java.lang.ArrayStoreException: generics.Orange
// 而泛型直接编译不通过,避免了运行期的类型转换错误
List<Fruit> fruit = new ArrayList<Apple>(); // 编译不通过
以上例子就说明了,当装有水果的篮子引用了只能装苹果篮子的实例,而去装橘子时,就会出现类型转换错误.而泛型的好处就是从运行期的错误提前到了编译器.
那有什么办法可以让泛型容器在定义了持有对象时即能够向上转型又能够保证类型安全呢?这时候就可以定义通配符
什么是通配符
通配符就是定义泛型容器的上下界,使其泛型容器类型可以做到类型安全的自动类型转换
通配符的语法
使用 <?> 来表示类型范围
上界
使用 <? extend Fruit> 来表示上界.表示该集合都继承于Fruit,都能返回Fruit的集合.所以读取该集合的元素是类型安全的.添加元素是类型不安全的.
List<? extends Fruit> fruit = new ArrayList<Fruit>();
Fruit fruit = fruit.get(0); // ok
Fruit apple = new Apple();
fruit.add(apple); // error
下界
使用 <? super Fruit> 来表示下界.表示该集合父类至少是Fruit的集合.所以添加元素是类型安全.读取元素是类型不安全的,只能返回Object.
List<? super Fruit> fruit = new ArrayList<Fruit>();
Fruit fruit = fruit.get(0); // error
Fruit apple = new Apple();
fruit.add(apple) // ok
无界
使用<?>来表示无界,表示该集合可以是任何类型,但是很遗憾,如果使用无界则不能添加元素,因为元素是任何类型,在读取时只能是Objcet,将丢失添加的类型,向下转型有风险.所以是类型不安全的.
List<?> list = new ArrayList<>();
Object o = list.get(0);
list.add(new String()); //error
list.add(new Object()); //error