廖雪峰Java教程笔记-泛型


泛型

什么是泛型

1
2
3
4
5
6
7
8
9
ArrayList是一种可变长数组,其内部使用的是Object类型

public class ArrayList {
private Object[] array;
private int size;
public void add(Object e) {...}
public void remove(int index) {...}
public Object get(int index) {...}
}
1
2
3
如果用上述ArrayList存储String类型,会有这么几个缺点:
需要强制转型;
不方便,易出错。
1
2
3
4
ArrayList list = new ArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String first = (String) list.get(0);
1
2
3
4
很容易出现ClassCastException,因为容易“误转型”:
list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
要解决上述问题,我们可以为String单独编写一种ArrayList:
public class StringArrayList {
private String[] array;
private int size;
public void add(String e) {...}
public void remove(int index) {...}
public String get(int index) {...}
}

这样一来,存入的必须是String,取出的也一定是String,不需要强制转型,因为编译器会强制检查放入的类型:
StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不允许放入非String类型:
list.add(new Integer(123));
1
2
3
4
5
6
7
8
9
10
问题暂时解决。

然而,新的问题是,如果要存储Integer,还需要为Integer单独编写一种ArrayList:
实际上,还需要为其他所有class单独编写一种ArrayList:

LongArrayList
DoubleArrayList
PersonArrayList
...
这是不可能的,JDK的class就有上千个,而且它还不知道其他人编写的class。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
为了解决新的问题,我们必须把ArrayList变成一种模板:ArrayList<T>,代码如下:
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:
// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();
1
2
3
4
5
6
7
8
9
10
11
因此,泛型就是定义一种模板,例如ArrayList<T>,然后在代码中为用到的类创建对应的ArrayList<类型>:
ArrayList<String> strList = new ArrayList<String>();

由编译器针对类型作检查:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

这样一来,既实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型。

向上转型

1
2
3
4
5
6
7
8
在Java标准库中的ArrayList<T>实现了List<T>接口,它可以向上转型为List<T>:

public class ArrayList<T> implements List<T> {
...
}

List<String> list = new ArrayList<String>();
即类型ArrayList<T>可以向上转型为List<T>。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
要特别注意:不能把ArrayList<Integer>向上转型为ArrayList<Number>或List<Number>。

这是为什么呢?假设ArrayList<Integer>可以向上转型为ArrayList<Number>,观察一下代码:

// 创建ArrayList<Integer>类型:
ArrayList<Integer> integerList = new ArrayList<Integer>();
// 添加一个Integer:
integerList.add(new Integer(123));
// “向上转型”为ArrayList<Number>:
ArrayList<Number> numberList = integerList;
// 添加一个Float,因为Float也是Number:
numberList.add(new Float(12.34));
// 从ArrayList<Integer>获取索引为1的元素(即添加的Float):
Integer n = integerList.get(1); // ClassCastException!
1
2
3
4
5
我们把一个ArrayList<Integer>转型为ArrayList<Number>类型后,这个ArrayList<Number>就可以接受Float类型,因为Float是Number的子类。但是,ArrayList<Number>实际上和ArrayList<Integer>是同一个对象,也就是ArrayList<Integer>类型,它不可能接受Float类型, 所以在获取Integer的时候将产生ClassCastException。

实际上,编译器为了避免这种错误,根本就不允许把ArrayList<Integer>转型为ArrayList<Number>。

ArrayList<Integer>和ArrayList<Number>两者完全没有继承关系。

总结

1
2
3
4
5
泛型就是编写模板代码来适应任意类型;

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

注意泛型的继承关系:可以把`ArrayList<Integer>`向上转型为`List<Integer>`(`T`不能变!),但不能把`ArrayList<Integer>`向上转型为`ArrayList<Number>`(`T`不能变成父类)。

使用泛型

1
2
3
4
5
6
7
8
9
使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object:

// 编译器警告:
List list = new ArrayList(); //这里吧ArrayList向上转型为List
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);
此时,只能把<T>当作Object使用,没有发挥泛型的优势。
1
2
3
4
5
6
7
8
9
当我们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>:

// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);
1
2
3
4
5
6
7
当我们定义泛型类型<Number>后,List<T>的泛型接口变为强类型List<Number>:

List<Number> list = new ArrayList<Number>();
list.add(new Integer(123));
list.add(new Double(12.34));
Number first = list.get(0);
Number second = list.get(1);
1
2
3
4
5
6
7
编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。例如,对于下面的代码:

List<Number> list = new ArrayList<Number>();
编译器看到泛型类型List<Number>就可以自动推断出后面的ArrayList<T>的泛型类型必须是ArrayList<Number>,因此,可以把代码简写为:

// 可以省略后面的Number,编译器可以自动推断泛型类型:
List<Number> list = new ArrayList<>();

泛型接口

1
2
3
4
5
6
7
8
9
10
除了ArrayList<T>使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:

public interface Comparable<T> {
/**
* 返回负数: 当前实例比参数o小
* 返回0: 当前实例与参数o相等
* 返回正数: 当前实例比参数o大
*/
int compareTo(T o);
}
1
2
3
4
5
6
7
8
9
可以直接对String数组进行排序:
// sort
import java.util.Arrays;

