泛型
术语意思:是适用于许多的类型,即使代码可以应用于多种类型。
目的:希望类或者方法能够具备最广泛的表达能力,并且将错误检测移入到编译 期间。
核心概念:告诉编译器想使用什么类型,然后编译器帮你处理好一切细节。
举个例子:
void test(){
List<User> listTest = new ArrayList<User>();
listTest.add(new User());//正确
listTest.add(new Dog());//错误 在编译期间就提示出来 所以细节由编译器处理
}
定义带类型参数的类
public class Test<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
Test<String> t =new Test<String>();
t.setT("test");//只能放String类型 不然就报错
System.out.println(t.getT());
}
}
定义带类型参数的方法
泛型方法可以使得该方法能够独立于类而产生变化。一个基本原则导论:在能够使用泛型方法的时候尽量使用泛型方法,因为他可以让事情更加清楚明白。
要定义泛型方法,只需要将泛型参数列表置于方法返回值之前就好,例子:
public class Test {
public <T> void test(T x){
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
Test t =new Test();
t.test("输出 java.lang.String");
t.test(1);//输出 java.lang.Integer
t.test(1.1);//输出 java.lang.Double
t.test(1.1f);//输出 java.lang.Float
t.test(t);//输出 com.hlj.test.Test
t.test('c');//输出 java.lang.Character
t.test(true);//输出 java.lang.Boolean
}
}
类型参数推断
Test<String> t =new Test<String>();
这样写重复了泛型参数列表,即写了两次,可以编写一个工具类,利用类型参数推断的机制,简化代码。
public class New {
public static <K,V> Map<K,V> map(){
return new HashMap<K, V>();
}
public static <K> Set<K> set(){
return new HashSet<K>();
}
public static <K> List<K> list(){
return new ArrayList<K>();
}
public static void main(String[] args) {
List<String> list = New.list();//不用写两次<String>了
}
}
注意:
类型推断只对赋值操作有效,其他时候不管用。如果将一个泛型方法的返回值作为参数传递给另一个方法,编译器是不会执行类型推断的。因为调用泛型后,其返回值被赋给一个Object类型的变量。
static void f(List<String> list){}
public static void main(String[] args) {
List<String> list = New.list();//不用写两次<String>了
f(New.list());//编译失败
}
编译失败的原因就是New.list()返回的是List 和List不相符,而前面的赋值操作可以是因为前面写了List.
可以写成这样New.list()就不会报错了,这个叫做显示的类型说明。
擦除
Java泛型是通过使用擦除来完成的,这意味着当你使用泛型的时候,任何具体的参数类型都会被擦除,占位符T(一般是T 随便写)将会用Object代替(如果有边界的话就是用边界代替,待会说)。
如之前的代码
public class Test<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
擦除后
public class Test{
private Object t;
public Object getT() {
return t;
}
public void setT(Object t) {
this.t = t;
}
}
由上面的可以得知,例如
List和List在运行时是相同的类型即 List.getClass()==List.getClass()。
擦除的原因
因为泛型是在JDK5才引入的,所以擦除的核心动机就是为了向后兼容,也就是说让使用泛型的客户端可以调用没有使用泛型的类库。既要保证当前代码的合法性又要保持之前的含义向后兼容,擦除是唯一可行的办法,让泛型成为伪泛型。
擦除的边界
首先看个例子:
public class Test<T> {
private T t;
public Test(T t){
this.t = t;
}
public void f(){
t.f();//这里会报错
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
Test2 t2 = new Test2();
Test<Test2> t =new Test<Test2>(t2);
}
}
class Test2{
public void f(){};
}
Test的类型参数是Test2,并将Test2实例对象传递给了Test,并在Test中调用了Test2的方法,但是编译的时候不正确,因为擦除的原因,T被Object代替了,而Object却没有相应的方法,所以会报错。
*补充:
既然T被Object代替了,那为什么不能接受其他类型呢?因为编译器会事先检查(反射)能不能将其转化为Test2类型,如何可以则执行转换。否则将抛出ClassCastException异常。
而检查是针对对象的引用,即Test t 中的t,所以Test t =new Test(t2);这样写跟没写泛型一样。new Test只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用t来调用它的方法,比如说调用setT()方法。所以t引用能完成泛型类型的检查。*
解决刚刚报错的问题,Object没有相应的方法,那我们就想办法让JVM不完全擦除,只擦除到某个边界即可,而这个边界就是Test2,即用Test2带替T,写法为<T extends Test2>。
刚刚的例子改进
public class Test<T extends Test2> {
private T t;
public Test(T t){
this.t = t;
}
public void f(){
t.f();//不报错啦 原因是T被Test2代替啦
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
Test2 t2 = new Test2();
Test<Test2> t =new Test<Test2>(t2);
}
}
class Test2{
public void f(){};
}
在定义类时候,类型参数改为就可以了。若有一个类Test3
继承了Test2,那么调用的时候也可以Test<Test3> t =new Test<Test3>();
型参数是Test2的子类就可以了。
也可以是
Test<Test2> t =new Test<Test2>();
t.setT(new Test3());
擦除的补偿
擦除丢失了代码的某些执行操作,比如调用方法,生成实例对象等等。也就是说任何在运行时需要知道确切类型信息的操作都不能实行。如:
T t = new T(); //编译出错
解决方法就是传递一个class对象给他,例子:
public class Test<T> {
private Class<T> c;
public Test(Class<T> c){
this.c = c;
}
public void f(Object obj) throws InstantiationException, IllegalAccessException{
if(c.isInstance(obj)){
System.out.println("我判断了C的确切类型");
}else{
System.out.println("我判断了C的确切类型,虽然不正确");
}
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
Test<Test2> test =new Test<Test2>(Test2.class);
test.f(new Test2());
}
}
class Test2{void f(){};}
<? extends T> 与 <? super T>
<? extends T>的?是T的某一种子类的意思,记住是一种,单一的一种,问题来了,由于连哪一种都不确定,带来了不确定性,所以是不可能通过
add()来加入元素。
例子:的?是T的某一种子类的意思,记住是一种,单一的一种,问题来了,由于连哪一种都不确定,带来了不确定性,所以是不可能通过
add()来加入元素。
例子:
public static void main(String[] args) {
List<? extends Fruit> list = new ArrayList<Apple>();
list.add(new Apple());//编译出错
list.add(new Fruit());//编译出错
list.add(null);//正确
}
class Fruit{}
class Apple extends Fruit{}
List类型现在是<? Extends Fruit>,
可以读作“具有任何从Fruit继承的类型的列表”,但是并不意味着这个list可以持有任何类型的fruit。
但是如果调用了他的返回方法,比如get()就是安全的,比如:
List<? extends Fruit> list = Arrays.asList(new Apple());
Fruit fruit = list.get(0);
因为List中的类型都是fruit的子类,所以允许这么做。
虽然不能调用add(new Apple())方法,但是可以调用contains(new Apple),
indexOf(new Apple)等带有参数的方法,为什么呢?
list.contains(new Apple());//不报错
list.indexOf(new Apple());//不报错
查看文档可以发现,add()方法接收的是一个具有泛型参数类型的参数,但是其他的方法接收的却是object类型的参数,由于指定了类型参数为
void test(List<? super Apple> list){
list.add(new Apple());//正确
list.add(new SmallApple());//正确
list.add(new Fruit());//编译错误
}
class Fruit{}
class Apple extends Fruit{}
class SmallApple extends Apple{}
编译错误的原因:
首先理解<? Super T>
指的是new出来的实例list)或者赋值或者接收参数的时候,类型参数的边界是T的基类,
List<? super Apple> list = Arrays.asList(new Fruit());
并不是指可以持有T的基类。
由于Apple是下界,所以只能存放apple或者apple的子类,
Super不可用于的返回类型限定,能用于参数类型限定