JAVA泛型整理
泛化之前
在面向对象编程语言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设置为基类,那么该方法就可以接受继承这个基类的任何类作为参数,这样的方法将更具有通用性。此外,如果将方法参数声明为接口,将更加灵活。
在java增加泛型类型之前,通用程序的设计就是利用继承来实现的。例如,ArrayList类只维护一个Object引用的数组,Object为所有类的基类。
public class BeforeGeneric { // 泛型之前的通用程序设计 static class ArrayList { private Object[] elments = new Object[0]; public Object get(int i) { return elments[i]; } public void add(Object o) { //这里的实现,只是为了演示,不具有任何参考价值 int length = elments.length; Object[] newElements = new Object[length + 1]; for (int i = 0; i < length; i++) { newElements[i] = elments[i]; } newElements[length] = o; elments = newElements; } } public static void main(String[] args) { ArrayList stringValues = new ArrayList(); // 可以向数组中添加任意的数据类型 stringValues.add("str"); stringValues.add(1); // 取值后要强转String String str = (String) stringValues.get(0); System.out.println(str); // 取值后强转Integer int a = (Integer) stringValues.get(1); System.out.println(a); } }
以上的设计有两个问题:
1:获取值的时候,必须要进行强制类型转换。
2:假定我们预想的是利用StringValues来存放String集合,因为ArrayList只是维护一个Object引用的数组,我们无法阻止讲Integer类型放入StringValues(因为Integer也是Object子类)。然而,当我们使用数据的时候,需要将获取到的object类型强制转换成我们期望的数据类型String。如果向集合中添加了非预期的数据类型Integer,编译时期不会有任何的错误提示,但当我们运行程序却会报异常:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
这显然不是我们所期望看到的结果。我们更期望程序能在编译时期给我们提示,而不是运行异常。
泛型
针对利用继承来实现通用程序设计所产生的问题,泛型提供了更好的解决方案:类型参数。例如,ArrayList类用一个类型参数来指出元素的类型。
1 ArrayList<String> stringValues=new ArrayList<String>();
这样的代码具有更好的可读性,我们一看就知道该集合是用来保存String类型的对象,而不仅仅是依赖变量名来暗示我们期望的类型。
public class GenericType {
public static void main(String[] args) {
ArrayList<String> stringValues=new ArrayList<String>();
stringValues.add("str");
stringValues.add(1); //编译错误
}
}
现在,如果我们向ArrayList<String>中添加Integer类型的对象,将会出现编译错误。
编译器会自动帮助我们进行检查,避免向集合中插入错误数据类型的对象,从而使得程序具有更好的安全性。
总结:泛型通过类型参数使得我们的程序具有更好的安全性和可读性。
java泛型的实现原理
擦除
编译之前:
public class GenericType { public static void main(String[] args) { ArrayList<String> arrayString=new ArrayList<String>(); ArrayList<Integer> arrayInteger=new ArrayList<Integer>(); System.out.println(arrayString.getClass()==arrayInteger.getClass()); } }
输出结果:true
编译之后
public class Generic { public Generic() { } public static void main(String[] args) { ArrayList stringValues = new ArrayList(); ArrayList integerValues = new ArrayList(); System.out.println(stringValues.getClass().equals(integerValues.getClass())); } }
在上面的例子,我们定义了两个ArrayList数组,一个是String数组只能用来存储String类型,另一个是Integer数组只能用来存储Integer类型。最后我们通过stringValues对象和integerValues对象的getClass方法来获取类信息进行比较,发现结果为true。
产生以上的原因就是因为:在编译期,所有的泛型信息都会被擦除。参照:编译之后。
java中的泛型基本都是在编译器这个层次来实现的,这也是java泛型被称为“伪泛型”的原因。
原始类型
原始类型就是泛型类型擦除了泛型信息后,在字节码中真正的类型。无论何时定义一个泛型类型,相应的原始类型都会被自动提供,原始类型的名字就是删除类型参数后的泛型类型的类名。
擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换)
举个例子来说明上面的具体含义:
// 泛型类型 class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
原始类型的名字就是删除类型参数后的泛型类型的类名:这个泛型类型的类的名字就是Pair<T>,类型参数就是T,删除类型参数后泛型类型的类名就是Pair。
T为无限定的类型变量,用Object替换。
//原始类型 class Pair { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
如果上述类型变量是Pair<T extends Number>,擦除后,类型变量用Number替换。
接下来再看一个例子
public class ReflectIntGeneric { public static void main(String[] args) { // 定义一个存放Integer数据类型的集合 ArrayList<Integer> array = new ArrayList<Integer>(); array.add(1); try { // 利用反射存放一个String数据类型 array.getClass().getMethod("add", Object.class).invoke(array, "hello"); for (int i = 0; i < array.size(); i++) { System.out.println(array.get(i)); } } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } } }
上面程序的执行结果:
1
hello
为什么呢?我们在介绍泛型时指出向ArrayList<Integer>中插入String数据类型的时候,编译器会报错。现在为什么又可以了呢?
我们在程序中定义了一个ArrayList<Integer>泛型数据,如果直接调用add方法,那么只能存储Integer数据类型。但是当我们利用反射调用了add方法的时候,却又可以存储String数据类型。这恰恰说明了ArrayList<String>泛型数据在编译之后被擦除了,只保留了原始数据类型,类型变量(T)被替换为Object,在运行时,我们可以向其中插入任意的数据类型。
但是,我们并不推荐以这种方式操作泛型类型,因为这违背了泛型类型设计的初衷(减少强制类型转换以确保数据类型安全)。当我们从集合中获取元素的时候,默认会将类型强制转换成泛型参数指定的类型(Integer),如果放入非法的对象类型,这个强制类型转换就会出现异常。
泛型方法的类型推断
在调用泛型方法的时候,可以指定泛型类型,也可以不指定泛型类型。
在指定泛型类型的情况下,类型只能传入指定的类型或者其子类类型。
在不指定泛型类型的情况下,泛型类型为该方法几种参数类型的共同父级的最小级,直到Object。
public class Test { public static void main(String[] args) { /**不指定泛型的时候*/ int i = Test.add(1, 2); //这两个参数都是Integer,所以T替换为Integer类型 Number f = Test.add(1, 1.2);//这两个参数一个是Integer,另一个是Float,所以取同一父类的最小级,为Number Object o = Test.add(1, "asd");//这两个参数一个是Integer,另一个是String,所以取同一父类的最小级,为Object /**指定泛型的时候*/ int a = Test.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类 int b = Test.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float } public static <T> T add(T x, T y) { return y; } }
既然说类型变量在编译的时候会被擦除掉,那为什么定义了ArrayList<Integer>的时候,不允许插入String数据类型呢?不是说泛型变量Integer会在编译的时候会被擦除为原始数据类型Object的么?为什么不能放别的数据类型?既然是擦除,如何保证我们只能使用泛型类型变量限定的数据类型呢?
java是如何解决这个问题的呢?java编译器是通过先检查代码中的泛型的类型,然后再进行类型擦除,再进行编译的。
public class TestPair { public static void main(String[] args) { Pair<Integer> pair=new Pair<Integer>(); pair.setValue(3); Integer integer=pair.getValue(); System.out.println(integer); } }
查看该类编译后的字节码文件
public class TestPair { public TestPair() { } public static void main(String[] args) { Pair pair = new Pair(); pair.setValue(Integer.valueOf(3)); Integer integer = (Integer)pair.getValue(); System.out.println(integer); } }
擦除getValue()的返回类型后将返回Object类型,编译器将自动Integer强制类型转换。也就是说,编译器把这个方法调用翻译为两条字节码指令:
1:对原始方法Pair.getValue()的调用
2:将放回的Object对象强制类型转换为Integer
此外,取出一个泛型域时,也要插入强制类型转换。因此,我们说java的泛型是在编译器的层次进行实现的。被称为“伪泛型”,相对于C++。
泛型相关问题
1、泛型类型引用传递问题
在java中,像下面形势的引用传递是不允许的
1 ArrayList<String> arrayList1=new ArrayList<Object>();//编译错误 2 ArrayList<Object> arrayList1=new ArrayList<String>();//编译错误
首选分析下第一行的代码,我们将第一行的代码拓展为下面的形式:
1 ArrayList<Object> arrayList1=new ArrayList<Object>(); 2 arrayList1.add(new Object()); 3 arrayList1.add(new Object()); 4 ArrayList<String> arrayList2=arrayList1;//编译错误
在代码的第四行就会出现编译错误,我们假定它不会编译错误。当我们从arrayList2中获取对象的时候,会默认强制类型转换为String,而实际上我们已经存放了两个Object对象,Object对象在强制转换为String对象的时候就会有ClassCastException了。所以为了避免这种极易出现的错误,java不允许这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的的问题,我们不能违背这一初衷)
再分析一下第二行的代码,我们将第二行的代码拓展为下面的形势:
1 ArrayList<String> arrayList1=new ArrayList<String>(); 2 arrayList1.add(new String()); 3 arrayList1.add(new String()); 4 ArrayList<Object> arrayList2=arrayList1;//编译错误
没错,这样的情况比第一种情况好多了,最起码,我们在用arrayList2取值的时候,不会出现ClassCastException错误。因为String强制转换为Object类型这是允许的。可是这样做有什么意义?泛型的出现,就是为了解决类型转换的问题。我们使用了泛型,到头来,还要自己去强制类型转换。违背了泛型设计的初衷。所以java不允许这么操作。再说,如果在后续的操作中,再arrayList2中添加Integer类型,我们取值的时候,到底取的是String类型还是Integer类型?
所以要格外注意泛型引用中的传递问题。
2.泛型类型变量不能是基本数据类型
没有ArrayList<int>,只有ArrayList<Integer>。因为在类型擦除后,ArraList的原始类中的类型变量(T)替换为Object,但Object类型不能存放int值。
3、运行时类型查询
ArrayList<String> arrayList=new ArrayList<String>();
错误:
if( arrayList instanceof ArrayList<String>)
正确:
if( arrayList instanceof ArrayList<?>)
原因:ArrayList在类型擦除完以后只剩下原始数据类型,泛型信息String不存在。
这里带出一个符号?,?为通配符,非限定通配符
4、泛型在静态方法和静态类中的问题
泛型类中的静态方法和静态变量不能使用泛型类所声明的泛型类型参数
public class Test2<T> { public static T one; //编译错误 public static T show(T one){ //编译错误 return null; } }
原因:因为泛型类中的泛型参数的实例化是在定义泛型类型对象(ArrayList<Integer>)的时候指定的,而静态方法和静态变量是不需要使用对象来调用,对象都没有实例化,如何确定这个泛型参数是何种类型。所以当然错误。
但是要区分下面的一种情况:
1 public class Test2<T> { 2 public static <T >T show(T one){//这是正确的 3 return null; 4 } 5 }
因为这是一个泛型方法。在泛型方法中的T是自己在方法中定义的T,而不是泛型类中的T。
泛型相关面试题
1. Java中的泛型是什么 ? 使用泛型的好处是什么?
泛型是一种参数化的机制。它可以使代码适用于各种类型,从而编写出更加通用的代码,例如集合框架。
泛型是一种编译时类型确认机制。它提供了编译期的类型安全。确保在泛型类型(通常是泛型集合)只能使用正确类型的对象。避免在运行时期出现ClassCastException。3
2.泛型是如何工作的?什么是类型擦除?
泛型的正常工作是依赖编译期在编译源码的时候,先进行类型检查,然后再进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。
编译器在编译时期擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。比如List<String>,在编译期的时候擦除String,在运行期的时候,只有List信息,在取值的地方强制转换(String)。
为什么要进行类型擦除,是为了避免类型膨胀。
3.什么是泛型中的限定通配符和非限定通配符
限定通配符对类型进行了限定,有两种限定通配符。一种是<? extends T>它通过确保类型必须是T的子类来限定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来限定类型的下界。
泛型类型必须用限定内的类型进行初始化,否则会导致编译失败。另一方面<?>标识非限定通配符,因为<?>可以用任意类型来替代。
4.List<? super T> 和List<? extends T>有什么区别?
第一个下界限定,第二个个是上界限定。List<? super T> 表示泛型类型必须是T类型的父类型,List<? extends T>标识泛型类型必须是T类型的子类型。
比如:List<? extends Number> 可以接受List<Integer> 或者List<Float>。
5.如何编写一个泛型方法,让它能接受泛型类型参数并返回泛型类型。
public V put(K key, V value) { return cache.put(key, value); }
6.java中如何使用泛型编写带有参数的类?
这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符
例如:
public class Service<E extends SuperA, V extends SuperB> { }
7.你可以把List<String>传递给一个接受List<Object>参数的方法么?
不可以,因为List<String>只可以接受String类型,而List<Object>除了可以接受String还可以接受Integer,Float等类型,在类型擦除后,一个是强制转换String,一个是强制转换Object。
8.Java中List<Object>和原始类型List之间的区别?
原始类型和带参数类型的<Object>之间的最主要区别是:在编译时编译器不会对原始类型进行安全检查,对带参数类型的<Object>进行安全检查。
第二点区别:你可以把任何带参数的泛型类型传递给接受原始类型List的方法,但却不能把List<String>传递给接受List<Object>的方法,因为会产生编译错误。
9.java中List<?>和List<Object>之间的区别是什么?
List<?>是一个未知类型的List,而List<Object>其实是任意类型的List。你可以把List<String>,List<Integer>赋值给List<?>,但是不能把他们赋值给List<Object>。
通配符(这点内容要深刻理解)
通配符上界
public class Test { public static void printIntValue(List<? extends Number> list) { for (Number number : list) { System.out.print(number.intValue()+" "); } System.out.println(); } public static void main(String[] args) { List<Integer> integerList=new ArrayList<Integer>(); integerList.add(2); integerList.add(2); printIntValue(integerList); List<Float> floatList=new ArrayList<Float>(); floatList.add((float) 3.3); floatList.add((float) 0.3); printIntValue(floatList); } }
输出:
3 0
public class Test { public static void fillNumberList(List<? extends Number> list) { list.add(new Integer(0));//编译错误 list.add(new Float(1.0));//编译错误 } public static void main(String[] args) { List<? extends Number> list=new ArrayList(); list.add(new Integer(1));//编译错误 list.add(new Float(1.0));//编译错误 } }
List<? extends Number>可以代表List<Integer>或List<Float>,为什么不能像其中加入Integer或者Float呢?
首先,我们知道List<Integer>之中只能加入Integer。并且如下代码是可行的:
List<? extends Number> list1=new ArrayList<Integer>(); List<? extends Number> list2=new ArrayList<Float>();
假设前面的例子没有编译错误,如果我们把list1或者list2传入方法fillNumberList
public static void fillNumberList(List<? extends Number> list) { // 传入的参数list里面可能放的Integer 也可能放的Float。类型不明确 list.add(new Integer(0));//编译错误 list.add(new Float(1.0));//编译错误 } public static void main(String[] args) {
List<? extends Number> list1=new ArrayList<Integer>();
List<? extends Number> list2=new ArrayList<Float>();
fillNumberList(list1);
fillNumberList(list2);
}
显然都会出现类型不匹配的情况,假设不成立
结论:不能往List<? extends T> 中添加任意对象,除了null。
那为什么对List<? extends T>进行迭代可以呢,因为子类必定有父类相同的接口,这正是我们所期望的。
通配符下界
public class Test { public static void fillNumberList(List<? super Number> list) { list.add(new Integer(0)); list.add(new Float(1.0)); } public static void main(String[] args) { List<? super Number> list=new ArrayList(); list.add(new Integer(1)); list.add(new Float(1.1)); } }
无界通配符
常规使用
1、当方法是使用原始的Object类型作为参数时,如下:
public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + ""); System.out.println(); }
可以选择改为如下实现:
public static void printList(List<?> list) { for (Object elem: list) System.out.print(elem + ""); System.out.println(); }
这样就可以兼容更多的输出,而不单纯是List<Object>,如下
List<Integer> li = Arrays.asList(1, 2, 3); List<String> ls = Arrays.asList("one", "two", "three"); printList(li); printList(ls);
转:http://blog.csdn.net/sunxianghuang/article/details/51982979#reply
更多参照:https://www.zhihu.com/question/20400700
提出疑问:
第一个:List<? super T>可以接受任何T的父类构成的List.
第二个:List<? super Number>可以代表List<T>,其中T为Number父类,(虽然Number没有父类)。如果说,T为Number的父类,我们想List<T>中加入Number的子类肯定是可以的.
以上不矛盾么?
第一个说接受任何T的父类。
第二个却放了一个T 的子类。
回答疑问:
Human,Father,Son依次继承。
List< ? super Father> list1 = new ArrayList<Father>();
List< ? super Father> list2 = new ArrayList<Human>();
此时list2 确实是Father的父类,里面放的是Human。
也就是说类型参数是:Human。
这个时候就有点面向接口编程的意思了。类型参数是Human,当然可以往里面添加Son这个子类了。