泛型
一、泛型的定义和使用
类定义:在定义一个泛型类时,需要在类名后加上 <T>
,以指示这是一个泛型类。例如:
public class Pair<T> { ... }
方法定义:在定义泛型方法时,需要在返回类型前加上 <T>
,这样编译器才会知道这是一个泛型方法。例如:
public <T> T add(Pair<T> p) { ... }
变量使用:在类或方法内部,泛型参数 T
可以直接用于定义变量,而不需要加上 <T>
。例如:
T first = p.getFirst();
对象初始化:在创建泛型对象时,可以显式指定类型。例如:
Pair<Integer> p = new Pair<>(123, 456);
类型推断:在调用泛型方法时,不需要显式写出类型参数,编译器会根据传入的参数自动推断。例如:
Integer result = add(p); // 这里不需要 add<Integer>(p)
类型擦除:记住,Java 的泛型在编译时进行类型擦除,运行时类型会被替换为 Object,因此在运行时,泛型信息并不保留。
二、泛型的引入
1. 泛型的引入
在Java中,ArrayList是一种“可变长度”的数组,内部实际上是一个 Object[]
数组。这样虽然灵活,但存在几个问题:
- 强制类型转换:使用
Object
类型时,取出元素需要强制转换,容易出错。 - 类型安全:如果错误地添加不同类型的元素,可能导致
ClassCastException
。
例如:
ArrayList list = new ArrayList();
list.add("Hello");
String first = (String) list.get(0); // 需要强制转型
如果不小心添加了其他类型,如 Integer
,就会引发错误:
list.add(new Integer(123));
String second = (String) list.get(1); // 会抛出 ClassCastException
2. 解决方案
为了解决这个问题,我们可以为每种类型编写专门的 ArrayList,例如 StringArrayList
和 IntegerArrayList
。但这并不实际,因为需要为每个类创建一个新版本的 ArrayList,这在类的数量很多时几乎不可能。
3. 泛型的实现
泛型通过使用类型参数(如 T
)来解决这个问题,使得同一个类可以适应不同的类型:
public class ArrayList<T> {
private T[] array;
private int size;
public void add(T e) {...}
public T get(int index) {...}
}
这样,你可以创建任意类型的 ArrayList:
ArrayList<String> strList = new ArrayList<String>();
ArrayList<Integer> intList = new ArrayList<Integer>();
编译器会在编译时检查类型,确保安全性:
strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
4. 向上转型
泛型实现了继承关系,可以将 ArrayList<T>
向上转型为其接口 List<T>
:
List<String> list = new ArrayList<String>();
但有个关键点是:不能将 ArrayList<Integer>
向上转型为 ArrayList<Number>
,因为这会导致类型安全问题。
例子说明
如果允许这样的转换:
ArrayList<Integer> integerList = new ArrayList<Integer>();
integerList.add(new Integer(123));
ArrayList<Number> numberList = integerList; // 错误
numberList.add(new Float(12.34)); // 可能导致错误
Integer n = integerList.get(1); // ClassCastException!
上面的代码在获取 Integer
类型元素时可能会出现错误,因为 numberList
中可能包含其他类型的元素。
5. 总结
- 泛型是Java提供的一种模板机制,允许我们编写可以处理任意类型的代码。
- 类型安全:泛型通过编译器进行类型检查,避免了强制转换的错误。
- 继承关系:可以向上转型为接口类型,但不能变更类型参数的实际类型。
通过使用泛型,我们可以编写更安全、更灵活的代码,而无需为每种类型重复实现类。这是泛型在Java中的核心价值。
三、泛型的使用
泛型的总结与优化
1. 泛型的使用
在Java中,如果使用 ArrayList
而不指定泛型类型,泛型实际上被视为 Object
类型:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0); // 强制转型
String second = (String) list.get(1);
这种情况下,只能把泛型当作 Object
使用,失去了泛型的优势。
2. 指定泛型类型
当我们定义泛型类型为 String
时,编译器提供了类型安全检查:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
String first = list.get(0); // 无需强制转型
String second = list.get(1);
同理,定义为 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);
3. 类型推断
编译器能够自动推断泛型类型,因此可以简化代码:
List<Number> list = new ArrayList<>(); // 省略后面的类型
4. 泛型接口的应用
泛型不仅可以在类中使用,也可以在接口中定义。例如,Comparable<T>
接口可以用于比较不同类型的对象:
public interface Comparable<T> {
int compareTo(T o); //负数是升序,正数是降序
}
对 String
数组进行排序:
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));
}
}
如果我们尝试对自定义的 Person
类型进行排序,而未实现 Comparable
接口,将导致 ClassCastException
:
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;
}
}
为了解决这个问题,Person
必须实现 Comparable<Person>
接口:
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;
}
}
5. 小结
- 使用泛型时,应指定具体类型,如
ArrayList<String>
或ArrayList<Number>
。 - 编译器能自动推断类型,可以简化代码为
List<String> list = new ArrayList<>();
。 - 不指定泛型参数时,编译器会警告,并且泛型被视为
Object
类型,失去类型安全。 - 接口也可以使用泛型,类实现接口时必须正确实现泛型类型。
四、编写泛型
泛型类的编写指南
编写泛型类相较于普通类更为复杂,通常用于集合类(如 ArrayList<T>
)。如果确实需要编写泛型类,可以遵循以下步骤:
1. 编写普通类
首先,从某种特定类型(如 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;
}
}
2. 转换为泛型类
将特定类型替换为泛型类型 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;
}
}
3. 静态方法的注意事项
泛型类型 T
不能用于静态方法的参数和返回类型。以下代码将导致编译错误:
public class Pair<T> {
// ... 属性和构造方法
// 错误的静态方法定义
public static Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
}
- 错误原因:静态方法不能直接使用类的泛型类型是因为静态方法不属于某个具体的实例,而是属于类本身。在泛型类中,泛型参数
T
依赖于特定的实例,而静态方法没有上下文来确定哪个实例的泛型类型。
正确的做法
为静态方法定义不同的泛型类型,例如 <K>
:
public class Pair<T> {
// ... 属性和构造方法
public static <K> Pair<K> create(K first, K last) {
return new Pair<K>(first, last);
}
}
这样做可以清楚地区分静态方法的泛型类型和实例类型的泛型类型。
4. 多种泛型类型
你可以定义多个泛型类型,例如 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() {
return first;
}
public K getLast() {
return last;
}
}
使用时,需要指定这两种类型:
Pair<String, Integer> p = new Pair<>("test", 123);
Java 标准库中的 Map<K, V>
就是使用两种泛型类型的例子,分别表示键和值。
小结
- 编写泛型类时,需要在类定义中声明泛型类型
<T>
。 - 静态方法不能直接使用实例的泛型类型
<T>
,应定义不同的泛型类型(如<K>
)。 - 泛型类可以定义多个类型参数,例如
Pair<T, K>
,以适应不同的使用场景。
五、擦拭法
泛型是一种“模板代码”的技术,在不同语言中的实现方式各异。Java的泛型采用了擦拭法(Type Erasure),即虚拟机对泛型一无所知,所有的工作由编译器完成。
擦拭法的实现
在Java中,编写的泛型类(如 Pair<T>
)在编译后被转换为不带泛型的类(如 Pair
),其中所有的泛型类型都被视为 Object
:
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泛型的局限性
-
基本类型限制:泛型不能是基本类型,例如
int
,因为泛型会被视为Object
。Pair<int> p = new Pair<>(1, 2); // 编译错误
-
无法获取泛型的
Class
:所有泛型实例在获取Class
时返回相同的类。Pair<String> p1 = new Pair<>("Hello", "world"); Pair<Integer> p2 = new Pair<>(123, 456); System.out.println(p1.getClass() == p2.getClass()); // true
-
无法判断泛型类型:
Pair<Integer> p = new Pair<>(123, 456); if (p instanceof Pair<String>) { // 编译错误 }
-
无法实例化泛型类型:无法直接使用
new T()
来创建泛型类型的实例。public class Pair<T> { public Pair() { first = new T(); // 编译错误 } }
需要借助
Class<T>
进行反射实例化:public Pair(Class<T> clazz) { first = clazz.newInstance(); // 合法 }
-
方法重写问题:定义的方法可能由于擦拭而无法编译。
public boolean equals(T t) { // 编译错误 return this == t; }
- 错误原因:类本身就继承于object类,这个时候类型擦除T变为object和object类中的equals冲突
可改名避免冲突:
public boolean same(T t) { ... }
泛型继承
类可以继承泛型类,子类能够访问父类的泛型类型:
public class IntPair extends Pair<Integer> { ... }
获取父类的泛型类型可以通过反射:
Type t = IntPair.class.getGenericSuperclass();
if (t instanceof ParameterizedType) {
Type[] types = ((ParameterizedType) t).getActualTypeArguments();
// 处理泛型类型
}
小结
- Java的泛型采用擦拭法实现,导致多项局限。
- 泛型不能为基本类型,无法获取、判断泛型的类型,无法直接实例化泛型。
- 泛型方法需避免与
Object
的方法重名。 - 子类可获取父类的泛型类型,提供了更好的类型安全性。
- T类型擦除后,只有在运行的时候会通过之前桥接的方法判断类信息,其它不会
六、extends通配符
泛型的继承关系
在 Java 中,Pair<Integer>
并不是 Pair<Number>
的子类。因此,尽管 Integer
是 Number
的子类,Pair<Integer>
和 Pair<Number>
之间没有继承关系。例子如下:
public class Pair<T> {
private T first;
private T last;
// 构造函数和方法...
}
方法参数与泛型
当我们定义一个接受 Pair<Number>
的静态方法时:
public class PairHelper {
static int add(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
调用 add(new Pair<Number>(1, 2));
是有效的,但如果尝试传入 Pair<Integer>
,将会出现编译错误:
Pair<Integer> p = new Pair<>(123, 456);
int n = PairHelper.add(p); // 编译错误
这是因为 Pair<Integer>
不能直接转换为 Pair<Number>
。
使用通配符 ? extends Number
为了使方法接受 Pair<Integer>
类型,我们可以使用上界通配符 ? extends Number
:
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
此时,add(p)
可以接受 Pair<Integer>
、Pair<Double>
等,因为它们都是 Number
的子类。
通配符的限制
调用 setFirst()
方法时,将出现编译错误:
p.setFirst(new Integer(first.intValue() + 100)); // 编译错误
这是因为 Pair<? extends Number>
只允许读取,不允许写入(除了 null
)。
实际应用示例
在处理 Java 标准库的 List<T>
时,可以定义如下方法:
int sumOfList(List<? extends Integer> list) {
int sum = 0;
for (int i = 0; i < list.size(); i++) {
Integer n = list.get(i);
sum += n;
}
return sum;
}
这种方式确保方法内部只读取列表元素,不会修改它们。
使用 extends
限定泛型类
在定义泛型类时,可以使用 extends
来限制类型:
public class Pair<T extends Number> { ... }
这样,只能定义 Pair<Number>
、Pair<Integer>
和 Pair<Double>
等,但无法定义 Pair<String>
或 Pair<Object>
。
小结
- 通配符
? extends Number
表示方法内部可以读取Number
的引用,但不能修改(只读)。 - 使用
T extends Number
定义泛型类表示泛型类型只能是Number
或其子类。
七、super通配符
泛型的继承关系
在 Java 中,Pair<Integer>
并不是 Pair<Number>
的子类。这意味着,当我们有一个方法 set
,如果它的参数类型是 Pair<Integer>
,则只允许传入 Pair<Integer>
类型的对象,而不允许传入 Pair<Number>
。
使用 super
通配符
如果我们希望一个方法能够接受 Pair<Integer>
以及 Pair<Number>
或 Pair<Object>
,可以使用 super
通配符来定义方法:
void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}
这里,Pair<? super Integer>
表示该方法可以接受任何 Integer
的父类的 Pair
类型。这样,传入 Pair<Number>
和 Pair<Object>
都是允许的,因为 Number
和 Object
是 Integer
的父类。
示例代码
下面是一个完整的示例,展示了如何使用 super
通配符:
public class Main {
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);
}
}
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 void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
getFirst
方法的返回值
对于 Pair<? super Integer>
的 getFirst()
方法,返回值的类型是 ? super Integer
,这意味着返回值类型可能是 Object
或 Number
,因此我们不能将返回值直接赋值给 Integer
:
Integer x = p.getFirst(); // 编译错误
Object obj = p.getFirst(); // 合法
extends
与 super
的区别
特性 | <? extends T> | <? super T> |
---|---|---|
允许读取 | 是 | 否 |
允许写入 | 否 | 是 |
<? extends T>
允许调用get()
方法获取T
的引用,但不允许调用set(T)
。<? super T>
允许调用set(T)
方法传入T
的引用,但不允许安全地读取T
的引用(可以读取为Object
)。
Collections.copy()
方法示例
Java 标准库中的 Collections.copy()
方法是 super
和 extends
使用的一个经典例子:
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是生产者
dest.add(t); // dest是消费者
}
}
dest
为List<? super T>
,表示我们可以安全地将T
添加到目标列表中。src
为List<? extends T>
,表示我们可以从源列表中安全地读取T
的引用。
PECS 原则
PECS(Producer Extends, Consumer Super)是使用 extends
和 super
的简单记忆法:
- Producer Extends:如果需要返回类型
T
,则使用extends
通配符。 - Consumer Super:如果需要写入类型
T
,则使用super
通配符。
无限定通配符
无限定通配符 ?
是一个特殊的情况,表示可以接受任何类型:
void sample(Pair<?> p) {
}
在这种情况下:
- 不能安全地写入任何具体类型(除了
null
)。 - 只能读取为
Object
类型。
例如,检查 null
的方法:
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
使用 <T>
来替代 ?
更为清晰:
static <T> boolean isNull(Pair<T> p) {
return p.getFirst() == null || p.getLast() == null;
}
小结
- 使用
? super Integer
:方法内部可以调用set()
,但不能安全地调用get()
(只能获取为Object
)。 - 使用
extends
和super
遵循 PECS 原则:extends
用于生产者,super
用于消费者。 - 无限定通配符:可用于接收任何类型,但限制了读取和写入。通常可以通过引入泛型参数来替代。