public class Main {
public static void main(String[] args) {
String[] ss = new String[] { "Orange", "Apple", "Pear" };
Arrays.sort(ss);
System.out.println(Arrays.toString(ss));
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
这是因为String本身已经实现了Comparable<String>接口。如果换成我们自定义的Person类型试试:
// sort
import java.util.Arrays;

public class Main {
public static void main(String[] args) {
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));

class Person {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return this.name + "," + this.score;
}
}
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
30
31
运行程序,我们会得到ClassCastException,即无法将Person转型为Comparable。我们修改代码,让Person实现Comparable<T>接口:

// sort
import java.util.Arrays;

public class Main {
public static void main(String[] args) {
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));
}
}

class Person implements Comparable<Person> {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
public String toString() {
return this.name + "," + this.score;
}
}
1
2
3
运行上述代码,可以正确实现按name进行排序。

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

总结

1
2
3
4
5
6
7
使用泛型时,把泛型参数<T>替换为需要的class类型,例如:ArrayList<String>,ArrayList<Number>等;

可以省略编译器能自动推断出的类型,例如:List<String> list = new ArrayList<>();;

不指定泛型参数类型时,编译器会给出警告,且只能将<T>视为Object类型;

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

编写泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
编写泛型类比普通类要复杂。通常来说,泛型类一般用在集合类中,例如ArrayList<T>,我们很少需要编写泛型类。

如果我们确实需要编写一个泛型类,那么,应该如何编写它?
可以按照以下步骤来编写一个泛型类。
首先,按照某种类型,例如:String,来编写类:

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;
}
}
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
30
31
32
33
34
35
然后,标记所有的特定类型,这里是String:

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,并申明<T>:

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;
}
}

熟练后即可直接从T开始编写。

静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法。例如:

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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上述代码会导致编译错误,我们无法在静态方法create()的方法参数和返回类型上使用泛型类型T。

有些同学在网上搜索发现,可以在static修饰符后面加一个<T>,编译就能通过:
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 <T> Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
}
但实际上,这个<T>和Pair<T>类型的<T>已经没有任何关系了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对于静态方法,我们可以单独改写为“泛型”方法,只需要使用另一个类型即可。对于上面的create()静态方法,我们应该把它改为另一种泛型类型,例如,<K>:

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);
}
}

这样才能清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

多个泛型类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
泛型还可以定义多种类型。例如,我们希望Pair不总是存储两个类型一样的对象,就可以使用类型<T, K>:

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() { ... }
}

使用的时候,需要指出两种类型:
Pair<String, Integer> p = new Pair<>("test", 123);

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

总结

1
2
3
4
5
编写泛型时,需要定义泛型类型<T>;

静态方法不能引用泛型类型<T>,必须定义其他类型(例如<K>)来实现静态泛型方法;

泛型可以同时定义多种类型,例如Map<K, V>。

擦拭法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。
Java语言的泛型实现方式是擦拭法(Type Erasure)。
所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类Pair<T>,这是编译器看到的代码:
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
16
17
18
19
20
而虚拟机根本不知道泛型。这是虚拟机执行的代码:

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>实现安全的强制转型。
1
2
3
4
5
6
7
8
9
10
使用泛型的时候,我们编写的代码也是编译器看到的代码:

Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();
而虚拟机执行的代码并没有泛型:

Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();
1
2
3
4
5
6
所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:
局限一:<T>不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:

Pair<int> p = new Pair<>(1, 2); // compile error!
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
局限二:无法取得带泛型的Class。观察以下代码:

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;
}
}
1
2
3
因为T是Object,我们对Pair<String>和Pair<Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。

换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>。
1
2
3
4
5
6
7
8
局限三:无法判断带泛型的类型:

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

原因和前面一样,并不存在Pair<String>.class,而是只有唯一的Pair.class。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
局限四:不能实例化T类型:

public class Pair<T> {
private T first;
private T last;
public Pair() {
// Compile error:
first = new T();
last = new T();
}
}
上述代码无法通过编译,因为构造方法的两行语句:

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

擦拭后实际上变成了:

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

这样一来,创建new Pair<String>()和创建new Pair<Integer>()就全部成了Object,显然编译器要阻止这种类型不对的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
要实例化T类型,我们必须借助额外的Class<T>参数:

public class Pair<T> {
private T first;
private T last;
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
上述代码借助Class<T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class<T>。例如:

Pair<String> pair = new Pair<>(String.class);
因为传入了Class<String>的实例,所以我们借助String.class就可以实例化String类型。

不恰当的覆写方法

总结


本文标题:廖雪峰Java教程笔记-泛型

文章作者:TTYONG

发布时间:2023年03月29日 - 18:03

最后更新:2023年03月30日 - 01:03

原始链接:http://tianyong.fun/%E5%BB%96%E9%9B%AA%E5%B3%B0Java%E6%95%99%E7%A8%8B%E7%AC%94%E8%AE%B0-%E6%B3%9B%E5%9E%8B.html

许可协议: 转载请保留原文链接及作者。

多少都是爱
0%