【JAVA基础面经】JAVA中的泛型

泛型


定义

一种参数化类型机制

泛型是 Java 5 引入的一种 参数化类型 机制,允许在定义类、接口、方法时使用类型参数(如 T、E),在具体使用时才指定实际类型。泛型在编译时进行类型检查和类型擦除,生成不带泛型信息的字节码,因此属于编译时多态

泛型出现之前的问题

在泛型出现之前,Java集合类(如ArrayList)存储的是Object类型,这意味着可以向集合中添加任何类型的对象。

java 复制代码
List list = new ArrayList();
list.add("Hello");
list.add(123); // 可以添加Integer,但容易引发问题

// 取出时需要强制转换
String str = (String) list.get(0); // 没问题
String error = (String) list.get(1); // 运行时抛出 ClassCastException

这段代码在编译时完全正常,但在运行时却会崩溃。问题的根源在于:编译器无法知道集合中元素的实际类型,类型安全只能靠程序员手动保证。

泛型提供了编译时类型安全检查机制,在定义类、接口、方法时可以指定类型参数

java 复制代码
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译错误:不兼容的类型

String str = list.get(0); // 无需强制转换

泛型类

泛型类是在类名后使用尖括号 <T> 声明类型参数,类内部可以使用该类型,此时Box<String> 中的 T 被替换为 String,编译器会确保 setContent 只能接收 String 类型,getContent 返回 String,无需强转。

java 复制代码
public class Box<T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }

    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setContent("Hello");
        String str = stringBox.getContent();  // 无需强制转换
        System.out.println(str);

        Box<Integer> intBox = new Box<>();
        intBox.setContent(123);
        Integer num = intBox.getContent();
        System.out.println(num);
    }
}

泛型接口

泛型接口与泛型类类似,在接口名后声明类型参数。Pair<K, V> 接口,表示键值对。

java 复制代码
public interface Pair<K, V> {
    K getKey();
    V getValue();
}
java 复制代码
public class OrderedPair<K, V> implements Pair<K, V> {
    private K key;
    private V value;

    public OrderedPair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public K getKey() { return key; }

    @Override
    public V getValue() { return value; }

    public static void main(String[] args) {
        Pair<String, Integer> p1 = new OrderedPair<>("age", 30);
        Pair<Integer, String> p2 = new OrderedPair<>(1, "One");

        System.out.println(p1.getKey() + " = " + p1.getValue());
        System.out.println(p2.getKey() + " = " + p2.getValue());
    }
}

泛型方法

泛型方法是指方法声明时带有类型参数的方法,可以独立于类是否泛型。类型参数 <T> 放在方法返回值之前,调用时通常不需要显式指定类型,编译器会根据参数推断。

java 复制代码
public class GenericMethodDemo {
    // 泛型方法,<T> 声明类型参数,放在返回值之前
    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[] strs = {"A", "B", "C"};
        swap(strs, 0, 2);  // 类型推断为 String
        System.out.println(Arrays.toString(strs)); // [C, B, A]

        Integer[] ints = {1, 2, 3};
        swap(ints, 0, 1);  // 类型推断为 Integer
        System.out.println(Arrays.toString(ints)); // [2, 1, 3]
    }
}

泛型通配符

泛型通配符 ? 用于表示未知的类型,在无法确定具体类型时提供灵活性。通配符主要分为三类:无界通配符、上界通配符、下界通配符。

对于下面的代码,可以发现由于泛型是不变的,List<String>并不是 List<Object> 的子类型,如果允许这样做,我们就可以向strings中添加非String类型的对象,破坏了类型安全。此时可以通过无界通配符来表示一个可以接受任何List的方法。

java 复制代码
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // 编译错误!

无界通配符

无界通配符 ? 表示未知类型,如下面代码可以接收任何类型的泛型集合,但不能向集合中写入元素(除了 null),因为无法确定类型安全。

java 复制代码
public void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
        // list.add("hello");   // 编译错误:不能添加元素(除null外)
        // list.add(123);       // 编译错误
        list.add(null);         // 唯一允许的添加
    }
}

// 可以传入任何类型的 List
List<String> names = Arrays.asList("Alice", "Bob");
List<Integer> numbers = Arrays.asList(1, 2, 3);
printList(names);   // OK
printList(numbers); // OK

上界通配符

上界通配符表示类型是 T 或 T 的子类,即< ? extends T>。例如List<? extends Number> 表示元素类型是 Number 或其子类(如 Integer、Double),除了 null,不能向集合添加任何元素,因为无法确定具体子类型

java 复制代码
public class UpperBoundWildcard {
    // 计算数字列表的总和(生产者:只读)
    public static double sum(List<? extends Number> numbers) {
        double total = 0.0;
        for (Number n : numbers) {   // 读取为 Number
            total += n.doubleValue();
        }
        return total;
    }

    public static void main(String[] args) {
        List<Integer> ints = Arrays.asList(1, 2, 3);
        List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
        System.out.println(sum(ints));    // 6.0
        System.out.println(sum(doubles)); // 7.5

        // 不能写入
        // ints.add(4);            // 编译错误:不能添加元素
    }
}

下界通配符

下界通配符表示类型是 T 或 T 的父类,即 <? super T>。例如List<? super Integer> 表示元素类型是 Integer 或其父类(如 Number、Object)

java 复制代码
public class LowerBoundWildcard {
    // 将整数列表复制到目标列表(消费者:只写)
    public static void copy(List<Integer> src, List<? super Integer> dest) {
        for (Integer num : src) {
            dest.add(num);   // 可以添加 Integer 或其子类
        }
    }

