本文从泛型的基本概念,使用编写泛型及泛型的局限性,再到泛型实现的擦拭法分析,还从extends与super通配符对泛型的应用做了系统性介绍。
泛型概念
- ArrayList类内部方法
public class ArrayList {
private Object[] array;
private int size;
public void add(Object e) {...}
public void remove(int index) {...}
public Object get(int index) {...}
}
如果将上述ArrayList存储为String,Integer等类型,需要强制转换,容易出错。
为了解决这个问题,我们必须把ArrayList
变成一种模板:ArrayList
,代码如下:
public class ArrayList<T> {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}
- 泛型就是定义一种模板,编写模板代码来适应任意类型。例如
ArrayList
,然后在代码中为用到的类创建对应的ArrayList<类型>
:
ArrayList<String> strList = new ArrayList<String>();
- 在Java标准库中的
ArrayList
实现了List
接口,它可以向上转型为List
。但不能把ArrayList<Integer>
向上转型为ArrayList<Number>
或List<Number>
。
使用泛型
使用
ArrayList
时,如果不定义泛型类型时,泛型类型实际上就是Object
,没有发挥泛型的优势。```java List list = new ArrayList(); list.add(“Hello”); list.add(“World”); String first = (String) list.get(0); String second = (String) list.get(1);<br />当我们定义泛型类型`<String>`后,`List<T>`的泛型接口变为强类型`List<String>`:```java
// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);
编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。 如:
java List<Number> list = new ArrayList<Number>();
泛型接口
除了ArrayList
使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])
可以对任意数组进行排序,但待排序的元素必须实现Comparable
这个泛型接口:```java public interface Comparable{ /** - 返回-1: 当前实例比参数o小
- 返回0: 当前实例与参数o相等
- 返回1: 当前实例比参数o大
*/
int compareTo(T o);
}
}; Arrays.sort(ps); System.out.println(Arrays.toString(ps)); } }new Person("Bob", 61), new Person("Alice", 88), new Person("Lily", 75),
class Person implements Comparable
输出: [Alice,88, Bob,61, Lily,75]
<a name="ed7e82a4"></a>
# 编写泛型
1. 编写泛型步骤<br />(1)按照某种类型,如String,来编写类```java
public class Pair {
private String first;
private String last;
public Pair(String first, String last) {
this.first = first;
this.last = last;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
}
(2)标记所有的特征类型
(3)把特定类型String替换成T,并申明```java
public class Pair
4. **静态方法**<br />编写泛型类时,要特别注意,泛型类型`<T>`不能用于静态方法。```java
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... }
// 静态泛型方法应该使用其他类型区分:
public static <K> Pair<K> create(K first, K last) {
return new Pair<K>(first, last);
}
}
- 多个泛型类型```java
public class Pair
Pair
<a name="0780da18"></a>
# 擦拭法
1.Java语言的泛型实现方式是擦拭法(Type Erasure)。所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
例如,我们编写了一个泛型类`Pair`,这是编译器看到的代码:<br />
```java
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
而虚拟机根本不知道泛型。这是虚拟机执行的代码:
public class Pair {
private Object first;
private Object last;
public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}
public Object getFirst() {
return first;
}
public Object getLast() {
return last;
}
}
因此,Java使用擦拭法实现泛型,导致了:
- 编译器把类型
<T>
视为Object
; - 编译器根据
<T>
实现安全的强制转型。
使用泛型的时候,我们编写的代码也是编译器看到的代码, 而虚拟机执行的代码并没有泛型。
2.Java泛型的局限性
- 不能是基本类型,如int
- 无法取得带泛型的class
Pair<String> p1 = new Pair<>("Hello", "world");
Pair<Integer> p2 = new Pair<>(123, 456);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
System.out.println(c1==c2); // true
System.out.println(c1==Pair.class); // true
- 无法判断带泛型的class
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}
原因和前面一样,并不存在Pair.class
,而是只有唯一的Pair.class
。
- 不能实例化类型
要实例化T
类型,我们必须借助额外的Class
参数:```java public class Pair{ private T first; private T last; public Pair(Class clazz) {
} }first = clazz.newInstance(); last = clazz.newInstance();
<br />上述代码借助`Class`参数并通过反射来实例化`T`类型,使用的时候,也必须传入`Class`。例如:```java Pair<String> pair = new Pair<>(String.class);
因为传入了Class
的实例,所以我们借助String.class
就可以实例化String
类型。
extends通配符
1.泛型的继承关系:Pair不是Pair的子类, 使用Pair<? extends Number>
使得方法接收所有泛型类型为Number
或Number
子类的Pair
类型。
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
//Pair<? extends Number> p
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
2.使用<? extends Number> 的泛型定义称之为上界通配符 ,即把泛型类型T
的上界限定在Number
了。
3.extends通配符作用:只可读,不可写
- 允许调用
get()
方法获取Integer
的引用; - 不允许调用
set(? extends Integer)
方法并传入任何Integer
的引用(null
除外)。
4.也可使用extends限定T类型,比如public class Pair<T extends Number> { ... }
,泛型类型就限定为Number以及Number的子类。
super通配符
注意到
Pair<? super Integer>
表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。```java public static void main(String[] args) {Pair<Number> p1 = new Pair<>(12.3, 4.56); Pair<Integer> p2 = new Pair<>(123, 456); setSame(p1, 100); setSame(p2, 200); System.out.println(p1.getFirst() + ", " + p1.getLast()); System.out.println(p2.getFirst() + ", " + p2.getLast());
}
static void setSame(Pair<? super Integer> p, Integer n) { p.setFirst(n); p.setLast(n); } ```
2.<? super Integer>
不允许调用get()
方法获得Integer
的引用。 唯一例外是可以获取Object
的引用:Object o = p.getFirst()
。
3.对比extends跟super通配符
作为方法参数,<? extends T>
类型和<? super Integer>
类型的区别在于:
<? extends T>
允许调用读方法T get()
获取T
的引用,但不允许调用写方法set(T)
传入T
的引用(传入null
除外);<? super Integer>
允许调用写方法set(T)
传入T
的引用,但不允许调用读方法T get()
获取T
的引用(获取Object
除外)。
一个是允许读不允许写,另一个是允许写不允许读。
4.PECS原则
我们可以用PECS原则:Producer Extends Consumer Super。
即:如果需要返回T
,它是生产者(Producer),要使用extends
通配符;如果需要写入T
,它是消费者(Consumer),要使用super
通配符。
以Collections
的copy()
方法为例:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}
需要返回T
的src
是生产者,因此声明为List
,需要写入T
的dest
是消费者,因此声明为List
。
5.无限定通配符
因为<?>
通配符既没有extends
,也没有super
,因此:
- 不允许调用
set(T)
方法并传入引用(null
除外); - 不允许调用
T get()
方法并获取T
引用(只能获取Object
引用)。
换句话说,既不能读,也不能写,那只能做一些null
判断:
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
<?>
通配符很少使用,有一个独特的特点,就是:Pair<?>
是所有Pair<T>
的超类。