JAVA之泛型
JAVA之泛型
泛型的基本概念
泛型是 Java 提供的一种机制,允许我们定义类、接口和方法时使用类型参数,而不是使用具体的类型。泛型可以帮助我们编写类型安全的代码,避免了类型转换错误,并使得代码更加通用和易于维护。
泛型的定义和使用
泛型类
下面是一个泛型类
ArrayList<T>
的示例: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
35public class ArrayList<T> {
private T[] array;
private int size;
public ArrayList() {
// 初始化数组的方式略有不同,因为无法直接创建泛型数组
array = (T[]) new Object[10];
size = 0;
}
public void add(T e) {
if (size == array.length) {
// 扩展数组
T[] newArray = (T[]) new Object[array.length * 2];
System.arraycopy(array, 0, newArray, 0, size);
array = newArray;
}
array[size++] = e;
}
public T get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
return array[index];
}
public void remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
System.arraycopy(array, index + 1, array, index, size - index - 1);
size--;
}
}这个
ArrayList<T>
类可以存储任意类型的对象,使用泛型参数T
来替代具体的类型。这样一来,我们可以创建不同类型的ArrayList
实例:1
2ArrayList<String> strList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();泛型接口
泛型接口的定义与泛型类类似。例如:
1
2
3
4
5public interface List<T> {
void add(T e);
T get(int index);
void remove(int index);
}ArrayList<T>
实现了这个泛型接口:1
2
3public class ArrayList<T> implements List<T> {
// 实现接口中的方法
}泛型方法
泛型不仅可以用于类和接口,还可以用于方法。例如:
1
2
3
4
5
6
7public class Utils {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
}使用泛型方法:
1
2
3
4Integer[] intArray = {1, 2, 3};
String[] strArray = {"Hello", "World"};
Utils.printArray(intArray);
Utils.printArray(strArray);
泛型与继承关系
向上转型
泛型的向上转型与普通类的向上转型有所不同。在 Java 中,我们可以将
ArrayList<T>
类型的对象转型为List<T>
类型,但不能将ArrayList<Integer>
转型为ArrayList<Number>
,这主要是为了保证类型安全。1
List<String> list = new ArrayList<>();
这表示
ArrayList<String>
实现了List<String>
接口,ArrayList<String>
可以被视作List<String>
的实例。不允许的类型转换
尽管
ArrayList<Integer>
和ArrayList<Number>
都实现了List
接口,但它们之间没有继承关系,因此不能互相转型:1
2ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<Number> numberList = integerList; // 编译错误这种类型不兼容的情况是因为
ArrayList<Integer>
和ArrayList<Number>
是两个不同的类,它们之间没有继承关系。这样做可以避免以下潜在的问题:1
2
3
4
5
6
7ArrayList<Integer> integerList = new ArrayList<>();
integerList.add(123);
ArrayList<Number> numberList = integerList; // 编译错误
numberList.add(12.34); // 编译错误
Integer integer = integerList.get(0); // ClassCastException!这里,
ArrayList<Number>
被允许添加Float
类型的元素,而integerList
只能接受Integer
类型的元素,这可能导致类型安全问题。泛型继承关系
List<T>
是一个泛型接口。ArrayList<T>
实现了List<T>
接口。ArrayList<Integer>
和ArrayList<Number>
是不同的泛型类,没有直接的继承关系。
泛型的优势
- 类型安全:泛型提供了编译时的类型检查,避免了类型转换错误。
- 代码重用:通过泛型,可以编写通用的代码和数据结构,减少重复代码。
- 易于维护:泛型代码更加清晰,易于理解和维护。
泛型的使用
在 Java 中,泛型允许我们定义通用的类、接口和方法,以提高代码的重用性和类型安全。
使用泛型增强类型安全
未定义泛型类型的情况
如果在使用
ArrayList
时不定义泛型类型,实际上泛型类型默认为Object
。这会导致类型安全问题,因为我们必须进行强制类型转换,容易引发ClassCastException
。例如:1
2
3
4
5
6
7List list = new ArrayList();
list.add("Hello");
list.add("World");
// 获取元素时需要强制类型转换
String first = (String) list.get(0);
String second = (String) list.get(1);如果误操作,添加了错误类型的元素,会引发运行时异常:
1
2list.add(123); // 添加整数
String wrong = (String) list.get(2); // 会抛出 ClassCastException定义泛型类型的情况
使用泛型后,我们可以在编译时确保类型安全,无需强制转换。例如,定义
ArrayList<String>
时,编译器会确保只有String
类型的对象被添加到列表中:1
2
3
4
5
6
7List<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
// 编译器已检查类型,无需强制转换
String first = list.get(0);
String second = list.get(1);同样,定义
ArrayList<Number>
时,可以存储Number
类型及其子类的对象:1
2
3
4
5
6
7List<Number> list = new ArrayList<>();
list.add(123); // Integer
list.add(12.34); // Double
// 获取元素时,返回的是 Number 类型
Number first = list.get(0);
Number second = list.get(1);编译器会确保所有元素都是
Number
类型或其子类,而不会抛出ClassCastException
。自动推断泛型类型
从 Java 7 开始,编译器支持类型推断,允许在创建泛型对象时省略类型参数。例如:
1
List<Number> list = new ArrayList<>();
这里,
ArrayList<>()
中的<>
可以由编译器推断为ArrayList<Number>
。这样可以减少代码的冗余,使其更加简洁。
泛型接口的应用
泛型接口允许我们在接口中定义类型参数,从而提供更多的灵活性。例如,Comparable<T>
是一个泛型接口,用于比较两个对象的大小:
1 | public interface Comparable<T> { |
当我们希望对某些类型的对象进行排序时,这些对象必须实现
Comparable<T>
接口。以下是如何实现和使用
Comparable<T>
接口的例子:
对数组排序的基本使用
对
String
数组进行排序,因为String
实现了Comparable<String>
接口:1
2
3
4
5
6
7
8
9import 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
[Apple, Orange, Pear]
自定义类实现
Comparable<T>
接口当我们对自定义类进行排序时,需要实现
Comparable<T>
接口。例如,对Person
类进行排序: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
33import 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
[Alice, 88, Bob, 61, Lily, 75]
这里,
Person
类实现了Comparable<Person>
接口,通过compareTo
方法指定了排序的规则。上述例子是按照name
字段进行排序。你可以修改
compareTo
方法来按score
从高到低排序:1
2
3
4
public int compareTo(Person other) {
return Integer.compare(other.score, this.score); // 从高到低排序
}这样,
Person
对象将按照score
从高到低排序。
编写泛型类的详细步骤
泛型类允许我们在类定义时使用占位符(泛型类型),以便在实例化时指定实际的类型。虽然泛型类的编写比普通类稍显复杂,但它提供了更强的类型安全和灵活性。以下是编写泛型类的详细步骤和注意事项。
1. 编写基本的泛型类
首先,我们可以从一个特定类型的类开始,例如:
1 | public class Pair { |
然后,将特定类型(这里是 String
)替换为泛型类型
T
:
1 | public class Pair<T> { |
这样,Pair
类就变成了一个泛型类,它可以处理任意类型的对象。
2. 编写静态方法
在泛型类中,静态方法不能直接使用类的泛型参数
T
。如果需要定义一个泛型静态方法,可以使用不同的泛型类型参数。例如:
1 | public class Pair<T> { |
在这个例子中,静态方法 create
使用了不同的泛型类型参数
K
,与类的泛型类型 T
区分开来。这避免了编译错误,并确保静态方法能正确工作。
3. 使用多个泛型类型
有时,我们需要定义一个可以处理多种不同类型的泛型类。例如,我们希望
Pair
类能够存储两个不同类型的对象:
1 | public class Pair<T, K> { |
使用这个泛型类时,我们可以指定两种不同的类型:
1 | Pair<String, Integer> p = new Pair<>("test", 123); |
在这个例子中,Pair
的第一个类型参数 T
是
String
,第二个类型参数 K
是
Integer
。这使得 Pair
类可以存储一个
String
和一个 Integer
对象。
4. 示例:Java 标准库中的泛型
Java 标准库中的 Map<K, V>
就是一个使用了两个泛型类型的例子。Map
类对键(Key
)和值(Value
)使用了不同的类型参数:
1 | import java.util.HashMap; |
在这个例子中,Map
的键是 String
类型,值是
Integer
类型。HashMap
是 Map
接口的一个实现,它可以正确地处理这些类型。
- 基本泛型类:定义一个带有泛型参数的类,以便在实例化时指定实际的类型。
- 静态方法:静态方法不能使用类的泛型参数。可以在静态方法中使用不同的泛型参数以实现泛型功能。
- 多个泛型类型:可以在泛型类中定义多个类型参数,以支持存储不同类型的对象。
- Java 标准库中的泛型:学习和理解标准库中的泛型实现可以帮助更好地掌握泛型的使用方式。
擦拭法(Type Erasure)
擦拭法的基本概念
在Java中,泛型的实现通过擦拭法来实现。擦拭法的核心思想是,泛型类型的信息在运行时被“擦拭掉”,即Java虚拟机(JVM)并不知道泛型的存在。所有泛型信息只在编译时存在,JVM
只处理擦拭后的原始类型(原始类),通常是 Object
类型。
编译器视角
编译器看到的泛型类代码:
1 | public class Pair<T> { |
经过擦拭法处理后的代码:
1 | public class Pair { |
擦拭法的局限性
由于擦拭法的特性,Java泛型存在一些局限性:
1. 泛型类型不能是基本类型
因为擦拭法将泛型类型擦拭为 Object
,而
Object
无法直接持有基本类型。示例代码会报编译错误:
1 | Pair<int> p = new Pair<>(1, 2); // compile error! |
为了解决这个问题,Java的泛型设计中并不允许直接使用基本类型,而是使用其包装类,例如
Integer
、Double
等。
2. 无法获取带泛型的
Class
对象
因为所有的泛型类型在运行时都被擦拭为原始类型
Object
,所以无法通过 getClass()
方法获取具体的泛型类型。示例代码如下:
1 | public class Main { |
在这个例子中,p1
和 p2
都会返回相同的
Class
对象,即
Pair.class
,因为泛型类型信息在运行时不可用。
3. 无法判断带泛型的类型
无法使用 instanceof
判断泛型类型。例如:
1 | Pair<Integer> p = new Pair<>(123, 456); |
因为 Pair<String>
和
Pair<Integer>
在运行时都被视为
Pair
,所以编译器无法判断具体的泛型类型。
4. 不能实例化泛型类型
在泛型类中无法直接实例化泛型类型,因为擦拭法将泛型类型视为
Object
。以下代码会报编译错误:
1 | public class Pair<T> { |
要解决这个问题,可以使用反射传递 Class<T>
参数:
1 | public class Pair<T> { |
使用时需要传入具体类型的 Class
实例:
1 | Pair<String> pair = new Pair<>(String.class); |
不恰当的覆写方法
在泛型类中,定义的方法如果名字与 Object
类中的方法冲突,可能会导致编译错误。例如:
1 | public class Pair<T> { |
由于泛型方法在编译后会被擦拭成 equals(Object t)
,与
Object
类中的 equals
方法冲突。因此,编译器会阻止这种定义。可以使用其他方法名避免这种冲突:
1 | public class Pair<T> { |
泛型继承
一个类可以继承自一个泛型类。例如:
1 | public class IntPair extends Pair<Integer> { |
在这种情况下,子类 IntPair
会继承父类
Pair<Integer>
的泛型信息。通过反射,可以获取到父类的泛型类型:
1 | import java.lang.reflect.ParameterizedType; |
通配符
泛型的局限性
擦拭法(Type Erasure)
- Java
泛型是通过擦拭法实现的,泛型信息在编译时被擦除,所有泛型类型参数都被替换为
Object
类型。泛型类型参数T
实际上是Object
,编译器在需要时会插入类型转换。
- Java
泛型是通过擦拭法实现的,泛型信息在编译时被擦除,所有泛型类型参数都被替换为
基本类型限制
- 泛型参数不能是基本数据类型(如
int
,char
等)。因为擦拭法将泛型类型参数视为Object
,而Object
无法直接持有基本类型。例如,Pair<int>
是非法的,而Pair<Integer>
是合法的。
- 泛型参数不能是基本数据类型(如
获取
Class
对象- 泛型在运行时不可区分具体类型。例如,
Pair<String>
和Pair<Integer>
在运行时都被视为Pair
,它们的Class
对象是相同的,即Pair.class
。
- 泛型在运行时不可区分具体类型。例如,
实例化泛型类型
无法直接在构造函数中实例化泛型类型
T
,因为编译器不允许new T()
。需要通过传入Class<T>
对象来实例化,例如:1
2
3
4
5
6
7
8public class Pair<T> {
private T first;
private T last;
public Pair(Class<T> clazz) throws InstantiationException, IllegalAccessException {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
方法覆写问题
- 泛型方法与
Object
类的方法可能会发生冲突。例如,定义一个equals(T t)
方法会与Object
类的equals(Object o)
方法冲突。
- 泛型方法与
泛型通配符
上界通配符(
<? extends T>
)使用上界通配符可以接受
T
的子类,例如Pair<? extends Number>
。它允许传入Pair<Integer>
,Pair<Double>
等,但不能用来修改集合元素,只能读取:1
2
3
4
5public static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
下界通配符(
<? super T>
)使用下界通配符可以接受
T
的超类。例如List<? super Integer>
可以接受List<Number>
和List<Object>
。它用于写入操作:1
2
3
4public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
实际使用
处理泛型参数
- 使用
<? extends T>
表示对泛型参数的只读访问,而<? super T>
表示对泛型参数的写入访问。了解这些限制和特性能够帮助你编写更安全和灵活的代码。
- 使用
泛型继承
泛型类可以继承其他泛型类。例如:
1
2
3
4
5public class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}在这种情况下,
IntPair
确保了Pair
中的T
是Integer
类型。
标准库的使用
- 例如
List<T>
接口,通过List<? extends Integer>
只能读取列表元素,但不能修改。方法签名应清晰地表明其对参数的读写操作限制。
- 例如
示例代码
1 | import java.util.List;0 |
泛型和反射
1. Class<T>
泛型类
Class<T>
是 Java 反射 API
的一个泛型类,表示某种类型的类对象。例如:
1 | Class<String> clazz = String.class; |
这里,Class<String>
表示 String
类型的类对象。clazz.newInstance()
用于创建
String
的新实例。
2.
Class<? super T>
和 Class<T>
之间的关系
当你调用 Class<T>
的 getSuperclass()
方法时,返回的是 Class<? super T>
类型。例如:
1 | Class<? super String> sup = String.class.getSuperclass(); |
sup
可能是 Object.class
,因为
String
的直接超类是 Object
。
3. Constructor<T>
泛型类
Constructor<T>
表示一个带有泛型参数的构造函数。例如:
1 | Class<Integer> clazz = Integer.class; |
这里,Constructor<Integer>
表示
Integer
类型的构造函数,它可以用来创建 Integer
实例。
4. 泛型数组
Java 不允许直接创建泛型数组,但可以通过强制转换实现。例如:
1 | Pair<String>[] ps = (Pair<String>[]) new Pair[2]; |
这种强制转换会在运行时导致潜在的类型安全问题,因为 Java 的数组类型在运行时是擦除的。具体示例如下:
1 | Pair[] arr = new Pair[2]; |
5. 使用 Class<T>
创建泛型数组
由于数组类型的擦除,直接创建泛型数组是不可能的。可以使用
Array.newInstance()
方法创建泛型数组:
1 | T[] createArray(Class<T> cls) { |
6. 使用可变参数创建泛型数组
虽然可以通过可变参数创建泛型数组,但要小心,因为这会导致类型安全问题。例如:
1 | public class ArrayHelper { |
这段代码看似安全,但在某些情况下(如 pickTwo
方法的使用),会产生 ClassCastException
:
1 | static <K> K[] pickTwo(K k1, K k2, K k3) { |