【数据结构前置知识】泛型
目录
1. 核心概念:什么是泛型?
2. 为什么需要泛型?(解决的问题)
问题示例:没有泛型的时代
泛型的解决方案
3. 泛型的基本语法和使用
3.1 泛型类
3.2 泛型接口
3.3 泛型方法
4. 泛型通配符:?, extends, super
5. 泛型与数据结构的关系(复习重点)
6. 类型擦除:泛型的实现机制
1. 核心概念:什么是泛型?
泛型 的本质是参数化类型。
简单来说:
-
就像方法有参数一样(
void print(String message)
),类型也可以有参数。 -
泛型允许我们在定义类、接口或方法时,使用一个类型占位符(如
<T>
),等到实际使用时再指定具体的类型。
核心思想:编写一次代码,适用于多种数据类型,同时保证类型安全。
2. 为什么需要泛型?(解决的问题)
在泛型出现之前,我们主要使用 Object
类来实现"通用"的容器,但这会带来严重问题。
问题示例:没有泛型的时代
// 一个可以放任何东西的"盒子"类
class OldBox {private Object content;public void setContent(Object content) {this.content = content;}public Object getContent() {return content;}
}// 使用这个盒子
public static void main(String[] args) {OldBox stringBox = new OldBox();stringBox.setContent("Hello"); // 放入String// 问题1:需要强制类型转换,麻烦且容易出错String message = (String) stringBox.getContent();// 问题2:编译器无法进行类型检查,运行时才会报错!stringBox.setContent(123); // 误放入Integer,编译器不会警告String wrongMessage = (String) stringBox.getContent(); // 运行时ClassCastException!
}
泛型的解决方案
// 使用泛型的盒子类
class Box<T> { // T是类型参数private T content;public void setContent(T content) {this.content = content;}public T getContent() { // 返回类型就是T,不需要强制转换return content;}
}// 使用泛型盒子
public static void main(String[] args) {Box<String> stringBox = new Box<>(); // 创建时指定T为StringstringBox.setContent("Hello");// 优点1:不需要强制类型转换String message = stringBox.getContent();// 优点2:编译时类型检查,提前发现错误!// stringBox.setContent(123); // 编译错误!无法将Integer放入String的盒子Box<Integer> intBox = new Box<>(); // 可以创建Integer类型的盒子intBox.setContent(123);int number = intBox.getContent(); // 自动拆箱
}
泛型的核心优势:
-
类型安全:在编译期就能发现类型错误,而不是在运行时。
-
消除强制类型转换:代码更简洁、更清晰。
-
代码复用:一套代码可以用于多种数据类型。
3. 泛型的基本语法和使用
3.1 泛型类
在类名后面添加类型参数 <T>
,T
是类型占位符(可以用任何大写字母,如 E
, K
, V
等)。
// 定义一个泛型类
public class Container<T> {private T value;public Container(T value) {this.value = value;}public T getValue() {return value;}public void setValue(T value) {this.value = value;}
}// 使用
Container<String> stringContainer = new Container<>("Hello");
Container<Integer> intContainer = new Container<>(100);
3.2 泛型接口
接口也可以使用泛型。
// 泛型接口
public interface Repository<T> {void save(T entity);T findById(int id);
}// 实现泛型接口时指定具体类型
public class UserRepository implements Repository<User> {@Overridepublic void save(User user) { /* 实现 */ }@Overridepublic User findById(int id) { /* 实现 */ return null; }
}
3.3 泛型方法
即使类不是泛型类,方法也可以是泛型方法。
public class ArrayUtils {// 泛型方法:交换数组中两个元素的位置public static <T> void swap(T[] array, int i, int j) {T temp = array[i];array[i] = array[j];array[j] = temp;}// 使用泛型方法public static void main(String[] args) {String[] words = {"Hello", "World"};Integer[] numbers = {1, 2, 3};ArrayUtils.swap(words, 0, 1); // T 被推断为 StringArrayUtils.swap(numbers, 0, 1); // T 被推断为 Integer}
}
4. 泛型通配符:?
, extends
, super
这是泛型中比较难理解但非常重要的概念,用于增加API的灵活性。
4.1 无界通配符 <?>
表示"未知类型",当你只关心容器,不关心容器中元素的具体类型时使用。
// 打印任何List的内容,不关心元素类型
public static void printList(List<?> list) {for (Object elem : list) {System.out.print(elem + " ");}
}// 可以接受List<String>, List<Integer>等
printList(Arrays.asList("A", "B", "C"));
printList(Arrays.asList(1, 2, 3));
4.2 上界通配符 <? extends T>
表示"T或其子类型",用于生产者(主要从中读取数据)。
// 只能接受Number及其子类(Integer, Double等)的List
public static double sumOfList(List<? extends Number> list) {double sum = 0.0;for (Number num : list) { // 可以安全地当作Number读取sum += num.doubleValue();}return sum;
}List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);System.out.println(sumOfList(intList)); // 正确
System.out.println(sumOfList(doubleList)); // 正确
4.3 下界通配符 <? super T>
表示"T或其父类型",用于消费者(主要向其中写入数据)。
// 只能接受Integer及其父类(Number, Object)的List
public static void addNumbers(List<? super Integer> list) {for (int i = 1; i <= 5; i++) {list.add(i); // 可以安全地添加Integer}
}List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();addNumbers(numberList); // 正确
addNumbers(objectList); // 正确
PECS原则(Producer-Extends, Consumer-Super):
-
如果参数是生产者(提供数据),使用
<? extends T>
-
如果参数是消费者(消耗数据),使用
<? super T>
5. 泛型与数据结构的关系(复习重点)
集合框架就是泛型最典型、最重要的应用!
// List<E> 接口是泛型接口
List<String> stringList = new ArrayList<>(); // E 被指定为 String
List<Integer> intList = new ArrayList<>(); // E 被指定为 Integer// Map<K, V> 也是泛型接口
Map<String, Integer> ageMap = new HashMap<>(); // K=String, V=Integer// 没有泛型的"远古时代"写法(不要这样写!)
List oldList = new ArrayList(); // 默认是List<Object>
oldList.add("hello");
oldList.add(123); // 编译器不报错,但...
String str = (String) oldList.get(1); // 运行时ClassCastException!
正是因为泛型,集合框架才能:
-
保证类型安全:
ArrayList<String>
只能存放String。 -
避免强制转换:从
list.get(0)
直接得到String,而不是Object。 -
提供更好的API:IDE能提供准确的代码提示和类型检查。
6. 类型擦除:泛型的实现机制
Java的泛型是编译期概念,在编译后,泛型信息会被擦除,这个过程叫做类型擦除。
// 源代码
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();// 编译后(概念上),泛型信息被擦除,都变成原始类型List
List stringList = new ArrayList();
List intList = new ArrayList();
类型擦除的影响:
-
运行时无法获取泛型的具体类型信息(如
T.class
是不允许的) -
不能创建泛型数组(如
new T[]
) -
是Java为了向后兼容而采取的设计
了解泛型擦除详情,请看这篇帖子 ——> 泛型擦除