泛型类
publicclassContainer<K, V>{
private K key;
private V value;
publicContainer(K k, V v){
key = k;
value = v;
}
public K getKey(){
return key;
}
publicvoid setKey(K key){
this.key = key;
}
public V getValue(){
return value;
}
publicvoid setValue(V value){
this.value = value;
}
}
从泛型类派生子类:
publicclass A extends Apple<T> //错误
publicclass A extends Apple<String>//正确,把Apple<T>中的T当作String处理
publicclass A extends Apple //可以,但泛型检查有警告:未检查或不安全的操作,Apple<T>中的T当作Object处理
泛型接口
泛型接口实现的两种方式:
1、定义子类:在子类的定义上也声明泛型类型:
interface Info<T>{ // 在接口上定义泛型
public T getVar();// 定义抽象方法,抽象方法的返回值就是泛型类型
}
classInfoImpl<T> implements Info<T>{ // 定义泛型接口的子类
private T var ; // 定义属性
publicInfoImpl(T var){ // 通过构造方法设置属性内容
this.setVar(var);
}
publicvoid setVar(T var){
this.var = var ;
}
public T getVar(){
returnthis.var ;
}
};
publicclassGenericsDemo24{
publicstaticvoid main(String arsg[]){
Info<String> i = null; // 声明接口对象
i =newInfoImpl<String>("李兴华");// 通过子类实例化对象
System.out.println("内容:"+ i.getVar());
}
};
2、如果现在实现接口的子类不想使用泛型声明,则在实现接口的时候直接指定好其具体的操作类型即可:
interface Info<T>{ // 在接口上定义泛型
public T getVar();// 定义抽象方法,抽象方法的返回值就是泛型类型
}
classInfoImpl implements Info<String>{ // 定义泛型接口的子类
privateString var ; // 定义属性
publicInfoImpl(String var){ // 通过构造方法设置属性内容
this.setVar(var);
}
publicvoid setVar(String var){
this.var = var ;
}
publicString getVar(){
returnthis.var ;
}
};
publicclassGenericsDemo25{
publicstaticvoid main(String arsg[]){
Info i = null; // 声明接口对象
i =newInfoImpl("李兴华"); // 通过子类实例化对象
System.out.println("内容:"+ i.getVar());
}
};
泛型方法
[访问权限]<泛型标识> 返回类型 函数名称(参数类型)
classDemo{
public<T> T fun(T t){ // 可以接收任意类型的数据
return t ; // 直接把参数返回
}
};
publicclassGenericsDemo26{
publicstaticvoid main(String args[]){
Demo d =newDemo(); // 实例化Demo对象
String str = d.fun("李兴华");// 传递字符串
int i = d.fun(30); // 传递数字,自动装箱
System.out.println(str); // 输出内容
System.out.println(i); // 输出内容
}
};
泛型数组
publicclassGenericsDemo30{
publicstaticvoid main(String args[]){
Integer i[]= fun1(1,2,3,4,5,6); // 返回泛型数组
fun2(i);
}
publicstatic<T> T[] fun1(T...arg){ // 接收可变参数
return arg ; // 返回泛型数组
}
publicstatic<T>void fun2(T param[]){ // 输出
System.out.print("接收泛型数组:");
for(T t:param){
System.out.print(t +"、");
}
}
};
类型擦除
使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。如:在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类:这个具体类一般是Object;如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T.get()方法声明就变成了Object.get();List<String>就变成了List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法,这个时候就由编译器来动态生成这个方法。
- 泛型类并没有自己独有的Class类对象,不管泛型类型的实际类型参数是什么,它们在运行时都有同样的类(class)。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
Class c1 =newArrayList<Integer>().getClass();
Class c2 =newArrayList<String>().getClass();
System.out.println(c1 == c2);
/* Output
true
*/
- 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。所以在静态方法、静态变量的声明和初始化中不允许使用类型形参:
![](https://images2015.cnblogs.com/blog/947400/201607/947400-20160712181656701-1187824863.jpg)
- instanceof运算符后不能使用泛型类
![](https://images2015.cnblogs.com/blog/947400/201607/947400-20160712181657217-358474607.jpg)
类型擦除原因:
由于泛型并不是从Java诞生就存在的一个特性,而是等到SE5才被加入的,所以为了兼容之前并未使用泛型的类库和代码,不得不让编译器擦除掉代码中有关于泛型类型信息的部分,这样最后生成出来的代码其实是『泛型无关』的,我们使用别人的代码或者类库时也就不需要关心对方代码是否已经『泛化』,反之亦然。
在泛型代码内部,无法获得任何有关泛型参数类型的信息。泛型参数的类型对于JVM来说,实际上只是一堆占位符。
publicclassMain<T>{
public T[] makeArray(){
// error: Type parameter 'T' cannot be instantiated directly
returnnew T[5];
}
}
我们无法在泛型内部创建一个T类型的数组,原因也和之前一样,T仅仅是个占位符,并没有真实的类型信息,实际上,除了new表达式之外,instanceof操作和转型(会收到警告)在泛型内部都是无法使用的,而造成这个的原因就是之前讲过的编译器对类型信息进行了擦除。
同时,面对泛型内部形如T var;的代码时,记得多念几遍:它只是个Object,它只是个Object……
对泛型数组的擦除补偿,本质方法还是通过显示地传递类型标签,通过Array.newInstance(type, size)来生成数组,同时也是最为推荐的在泛型内部生成数组的方法。泛型内部生成数组的方法:
publicclassMain<T>{
public T[] create(Class<T> type){
return(T[])Array.newInstance(type,10);
}
publicstaticvoid main(String[] args){
Main<String> m =newMain<String>();
String[] strings = m.create(String.class);
}
}
虽然有类型擦除的存在,使得编译器在泛型内部其实完全无法知道有关T的任何信息,但是编译器可以保证重要的一点:内部一致性,也是我们放进去的是什么类型的对象,取出来还是相同类型的对象,这一点让Java的泛型起码还是有用武之地的。
编译器承担了全部的类型检查工作:一个方法如果接收List<Object>作为形式参数,那么如果尝试将一个List<String>的对象作为实际参数传进去,会无法通过编译(抛出ClassCastException)。
在java中,一般情况下:子类是可以替换父类。比如在参数传递中,String[]可以替换Object[],但List<String>是不能替换掉List<Object>的。Object是String的父类,但List<Object>不是List<String>的父类
publicvoid inspect(List<Object>list){
for(Object obj :list){
System.out.println(obj);
}
list.add(1);//这个操作在当前方法的上下文是合法的。
}
publicvoid test(){
List<String> strs =newArrayList<String>();
inspect(strs);//编译错误
}
类型通配符<?>
通配符所代表的其实是一组类型,但具体的类型是未知的。如List<?>就声明了List中包含的元素类型是未知的。不能通过new ArrayList<?>()的方法来创建一个新的ArrayList对象或往List<?>中添加元素,因为编译器无法知道具体的类型是什么;但是对于 List<?>中的元素确总是可以用Object来引用的,如调用List的get方法来返回指定索引处的元素,因为虽然类型未知,但肯定是Object及其子类。
![](https://images2015.cnblogs.com/blog/947400/201607/947400-20160712181658014-416526505.jpg)
使用上下界来限制未知类型的范围<? extends Shape>
List<? extends Number>说明List中可能包含的元素类型是Number及其子类
List<? super Number>说明List中包含的是Number及其父类。
当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List<? extends Number>的时候,就可以使用Number类的intValue等方法。同样由于编译器无法知道具体的类型是什么,故不能往其中添加元素。
使用:
![](https://images2015.cnblogs.com/blog/947400/201607/947400-20160712181658607-299910081.jpg)
- 通配符修饰的泛型不能用来直接创建变量对象。
- 通配符修饰相当于声明了一种变量,它可以作为参数在方法中传递。这么做带来的好处就是我们可以将应用于包含某些数据类型的列表的方法也应用到包含其子类型的列表中。相当于可以在列表中用到一些面向对象的特性。
最佳实践
- 在代码中避免泛型类和原始类型的混用。比如List<String>和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
- 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
- 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
- 不要忽视编译器给出的警告信息。
参考与阅读:
https://segmentfault.com/a/1190000002646193(泛型类、泛型接口和泛型方法)