泛型
- 泛型实现了参数化类型的概念,使得代码可以引用与多种类型
学习泛型的难点:了解Java泛型的局限是什么,以及为什么会有这些限制,Java泛型的边界在哪里。理解边界所在,才能成为程序高手
Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节
缘起
ArrayList list = new ArrayList() ;
list.add(new Integer(32));
Integer num = (Integer)list.get(0);
-
Jdk1.5版本以前的写法
list在编译期无法进行错误类型检查,因为它存储的都是Object
如果我们想要在ArrayList中存储单一一种数据类型(例如Integer),这样的写法就会存在运行时抛出异常的危险,而泛型的主要目标之一就是要将这种错误检查上升到编译期public class ArrayList 泛型引入后当你想要使用ArrayList存储指定类型的数据,就要指定的类型置于尖括号内
ArrayList list = new ArrayList() ; list.add(32); Integer num
= list.get(0); 现在,在创建ArrayList的时候我们指定了只能存储Integer类型的数据,如果添加的数据类型不是Integer编译器会报错
泛型类
public class Holder<T>{
private T item ;
public T get(){return item ;}
public void set(T v){item = v;}
public static void main(String[] args) {
Holder<Integer> holder = new Holder<>() ; //在使用的时候指定其类型
}
}
泛型方法
- 类中可以包含参数化方法,这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法与它所在的类是否是泛型没有关系。
- 使用泛型方法的基本准则:如果使用泛型方法可以取代整个类的泛型化,那么就应该尽量使用泛型方法,因为它可以使得事情更加清楚明白
对于一个static方法而言,它无法访问泛型类的泛型参数,所以,如果static方法需要实现泛型能力,就必须使其成为一个泛型方法
擦除的神秘之处
-
JVM没有泛型类型对象——所有的对象都属于普通类,在泛型代码内部,无法获取任何有关泛型参数类型的信息。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都会被擦除。例如:List和List在运行时事实上是相同的类型。这两个形式都会被擦除成它们的原生类型”List”
泛型类型参数将擦除到它的第一个边界(无指定边界则擦除为Object类型)
向上述例子一样,T擦除到了Object,就好像在类中声明用Object替换了T一样
擦除的问题
-
擦除主要的正当理由是从非泛化代码到泛化代码的转换过程,在不破坏现有类库的情况下,将泛型融入到Java语言中
-
擦除的代价是显著的!泛型不能用于显示地引用运行时类型的操作之中,例如转型、instanceof和new表达式。因为所有关于参数的类型信息都丢失了,无论如何,当你在编写泛型代码时,必须时刻提醒自己,它只是“看起来”好像拥有有关参数的类型信息而已
如果你编写了如下代码:
class Foo{T var;}
那么,当你在创建Foo实例的时候
Foo foo = new Foo<>();
看起来Foo中的代码应该知道工作于Cat之上,而且泛型语法也在给出这样的强烈暗示:在整个类的各个地方,T都将会被替换为Cat,但是事实并且如此!无论何时,当你在编写这个类的时候,都必须提醒自己“它(T)只是一个Object 或者是该类的第一个边界类型”
边界处的动作
看下面的例子
public class SimpleHolder {
private Object obj;
public void set(Object obj){this.obj = obj;}
public Object get(){return obj ;}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.set(new String("a"));
String str = (String) holder.get();
}
}
public class GenericHolder<T> {
private T t ;
public void set(T t){this.t = t ;}
public T get(){return t;}
public static void main(String[] args) {
GenericHolder<String> holder = new GenericHolder<>();
holder.set(new String("a"));
String str = holder.get();
}
}
-
上面两个类,一个是非泛型类,一个是泛型类 直接从反编译两个类给出结论:
1.对于传递进来的值,如set方法,在编译期进行检查2.对传递出去的值,对于泛型代码来说,虽然没有显示的给出强制类型转换,但是编译器会在返回值的地方额外插入转换语句(从非泛型类的get方法与泛型类的get方法得到相同字节码能说明这一点)
擦除的补偿
-
擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道类型确切信息的操作都无法工作。我们可以通过传递类型标签Class
来补偿擦除造成的类型信息丢失无法使用instanceof检测一个对象是否为T类型
传递类型标签进行补偿
创建类型的实例
-
创建类型实例,T var = new T()无法实现,是由于类型擦除(new
一个Object不是我所需要的)。解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是Class对象
不过使用这种方法有一种缺陷,比如这个类型(例如Integer)没有默认构造器,那么newInstance()在运行时会报异常,但是在编译期却检查不出来 -
最好的方法是使用显示工厂去创建泛型,看代码: 定义一个工厂,
由工厂对象为我们创建对象
//生产Integer的工厂
实例化时要求调用者传入工厂对象帮助我们创建泛型对象
Supplier 就是一个很常用的工厂类
具体使用,要求调用者传递工厂对象
Lambda表达式实现get方法
创建泛型数组
-
由于擦除导致不能创建泛型数组,一般的解决方案是在任何想要使用泛型数组的地方使用ArrayList
如果确实需要泛型数组,那么使用Array.newInstance的方式来创建
一种不是很恰当的处理方法(下面有注解)
请注意!数组在创建的时候会跟踪它们的实际类型!所以上面arr数组的实际类型是Object,运行时get和set都不会发生问题,但是在调用rep方法时,你实际是将Object[]转换为Integer[],这会抛出ClassCastException,但是这些在编译期编译器检查不出来,所以尽量不要酱紫操作,使用Array.newInstance(Class<?> componentType, int length)方法创建数组,传递类型标签让它指定所需要的类型
边界
-
边界使得你可以在用于泛型的参数类型上设置限制条件,你可以规定泛型可以应用的类型,更重要的一个效果是你可以按照给出边界类型来调用方法。Java重用了extends关键字来给泛型添加边界(C#中的约束),多个约束以’&’分隔
Interface HasColor{ Color getColor();} class Colored<T extends HasColor> { T item ; Color color(){return item.getColor();} }
通配符
泛型不支持子类继承,看下面例子
List listInt = new ArrayList<>();
List listObj ;
//listObj = listInt ;
Integer是Object的子类,那么ArrayList 对象可以赋值给ArrayList完成所谓的向上转型吗? 不可以
假设操作可以完成
listObj = listInt ;
现在listObj 运行时的实际类型是listInt
那么进行如下操作:
listObj.add(new String(“”));往一个Integer的List里存储String显然是不行的,但是编译器将不能检测出存在的错误(object引用的List当然可以添加任意类型的数据),只有在运行时才能发现错误
结论:Object的List持有Object以及Object的子类,Integer的List持有Integer以及Integer的子类,List 不等价于List
如果想要在两个类型之间建立某种类型的向上转型关系,可以使用通配符
例如:
List<? extends Number> list <? extends T> 表示:任何T或从T继承的类型我都接受 List<? extends Number> 表示:存储任何T或从T继承的类型数据的列表
List<? extends Number> list = new ArrayList() ;
List<? extends Number> list = new ArrayList() ;
List<? extends Number> list = new ArrayList() ;
- 要注意的一点是使用通配符 List<? extends Number> list
不能进行add操作,因为你压根不知道list存储的具体类型是什么,它有可能是List也有可能是List(因为它们都可以协变为List<?
extends
Number>),如果要是可以进行add操作那么会存在类型安全的问题(向List里面添加float类型的数据??),这样很危险 - 查看ArrayList文档可以发现add()接受一个泛型参数,contains和indexOf接受Object类型的参数。当你指定一个ArrayList<?
extends Number>时,add()的参数列表也变成了<? extends
Number>。从这个描述中,编译器并不能了解传递过来的是Number的哪个具体子类,因此add方法不会接受任何类型的Number。 - 在使用contains和indexOf是,参数类型是Object的,因此不涉及任何通配符,编译器将允许这个调用。这意味着泛型类的设计者要考虑清楚哪些调用是“安全的”。
- 调用get方法,它只会返回Number类型,这就是在给定“任何扩展自Number的对象”这一边界后,它知道这些类型都可以向上转型到Number,所以编译器允许这样的操作。
逆变
可以声明通配符是由某个特定类的任何父类来界定“<? super T>”限定为任何T或T 的父类对象这一边界
<? super Number>限定参数是Number或者Number的某种子类型,这样就可以知道向其中添加Number或Number的子类型是安全的
添加Object是不安全的,Object是Number的父类,如果可以添加Object的话,那么这个口子是敞开的,这个Object里面存的可以会是其他的一些类型。
无界通配符
无界通配符<?>意味着”任何事物”
编译器很少关心使用的是原生类型还是<?>
无界通配符的一个重要应用。当你在处理多个泛型参数的时候,有时允许个别参数是任何类型的
List 和 List<?>
List表示 ”持有Object类型的原生List”
List<?>表示 ”持有某种特定类型的非原生List,只是不知道是哪种具体的类型”
public class Holder<T> {
T item ;
public Holder(){}
public Holder(T item){this.item = item ;}
public void set(T val){item = val ;}
public T get(){return item ; }
}
public static void rawArgument(Holder holder,Object arg){
/*
* 编译器知道Holder是一个泛型类,尽管这里被表示成一个原生类型,
* 编译器任知道向set方法传递一个对象是不安全的,因此抛出警告
* Warning: unchecked call to set(T)
* */
holder.set(arg);
//the type information has been lost
Object obj = holder.get();
}
static void testMethod(){
Holder<Long> holder = new Holder<>() ;
long ln = 1l ;
rawArgument(holder, new Object());
/*
* 这里会抛出异常 ClassCastException
* 尝试把object对象赋值给long类型变量
* rawArgument方法中的holder.set方法是危险的
* */
ln = holder.get() ;
System.out.println(ln);
}
因此,只要使用了原生类型,都会放弃编译器检查
unboundedArg 和 rawArgument 给出了Holder 和 Holder 的区别!
public static void unboundedArg(Holder<?> holder,Object arg){
/*
* 这里调用holder的set方法会直接报错而不是警告
* 请对照rawArgument 方法
* */
// holder.set(arg);
//the type information has been lost
Object obj = holder.get() ;
}
因为,原生的Holder将可以持有任何类型!而Holder 只能持有某种特定类型
问题
使用Java泛型时会出现的各类问题
1.任何基本类型都不能作为类型参数
解决方法时使用它们的包装类
2.实现参数化接口(基类型劫持了接口)
一个类不能实现同一个泛型接口的两种不同变体,由于擦除的原因,这两个变体会称为相同的接口,看代码:
有趣的是,如果Payable都移除掉泛型参数(像编译器在擦除阶段所作的那样),这段代码就可以编译
这个问题在某些时候十分令人恼火,例如Comparable接口!
假设我有一个pet类,它可以和其他pet对象进行比较
对与Pet的子类型进行比较的类型窄化是有意义的
假如,我希望Cat对象进行比较
这段代码将不能工作
自限定的类型
class SelfBounded<T extends SelfBounded>{}
SelfBounded类接受泛型参数T,这个T有一个边界,限制这个T必须是继承自SelfBounded本身