    public static void main(String[] args) {
        List<Integer> src = Arrays.asList(1, 2, 3);
        List<Number> destNumber = new ArrayList<>();
        List<Object> destObject = new ArrayList<>();
        copy(src, destNumber);   // 可以,Number 是 Integer 的父类
        copy(src, destObject);   // 可以,Object 是 Integer 的父类

        System.out.println(destNumber); // [1, 2, 3]
        // 读取时只能作为 Object
        for (Object obj : destNumber) {
            System.out.println(obj);
        }
    }
}

PECS 原则

PECS ( Producer Extends, Consumer Super ) 帮助我们决定何时使用 extends 和 super

  • Producer(生产者):如果参数化类型代表一个生产者,即提供数据(只读),用 <? extends T>
  • Consumer(消费者):如果参数化类型代表一个消费者,即消费数据(只写),用 <? super T>

类型擦除

在编译时提供类型检查,编译后的字节码中不保留泛型类型信息

Java 泛型在编译时提供类型检查,但编译后的字节码中不保留泛型类型信息。这个过程称为类型擦除。

  • 编译器会将泛型类型中的类型参数替换为它的第一个上界(如果有 extends 指定)或 Object(如果没有指定上界)
  • 插入必要的强制转换
  • 当子类重写父类方法,且方法签名在擦除后出现冲突时,编译器会生成桥接方法(Bridge Method)来保持多态性。最常见的场景是类实现了泛型接口或继承了泛型类
java 复制代码
//无上界的情况替换为Obcjet
//原始代码
public class Box<T> {
    private T content;
    public void setContent(T content) { this.content = content; }
    public T getContent() { return content; }
}

//擦除后
public class Box {
    private Object content;
    public void setContent(Object content) { this.content = content; }
    public Object getContent() { return content; }
}
java 复制代码
//插入必要的强制转换
//原始代码
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);   // 原本不需要强转

//擦除后
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0);   // 编译器自动插入强转
java 复制代码
//生成桥接方法以保持多态性
//原始代码
public class IntegerBox implements Comparable<IntegerBox> {
    private int value;
    public IntegerBox(int value) {this.value = value;}

    @Override
    public int compareTo(IntegerBox other) {
        return Integer.compare(this.value, other.value);
    }
}

//Comparable<IntegerBox> 在擦除后会变成 Comparable
//接口Comparable的 compareTo 方法签名变为 compareTo(Object)
//但 IntegerBox 中实现的是 compareTo(IntegerBox),签名不匹配,多态会失效
//擦除后编译器生成的桥接方法
public int compareTo(Object other) {
    return compareTo((IntegerBox) other);
}

另一个常见场景:子类重写父类方法但返回值类型不同(协变返回类型)

Parent.get() 返回 Number,Child.get() 返回 Integer,签名不匹配(返回值类型在方法重写中不重要,但在字节码层面需要匹配)。编译器会在 Child 中生成桥接方法

java 复制代码
class Parent {
    public Number get() { return 0; }
}
class Child extends Parent {
    @Override
    public Integer get() { return 100; }   // 协变返回类型
}

//生成桥接方法
public Number get() { return get(); }   // 调用 Integer get()

类型擦除带来的问题

  1. 运行时无法区分泛型类型
java 复制代码
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true,都是 ArrayList
  1. 不能使用 instanceof 检查泛型类型(instanceof 用于判断一个对象是否是某个类、其子类或实现的接口的实例。它返回一个布尔值,表示判断结果)
java 复制代码
if (list instanceof List<String>) // 编译错误
  1. 不能创建泛型数组
java 复制代码
T[] array = new T[10]; // 编译错误
// 可以这样绕过:
List<T>[] array = (List<T>[]) new List[10]; // 但会有警告
  1. 静态上下文中不能使用泛型类型参数
java 复制代码
public class GenericClass<T> {
    private static T instance; // 编译错误
    public static T getInstance() { ... } // 编译错误
}

原始类型 List、List<Object>、List<?> 的区别

  • 原始类型是指不带任何泛型参数的泛型类型,编译时不进行泛型类型检查。
  • List<Object>是指定类型参数为 Object 的泛型类型,编译时进行类型检查,只允许添加 Object 或其子类型的对象(实际上任何对象都可以,因为所有类都继承自 Object)。典型的由于泛型不变性, List <Object>不是 List <String> 的父类型,因此不能进行转换。
  • List<?> 是未知类型的列表,类型参数为通配符 ?,表示元素类型未知。是所有 List<具体类型> 的父类型(如 List<String> 、List<Integer> 都可以赋值给 List<?>)
相关推荐
自然常数e2 小时前
预处理讲解
java·linux·c语言·前端·visual studio
大数据新鸟2 小时前
设计模式详解——模板方法模式
java·tomcat·模板方法模式
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第四期 - 抽象工厂模式】抽象工厂模式 —— 定义、核心结构、实战示例、优缺点与适用场景及模式区别
java·后端·设计模式·软件工程·抽象工厂模式
always_TT2 小时前
内存泄漏是什么?如何避免?
android·java·开发语言
白鸽梦游指南2 小时前
docker仓库的工作原理及搭建仓库
java·docker·eureka
※DX3906※2 小时前
SpringBoot之旅4: MyBatis 操作数据库(进阶) 动态SQL+MyBatis-Plus实战,从入门到熟练,再也不踩绑定异常、SQL拼接坑
java·数据库·spring boot·spring·java-ee·maven·mybatis
java1234_小锋2 小时前
Java高频面试题:怎么实现Redis的高可用?
java·开发语言·redis
jiankeljx2 小时前
MySQL-mysql zip安装包配置教程
java
FlagOS智算系统软件栈2 小时前
智源×Eclipse基金会携手打造PanEval,中欧协同开启“评测+开源+合规”新模式
java·eclipse·开源