1. 引言

泛型是Java的核心概念,于Java 5首次引入。几乎所有Java代码库都会使用泛型,开发者几乎必然会在工作中遇到它。因此正确理解泛型至关重要,这也是面试中高频考点的原因。

2. 问题

2.1 Q1. 什么是泛型类型参数?

类型指的是类或接口的名称。泛型类型参数就是将类型作为参数使用在类、方法或接口声明中

先看一个非泛型示例:

public interface Consumer {
    public void consume(String parameter)
}

这里consume()方法的参数类型固定为String,不可配置。

现在用泛型类型T(按惯例命名)替换String

public interface Consumer<T> {
    public void consume(T parameter)
}

实现时可以指定具体类型:

public class IntegerConsumer implements Consumer<Integer> {
    public void consume(Integer parameter)
}

这样就能灵活处理不同类型的数据。

2.2 Q2. 使用泛型有哪些优势?

主要优势包括:

  1. 避免强制类型转换,提供编译时类型安全
  2. 消除代码重复,实现通用算法

以集合操作为例:

// 非泛型写法
List list = new ArrayList();
list.add("foo");
Object o = list.get(0);
String foo = (String) o;  // 需要强制转换

如果误添加了Integer

list.add(1);
String foo = (String) list.get(0);  // 运行时抛出ClassCastException

使用泛型后:

List<String> list = new ArrayList<>();
list.add("foo");
String o = list.get(0);    // 无需转换
Integer foo = list.get(0); // 编译错误,直接暴露问题

泛型在编译时就能发现类型错误,避免运行时异常

2.3 Q3. 什么是类型擦除?

⚠️ 类型擦除指泛型类型信息在运行时对JVM不可用,仅存在于编译阶段

实现原理:

  1. 泛型类型被替换为Object
  2. 有界类型被替换为第一个边界类
  3. 插入隐式类型转换

这样设计是为了保持与Java 5之前版本的向后兼容。开发者常犯的错误是试图在运行时获取泛型类型:

public void foo(Consumer<T> consumer) {
   Type type = consumer.getGenericTypeParameter()  // 编译错误
}

泛型类型在运行时完全不可用,反射也无法获取。

2.4 Q4. 省略泛型类型参数还能编译吗?

可以编译,但会收到编译器警告:

List list = new ArrayList();  // 警告:未检查类型

虽然向后兼容允许这种写法,但强烈建议不要省略泛型参数,否则会失去类型安全检查。

2.5 Q5. 泛型方法与泛型类型有何区别?

泛型方法在方法内部引入类型参数,作用域仅限于该方法

public static <T> T returnType(T argument) { 
    return argument; 
}

与泛型类不同,泛型方法可以独立于类存在,且能利用类型推断简化调用。

2.6 Q6. 什么是类型推断?

类型推断指编译器根据方法参数自动推断泛型类型:

Integer inferredInteger = returnType(1);      // 推断T为Integer
String inferredString = returnType("String"); // 推断T为String

无需显式指定类型参数,代码更简洁。

2.7 Q7. 什么是有界类型参数?

有界类型参数限制泛型参数必须满足特定条件

public abstract class Cage<T extends Animal> {
    abstract void addAnimal(T animal)
}

这里T必须是Animal的子类:

Cage<Cat> catCage;      // 合法
Cage<Object> objectCage; // 编译错误,Object不是Animal子类

优势:

  • 可访问边界类的方法
  • 实现通用算法:
    public void firstAnimalJump() {
      T animal = animals.get(0);
      animal.jump();  // 所有Animal子类都有jump方法
    }
    

2.8 Q8. 可以声明多个有界类型参数吗?

可以,使用&连接多个边界:

public abstract class Cage<T extends Animal & Comparable>

要求:

  • 类型必须同时满足所有边界
  • 类边界必须在前Animal在前,Comparable在后)

2.9 Q9. 什么是通配符类型?

通配符?表示未知类型

public static void consumeListOfWildcardType(List<?> list)

可接受任何类型的列表,但无法添加元素(除null外)。

2.10 Q10. 什么是上界通配符?

上界通配符? extends T限制类型为T或其子类,解决集合继承问题:

public class Farm {
  public void addAnimals(Collection<? extends Animal> newAnimals) {
    animals.addAll(newAnimals);
  }
}

现在可以接受Animal的任何子类集合:

farm.addAnimals(cats); // 合法
farm.addAnimals(dogs); // 合法

而原始写法会编译错误:

public void addAnimals(Collection<Animal> newAnimals) {
    // 只能接受Animal集合,不接受子类集合
}

2.11 Q11. 什么是无界通配符?

无界通配符?表示任何类型,但不同于Object

List<?> wildcardList = new ArrayList<String>();  // 合法
List<Object> objectList = new ArrayList<String>(); // 编译错误

关键区别:

  • List<?>可接受任何具体类型的列表
  • List<Object>只能接受Object类型的列表

2.12 Q12. 什么是下界通配符?

下界通配符? super T限制类型为T或其超类

public static void addDogs(List<? super Animal> list) {
   list.add(new Dog("tom"));  // 可添加Dog(Animal子类)
}

可接受Animal及其超类:

ArrayList<Object> objects = new ArrayList<>();
addDogs(objects);  // 合法,Object是Animal超类

但不可接受子类:

ArrayList<Cat> cats = new ArrayList<>();
addDogs(cats);  // 编译错误,Cat不是Animal超类

2.13 Q13. 何时选择下界或上界通配符?

遵循PECS原则

  • Producer Extends:生产者使用? extends T
    public static void makeLotsOfNoise(List<? extends Animal> animals) {
        animals.forEach(Animal::makeNoise);  // 只读取不修改
    }
    
  • Consumer Super:消费者使用? super T
    public static void addCats(List<? super Animal> animals) {
        animals.add(new Cat());  // 只添加不读取
    }
    

当集合既是生产者又是消费者时,使用无界通配符?

2.14 Q14. 泛型类型信息在运行时可用吗?

仅有一种情况:当泛型是类签名的一部分时

public class CatCage implements Cage<Cat>

可通过反射获取:

(Class<T>) ((ParameterizedType) getClass()
  .getGenericSuperclass()).getActualTypeArguments()[0];

但这种方式很脆弱,依赖具体类层次结构。大多数情况下泛型信息在运行时不可用


原始标题:Java Generics Interview Questions (+Answers)