Generic types & Wildcards
Use generic types
泛型的好处
- 编译时期的强类型检查。有利于提早发现问题,解决问题。
- 不用再使用类型强转。
- 能够利用泛型编写简单易读的通用代码。
泛型通常命名规则:
E - Element
K - Key
N - Number
T - Type
V - Value
public class Stack<E> {
...
public Stack() { ... }
public void push(E e) { ... }
public E pop() { ... }
public boolean isEmpty { ... }
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
}
Use wildcards
public class Stack<E> {
...
public Stack() { ... }
public void push(E e) { ... }
public E pop() { ... }
public boolean isEmpty { ... }
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
}
Wildcards and Subtyping
假设有以下两个类:
class A { /* ... */ }
class B extends A { /* ... */ }
像下面这么写是合理的:
B b = new B();
A a = b;
但是如果像下面这么写就会报错:
List<B> lb = new ArrayList<>();
List<A> la = lb; // compile-time error
也就是说虽然B是A的子类,但是List却不是List的子类。那他们之间有什么关系呢:
List和List的公共父类
除此之外,还有以下层级关系:
A hierarchy of several generic List class declarations
/**
* 上有界通配符
* @param list
*/
public static void upperBoundedWildcards(List<? extends String> list){
}
/**
* 无界通配符
* @param list
*/
public static void unboundedWildcards(List<?> list){
}
/**
* 下有界通配符
* @param list
*/
public static void lowerBoundedWildcards(List<? super Integer> list){
}
1.<T extends E>:用于定义类型参数;
它声明一个类型参数T,可以放在泛型类定义中类名后面、泛型方法的返回值前面;
2.<? extends E>:用于实例化类型参数;
它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道他是E或者E的某个子类;
- 从泛型类派生子类
//这样写是错误的,实现的接口或父类不能再包含类型形参
public class ChildApple extends Apple<T>{
}
//正确的写法是以下三种:在使用接口或父类时传入类型实参或者不传
//继承泛型类(子类不是泛型类)
public class ChildApple extends Apple<String>{
}
//不传时,编译器会发出警告,系统默认把Apple<T>类中的T形参当成Object类型处理
public class ChildApple extends Apple{
}
//继承泛型类(子类也是泛型类)
public class ChildApple<T> extends Apple<T>{
}
public class Apple<T> {
//不能在静态属性声明时使用类型形参,编译报错
static T info ;
//不能在静态方法声明中使用类型形参,编译报错
public static void fun(T t){
}
//系统并不会真正生成泛型类,所以不能使用instanceof运算符,编译报错;
if(a1 instanceof Apple<String>){}
}
类型擦除
什么是类型擦除
Java 的泛型使用了类型擦除机制,这个引来了很大的争议,以至于 Java 的泛型功能受到限制,只能说是”伪泛型“。什么叫类型擦除呢?简单的说就是,类型参数只存在于编译期,在运行时,Java 的虚拟机 ( JVM ) 并不知道泛型的存在。先看个例子:
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
System.out.println(c1 == c2);
}
}
上面的代码有两个不同的 ArrayList:ArrayList 和 ArrayList。在我们看来它们的参数化类型不同,一个保存整性,一个保存字符串。但是通过比较它们的 Class 对象,上面的代码输出是 true。这说明在 JVM 看来它们是同一个类。而在 C++、C# 这些支持真泛型的语言中,它们就是不同的类。
泛型参数会擦除到它的第一个边界,比如说上面的 Holder2 类,参数类型是一个单独的 T,那么就擦除到 Object,相当于所有出现 T 的地方都用 Object 替换。所以在 JVM 看来,保存的变量 a 还是 Object 类型。之所以取出来自动就是我们传入的参数类型,这是因为编译器在编译生成的字节码文件中插入了类型转换的代码,不需要我们手动转型了。如果参数类型有边界那么就擦除到它的第一个边界,这个下一节再说。
擦除带来的问题
擦除会出现一些问题,下面是一个例子:
class HasF {
public void f() {
System.out.println(“HasF.f()”);
}
}
public class Manipulator {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void manipulate() {
obj.f(); //无法编译 找不到符号 f()
}
public static void main(String[] args) {
HasF hasF = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hasF);
manipulator.manipulate();
}
}
上面的 Manipulator 是一个泛型类,内部用一个泛型化的变量 obj,在 manipulate 方法中,调用了 obj 的方法 f(),但是这行代码无法编译。因为类型擦除,编译器不确定 obj 是否有 f() 方法。解决这个问题的方法是给 T 一个边界:
class Manipulator2 {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
}
现在 T 的类型是 ,这表示 T 必须是 HasF 或者 HasF 的导出类型。这样,调用 f() 方法才安全。HasF 就是 T 的边界,因此通过类型擦除后,所有出现 T 的
地方都用 HasF 替换。这样编译器就知道 obj 是有方法 f() 的。
但是这样就抵消了泛型带来的好处,上面的类完全可以改成这样:
class Manipulator3 {
private HasF obj;
public Manipulator3(HasF x) { obj = x; }
public void manipulate() { obj.f(); }
}
所以泛型只有在比较复杂的类中才体现出作用。但是像 这种形式的东西不是完全没有意义的。如果类中有一个返回 T 类型的方法,泛型就有用了,因为这样会返回准确类型。比如下面的例子:
class ReturnGenericType {
private T obj;
public ReturnGenericType(T x) { obj = x; }
public T get() { return obj; }
}
这里的 get() 方法返回的是泛型参数的准确类型,而不是 HasF。
我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 则会被转译成普通的 Object 类型,如果指定了上限如 则类型参数就被替换成类型上限。
https://blog.****.net/briblue/article/details/76736356