Java 泛型(Generics)详解
Java泛型是JDK 5.0引入的重要特性,它提供了编译时类型安全检查,允许在编译时检测到不正确的类型使用。泛型的主要目的是实现"类型参数化",让代码更加灵活、安全,同时避免类型转换异常。
泛型 = 类型安全 + 代码重用 + 性能优化 + 编译时检查
- 🔒 类型安全:在编译时进行类型检查,减少运行时类型错误
- ♻️ 代码重用:编写适用于多种类型的通用代码,提高代码复用性
- ⚡ 性能优化:避免类型转换的开销,提升运行时性能
- 🧩 API设计:设计灵活且类型安全的API,增强代码健壮性
- 📝 自文档化:泛型参数提供了类型信息,增强代码可读性
1. 泛型基础概念
1.1 什么是泛型?
泛型是一种参数化类型的概念,它允许在定义类、接口和方法时使用类型参数。这些类型参数在使用时会被具体的类型替换,从而提供类型安全。
- 基本语法
- 使用示例
- 演进历史
1public class Box<T> {2 private T content;3 4 public void set(T content) {5 this.content = content;6 }7 8 public T get() {9 return content;10 }11}1// 创建String类型的Box2Box<String> stringBox = new Box<>();3stringBox.set("Hello World"); // 类型安全检查4String content = stringBox.get(); // 无需类型转换56// 创建Integer类型的Box7Box<Integer> intBox = new Box<>();8intBox.set(42); // 类型安全检查9Integer number = intBox.get(); // 类型安全1011// 编译时类型检查保护12// stringBox.set(42); // 编译错误!13// intBox.set("42"); // 编译错误!1// Java 5.0 之前 - 无泛型2Box oldBox = new Box();3oldBox.set("Test String");4String content = (String) oldBox.get(); // 需要强制类型转换56// Java 5.0 - 引入泛型7Box<String> newBox = new Box<String>();8newBox.set("Test String");9String newContent = newBox.get(); // 无需类型转换1011// Java 7.0 - 菱形操作符12Box<String> diamondBox = new Box<>(); // 类型推断Java 7引入的菱形操作符让泛型实例化更加简洁,编译器会自动推断类型参数。
1.2 泛型的好处
| 好处 | 说明 | 示例 |
|---|---|---|
| 类型安全 | 编译时检查类型匹配,避免运行时异常 | List<String> 只能存储String类型 |
| 消除类型转换 | 无需手动进行类型转换 | String s = list.get(0); 直接获取String |
| 代码复用 | 一个泛型类可以处理多种类型 | Box<T> 可以存储任何类型 |
| 编译时检查 | 在编译时发现类型错误 | 尝试存储错误类型时编译失败 |
- 不使用泛型的问题
- 使用泛型的优势
- 对比分析
1// 没有泛型 - 可能出错2List list = new ArrayList();3list.add("Hello");4list.add(42); // 可以添加任何类型,类型不安全5String s = (String) list.get(1); // 运行时异常!ClassCastException上面的代码在编译时不会报错,但在运行时会抛出 ClassCastException,因为尝试将 Integer 转换为 String。
这种错误只能在运行时被发现,增加了调试难度。
1// 使用泛型 - 类型安全2List<String> stringList = new ArrayList<>();3stringList.add("Hello");4// stringList.add(42); // 编译错误!类型不匹配5String s = stringList.get(0); // 安全,无需转换使用泛型后,如果尝试向 List<String> 中添加非 String 类型的元素,编译器会直接报错。
这种类型安全检查可以在编译阶段就发现错误,而不是等到运行时。
| 特性 | 不使用泛型 | 使用泛型 |
|---|---|---|
| 类型检查 | 运行时检查 | 编译时检查 |
| 类型转换 | 需要显式转换 | 自动转换 |
| 错误发现 | 运行时抛出异常 | 编译时提示错误 |
| 代码清晰度 | 类型信息不明确 | 类型信息明确 |
2. 泛型类型参数
2.1 类型参数命名约定
Java泛型使用类型参数来表示类型,这些参数通常使用单个大写字母命名,遵循以下约定:
| 类型参数 | 含义 | 示例 |
|---|---|---|
E | Element(元素) | Collection<E> |
T | Type(类型) | Box<T> |
K | Key(键) | Map<K,V> |
V | Value(值) | Map<K,V> |
N | Number(数字) | Number<N> |
S | Source(源) | Function<S,T> |
2.2 多个类型参数
泛型类可以定义多个类型参数,用逗号分隔:
1public class Pair<K, V> {2 private K key;3 private V value;4 5 public Pair(K key, V value) {6 this.key = key;7 this.value = value;8 }9 10 public K getKey() { return key; }11 public V getValue() { return value; }12 13 public void setKey(K key) { this.key = key; }14 public void setValue(V value) { this.value = value; }15}使用示例
1// 创建String-Integer对2Pair<String, Integer> pair1 = new Pair<>("Age", 25);3String key = pair1.getKey(); // String类型4Integer value = pair1.getValue(); // Integer类型56// 创建Integer-String对7Pair<Integer, String> pair2 = new Pair<>(1, "One");8Integer key2 = pair2.getKey(); // Integer类型9String value2 = pair2.getValue(); // String类型虽然可以使用任何标识符作为类型参数,但建议遵循Java的命名约定,这样代码更易读、更专业。
3. 泛型方法
3.1 泛型方法定义
泛型方法是在方法级别使用泛型,它可以在非泛型类中定义,也可以在泛型类中定义。
基本语法
1public class Utils {2 // 泛型方法:在返回类型前声明类型参数3 public static <T> void printArray(T[] array) {4 for (T element : array) {5 System.out.print(element + " ");6 }7 System.out.println();8 }9 10 // 泛型方法:返回泛型类型11 public static <T> T getFirst(T[] array) {12 if (array.length > 0) {13 return array[0];14 }15 return null;16 }17}使用泛型方法
1// 调用泛型方法2String[] strings = {"Hello", "World"};3Integer[] numbers = {1, 2, 3, 4, 5};45Utils.printArray(strings); // 输出: Hello World6Utils.printArray(numbers); // 输出: 1 2 3 4 578String firstString = Utils.getFirst(strings); // String类型9Integer firstNumber = Utils.getFirst(numbers); // Integer类型3.2 泛型方法与泛型类的区别
| 特性 | 泛型类 | 泛型方法 |
|---|---|---|
| 声明位置 | 类名后 | 方法返回类型前 |
| 作用范围 | 整个类 | 单个方法 |
| 类型参数 | 实例化时确定 | 调用时推断 |
| 使用方式 | new Box<String>() | Utils.<String>method() |
Java编译器通常能够自动推断泛型方法的类型参数,所以通常不需要显式指定:
1// 编译器自动推断T为String2Utils.printArray(new String[]{"Hello"});34// 显式指定类型参数(通常不需要)5Utils.<String>printArray(new String[]{"Hello"});4. 类型擦除(Type Erasure)
Java泛型是通过类型擦除实现的,这意味着在运行时,泛型信息会被擦除,所有的泛型类型都被转换为它们的原始类型(raw type)。
4.1 类型擦除机制
类型擦除是Java泛型实现的核心机制,编译器会在编译时移除所有泛型类型信息,这种设计主要是为了向后兼容性。
- 类型擦除过程
- 有界类型擦除
- 调试提示
1// 编译时:泛型类型2List<String> stringList = new ArrayList<>();3List<Integer> intList = new ArrayList<>();45// 运行时:类型被擦除,都变成List6// 实际类型:List stringList = new ArrayList();7// 实际类型:List intList = new ArrayList();1// 有界类型参数2class Box<T extends Number> {3 private T value;4 // ...5}67// 擦除后等效为8class Box {9 private Number value; // 使用上界作为实际类型10 // ...11}- 使用反射API查看运行时类型
- 在调试器中查看对象的实际类型
- 利用
getClass()方法比较不同泛型实例的类型
类型擦除是造成泛型一些限制的根源,如不能创建泛型数组、不能进行确切的运行时类型检查等。
验证类型擦除
1public class TypeErasureDemo {2 public static void main(String[] args) {3 List<String> stringList = new ArrayList<>();4 List<Integer> intList = new ArrayList<>();5 6 // 检查运行时类型7 System.out.println(stringList.getClass()); // class java.util.ArrayList8 System.out.println(intList.getClass()); // class java.util.ArrayList9 10 // 类型擦除后,两个列表的类对象是相同的11 System.out.println(stringList.getClass() == intList.getClass()); // true12 }13}4.2 类型擦除的影响
4.2.1 无法创建泛型数组
1// 编译错误:不能创建泛型数组2// T[] array = new T[10]; // 错误!34// 解决方案1:使用Object数组,然后转换5public class GenericArray<T> {6 private Object[] array;7 8 public GenericArray(int size) {9 array = new Object[size];10 }11 12 @SuppressWarnings("unchecked")13 public T get(int index) {14 return (T) array[index];15 }16 17 public void set(int index, T element) {18 array[index] = element;19 }20}4.2.2 无法使用instanceof检查泛型类型
1public class TypeCheckDemo {2 public static <T> void checkType(List<T> list) {3 // 编译错误:不能使用instanceof检查泛型类型4 // if (list instanceof List<String>) { } // 错误!5 6 // 正确的检查方式7 if (list instanceof List) {8 System.out.println("这是一个List");9 }10 }11}由于类型擦除,Java泛型在运行时无法获取具体的类型信息,这限制了某些高级泛型操作的使用。
5. 通配符(Wildcards)
泛型通配符提供了更灵活的类型参数使用方式,能够更好地支持多态性和子类型关系。
5.1 无界通配符(Unbounded Wildcard)
无界通配符使用 ? 表示,表示可以接受任何类型。它常用于不依赖于类型参数的泛型代码中。
基本用法
1public class WildcardDemo {2 // 接受任何类型的List3 public static void printList(List<?> list) {4 for (Object item : list) {5 System.out.print(item + " ");6 }7 System.out.println();8 }9 10 public static void main(String[] args) {11 List<String> stringList = Arrays.asList("Hello", "World");12 List<Integer> intList = Arrays.asList(1, 2, 3);13 14 printList(stringList); // 输出: Hello World15 printList(intList); // 输出: 1 2 316 }17}无界通配符的限制
1public class WildcardLimitations {2 public static void addElement(List<?> list) {3 // 编译错误:不能添加元素到无界通配符列表4 // list.add("Hello"); // 错误!5 6 // 只能添加null7 list.add(null); // 允许8 9 // 可以读取元素,但类型是Object10 Object item = list.get(0);11 }12}5.2 上界通配符(Upper Bounded Wildcard)
上界通配符使用 ? extends T 表示,表示类型必须是T或其子类型。
基本用法
1public class UpperBoundedWildcard {2 // 接受Number及其子类型的List3 public static double sumOfList(List<? extends Number> list) {4 double sum = 0.0;5 for (Number number : list) {6 sum += number.doubleValue();7 }8 return sum;9 }10 11 public static void main(String[] args) {12 List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);13 List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);14 15 System.out.println(sumOfList(intList)); // 输出: 15.016 System.out.println(sumOfList(doubleList)); // 输出: 6.617 }18}上界通配符的限制
1public class UpperBoundedLimitations {2 public static void addNumber(List<? extends Number> list) {3 // 编译错误:不能添加元素到上界通配符列表4 // list.add(42); // 错误!5 // list.add(3.14); // 错误!6 7 // 只能添加null8 list.add(null); // 允许9 10 // 可以读取元素,类型是Number11 Number first = list.get(0);12 }13}5.3 下界通配符(Lower Bounded Wildcard)
下界通配符使用 ? super T 表示,表示类型必须是T或其父类型。
基本用法
1public class LowerBoundedWildcard {2 // 接受Integer及其父类型的List3 public static void addIntegers(List<? super Integer> list) {4 list.add(1);5 list.add(2);6 list.add(3);7 }8 9 public static void main(String[] args) {10 List<Number> numberList = new ArrayList<>();11 List<Object> objectList = new ArrayList<>();12 13 addIntegers(numberList); // 可以添加Integer到Number列表14 addIntegers(objectList); // 可以添加Integer到Object列表15 16 System.out.println(numberList); // [1, 2, 3]17 System.out.println(objectList); // [1, 2, 3]18 }19}下界通配符的特点
1public class LowerBoundedCharacteristics {2 public static void processList(List<? super Integer> list) {3 // 可以添加Integer及其子类型4 list.add(42);5 list.add(100);6 7 // 可以读取元素,但类型是Object8 Object item = list.get(0);9 10 // 不能读取为Integer(可能不安全)11 // Integer number = list.get(0); // 错误!12 }13}- 通配符对比
- PECS原则
- 可视化说明
| 通配符类型 | 语法 | 读取操作 | 写入操作 | 应用场景 |
|---|---|---|---|---|
| 无界通配符 | List<?> | 只能作为Object读取 | 只能添加null | 只读取不关心具体类型的场景 |
| 上界通配符 | List<? extends T> | 可以作为T类型读取 | 只能添加null | 从列表中读取T类型元素的场景 |
| 下界通配符 | List<? super T> | 只能作为Object读取 | 可以添加T及其子类型 | 向列表中添加T类型元素的场景 |
- Producer Extends: 如果你需要从集合中读取类型T的数据,使用
<? extends T> - Consumer Super: 如果你需要向集合中写入类型T的数据,使用
<? super T>
这个原则帮助我们选择正确的通配符,以确保类型安全性和灵活性。
1// 从集合中读取(Producer) - 使用extends2public static double sumOfList(List<? extends Number> list) {3 double sum = 0.0;4 for (Number n : list) { // 可以安全读取为Number5 sum += n.doubleValue();6 }7 return sum;8}910// 向集合中写入(Consumer) - 使用super11public static void addNumbers(List<? super Integer> list) {12 list.add(1); // 可以安全添加Integer13 list.add(2);14}6. 泛型约束与边界
6.1 类型边界(Type Bounds)
类型边界用于限制泛型类型参数的范围,确保类型参数满足特定条件。
上界类型边界
1public class NumberBox<T extends Number> {2 private T number;3 4 public NumberBox(T number) {5 this.number = number;6 }7 8 public T getNumber() {9 return number;10 }11 12 // 可以调用Number的方法13 public double getDoubleValue() {14 return number.doubleValue();15 }16 17 public int getIntValue() {18 return number.intValue();19 }20}使用上界类型边界
1public class BoundedTypeDemo {2 public static void main(String[] args) {3 // 可以使用Number及其子类型4 NumberBox<Integer> intBox = new NumberBox<>(42);5 NumberBox<Double> doubleBox = new NumberBox<>(3.14);6 7 System.out.println(intBox.getDoubleValue()); // 42.08 System.out.println(doubleBox.getDoubleValue()); // 3.149 10 // 编译错误:String不是Number的子类型11 // NumberBox<String> stringBox = new NumberBox<>("Hello"); // 错误!12 }13}多重边界
1public class MultipleBounds<T extends Number & Comparable<T>> {2 private T value;3 4 public MultipleBounds(T value) {5 this.value = value;6 }7 8 public T getValue() {9 return value;10 }11 12 // 可以使用Number和Comparable的方法13 public double getDoubleValue() {14 return value.doubleValue();15 }16 17 public int compareTo(T other) {18 return value.compareTo(other);19 }20}6.2 递归类型边界
递归类型边界用于表示类型参数必须与自身相关。
1public class RecursiveTypeBound {2 // T必须实现Comparable<T>,即可以与自身比较3 public static <T extends Comparable<T>> T max(T a, T b) {4 if (a.compareTo(b) > 0) {5 return a;6 } else {7 return b;8 }9 }10 11 public static void main(String[] args) {12 // Integer实现了Comparable<Integer>13 Integer maxInt = max(10, 20);14 System.out.println(maxInt); // 2015 16 // String实现了Comparable<String>17 String maxString = max("Hello", "World");18 System.out.println(maxString); // World19 }20}7. 泛型接口与实现
7.1 泛型接口
泛型接口允许接口使用类型参数,实现类可以选择具体的类型或保持泛型。
基本泛型接口
1public interface Container<T> {2 void add(T element);3 T get(int index);4 int size();5 boolean isEmpty();6}实现泛型接口
1// 实现为具体类型2public class StringContainer implements Container<String> {3 private List<String> elements = new ArrayList<>();4 5 @Override6 public void add(String element) {7 elements.add(element);8 }9 10 @Override11 public String get(int index) {12 return elements.get(index);13 }14 15 @Override16 public int size() {17 return elements.size();18 }19 20 @Override21 public boolean isEmpty() {22 return elements.isEmpty();23 }24}2526// 保持泛型的实现27public class GenericContainer<T> implements Container<T> {28 private List<T> elements = new ArrayList<>();29 30 @Override31 public void add(T element) {32 elements.add(element);33 }34 35 @Override36 public T get(int index) {37 return elements.get(index);38 }39 40 @Override41 public int size() {42 return elements.size();43 }44 45 @Override46 public boolean isEmpty() {47 return elements.isEmpty();48 }49}7.2 泛型继承
泛型类可以继承其他泛型类,形成复杂的泛型层次结构。
1// 基础泛型类2public class Box<T> {3 protected T content;4 5 public Box(T content) {6 this.content = content;7 }8 9 public T getContent() {10 return content;11 }12 13 public void setContent(T content) {14 this.content = content;15 }16}1718// 继承泛型类,保持泛型19public class NumberBox<T extends Number> extends Box<T> {20 public NumberBox(T content) {21 super(content);22 }23 24 public double getDoubleValue() {25 return content.doubleValue();26 }27}2829// 继承泛型类,固定类型30public class StringBox extends Box<String> {31 public StringBox(String content) {32 super(content);33 }34 35 public int getLength() {36 return content.length();37 }38}8. 泛型最佳实践
8.1 命名约定
遵循Java泛型的命名约定,使代码更易读:
1// 推荐的命名2public class Cache<K, V> { }3public class Repository<T> { }4public class Service<E> { }56// 避免的命名7public class Cache<Key, Value> { } // 太长8public class Repository<Type> { } // 不够通用9public class Service<Element> { } // 太长8.2 类型边界使用
合理使用类型边界,避免过度约束:
1// 好的做法:适当的约束2public class NumberProcessor<T extends Number> {3 public double process(T number) {4 return number.doubleValue();5 }6}78// 避免:过度约束9public class NumberProcessor<T extends Number & Comparable<T> & Serializable> {10 // 这限制了太多类型11}1213// 好的做法:使用通配符14public static double sum(List<? extends Number> numbers) {15 double sum = 0.0;16 for (Number num : numbers) {17 sum += num.doubleValue();18 }19 return sum;20}8.3 避免原始类型
始终使用泛型,避免使用原始类型:
1// 错误:使用原始类型2List list = new ArrayList();3list.add("Hello");4list.add(42); // 可以添加任何类型56// 正确:使用泛型7List<String> stringList = new ArrayList<>();8stringList.add("Hello");9// stringList.add(42); // 编译错误,类型安全8.4 泛型方法设计
设计泛型方法时,考虑类型推断和易用性:
1public class CollectionUtils {2 // 好的设计:类型推断友好3 public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {4 List<T> result = new ArrayList<>();5 for (T item : list) {6 if (predicate.test(item)) {7 result.add(item);8 }9 }10 return result;11 }12 13 // 使用示例14 public static void main(String[] args) {15 List<String> names = Arrays.asList("Alice", "Bob", "Charlie");16 17 // 类型推断自动工作18 List<String> filtered = filter(names, name -> name.startsWith("A"));19 System.out.println(filtered); // [Alice]20 }21}9. 常见问题与解决方案
9.1 泛型数组问题
1public class GenericArraySolutions {2 // 方案1:使用Object数组3 public static <T> T[] createArray1(int size) {4 @SuppressWarnings("unchecked")5 T[] array = (T[]) new Object[size];6 return array;7 }8 9 // 方案2:使用反射10 public static <T> T[] createArray2(Class<T> clazz, int size) {11 @SuppressWarnings("unchecked")12 T[] array = (T[]) Array.newInstance(clazz, size);13 return array;14 }15 16 // 方案3:使用泛型集合17 public static <T> List<T> createList(int size) {18 return new ArrayList<>(size);19 }20}9.2 类型擦除的变通方案
1public class TypeErasureWorkarounds {2 // 使用Class对象保存类型信息3 public static class TypeAwareBox<T> {4 private T content;5 private Class<T> type;6 7 public TypeAwareBox(T content, Class<T> type) {8 this.content = content;9 this.type = type;10 }11 12 public T getContent() {13 return content;14 }15 16 public Class<T> getType() {17 return type;18 }19 20 public boolean isInstance(Object obj) {21 return type.isInstance(obj);22 }23 }24}10. 面试题精选
10.1 基础概念题
Q: 什么是Java泛型?它解决了什么问题?
A: Java泛型是JDK 5.0引入的特性,它提供了编译时类型安全检查。主要解决了以下问题:
- 类型安全:编译时检查类型匹配,避免运行时异常
- 消除类型转换:无需手动进行类型转换
- 代码复用:一个泛型类可以处理多种类型
- 编译时检查:在编译时发现类型错误
Q: 什么是类型擦除?它有什么影响?
A: 类型擦除是Java泛型的实现机制,在运行时泛型信息会被擦除,所有泛型类型都转换为原始类型。主要影响包括:
- 无法创建泛型数组:
T[] array = new T[10]编译错误 - 无法使用instanceof检查泛型类型:
list instanceof List<String>编译错误 - 运行时无法获取具体的泛型类型信息
10.2 通配符题
Q: 解释 ? extends T 和 ? super T 的区别?
A:
? extends T(上界通配符):表示类型必须是T或其子类型,可以读取元素(类型为T),但不能添加元素? super T(下界通配符):表示类型必须是T或其父类型,可以添加元素(类型为T),但读取时类型为Object
Q: 什么时候使用无界通配符 ??
A: 当方法只关心集合的结构(如大小、是否为空),而不关心元素类型时使用。例如:
1public static boolean isEmpty(Collection<?> collection) {2 return collection == null || collection.isEmpty();3}10.3 实践题
Q: 设计一个泛型缓存类,支持键值对存储
A:
1public class GenericCache<K, V> {2 private Map<K, V> cache = new HashMap<>();3 4 public void put(K key, V value) {5 cache.put(key, value);6 }7 8 public V get(K key) {9 return cache.get(key);10 }11 12 public boolean containsKey(K key) {13 return cache.containsKey(key);14 }15 16 public void remove(K key) {17 cache.remove(key);18 }19 20 public int size() {21 return cache.size();22 }23 24 public void clear() {25 cache.clear();26 }27}Q: 实现一个泛型方法,找出数组中的最大值
A:
1public static <T extends Comparable<T>> T findMax(T[] array) {2 if (array == null || array.length == 0) {3 throw new IllegalArgumentException("Array cannot be null or empty");4 }5 6 T max = array[0];7 for (int i = 1; i < array.length; i++) {8 if (array[i].compareTo(max) > 0) {9 max = array[i];10 }11 }12 return max;13}- 理解类型擦除:这是Java泛型的核心机制
- 掌握通配符:合理使用三种通配符类型
- 遵循最佳实践:避免原始类型,合理使用类型边界
- 实践应用:在实际项目中应用泛型提高代码质量
11. 总结
学习Java泛型的建议路径:
- 掌握基础语法和概念
- 理解类型擦除机制
- 学习通配符用法
- 掌握PECS原则
- 实践泛型类和方法的设计
通过合理使用泛型,可以编写更加通用、安全和可维护的代码。虽然类型擦除带来了一些限制,但总体而言,泛型给Java带来的好处远超过其缺点。对泛型的深入理解,是成为高级Java开发者的必备技能。
- 优势
- 局限性
- 最佳实践
✅ 类型安全: 在编译时捕获类型错误,避免运行时异常
✅ 代码复用: 一套代码可以处理多种不同类型
✅ 消除类型转换: 减少冗余的类型转换代码
✅ API设计: 使API更加清晰、类型安全
✅ 性能: 编译后的代码和手动类型转换的代码性能相当
⚠️ 类型擦除: 运行时类型信息丢失
⚠️ 泛型数组: 无法直接创建泛型数组
⚠️ 类型检查: 无法使用instanceof检查泛型类型
⚠️ 静态上下文: 无法在静态字段或方法中直接使用类类型参数
⚠️ 原始类型: 为了向后兼容性保留了原始类型,可能导致混淆
📌 使用通配符: 合理使用 ?, ? extends T 和 ? super T
📌 遵循PECS原则: Producer Extends, Consumer Super
📌 避免原始类型: 总是使用泛型类型参数
📌 优先考虑泛型方法: 使方法更加通用和类型安全
📌 关注类型推断: 让编译器做更多工作,减少冗余代码
通过本章的学习,你应该已经掌握了Java泛型的核心概念、语法规则和最佳实践。泛型是Java中非常重要的特性,它不仅能提高代码的类型安全性,还能增强代码的可读性和可维护性。在实际开发中,合理使用泛型可以避免很多运行时错误,让代码更加健壮。
12. 面试题精选
12.1 什么是Java泛型?泛型有什么优点?
答案: Java泛型是JDK 5引入的特性,允许在定义类、接口和方法时使用类型参数,这些参数在使用时会被具体类型替换。
泛型的主要优点:
- 类型安全:在编译时检查类型错误,避免运行时ClassCastException
- 消除类型转换:不需要显式转换对象类型,代码更加简洁
- 实现通用算法:可以编写适用于多种类型的通用代码
- 代码复用:减少因类型不同而导致的重复代码
- 更好的API设计:提供了类型约束,使API更加直观
12.2 什么是类型擦除?为什么Java泛型使用类型擦除?
答案: 类型擦除是Java泛型的基本实现机制,它指的是编译器在编译时会擦除所有泛型类型相关的信息,替换为原始类型(通常是Object或上界类型)。
Java泛型使用类型擦除的原因:
- 向后兼容性:保证泛型代码可以与Java 5之前的代码无缝协作
- 避免运行时开销:不需要在运行时维护额外的类型信息
- JVM限制:不需要修改Java虚拟机,简化了实现
类型擦除的主要后果:
- 泛型信息在运行时不可用
- 无法使用instanceof检查泛型类型
- 无法创建泛型类型的数组
- 静态上下文中不能引用类型参数
12.3 解释泛型中的通配符,以及PECS原则
答案: 通配符是Java泛型中的特殊符号,用于表示未知类型,有三种形式:
- 无界通配符(?):
List<?>表示可以是任何类型的列表 - 上界通配符(? extends T):
List<? extends Number>表示Number或其子类的列表 - 下界通配符(? super T):
List<? super Integer>表示Integer或其父类的列表
PECS原则(Producer-Extends, Consumer-Super):
- 当你的泛型类是生产者(提供数据)时,使用
? extends T - 当你的泛型类是消费者(接收数据)时,使用
? super T
例如:
1// 从列表读取数据(生产者),使用extends2public void readFrom(List<? extends Number> list) {3 Number n = list.get(0); // 安全,知道是Number或子类4}56// 向列表写入数据(消费者),使用super7public void writeTo(List<? super Integer> list) {8 list.add(42); // 安全,知道list可以接收Integer9}12.4 泛型类型参数的命名约定是什么?常见的类型参数名称代表什么?
答案: Java泛型类型参数通常使用单个大写字母表示。最常见的类型参数命名约定:
- E: Element(元素),通常用于集合类
- T: Type(类型),最常用的泛型类型参数
- K: Key(键),常用于Map的键类型
- V: Value(值),常用于Map的值类型
- N: Number(数字),表示数值类型
- S, U, V: 第2、3、4个类型参数,当需要多个泛型类型时使用
例如:
1// T代表任意类型2public class Box<T> { }34// K代表键类型,V代表值类型5public interface Map<K, V> { }67// E代表元素类型8public interface List<E> { }12.5 如何解决泛型数组创建的问题?
答案: 由于类型擦除,在Java中无法直接创建泛型数组(如new T[10])。解决这个问题有以下几种方法:
- 使用Object数组加类型转换:
1public class GenericArray<T> {2 private T[] array;3 4 @SuppressWarnings("unchecked")5 public GenericArray(int size) {6 // 创建Object数组,然后强制类型转换7 array = (T[]) new Object[size];8 }9}- 使用反射创建数组:
1public class GenericArray<T> {2 private T[] array;3 4 @SuppressWarnings("unchecked")5 public GenericArray(Class<T> type, int size) {6 // 使用反射API创建特定类型的数组7 array = (T[]) Array.newInstance(type, size);8 }9}- 使用ArrayList代替数组:
1public class GenericCollection<T> {2 private List<T> list;3 4 public GenericCollection(int size) {5 list = new ArrayList<>(size);6 }7}每种方法都有其优缺点,选择取决于具体需求和使用场景。
参与讨论