JAVA-泛型
sschrodinger
2018/11/15
简介
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类
、泛型接口
、泛型方法
。
在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况 ,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型的好处:使用泛型,首先可以通过IDE进行代码类型初步检查,然后在编译阶段进行编译类型检查,以保证类型转换的安全性;并且所有的强制转换都是自动和隐式的,可以提高代码的重用率。
语法语法
基本语法
我们一般在 JAVA 代码中用 <>
定义泛型参数(类型参数)。
泛型参数的定义很简单,在<>
中填上占位符代表类,注意占位符可以有多个。如下所示:
//泛型类,T为占位符
class ClassName {
T item;
T getT() {}
}
//泛型接口
interface InterfaceName {
print(T t);
}
//泛型方法
class ClassName {
public void(T t) {}
public static print(E e) {}
}
note
- 对于类和接口的泛型定义,直接在类名或者接口名之后添加
<>
- 泛型方法定义为
<占位符> 返回值 方法名()
。注意静态方法的泛型定义只能使用这种方法,因为静态方法不能访问泛型类的泛型参数,必须要自己定义泛型参数。
边界
我们来看一个 C++ 的模板例子。
template class Manipulator {
T obj;
Manipulator(T x) {obj = x;}
void manipulate() {obj.f();}
};
class HasF {
public:
void f() {count << "Has::f()" << endl;}
}
int main() {
HasF hf;
Manipulator manipulator(hf);
manipulator.manipulate();
}
在编译期时,编译器能够自己判断实例化的对象有无f()方法,如果没有则报编译期错误。
JAVA 使用擦除来实现泛型,对于
来说,我们会将其擦除到他的第一个边界,在这里,第一个边界就是指Object
。我们来看一个最简单的例子。
public class Holder {
T item;
public Holder(T item) { this.item = item;}
public T get() {return item;}
public static void main(String[] args) {
Holder holder = new Holder(Long.valueOf(0L));
Long long1 = holder.get();
Object object = holder.get();
Number number = holder.get();
//编译器报错,类型为Long,不能转化为Integer
//Integer integer = holder.get();
}
}
这是最简单的一个容器类,可以存储一个指定类型的类。
反编译 Holder 的 class 代码,如下所示:
public class test.Holder {
T item;
public test.Holder(T);
Code:
0: aload_0
1: invokespecial #13 // Method java/lang/Object."":()V
4: aload_0
5: aload_1
6: putfield #16 // Field item:Ljava/lang/Object;
9: return
public T get();
Code:
0: aload_0
1: getfield #16 // Field item:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #1 // class test/Holder
3: dup
4: lconst_0
5: invokestatic #29 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
8: invokespecial #35 // Method "":(Ljava/lang/Object;)V
11: astore_1
12: aload_1
13: invokevirtual #37 // Method get:()Ljava/lang/Object;
16: checkcast #30 // class java/lang/Long
19: astore_2
20: aload_1
21: invokevirtual #37 // Method get:()Ljava/lang/Object;
24: astore_3
25: aload_1
26: invokevirtual #37 // Method get:()Ljava/lang/Object;
29: checkcast #39 // class java/lang/Number
32: astore 4
34: return
}
观察test.Holder(T)
,我们发现putfield
填充的域是Ljava/lang/Object
类型,即 item 在编译好的class文件中,以Object
类型存储, 这可以证明到了Object
。只不过在调用get
函数之后(即main
函数13,21,26行之后)编译器会帮我们自动添加checkcast
机器码来检查类型是否非法,而不需要我们在程序中显示的对类型进行强制转换。
既然在内部存储为Object
,那么在类中就只能调用Object
的方法,如果使用了其他的方法,如java.lang.Long
类的longValue()
方法,就会出现 The method longValue() is undefined for the type T
错误,我们在原来的类的基础上增加print
函数演示,如下所示:
public class MiddleHolder {
T item;
public MiddleHolder(T item) { this.item = item;}
public MiddleHolder() {
// TODO Auto-generated constructor stub
}
public void print() {
System.out.println(item.toString());
//The method longValue() is undefined for the type T
//System.out.println(item.longValue());
}
public T get() {return item;}
public static void main(String[] args) {
Holder holder = new Holder(Long.valueOf(0L));
Long long1 = holder.get();
Object object = holder.get();
Number number = holder.get();
//Integer integer = holder.get();
}
}
为了引进和 C++ 一样的功能,JAVA 改写了extends关键字,extends的申明方式是在占位符之后增加具体的类型,如下所示:
class ClassName {}
上面的定义表达的意思是,我是一个具体的类型,并且一定是 BaseClassName 的一个子类。
在我们这里,为了让 Holder 可以调用 Long 的方法,我们改写自己的程序如下所示:
public class Holder {
T item;
public Holder(T item) { this.item = item;}
public Holder() {
// TODO Auto-generated constructor stub
}
public void print() {
item.longValue();
}
public T get() {return item;}
public static void main(String[] args) {
Holder holder = new Holder(Long.valueOf(0L));
Long long1 = holder.get();
Object object = holder.get();
Number number = holder.get();
}
}
这里告诉编译器我的 T 一定是 Long 的子类,那么编译器就会把类型擦除到 Long,反编译 class 文件,部分如下所示:
public class test.MiddleHolder {
T item;
public test.MiddleHolder(T);
Code:
0: aload_0
1: invokespecial #13 // Method java/lang/Object."":()V
4: aload_0
5: aload_1
6: putfield #16 // Field item:Ljava/lang/Long;
9: return
机器码第6行我们可以看到域的类型的确为 Long。
在这里,extends关键字也支持类和多个接口的定义,形式如下所示:
class ClassName
上面定义的意思就是说我的 T 是一个具体的类型,且继承了 BaseClassName,并且实现了 interfaceName1,interfaceName1 两个接口。那么也只能继承了 BaseClassName并且实现了 interfaceName1,interfaceName1 两个接口的类才可以作为他的泛型类型。
note
- 每个名字间以 & 连接
- 类只能有一个且只能放在左边,接口可以有多个
自限定类型的定义
我们考虑 Comparable 接口,JAVA 中 Comparable接口定义如下:
interface Comparable {
public int compareTo(T o);
}
在实现过程中,我们往往希望是同种类型的不同实例进行比较,而不是跨类型的比较,那么我们就希望自己的函数有如下的形式:
class ComparableImp implements Comparable {
@Override public int compareTo(ComparableImp o) {
return 0;
}
}
我们用类本身去填充 Comparable 的类型参数,就可以实现同一种类型的比较。
我们实现一个高级的 Holder,他可以存储一个可以在同类型中比较的对象,我们最开始的 HOlder 类型,当然可以存储这个对象,但是我们希望它能够更严格的定义,使得不能进行自比较的泛型无法存入这个 Holder 中,那么我们就需要重新定义泛型参数。如下所示:
public class AdvancedHolder> {
T item;
public T get() {
return item;
}
public void set(T t) {
item = t;
}
public int compare(T other) {
return item.compareTo(other);
}
public AdvancedHolder() {
// TODO Auto-generated constructor stub
}
}
上述泛型的意义是我的 T 是实现了COmparable
的子类,可以进行相互比较。
我们再定义一个无法自己比较的类如下所示,这个类和 String 进行比较:
class NormalComparableImp implements Comparable {
@Override
public int compareTo(String o) {
// TODO Auto-generated method stub
return 0;
}
}
在编译器中,就会告诉我们 NormalComparableImp 不能作为泛型参数,如下所示:
public class test {
public static void main(String[] args) {
ComparableImp AWithCOmpare = new ComparableImp();
NormalComparableImp normalComparableImp = new NormalComparableImp();
AdvancedHolder holder1 = new AdvancedHolder<>();
//Bound mismatch: The type NormalComparableImp is not a valid substitute for the bounded parameter > of the type AdvancedHolder
//AdvancedHolder holder2 = new AdvancedHolder<>();
}
}
note
- 自限定类型的检查完全是编译器完成,在编译完成的class代码中,以
AdvancedHolder
为例,item会被擦除成Comparable
泛型使用
确定类型填充
我们需要在使用的时候填充占位符,有两种方式,一种方式是使用确定类型填充,一种方式是使用通配符填充。
确定类型填充即使指用变量名填充占位符,如:
class Holder {}
class AdvancedHolder> {}
public class Test {
public static void main(String[] args) {
Holder holder1 = new Holder<>();
AdvancedHolder holder2 = new AdvancedHolder<>();
}
}
以上都是使用确定类型填充占位符。需要注意的是确定类型要符合占位符的继承要求。
无界通配符
无界通配符为一个?
,表示可能为任意满足extends要求的类型。下面是一个可能的使用案例。
public class Test {
public static void main(String[] args) {
Holder> holder2 = new Holder<>();
}
}
有界通配符
有界通配符有两种基本的形式,一种是? extends BaseClassName
,代表继承了 BaseClassName 的某一个类;一种形式是? super BaseClassName
,代表子类是 BaseClassName 的某一个类。
我们考虑一组继承关系,继承关系如下:
/*
Fruit
/ \
/ \
Apple Orange
/ \
/ \
Red Green
*/
那么 extends Apple>
就代表 Apple,Red,Green 其中的某一类。 super Apple>
就代表 Apple,Fruit 中的某一类。
import java.util.LinkedList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List extends Fruit> fruits = new LinkedList();
fruits.add(null);
//The method add(capture#2-of ? extends Fruit) in the type List is not applicable for the arguments (Object)
//fruits.add(new Object());
//same exception
//fruits.add(new Fruit());
}
}
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
class Red extends Apple {}
class Green extends Fruit {}
在以上例子中,在List extends Fruit> fruits = new LinkedList
中,因为 Apple 为 Fruit 的子类,所以一定满足 extends Fruit>
条件,可以类比向上转型得到 List extends Fruit>
,即如果P是S的超类,那么 Pair就是Pair extends P>的子类型。
但是向上转型之后,List extends Fruit>
丢失了他具体的类型信息,他只知道自己存储的类型应该为 Fruit 的子类,但是并不知道是具体的哪个子类,有可能是 Apple,有可能是 Orange,甚至有可能是 Fruit。因为有可能在List内部可能会实际类的具体方法,如 MiddleHolder 所示,如果不知道具体的边界在哪里,很有可能会出现错误,所以编译器干脆禁止所有的类型添加,只允许 null 的添加。
note
Pair>
就是Pair extends Object>
, 因此,无限定通配符可以作为返回值,不可做入参。返回值只能保存在Object中。P>
不等于P
,P
是P>
的子类。
我们来看 super 关键字,对程序略作修改。
import java.util.LinkedList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List super Fruit> fruits = new LinkedList
super 关键字限定了泛型的下界,即我一定是子类本身或其祖先类。在编译器眼里,虽然不知道具体的类型,但是知道这个类一定是 Fruit 的父类,那么添加 Fruit 的子类到类表中就是安全的。但是同理,因为不知道是父类中的具体哪个类,所以添加父类(包括Object)都认为是不安全的。
类型信息
在运行期,泛型会被完全的擦除,只保留占位符。我们利用反射获得泛型参数的类型,代码如下列所示:
public class Test {
public static void main(String[] args) {
List strings = new LinkedList<>();
TypeVariable[] variables = strings.getClass().getTypeParameters();
for (TypeVariable typeVariable : variables) {
System.out.println(typeVariable);
}
}
}
//out: E
可以看到,程序并不能知道泛型的具体类型是什么,只能知道有一个泛型参数。实际上,程序在运行时,所有的泛型类方法都会变成原始类。如下所示:
public class Test {
public static void main(String[] args) {
ArrayList arrayList1=new ArrayList();
arrayList1.add("abc");
ArrayList arrayList2=new ArrayList();
arrayList2.add(123);
System.out.println(arrayList1.getClass()==arrayList2.getClass());
}
}
我们可以看到最后arrayList1
和arrayList2
的类型是相同的,都是 ArrayList 类。
JAVA 不允许在泛型中使用类字面量来获得类的类型,但是原始类型可以。所谓类字面量就是通过.class
关键词获得一个 Class 对象,例子如下:
//不合法
Class clazz = ArrayList.class;
//不合法
List list;
//合法
Class clazz1 = ArrayList.class;
同时,在使用instanceof
关键词时,使用参数化类型而非无限制通配符类型都是非法的,在这个时候使用无限制通配符类型替代原生态类型(对 instanceof 右操作数的规则),不会对instanceof
操作产生任何不良的影响,即:
public class Test {
public static void main(String[] args) {
List list = null;
List> list3 = null;
List list2 = null;
//非法,Cannot perform instanceof check against parameterized type List. Use the form List> instead since further generic type information will be erased at runtime
if (list instanceof List) {}
//非法,理由同上
if (list2 instanceof List) {}
//非法,理由同上
if (list3 instanceof List) {}
if (list instanceof List>) {}
if (list2 instanceof List>) {}
if (list3 instanceof List>) {}
if (list instanceof List) {}
if (list2 instanceof List) {}
if (list3 instanceof List) {}
}
}
可以看到, instanceof 的右操作数不能够为泛型,只能为无限制通配符或者原生类型。
如上,因为在运行态泛型会被擦除,所以与 List
因为在泛型擦除后,保留的是原始类型,所以特别要注意重载和重写。JAVA 不允许不同的泛型参数作为参数进行重写,例子如下:
class A {
public A() {}
//下面两个print为非法重载,因为擦除后都是List,不能作为签名
public void print(List list) {}
public void print(List list) {}
}
既然(List
和(List
都是一个签名,不允许重载,那么是不是就允许重写了呢?实际上 JAVA 也不允许这两个进行重写,例子如下:
class A {
public void print(List list) {}
}
class B extends A {
//非法,The method print(List) of type B must override or implement a supertype method
@Override public void print(List list) {}
}
编译器会要求你去掉@override
注解,以表明这不是一个继承的方法,但是去掉@Override
注解之后,又会出现新的错误如下:
Name clash: The method print(List) of type B has the same erasure as print(List) of type A but does not override it
所以说,在泛型类作为参数时,要尽量避免重写和重载,不然,只有更换名字。
同时,因为这个原因,我们还需要注意一点的是一个类或类型变量不可成为两个不同参数化的接口类型的子类型。如下例子:
class Parent implements Comparator{
@Override
public int compare(Parent o1, Parent o2) {
return 0;
}
}
//非法
class Son extends Parent implements Comparator {
//The interface Comparator cannot be implemented more than once with different arguments: Comparator and Comparator
}
这很容易看出来,因为 Parent 继承了 Comparator
协变
参数话类型是不可变的,即对于任意两个截然不同的类型 Type1 和 Type2 而言,不管 Type1 和 Type2 有什么继承关系,List
public class Test {
public static void main(String[] args) {
List list = null;
//非法,The method print(List) in the type Test is not applicable for the arguments (List)
print(list);
//合法,子类作为参数
print(Integer.valueOf(0));
}
public static void print(List list) {}
public static void print(Number number) {}
}
实例
PECS 模型
PECS 模型指的是如果参数话类型表示一个 T 生产者,就使用 extends T>
,如果它表示一个 T 消费者,就是用 super T>
。这也叫Get and Put Principal。通过一个实例来解式这个模型。
如下,我们考虑实现一个最简单的堆栈。如下是他的公共API:
public class Stack {
public Stack() {}
public void push(E e) {}
public E pop() {}
public boolean isEmpty() {}
}
现我们考虑增加一个方法,将它按照顺序将一系列的元素全部放在堆栈中,代码如下:
public void pushAll(Iterable src) {
for (E e:src)
push(e);
}
考虑一个情景,我们有一个 Stack
public class Test {
public static void main(String[] args) {
Stack stack = new Stack<>();
Iterable src = new ArrayList<>();
//非法,The method pushAll(Iterable) in the type Stack is not applicable for the arguments (Iterable)
stack.pushAll(src);
}
}
class Stack {
public Stack() {}
public void push(E e) {}
public E pop() {return null;}
public boolean isEmpty() {return false;}
public void pushAll(Iterable src) {
for (E e:src)
push(e);
}
}
因为参数话类型的不可协变,Stack 实例的 pushAll 方法要求提供一个 Iterable
这就需要第一个原则,PE原则。因为在参数中,src 充当着生产者的角色,即 src 给其他地方提供数据,我们就使用 extends T>
代替T
,修改之后的代码如下:
class Stack {
public Stack() {}
public void push(E e) {}
public E pop() {return null;}
public boolean isEmpty() {return false;}
public void pushAll(Iterable extends E> src) {
for (E e:src)
push(e);
}
这样,我们就把通配符限制在了 E 及其子类中,在讲有界通配符时,我们知道 ClassNmae
我们继续增加一个popAll方法,将堆栈的数据添加到指定的几何中。代码如下:
public void popAll(Collection dst) {
while(!isEmpty())
dest.add(pop());
}
同理,我们想将元素全部弹出到一个泛型为 Object 的容器中,代码如下:
public class Test {
public static void main(String[] args) {
Stack stack = new Stack<>();
Collection
和上面是一样的问题,只不过这时我们需要向下转型,这就是第二个原则,CS原则。我们用 super T>
代替T
,修改之后的代码如下:
class Stack {
public Stack() {}
public void push(E e) {}
public E pop() {return null;}
public boolean isEmpty() {return false;}
public void pushAll(Iterable extends E> src) {
for (E e:src)
push(e);
}
public void popAll(Collection super E> dst) {
while(!isEmpty())
dest.add(pop());
}
}