Java在JDK5之后引入泛型,但是Java的泛型是伪泛型,Java虚拟机本身是不支持泛型的。Java的泛型是语法糖。
泛型的优点
- 增强编译类型检测,提前检测
- 泛型可以泛型算法,增加代码复用性
基本使用
-
泛型类/接口
//泛型类的两种实现方式 interface Generic1<T>{ } class Generics2<T> implements Generic1<T>{ public static void main(String[] args) { Generics2<Integer> generics2 = new Generics2<>(); } } class Generics3 implements Generic1<String>{ public static void main(String[] args) { Generics3 generics3 = new Generics3(); } }
-
泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型;
public class TestGenerics {
//泛型方法,只有有定义了<T>才是
public <T> void test(T t){
}
//只是方法里面的参数类型使用到了泛型,不是泛型方法,普通方法
public void test2(List<String> list){
}
//普通方法
public void test3(List<?> list){
}
}
泛型与可变长参数
public <T> void show(T... ts) {
for (T t : ts) {
System.out.println(t.toString());
}
}
genericity.show("Demo", 2333, 23.22);
静态泛型方法
public class Genericity<T> {
//静态泛型方法
public static <T> void show(T t) {
}
//以下方法编译器报错:
public static void show(T t) {
}
}
静态方法无法访问类上定义的泛型,所以只能把泛型定义在方法上。
- 通配符
同一个泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
泛型的PESC原则
//PESC原则
//这就是Producer extends原则
//当只想从集合中获取元素,请把这个集合看成是生产者,请使用<? extends T>
//这就是Consumer super原则
//当你仅仅想增加元素到集合,把这个集合看成消费者,请使用<? extends T>
//上限通配符 只能读 <? extends Number>
//更灵活 PE List Producter生产者,可以从它里面拿数据,但是你没办法往里面添加数据
public static double sumOfList(List<? extends Number> list){
//副作用
//list.add(1) //上限 in 只读,但这个不是严格限制
// 反射调用最新的不能调用
// Cause by: java.lang.UnsupportedOperationExcepion
Class<?> clazz = list.getClass();
try {
Method method = clazz.getMethod("add",java.lang.Object.class);
method.setAccessible(true);
method.invoke(list,100);
System.out.println(list.toString());
}catch (Exception e){
}
return 0d;
}
//下限通配符 只能写 super
public static void addNumber(List<? super Integer> list){
}
/**
* 1.如果你正在编写一个可以使用Object类中提供的功能实现的方法。
* 2.当代码使用通用类中不依赖与类型参数的方法时,例如list.size或list.clear
* 事实上,Class<?>之所以这么经常使用,是因为Class<T>中的大部分方法都不依赖与T
*
* @param list
*/
//不受限
// ? 退化了,不能使用List中任何依赖类型参数[T]的方法
public static void productList(List<?> list){
//也有副作用
//list.add("sss"); //不能使用该方法
list.size();
list.add(null);
list.get(0);
list.contains(12);
}
上下限通配符的另一个作用
public class Genericity<T> {
private T t;
public Genericity(T t) {
this.t = t;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
上限通配符:
private static void show(Genericity<? extends Number> genericity) {
System.out.println(genericity.getT());
}
表示传入的实参必须是 Number 或 Number 的子类。
下限通配符:
private static void show(Genericity<? super Integer> genericity) {
System.out.println(genericity.getT());
}
表示传入的实参必须是 Integer 或者其父类。
但是需要注意的是,泛型的上下边界添加,必须与泛型的声明在一起:
//错误声明
private <T> T show(Genericity<T extends Integer> genericity) {
}
//正确声明
private <T extends Integer> T show(Genericity<T> genericity) {
return genericity.getT();
}
-
类型擦除以及带来的问题
- 通过下面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行检测,而无关它真正引用的对象。
public class Test10 { public static void main(String[] args) { ArrayList<String> arrayList1=new ArrayList(); arrayList1.add("1");//编译通过 arrayList1.add(1);//编译错误 String str1=arrayList1.get(0);//返回类型就是String ArrayList arrayList2=new ArrayList<String>(); arrayList2.add("1");//编译通过 arrayList2.add(1);//编译通过 Object object=arrayList2.get(0);//返回类型就是Object new ArrayList<String>().add("11");//编译通过 new ArrayList<String>().add(22);//编译错误 String string=new ArrayList<String>().get(0);//返回类型就是String } }
-
格外注意泛型中引用传递的问题
ArrayList<String> arrayList1=new ArrayList<String>(); arrayList1.add(new String()); arrayList1.add(new String()); ArrayList<Object> arrayList2=arrayList1;//编译错误 ArrayList<Object> arrayList1=new ArrayList<Object>(); arrayList1.add(new Object()); arrayList1.add(new Object()); ArrayList<String> arrayList2=arrayList1;//编译错误
-
虚拟机巧妙的使用了桥方法,来解决类型擦除和多态的冲突。
类型擦除最后都转成Object了(原来Date),这样变成了方法重载。这样类型擦除就和多态有了冲突,JVM的本意知道吗,知道!但是它不能直接实现,于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。
class DateInter extends Pair<Date> { @Override public void setValue(Date value) { super.setValue(value); } @Override public Date getValue() { return super.getValue(); } } //但是 public static void main(String[] args) throws ClassNotFoundException { DateInter dateInter=new DateInter(); dateInter.setValue(new Date()); dateInter.setValue(new Object());//编译错误 确实报错了 }
首先,我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>"
:()V
4: return
public void setValue(java.util.Date); //我们重写的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue
:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue(); //我们重写的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue
:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn
public java.lang.Object getValue(); //编译时由编译器生成的巧方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法
;
4: areturn
public void setValue(java.lang.Object); //编译时由编译器生成的巧方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法
)V
8: return
}
可以看到,竟然有四个方法,最后的两个方法,就是编译器自己生成的桥方法。桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类的两个方法的就是这两个我们看不多的方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。setValue方法是为了解决类型擦除与多态之间的冲突。 而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:
那么父类的setValue方法如下:
public Object getValue() {
return super.getValue();
}
而子类重写的方法是:
public Date getValue() {
return super.getValue();
}
其实这在普通的类继承中也是普遍存在的重写,这就是协变。 关于协变:。。。。。。 并且,还有一点也许会有疑问,子类中的桥方法 Object getValue()和Date getValue()是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。
4. 泛型类型变量不能是基本数据类型
不能用类型参数替换基本类型。就比如,没有ArrayList,只有ArrayList。因为当类型 擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。
5. 运行时类型查询
ArrayList<String> arrayList=new ArrayList<String>();
因为类型擦除之后,ArrayList只剩下原始类型,泛型信息String不存在了。那么,运行时进行类 型查询的时候使用下面的方法是错误的
if( arrayList instanceof ArrayList<String>)
Java限定了这种类型查询的方式:
if( arrayList instanceof ArrayList<?>)
6. 异常中使用泛型的问题
7. 泛型在静态方法和静态类中的问题
//因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有 //创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
public class Test2<T> {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
public class Test2<T> {
public static <T >T show(T one){//这是正确的
return null;
}
}
因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。
8. 类型擦除后的冲突
class Pair<T> {
public boolean equals(T value) {
return null;
}
}
//实际上,擦除后方法boolean equals(T)变成了方法 boolean equals(Object)这与Object.equals方法是冲突 // 的!当然,补救的办法是重新命名引发错误的方法
9. 泛型规范说明提及另一个原则“要支持擦除的转换,需要强行制一个类或者类型变量不能同时成为两个接口 的子类,而这两个子类是同一接品的不同参数化。”
//ERROR
class Calendar implements Comparable<Calendar>{ ... }
class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{...}
//合法
class Calendar implements Comparable{ ... }
class GregorianCalendar extends Calendar implements Comparable{...}
10. 无法创建参数化类型数组。无法创建类型参数实例(new T()),但是可以通过类类型的反射来创建。重载时注意,比如参数的泛型问题。
- 其他:
泛型并不是最终都转换成Object
class Node1<T extends Comparable<T>> //这里最终替换成Comparable类
受限的类型参数:
-
面试题
-
array(数组)中可以使用泛型吗?
不能,Effective建立使用List,因为List可以提供编译器的类型安全保证,而Array却不能。
-
Java中Set与Set<?>到底区别在哪里?
一个是受类型检查的,一个是不受类型检查的
List<?>和List的区别在哪里?
?是一个位置类型的List,而List其实是任意类型的List。你可以List,List赋值给List<?>,却不能把List赋值给List
-
泛型优点
- 类型检测提前到编译期,便于更早发现错误
- 代码复用
-
限定通配符 extends super,非限定通配符? PESC原则
-
C++模板和java泛型有什么不同?提供了宏指令,细化成真正的模板代码,需要仔细研究(如果要求C++)。
参考文章:
-