JAVA基础——泛型

前言

泛型就是编写模板代码来适应任意类型;

泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;

ArrayList 内部就是一个 Object[] 数组,配合存储一个当前分配的长度,就可以充当“可变数组”:

1
2
3
4
5
6
7
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类型,会有这么几个缺点:

  • 需要强制转型;
  • 不方便,易出错。

我们也不可能为了单独数据类型创建ArrayList,此时就需要利用泛型,比如我们的ArrayList可以这么写:

1
2
3
4
5
6
7
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) {...}
}

T 可以是任何class。这样一来,我们就实现了:编写一次模版,可以创建任意类型的ArrayList:

1
2
3
4
5
6
// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

使用泛型

总结

  • 使用泛型时,把泛型参数替换为需要的class类型,例如:ArrayList,ArrayList等;
  • 可以省略编译器能自动推断出的类型,例如:List list = new ArrayList<>();
  • 不指定泛型参数类型时,编译器会给出警告,且只能将视为Object类型;

举例

使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object:

1
2
3
4
5
6
// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

此时,只能把当作Object使用,没有发挥泛型的优势。

当我们定义泛型类型后,List的泛型接口变为强类型List

1
2
3
4
5
6
7
// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);

// 可以省略后面的Number,编译器可以自动推断泛型类型:

1
List<String> list = new ArrayList<>();

泛型接口

总结

可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

举例

除了ArrayList使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[]) 可以对任意类型数组进行排序,但待排序的元素必须实现Comparable这个泛型接口:

1
2
3
4
5
6
7
8
public interface Comparable<T> {
/**
* 返回-1: 当前实例比参数o小
* 返回0: 当前实例与参数o相等
* 返回1: 当前实例比参数o大
*/
int compareTo(T o);
}

比如对String[] 排序。

1
2
3
String[] ss = new String[] { "Orange", "Apple", "Pear" };
Arrays.sort(ss);
System.out.println(Arrays.toString(ss));
[Apple, Orange, Pear]

这是因为String本身已经实现了Comparable接口。如果换成我们自定义的Person类型试试:

1
2
3
4
5
6
7
Person[] ps = new Person[] {
new Person("Bob", 61),
new Person("Alice", 88),
new Person("Lily", 75),
};
Arrays.sort(ps);
System.out.println(Arrays.toString(ps));

运行程序,我们会得到 ClassCastException,即无法将Person转型为Comparable。我们修改代码,让Person实现Comparable接口:

1
class Person implements Comparable<Person> {

运行上述代码,可以正确实现按name进行排序。

也可以修改比较逻辑,例如,按score从高到低排序。请自行修改测试。

编写泛型

总结

  • 编写泛型时,需要定义泛型类型
  • 静态方法不能引用泛型类型,必须定义其他类型(例如)来实现静态泛型方法;
  • 泛型可以同时定义多种类型,例如Map<K, V>。

举例

原类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}

把特定类型String替换为T,并申明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}

编写泛型类时,要特别注意,泛型类型不能用于静态方法。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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() { ... }

// 对静态方法使用<T>:
public static Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
}

上述代码会导致编译错误,我们无法在静态方法create()的方法参数和返回类型上使用泛型类型T。

对于静态方法,我们可以单独改写为“泛型”方法,只需要使用另一个类型即可。对于上面的create()静态方法,我们应该把它改为另一种泛型类型,例如 <K>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
}

泛型还可以定义多种类型。例如,我们希望Pair不总是存储两个类型一样的对象,就可以使用类型 <T, K>

1
2
3
4
5
6
7
8
9
10
public class Pair<T, K> {
private T first;
private K last;
public Pair(T first, K last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public K getLast() { ... }
}

使用的时候,需要指出两种类型:

1
Pair<String, Integer> p = new Pair<>("test", 123);

Java标准库的 Map<K, V> 就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另一种类型。

擦拭法

总结

Java的泛型是采用擦拭法实现的;

擦拭法决定了泛型

