目录
泛型的意义:使得程序具有更好的可读性和安全性。
泛型三种形式
泛型类:public class Axxx<T> {} ,public interface Iterable<T>
泛型方法:public static <T> getMiddle(T a) {}
类型变量:
public <T extends Comparable & Serializable> void method(T[] strs) {
T a = strs[0];
T b = strs[1];
a.compareTo(b);
}
泛型继承规则
上图是正确的类型继承方式。但是下面的情况是不允许的。
public interface A<T> {
void a(T a);
}
public class B<T> implements A<T> {
@Override
public void a(T a) {
System.out.println(this);
}
}
public class Pair<T> {
void print(Pair<T> pair) {
System.out.println(pair);
}
}
public class Test {
public static void main(String[] args) {
Pair<A> pairA = new Pair<>();
Pair<B> pairB = new Pair<>();
pairA.print(pairB); // 这里编译会出错。
}
}
应对这种情况 Java的设计者发明了一种巧妙的 ( 仍然是安全的 ) “ 解决方案 ” : 通配符类型 。
通配符类型
通配符:<? extends A>, <? super A>
所以上面的上面的代码可以换成:
public class Pair<T> {
void print(Pair<? extends T> pair) {
System.out.println(pair);
}
}
public class Test {
public static void main(String[] args) {
Pair<A> pairA = new Pair<>();
Pair<B> pairB = new Pair<>();
pairA.print(pairB); // 这里编译会通过。
}
}
无限定通配符
还可以使用无限定的通配符 , 例如, Pair < ? > 。 初看起来 , 这好像与原始的 Pair 类型一样 。实际上 ,有很大的不同。 类型Pair < ? > 有以下方法 :
? getFirst ( )
void set First (?)
getFirst 的返回值只能赋给一个 Object 。 setFirst 方法不能被调用, 甚至不能用Object 调用 。 Pair < ? > 和 Pair 本质的不同在于 : 可以用任意 Object 对象调用原始 Pair 类的 setObject方法。
可以调用 setFirst ( null ) 。为什么要使用这样脆弱的类型 ? 它对于许多简单的操作非常有用。 例如 , 下面这个方法将用来测试一个 pair 是否包含一个 null 引用, 它不需要实际的类型 。
public static boolean hasNulls (Pair<?> p) { return p.getFirst() = null || p.getSecond() = null ; }
通过将 hasNulls 转换成泛型方法, 可以避免使用通配符类型 :
public static < T > boolean hasNulls ( Pair < T > p )
但是, 带有通配符的版本可读性更强 。
当然如果是下面的情况还是要选择泛型:
public static void swap (Pair <?> p) { ? t = p.getFirst() ; // Error, 编译错误。 p.setFirst(p.getSecond()); p.setSecond(t); }
应该改成:
public static <T> void swapHelper (Pair <T> p){ T t = p.getFirst(); p.setFirst(p.getSecond()); p.setSecond(t) ; }
泛型实现原理:类型擦除
其实Java的泛型是伪泛型。因为Java泛型的实现方式是类型檫除。
无论和是你定义一个泛型,都会自动给你提供一个相应的原始类型(raw type)。但是这个raw type我们并不能见到,jvm也见不到。但是可以通过下面的代码证明:
Assert.assertTrue(new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass());
通过字节码来看:
public static void main(String[] args) {
new Test().method(new Integer[]{1,2});
}
public <T extends Comparable & Serializable> void method(T[] strs) {
T a = strs[0];
T b = strs[1];
System.out.println(a.compareTo(b));
}
public <T extends java.lang.Comparable & java.io.Serializable> void method(T[]
);
descriptor: ([Ljava/lang/Comparable;)V
flags: ACC_PUBLIC
Code:
stack=3, locals=5, args_size=2
0: aload_1
1: iconst_0
2: aaload
3: astore_2
4: aload_1
5: iconst_1
6: aaload
7: astore_3
8: aload_3
9: checkcast #7 // class java/io/Serializable
12: astore 4
14: getstatic #8 // Field java/lang/System.out:Ljav
a/io/PrintStream;
17: aload_2
18: aload 4
20: invokeinterface #9, 2 // InterfaceMethod java/lang/Compa
rable.compareTo:(Ljava/lang/Object;)I
25: invokevirtual #10 // Method java/io/PrintStream.prin
tln:(I)V
28: return
LineNumberTable:
line 21: 0
line 22: 4
line 23: 8
line 24: 14
line 25: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/leetcode/tal/template/Test;
0 29 1 strs [Ljava/lang/Comparable;
4 25 2 a Ljava/lang/Comparable;
8 21 3 b Ljava/lang/Comparable;
14 15 4 s Ljava/io/Serializable;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 29 1 strs [TT;
4 25 2 a TT;
8 21 3 b TT;
Signature: #36 // <T::Ljava/lang/Comparable;:Ljava/
io/Serializable;>([TT;)V
}
这种状况下原始类型就会变成Comparable。
通过上面两段可以得出下面的结论。
照片来自:https://blog.csdn.net/shinecjj/article/details/52075499
特殊的擦除:桥方法
举个例子:
class Datelnterval extends Pair <LocalDate> {
public void setSecond() {
if ( second.compareTo(getFirst()) >= 0 )
super.setSecond(second);
}
}
它的raw type是:
class Datelnterval extends Pair // after erasure
{
public void setSecond ( LocalDate second ) { . . . }
}
但是下面这种情况:
Datelnterval interval = new Datelnterval ( . . . ) ;
Pair <LocalDate> pair = interval; / / OK assignment to superclass,raw type也是它
pair.setSecond(aDate) ;
编译器在类中生成一个桥方法 ( bridge method ) :
public void setSecond ( Object second ) { setSecond ( ( Date ) second ) ;
但是桥方法可能会变得十分奇怪。 假设方法也覆盖了 getSecond 方法 :
class Datelnterval extends Pair <LocalDate>{
public Local Date getSecond() { return (Date)super.getSecond().clone();
}
在 Datelnterval 类中, 有两个 getSecond 方法 :
- Local Date getSecond ( ) / / defined in Datelnterval
- Object getSecond 0 // overrides the method defined in Pair to call the first method
不能这样编写 Java 代码( 在这里, 具有相同参数类型的两个方法是不合法的 ) 。它们都没有参数。 但是 , 在虚拟机中 , 用参数类型和返回类型确定一个方法 。 因此 ,编译器可能产生两个仅返回类型不同的方法字节码, 虚拟机能够正确地处理这一情况 。
泛型的限制
- 不能用基本类型实例化类型参数。如Class<int>, Class<double>是不允许的,只能Class<Integer>, Class<Double>
- 运行时类型查询只适用于原始类型。如if ( a instanceof Class<String> ) 是不允许的。
- 不能创建参数化类型的数组。如Pair<String>[] table = new Pair<String> [10]; 也是不允许的。
这有什么问题呢 ? 擦除之后 , table 的类型是 Pair [ ]。 可以把它转换为 Object[]:
Object [] objarray = table ;
数组会记住它的元素类型 , 如果试图存储其他类型的元素 , 就会抛出一个 ArrayStoreException 异常 :
obj array [ 0 ] = " Hello " ; // Error component type is Pair可以声明通配类型的数组 , 然后进行类型转换 :
Pair <String>[] table = (Pair<String>[]) new Pair<?>[10]结果将是不安全的。如果在table[0] 中存储一个Pair<Employee>, 然后对 table [0].getFirst ( ) 调用一个 String 方法 , 会得到一个 ClassCastException 异常。
如果需要收集参数化类型对象 , 只有一种安全而有效的方法 : 使用 ArrayList : ArrayList <Pair<String>>
- Varargs 警告
考虑下面这个简单的方法, 它的参数个数是可变的 :
public static <T> void addAll(Collections coll , T . . . ts ){ for (t : ts) coll.add(t); }
应该记得, 实际上参数ts 是一个数组 , 包含提供的所有实参 。现在考虑以下调用 :
Col 1 ection <Pair <String>> table = . . . ; Pair<String> pairl = . . . ; Pair<String> pair2 = . . addAll(table , pairl , pair2);
为了调用这个方法, Java 虚拟机必须建立一个Pair<String>数组 ,这就违反了前面的规则。 不过 ,对于这种情况 , 规则有所放松 , 你只会得到一个警告 , 而不是错误 。
可以采用两种方法来抑制这个警告。 一种方法是为包含addAll 调用的方法增加注解 @SuppressWamings ( " unchecked " )。或者在 Java SE 7 中,还 可 以 用 @ SafeVarargs 直 接 标 注addAll 方法 :@SafeVarargs public static < T > void addAll ( Collection < T > coll , T ... ts )
现在就可以提供泛型类型来调用这个方法了。 对于只需要读取参数数组元素的所有方法, 都可以使用这个注解 , 这仅限于最常见的用例 。
但是这个时候要异常小心,否则会在别处得到一个异常 。
- 不能实例化类型变置。如public Pair ( ) { first = new T () ; second = new T ( ) ; } 是不允许的。
在 Java SE 8 之后 ,最好的解决办法是让调用者提供一个构造器表达式。 例如 :
Pair < String > p = Pair.makePair(String : : new) ;
makePair 方法接收一个 Supplier < T >, 这是一个函数式接口 , 表示一个无参数而且返回类型为 T 的函数 :public static <T> Pair <T> makePair(Supplier <T> constr){ return new Pair <>(constr.get()); }
比较传统的解决方法是通过反射调用 Class.newlnstance 方法来构造泛型对象。
public static <T> Pair <T> makePair (Class <T> cl) { try { return new Pair<>(cl.newInstance()); } catch ( Exception ex ) { return null; } }
- 不能构造泛型数组
- 泛型类的静态上下文中类型变量无效
- 不能抛出或捕获泛型类的实例
- 可以消除对受查异常的检查
通过使用泛型类 、 擦除和 @ SuppressWamings 注解 , 就能消除 Java 类型系统的部分基本限制。
总之
需要记住有关
Java 泛型转换的事实 :
• 虚拟机中没有泛型 , 只有普通的类和方法 。
• 所有的类型参数都用它们的限定类型替换 。
• 桥方法被合成来保持多态 。
• 为保持类型安全性 , 必要时插人强制类型转换 。