一、泛型的定义
泛型(Generics)是Java中的一种机制,用来定义可以适用于多种不同数据类型的类、接口和方法。它允许开发者在定义类、接口或方法时,不指定具体的类型,而是使用类型参数(如T
、E
、K
、V
等)作为占位符。这样可以在保持类型安全的前提下实现代码的通用性和重用性。
简单理解:泛型就是在编写代码时暂时不指定操作的数据类型,而是在使用的时候再指定具体的数据类型。
二、泛型中的类型参数
泛型的核心是类型参数。这些类型参数在定义类、接口或方法时使用,在使用它们时可以替换为具体的类型。
常见的泛型类型参数符号有:
T
(Type):表示一般类型,常用于泛型类或方法。(自己定义泛型类型时只使用这种参数符号,下面的两种符号不重要)E
(Element):表示集合中的元素类型,常用于集合类。K
(Key)、V
(Value):分别表示映射中的键和值类型,常用于映射类如Map<K, V>
。
这些类型参数是占位符,在使用时需要用实际的数据类型来替代它们。
- 一般类型的泛型:通常由开发者自己定义,像
Box<T>
这种类适合通用的场景。你可以自由设计这些类,利用泛型处理任意类型的数据。- 集合类和映射类的泛型:Java标准库已经定义好了它们的泛型接口(如
List<E>
、Map<K, V>
),你只需在使用时指定具体的类型参数即可,无需重新定义。
三、泛型的使用
泛型主要用于定义泛型类、泛型接口和泛型方法。
1. 泛型类
定义一个可以处理不同类型的类,例如一个可以存储任何类型数据的容器类:
// 泛型类的定义,T 是类型参数
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// 使用泛型类
public class Main {
public static void main(String[] args) {
// 使用泛型类时指定具体的类型
Box<String> stringBox = new Box<>();
stringBox.set("Hello Generics!");
System.out.println(stringBox.get()); // 输出 "Hello Generics!"
Box<Integer> integerBox = new Box<>();
integerBox.set(123);
System.out.println(integerBox.get()); // 输出 123
}
}
在这个例子中,Box<T>
是一个泛型类,T
是类型参数。在使用时,开发者用具体的类型替换了T
,比如Box<String>
、Box<Integer>
等。
2. 泛型方法
泛型方法是指方法的定义中使用了类型参数。这种方法在不改变类的情况下,可以处理不同类型的数据。
public class Util {
// 定义一个泛型方法,T 是类型参数
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
}
// 使用泛型方法
public class Main {
public static void main(String[] args) {
String[] stringArray = {"Apple", "Banana", "Cherry"};
Integer[] integerArray = {1, 2, 3};
Util.printArray(stringArray); // 输出 Apple, Banana, Cherry
Util.printArray(integerArray); // 输出 1, 2, 3
}
}
在上面的代码中,printArray
方法可以接受任何类型的数组并打印出每个元素。
3. 泛型接口
泛型也可以应用在接口中,例如在集合框架中,List<E>
就是一个泛型接口。
// 定义一个泛型接口
public interface Container<T> {
void add(T item);
T get(int index);
}
// 实现泛型接口
public class StringContainer implements Container<String> {
private List<String> items = new ArrayList<>();
@Override
public void add(String item) {
items.add(item);
}
@Override
public String get(int index) {
return items.get(index);
}
}
在实现泛型接口时,必须提供具体的类型,正如StringContainer
类中,我们指定了T
为String
类型。
四、泛型的注意事项
1. 类型擦除
Java中的泛型在编译时会进行类型擦除,即泛型信息在编译后会被移除,替换为原始类型(通常是Object
)。这意味着在运行时无法获取泛型的具体类型。
public class Box<T> {
private T value;
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
if (stringBox instanceof Box<String>) { // 编译错误
// 不能进行类型检查
}
}
}
由于类型擦除,泛型的类型信息在运行时不存在,因此不能使用instanceof
进行类型检查。
2. 不能使用基本数据类型
泛型不能直接使用Java的基本数据类型(如int
、char
等),必须使用它们的包装类(如Integer
、Character
)。
List<int> intList = new ArrayList<>(); // 编译错误
List<Integer> integerList = new ArrayList<>(); // 正确
3. 不能创建泛型数组
由于类型擦除,不能直接创建泛型类型的数组。
List<String>[] arrayOfLists = new List<String>[10]; // 编译错误
如果需要数组,可以使用List
或其他集合类来代替数组。
四、通配符(Wildcard)
通配符用于泛型的灵活性。它允许你在不知道或不想具体指定类型的情况下使用泛型。
?
:表示任意类型。? extends T
:当你需要从集合中读取数据时使用。你可以读取元素,因为所有元素都是T
或它的子类。? super T
:当你需要向集合中写入数据时使用。你可以向集合中添加T
或它的子类的对象,但读取时只能保证是最泛化的类型(如Object
)。
1.? extends适合从集合读取数据
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num); // 可以安全读取元素
}
// list.add(123); // 编译错误:不能往里添加元素
}
这段代码定义了个名叫printNumber的方法,这个方法的参数是一个线性表,线性表中的元素用泛型代替,然后将这个线性表中的每一个元素赋值给Number类型的num,然后再把num打印出来。
这里其实实现了个 Number num = list.get(i)的一个赋值操作,将list中的第i个节点的元素取出来并赋值给 Number类型的num,因为左边取出来的节点一定是Number类型的子类,而在Java中 子类是可以被赋值给父类,所以这里可以进行赋值操作。
至于为什么不能进行添加节点的操作呢,首先,编译器禁止向
List<? extends T>
中添加元素,其次,为什么禁止呢,因为不能确定你所创建的对应节点类型的线性表与你想添加的类型之间兼不兼容,就好比你设定的是以Integer类型为节点的线性表,但你添加的节点元素是3.14,是double类型的,这两种类型的数据格式不兼容,所以会报错。
同样这里指的适合从集合中读取数据并赋值也指的仅仅是赋值给extends后跟的那个数据类型。
2. ? super适合向集合写入数据
public static void addNumbers(List<? super Integer> list) {
list.add(123); // 可以安全添加Integer类型的元素
// Integer num = list.get(0); // 编译错误:不能确定读取到的类型
}
这里说的安全地向集合写入数据,也单纯的指的是你只能安全地写 Integer类型的数据,而不能写 Integer的父类类型的数据,你如果只写 Integer类型的数据,肯定会被父类兼容,因为Java面向对象中规定了,子类所创建的对象可以被父类引用。
不适合读也是因为,你读出来一个数据,但是你能把它赋值给你super后跟的那个数据类型(Integer),因为你读出来的数据是你super后跟的那个数据类型(Integer)的父类,而子类对象是不能被父类对象赋值的,所以读取数据会报错。
所以,这里的适合写数据只是指适合写你super后跟的这一种数据类型。
3. 误区
这里会有一个很严重的误区需要说明
- ? extends适合从集合读取数据只是适合读取? extends后跟的那种数据类型的数据。
- ? super适合向集合写入数据只是适合写入? super后跟的那种数据类型的数据。
五、有界类型参数
有时,你希望限制泛型参数必须是某个类的子类或实现了某个接口,这时可以使用有界类型参数。
public <T extends Number> void print(T number) {
System.out.println(number);
}
在这里,T
必须是Number
的子类,因此print
方法可以处理Integer
、Double
等类型的参数。
extends
关键字用于指定类型参数的上界(即类型的子类型),它表示泛型类型参数必须是指定类型的子类或该类型本身。super
关键字用于指定类型参数的下界(即类型的父类型),它表示泛型类型参数必须是指定类型的父类或该类型本身。