  • 不能是基本类型,例如:int;
  • 不能获取带泛型类型的Class,例如:Pair.class;
  • 不能判断带泛型类型的类型,例如:x instanceof Pair
  • 不能实例化T类型,例如:new T()。

泛型方法要防止重复定义方法,例如:public boolean equals(T obj);

子类可以获取父类的泛型类型 <T>

举例

Java语言的泛型实现方式是擦拭法(Type Erasure)。

所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类Pair,这是编译器看到的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}

而虚拟机并不能识别泛型,这是虚拟机执行的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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使用擦拭法实现泛型,导致了:

  • 编译器把类型视为Object;
  • 编译器根据实现安全的强制转型。

使用泛型的时候,我们编写的代码也是编译器看到的代码,而虚拟机执行的代码并没有泛型。所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:

局限一:不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:

局限二:无法取得带泛型的Class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
public static void main(String[] args) {
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<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;
}
}

因为T是Object,我们对Pair和Pair类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。

换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair

局限三:无法判断带泛型的类型:

1
2
3
4
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>) {
}

原因和前面一样,并不存在Pair.class,而是只有唯一的Pair.class。

局限四:不能实例化T类型:

1
2
3
4
5
6
7
8
9
public class Pair<T> {
private T first;
private T last;
public Pair() {
// Compile error:
first = new T();
last = new T();
}
}

上述代码无法通过编译,因为构造方法的两行语句:

1
2
first = new T();
last = new T();

擦拭后变成了:

1
2
first = new Object();
last = new Object();

这样一来,创建new Pair()和创建new Pair()就全部成了Object,显然编译器要阻止这种类型不对的代码。

有些时候,一个看似正确定义的方法会无法通过编译。例如:

1
2
3
4
5
public class Pair<T> {
public boolean equals(T t) {
return this == t;
}
}

这是因为,定义的equals(T t)方法实际上会被擦拭成equals(Object t),而这个方法是继承自Object的,编译器会阻止一个实际上会变成覆写的泛型方法定义。

一个类可以继承自一个泛型类。例如:父类的类型是Pair,子类的类型是IntPair,可以这么继承:

1
2
public class IntPair extends Pair<Integer> {
}

使用的时候,因为子类IntPair并没有泛型类型,所以,正常使用即可:

1
IntPair ip = new IntPair(1, 2);

在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair可以获取到父类的泛型类型Integer。获取父类的泛型类型代码比较复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Main {
public static void main(String[] args) {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
Type firstType = types[0]; // 取第一个泛型类型
Class<?> typeClass = (Class<?>) firstType;
System.out.println(typeClass); // Integer
}

}
}

extends 通配符

总结

使用类似 <? extends Number> 通配符作为方法参数时表示:

  • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();;

  • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。

即一句话总结:使用extends通配符表示可以读,不能写。

使用类似 <T extends Number> 定义泛型类时表示:

  • 泛型类型限定为Number以及Number的子类。

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}

static int add(Pair<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;
}
}

直接运行,会得到一个编译错误:

incompatible types: Pair<Integer> cannot be converted to Pair<Number>

有没有办法使得方法参数接受Pair?办法是有的,这就是使用 Pair<? extends Number> 使得方法接收所有泛型类型为Number或Number子类的Pair类型。我们把代码改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}

这样一来,给方法传入 Pair<Integer>类型时,它符合参数 Pair<? extends Number> 类型。这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。

除了可以传入Pair类型,我们还可以传入Pair类型,Pair类型等等,因为Double和BigDecimal都是Number的子类。

我们再来考察一下Pair的set方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
p.setFirst(new Integer(first.intValue() + 100));
p.setLast(new Integer(last.intValue() + 100));
return p.getFirst().intValue() + p.getFirst().intValue();
}
}

不出意外,我们会得到一个编译错误:

incompatible types: Integer cannot be converted to CAP#1
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number

原因还在于擦拭法。如果我们传入的p是 Pair<Double>,显然它满足参数定义 Pair<? extends Number>,然而,Pair<Double>的 setFirst() 显然无法接受Integer类型。

