泛型:类型参数化的威力
假设你需要一个容器类,用来存放元素。早期的 Java 程序员会这样写:
java
public class Container {
private Object element;
public void set(Object element) {
this.element = element;
}
public Object get() {
return element;
}
}使用时:
java
Container c = new Container();
c.set("hello");
String s = (String) c.get(); // 需要强制类型转换问题来了:如果不小心存了 Integer,却按 String 取呢?
java
c.set(123);
String s = (String) c.get(); // 运行时 ClassCastException编译器管不了这种错误,只能靠程序员的自律。但泛型的出现改变了这一切:
java
public class Container<T> {
private T element;
public void set(T element) {
this.element = element;
}
public T get() {
return element;
}
}使用时:
java
Container<String> c = new Container<>();
c.set("hello");
String s = c.get(); // 无需强制类型转换
c.set(123); // 编译错误!类型安全这就是泛型的核心价值:编译时类型检查 + 类型自动转换。
泛型的本质:类型擦除
这里有个惊人的事实:Java 的泛型是伪泛型——运行时的泛型信息被擦除了。
java
// 源代码
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 运行时
System.out.println(stringList.getClass() == intList.getClass()); // true两个 List 在运行时是同一个类,泛型信息被擦除了。
擦除到哪里了?
java
public class Container<T> {
private T element;
public T get() {
return element;
}
}编译后变成:
java
public class Container {
private Object element; // T 被替换为 Object
public Object get() {
return element;
}
}擦除规则:
- 如果 T 没有上限,替换为 Object
- 如果 T 有上限(如
<T extends Number>),替换为上限类型
java
public class Container<T extends Number> {
private T element;
public T get() {
return element;
}
}编译后变成:
java
public class Container {
private Number element; // T 被替换为 Number
public Number get() {
return element;
}
}通配符:灵活的泛型
无界通配符 <?>
表示「我不知道是什么类型」:
java
public void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}上界通配符 <? extends T>
表示「T 或 T 的子类」:
java
// 只能读取,不能写入(除了 null)
List<? extends Number> numbers = new ArrayList<Integer>();
// numbers.add(new Integer(1)); // 编译错误!
// numbers.add(new Double(1.0)); // 编译错误!
Number n = numbers.get(0); // 可以读取下界通配符 <? super T>
表示「T 或 T 的父类」:
java
// 只能保证能写入 T 或 T 的子类
List<? super Integer> numbers = new ArrayList<Number>();
numbers.add(new Integer(1)); // 可以写入
// numbers.add(new Double(1.0)); // 编译错误!
Object o = numbers.get(0); // 读取返回 ObjectPECS 原则
Producer Extends, Consumer Super
- 如果只读取数据,用
extends(生产者) - 如果只写入数据,用
super(消费者) - 如果既读又写,不要用通配符
java
// 生产者:提供数据
public double sum(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue(); // 只读取
}
return sum;
}
// 消费者:消费数据
public void addNumbers(List<? super Integer> list) {
list.add(1); // 写入
list.add(2);
}
// 读写兼备:不要用通配符
public void copy(List<Object> dest, List<Object> src) {
for (Object item : src) {
dest.add(item);
}
}泛型方法
java
public class Utils {
// 静态泛型方法
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 非静态泛型方法
public <T> T getFirst(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
}
// 使用
String[] arr = {"a", "b", "c"};
Utils.swap(arr, 0, 2);泛型数组的限制
不能直接创建泛型数组:
java
T[] array = new T[10]; // 编译错误!
// 原因:运行时类型被擦除,无法确定数组元素类型解决方法:
java
// 方法一:使用反射
T[] array = (T[]) Array.newInstance(clazz, capacity);
// 方法二:让调用者传入数组
public <T> T[] toArray(T[] a) {
return a;
}
// 方法三:使用 Object 数组 + 类型转换(不推荐)
Object[] objArray = new Object[10];
T[] array = (T[]) objArray; // 有警告但不报错泛型与反射结合
由于类型擦除,很多泛型信息在运行时丢失了。但通过反射可以获取:
java
public class GenericReflection {
public static void main(String[] args) throws Exception {
ArrayList<String> list = new ArrayList<>();
list.add("hello");
// 获取泛型类型
Class<? extends ArrayList> clazz = list.getClass();
Method addMethod = clazz.getMethod("add", Object.class);
addMethod.invoke(list, 123); // 可以添加 Integer!
System.out.println(list); // [hello, 123]
}
}这就是类型安全可以被绕过的地方。反射是泛型的天敌。
泛型接口
java
public interface Container<T> {
T get();
void set(T element);
}
// 实现时可以选择具体类型
class StringContainer implements Container<String> {
private String element;
@Override
public String get() {
return element;
}
@Override
public void set(String element) {
this.element = element;
}
}
// 或者继续是泛型
class GenericContainer<T> implements Container<T> {
private T element;
@Override
public T get() {
return element;
}
@Override
public void set(T element) {
this.element = element;
}
}面试追问方向
- 类型擦除后,如何获取泛型的实际类型信息?
- 为什么不能创建泛型数组?
<T>和<?>有什么区别?
留给你的思考题
考虑以下代码:
java
public static <T> void copy(List<T> dest, List<? extends T> src) {
for (T element : src) {
dest.add(element);
}
}
List<Number> numbers = new ArrayList<>();
List<Integer> integers = Arrays.asList(1, 2, 3);
copy(numbers, integers); // 为什么可以?以及一个更有挑战性的问题:
java
class A { }
class B extends A { }
class C extends B { }
// 以下调用哪个能编译,哪个不能?
List<A> aList = new ArrayList<>();
List<B> bList = new ArrayList<>();
List<C> cList = new ArrayList<>();
copy(aList, bList); // ?
copy(bList, cList); // ?
copy(cList, aList); // ?
copy(cList, bList); // ?解释为什么。理解了这个,你就真正掌握了泛型的核心概念。
