一、泛型概述
1.1 什么是泛型
泛型(Generics)是Java SE 5.0引入的一个重要特性,它允许在定义类、接口和方法时使用类型参数(Type Parameters)。泛型的本质是参数化类型,即所操作的数据类型被指定为一个参数。
专业解释:泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
通俗理解:就像方法有参数一样,泛型让类型也可以作为"参数"传递。比如List中的String就是一个类型参数,告诉编译器这个List只能存放String类型的对象。
1.2 为什么需要泛型
问题类型 | 没有泛型的情况 | 使用泛型的解决方案 |
---|---|---|
类型安全 | 需要强制类型转换,容易导致ClassCastException | 编译时检查类型安全,避免运行时异常 |
代码复用 | 为不同类型需要编写相似的代码 | 一套代码可以适用于多种类型 |
代码清晰度 | 需要查看文档或注释才能知道集合中存储的类型 | 直接从类型声明就能知道集合中存储的类型 |
示例对比:
// 不使用泛型
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 需要强制类型转换
// 使用泛型
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // 不需要强制类型转换
二、泛型基础
2.1 泛型类
泛型类是在类名后面添加类型参数声明部分。
// 定义一个简单的泛型类
public class Box<T> {
private T t; // T代表"类型"
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
// 使用示例
Box<String> stringBox = new Box<String>();
stringBox.set("Hello World");
String str = stringBox.get(); // 不需要类型转换
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(123);
int num = integerBox.get(); // 自动拆箱
2.2 泛型方法
泛型方法是在方法返回类型前声明类型参数。
public class Util {
// 泛型静态方法
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
// 使用示例
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2); // 显式类型参数
boolean same2 = Util.compare(p1, p2); // 类型推断
2.3 泛型接口
// 定义一个泛型接口
public interface Generator<T> {
T next();
}
// 实现泛型接口
public class FruitGenerator<T> implements Generator<T> {
@Override
public T next() {
return null; // 实际实现会返回T类型的对象
}
}
// 使用具体类型实现
public class StringGenerator implements Generator<String> {
@Override
public String next() {
return "Hello";
}
}
三、泛型通配符
3.1 通配符基本概念
Java泛型提供了三种通配符:
通配符类型 | 语法 | 描述 |
---|---|---|
无界通配符 | <?> | 表示未知类型 |
上界通配符 | <? extends T> | 表示T或T的子类 |
下界通配符 | <? super T> | 表示T或T的父类 |
3.2 无界通配符
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
// 可以接受任何类型的List
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
3.3 上界通配符
// 只接受Number及其子类(Integer, Double等)的List
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
s += n.doubleValue();
}
return s;
}
List<Integer> intList = Arrays.asList(1, 2, 3);
System.out.println(sumOfList(intList)); // 6.0
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sumOfList(doubleList)); // 6.6
3.4 下界通配符
// 接受Integer及其父类(Number, Object)的List
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
List<Object> objectList = new ArrayList<>();
addNumbers(objectList); // 可以
List<Number> numberList = new ArrayList<>();
addNumbers(numberList); // 可以
List<Integer> integerList = new ArrayList<>();
addNumbers(integerList); // 可以
List<Double> doubleList = new ArrayList<>();
// addNumbers(doubleList); // 编译错误
四、类型擦除
4.1 什么是类型擦除
Java泛型是通过类型擦除(Type Erasure)实现的,这意味着在编译时,所有的泛型类型信息都会被移除(擦除)。
专业解释:编译器在编译时去掉泛型类型信息,在运行时不存在泛型。例如List和List在运行时都是List。
通俗理解:就像魔术师把东西变没了一样,编译器在编译后把泛型的类型信息"变没"了,运行时JVM看到的只是原始类型。
4.2 类型擦除的影响
// 编译前
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
}
// 编译后(概念上)
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
}
4.3 桥方法
为了保持多态性,编译器会生成桥方法(Bridge Methods)。
// 编译前
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
@Override
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
// 编译后会生成桥方法
public class MyNode extends Node {
// 桥方法
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
五、泛型限制
5.1 不能实例化类型参数
public static <E> void append(List<E> list) {
// E elem = new E(); // 编译错误
// E[] array = new E[10]; // 编译错误
}
5.2 不能使用基本类型作为类型参数
// List<int> list = new ArrayList<int>(); // 编译错误
List<Integer> list = new ArrayList<Integer>(); // 正确
5.3 不能创建参数化类型的数组
// List<String>[] arrayOfLists = new List<String>[10]; // 编译错误
5.4 不能使用instanceof操作符
List<String> list = new ArrayList<>();
// if (list instanceof ArrayList<String>) { ... } // 编译错误
if (list instanceof ArrayList<?>) { ... } // 正确
六、高级泛型特性
6.1 泛型与继承
泛型类之间的关系:
// Integer是Number的子类,但Box<Integer>不是Box<Number>的子类
Box<Number> box = new Box<Integer>(); // 编译错误
// 但可以使用通配符建立关系
Box<? extends Number> box = new Box<Integer>(); // 正确
6.2 多重边界
class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
// T必须同时是A的子类,并实现B和C接口
class D <T extends A & B & C> { /* ... */ }
6.3 递归类型边界
// T必须实现Comparable<T>接口
public static <T extends Comparable<T>> T max(Collection<T> coll) {
T max = coll.iterator().next();
for (T elem : coll) {
if (elem.compareTo(max) > 0) max = elem;
}
return max;
}
七、实际应用案例
7.1 泛型缓存系统
import java.util.HashMap;
import java.util.Map;
// 定义一个泛型缓存类,用于存储键值对。
// 其中 K 表示键的类型,V 表示值的类型。
public class GenericCache<K, V> {
// 私有成员变量,使用 HashMap 来存储缓存数据。
// 键的类型为 K,值的类型为 V。
private Map<K, V> cache = new HashMap<>();
/**
* 向缓存中存入一个键值对。
* 如果键已经存在于缓存中,旧的值会被新的值替换。
*
* @param key 要存入的键,类型为 K。
* @param value 要存入的值,类型为 V。
*/
public void put(K key, V value) {
// 调用 HashMap 的 put 方法将键值对存入缓存。
cache.put(key, value);
}
/**
* 根据键从缓存中获取对应的值。
* 如果键不存在于缓存中,返回 null。
*
* @param key 要查找的键,类型为 K。
* @return 键对应的值,类型为 V;若键不存在,返回 null。
*/
public V get(K key) {
// 调用 HashMap 的 get 方法获取键对应的值。
return cache.get(key);
}
/**
* 检查缓存中是否包含指定的键。
*
* @param key 要检查的键,类型为 K。
* @return 如果缓存中包含该键,返回 true;否则返回 false。
*/
public boolean containsKey(K key) {
// 调用 HashMap 的 containsKey 方法检查键是否存在。
return cache.containsKey(key);
}
public static void main(String[] args) {
// 创建一个 GenericCache 实例,键的类型为 String,值的类型为 Integer。
// 用于存储姓名和年龄的映射关系。
GenericCache<String, Integer> nameToAgeCache = new GenericCache<>();
// 向缓存中存入 "Alice" 和她的年龄 30。
nameToAgeCache.put("Alice", 30);
// 向缓存中存入 "Bob" 和他的年龄 25。
nameToAgeCache.put("Bob", 25);
// 从缓存中获取 "Alice" 的年龄。
int aliceAge = nameToAgeCache.get("Alice");
// 打印 "Alice" 的年龄。
System.out.println("Alice's age: " + aliceAge);
}
}
7.2 泛型工具类
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
// 这是一个工具类,包含了一些集合操作的实用方法
public class CollectionUtils {
/**
* 合并两个列表为一个新的列表。
*
* @param <T> 列表元素的类型
* @param list1 第一个列表,其元素类型必须是 T 或 T 的子类
* @param list2 第二个列表,其元素类型必须是 T 或 T 的子类
* @return 合并后的列表,包含 list1 和 list2 的所有元素
*/
public static <T> List<T> merge(List<? extends T> list1, List<? extends T> list2) {
// 创建一个新的 ArrayList,初始容量为 list1 的大小
List<T> result = new ArrayList<>(list1);
// 将 list2 中的所有元素添加到结果列表中
result.addAll(list2);
// 返回合并后的列表
return result;
}
/**
* 在给定的列表中查找最大值。
*
* @param <T> 列表元素的类型,该类型必须实现 Comparable 接口
* @param list 要查找最大值的列表,其元素类型必须是 T 或 T 的子类
* @return 列表中的最大值
* @throws NoSuchElementException 如果列表为空
*/
public static <T extends Comparable<? super T>> T findMax(List<? extends T> list) {
// 如果列表为空,抛出 NoSuchElementException 异常
if (list.isEmpty()) throw new NoSuchElementException();
// 获取列表的迭代器
Iterator<? extends T> it = list.iterator();
// 初始化最大值为列表的第一个元素
T max = it.next();
// 遍历列表中的剩余元素
while (it.hasNext()) {
// 获取下一个元素
T next = it.next();
// 如果下一个元素比当前最大值大,则更新最大值
if (next.compareTo(max) > 0) {
max = next;
}
}
// 返回最大值
return max;
}
public static void main(String[] args) {
// 创建一个包含整数的列表
List<Integer> ints = Arrays.asList(1, 2, 3);
// 创建一个包含双精度浮点数的列表
List<Double> doubles = Arrays.asList(2.5, 3.5, 4.5);
// 合并整数列表和双精度浮点数列表为一个包含数字的列表
List<Number> numbers = merge(ints, doubles);
// 打印合并后的列表
System.out.println("Merged list: " + numbers);
// 查找整数列表中的最大值并打印
System.out.println("Max int: " + findMax(ints));
// 查找双精度浮点数列表中的最大值并打印
System.out.println("Max double: " + findMax(doubles));
}
}
八、泛型最佳实践
8.1 命名约定
类型参数 | 常用含义 |
---|---|
T | Type |
E | Element (集合中使用) |
K | Key |
V | Value |
N | Number |
S, U, V | 第二、第三、第四类型 |
8.2 何时使用泛型
- 当类、接口或方法需要处理多种数据类型时
- 需要类型安全的集合时
- 需要消除强制类型转换时
- 实现通用算法时
8.3 常见陷阱
- 忽略编译器警告(@SuppressWarnings(“unchecked”)要谨慎使用)
- 混淆List和List<?>
- 忘记类型擦除的影响
- 过度使用泛型导致代码可读性下降
九、泛型与其他语言对比
特性 | Java泛型 | C++模板 | C#泛型 |
---|---|---|---|
实现方式 | 类型擦除 | 代码生成 | 运行时支持 |
性能 | 无性能优势 | 有性能优势 | 有性能优势 |
原始类型 | 不支持 | 支持 | 支持 |
运行时类型信息 | 不可用 | 可用 | 可用 |
通配符 | 支持 | 不支持 | 有限支持 |
十、总结
Java泛型是一个强大的特性,它提供了以下优势:
- 类型安全:在编译时捕获类型错误
- 消除强制转换:使代码更简洁
- 代码复用:可以编写更通用的代码
- 更好的API设计:API可以更清晰地表达其意图
理解泛型需要掌握:
- 泛型类、方法和接口的定义与使用
- 通配符及其边界
- 类型擦除及其影响
- 泛型的限制和约束
以为玩转泛型了?实际开发时,复杂泛型组合能把你整成 “代码苦行僧”,边写边哭!
点赞保平安,不信你试试(微笑)。
想获取更多干货 / 精彩内容吗?微信搜索公众号 “Eric的技术杂货库” ,点击关注,第一时间解锁最新动态!