这就是 <? extends Number> 通配符的一个重要限制:方法参数签名 setFirst(? extends Number)无法传递任何Number类型给 setFirst(? extends Number)

这里唯一的例外是可以给方法参数传入null:

使用extends限定T类型

在定义泛型类型Pair的时候,也可以使用extends通配符来限定T的类型:

1
public class Pair<T extends Number> { ... }

现在,我们只能定义:

1
2
3
Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;

因为Number、Integer和Double都符合 <T extends Number>

非Number类型将无法通过编译:

super 通配符

总结

使用类似<? super Integer>通配符作为方法参数时表示:

  • 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);;
  • 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();。

即使用super通配符表示只能写不能读。

举例

考察下面的set方法:

1
2
3
4
void set(Pair<Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}

传入Pair是允许的,但是传入Pair是不允许的。

和extends通配符相反,这次,我们希望接受 Pair<Integer>类型,以及 Pair<Number>Pair<Object>,因为Number和Object是Integer的父类,setFirst(Number)和setFirst(Object)实际上允许接受Integer类型。

我们使用super通配符来改写这个方法:

1
2
3
4
void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}

再考察 Pair<? super Integer> 的getFirst()方法,它的方法签名实际上是:

1
? super Integer getFirst();

这里注意到我们无法使用Integer类型来接收getFirst()的返回值,即下面的语句将无法通过编译:

1
Integer x = p.getFirst();

因为如果传入的实际类型是Pair,编译器无法将Number类型转型为Integer。

注意:虽然Number是一个抽象类,我们无法直接实例化它。但是,即便Number不是抽象类,这里仍然无法通过编译。此外,传入Pair类型时,编译器也无法将Object类型转型为Integer。

唯一可以接收getFirst()方法返回值的是Object类型:

1
Object obj = p.getFirst();

对比extends和super通配符

我们再回顾一下extends通配符。作为方法参数,<? extends T> 类型和 <? super T> 类型的区别在于:

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个是允许读不允许写,另一个是允许写不允许读。

先记住上面的结论,我们来看Java标准库的Collections类定义的copy()方法:

1
2
3
4
5
6
7
8
9
public class Collections {
// 把src的每个元素复制到dest中:
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);
dest.add(t);
}
}
}

它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是 List<? super T>,表示目标List,第二个参数 List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型 <? extends T> 的变量src,我们可以安全地获取类型T的引用,而对于类型 <? super T>的变量dest,我们可以安全地传入T的引用。

这个copy()方法的定义就完美地展示了extends和super的意图:

  • copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
  • copy()方法内部也不会修改src,因为不能调用src.add(T)。

PECS原则

何时使用 extends,何时使用 super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。

即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。

无限定通配符

实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?:

1
2
void sample(Pair<?> p) {
}

因为<?>通配符既没有 extends,也没有 super,因此:

  • 不允许调用 set(T) 方法并传入引用(null除外);
  • 不允许调用 T get() 方法并获取T引用(只能获取Object引用)。

换句话说,既不能读,也不能写,那只能做一些null判断:

1
2
3
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}

<?> 通配符有一个独特的特点,就是:Pair<?> 是所有Pair<T> 的超类:

文章目录
  1. 1. 前言
  2. 2. 使用泛型
    1. 2.1. 总结
    2. 2.2. 举例
  3. 3. 泛型接口
    1. 3.1. 总结
    2. 3.2. 举例
  4. 4. 编写泛型
    1. 4.1. 总结
    2. 4.2. 举例
  5. 5. 擦拭法
    1. 5.1. 总结
    2. 5.2. 举例
  6. 6. extends 通配符
    1. 6.1. 总结
    2. 6.2. 举例
  7. 7. super 通配符
    1. 7.1. 总结
    2. 7.2. 举例
  8. 8. 对比extends和super通配符
  9. 9. PECS原则
  10. 10. 无限定通配符
|