Skip to content

Java 泛型

ZhangPan edited this page Jul 4, 2025 · 4 revisions

什么是泛型?泛型的作用?

泛型,英文为Generics,其本质是参数化类型,也就是将具体的数据类型指定为一个参数,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。 简单来说,泛型允许程序员在编写代码时不指定具体的数据类型,而是在使用时才指定类型。这使得同一段代码可以适用于多种数据类型,而不需要为每种类型都编写重复的代码

泛型的主要作用

  1. 类型安全性

泛型的核心作用是提供编译时类型安全检查,避免运行时的ClassCastException异常。

在没有泛型之前,集合类如List、Set等只能存储Object类型的元素,这就导致在取出元素时必须进行显式的类型转换,容易出错:

// 没有泛型的情况
List list = new ArrayList();
list.add("Hello");
list.add(100); // 可以添加任何类型
String str = (String)list.get(0); // 需要强制转换
Integer num = (Integer)list.get(1); // 运行时可能抛出ClassCastException

使用泛型后,可以在编译时就限制集合中元素的类型:

// 使用泛型
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(100); // 编译错误,无法添加非String类型
String str = stringList.get(0); // 无需强制转换

通过这种方式,泛型将运行时可能出现的类型错误转移到了编译时,大大提高了程序的可靠性。

  1. 消除强制类型转换 泛型消除了源代码中的许多强制类型转换,使代码更加简洁易读。

非泛型代码示例:

List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 需要强制转换

泛型代码示例:

List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 无需强制转换
  1. 提高代码复用性 泛型使得可以编写通用的代码模板,适用于不同类型的对象,而不需要为每种类型都编写重复的代码。

例如,一个简单的泛型Box类可以存储任何类型的值:

public class Box<T> {
    private T value;
    
    public void set(T value) {
        this.value = value;
    }
    
    public T get() {
        return value;
    }
}
// 使用示例
Box<Integer> intBox = new Box<>();
intBox.set(10);
Integer intValue = intBox.get();

Box<String> strBox = new Box<>();
strBox.set("Hello");
String strValue = strBox.get();

同样的Box类可以用于存储Integer、String或其他任何类型的对象,无需为每种类型创建单独的类。

  1. 性能优化 虽然泛型的主要优势不在于性能,但它确实可以带来一些性能上的好处:

避免了装箱(boxing)和拆箱(unboxing)操作:当使用泛型而非Object类型时,对于基本数据类型的包装类(如Integer、Double等),可以避免不必要的装箱和拆箱操作。 减少了类型检查的开销:由于类型在编译时就已经确定,运行时不需要额外的类型检查

泛型的使用方式有哪几种?

  1. 泛型类 泛型类是在类定义时声明类型参数的类,允许在实例化类时指定具体的类型。

定义语法:

修饰符 class 类名<类型变量, 类型变量, ...> {
    // 类体
}

示例:

public class Box<T> {
    private T content;
    
    public void setContent(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
}

// 使用示例
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String content = stringBox.getContent();
  1. 泛型接口 泛型接口是在接口定义时声明类型参数的接口,允许在实现接口时指定具体的类型。

定义语法:

修饰符 interface 接口名<类型变量, 类型变量, ...> {
    // 接口方法
}
示例java
public interface Processor<T> {
    T process(T input);
}

// 实现泛型接口
public class StringProcessor implements Processor<String> {
    @Override
    public String process(String input) {
        return input.toUpperCase();
    }
}
  1. 泛型方法 泛型方法是在方法定义时声明类型参数的方法,允许在调用方法时指定或推断具体的类型。

定义语法:

修饰符 <类型变量, 类型变量, ...> 返回值类型 方法名(形参列表) {
    // 方法体
}
示例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;
    }
}

// 使用示例
String[] strings = {"A", "B", "C"};
Utils.swap(strings, 0, 2); // 交换位置0和2的元素

需要注意的是,泛型方法与泛型类中使用泛型类型参数的普通方法是不同的。只有声明了类型参数的方法才是泛型方法

什么是泛型擦除机制?为什么要擦除?

泛型擦除是Java编译器处理泛型的一种机制,它会在编译期间移除所有泛型类型信息,在运行时JVM并不知道泛型的具体类型。这是Java为了向后兼容而采取的设计决策。

擦除的具体表现

  • 类型参数被替换:

    • 无界类型参数(如)被替换为Object

    • 有界类型参数(如)被替换为边界类型(Number)

类型检查:

  • 类型检查只在编译时进行

  • 运行时无法获取泛型的实际类型参数

擦除带来的影响

1.无法直接实例化泛型类型:

T obj = new T();  // 编译错误

2.无法创建泛型数组:

T[] array = new T[10];  // 编译错误

3.instanceof检查无效:

if (list instanceof List<String>)  // 编译错误

4.静态成员共享:

class MyClass<T> {
    static T value;  // 错误,静态成员不能使用类型参数
}

5.桥接方法

桥接方法是Java编译器为了解决类型擦除与多态性冲突而自动生成的一种合成方法,主要用于泛型继承和接口实现场景。

  • 泛型方法重写
class Parent<T> {
    public void set(T t) {}
}

class Child extends Parent<String> {
    @Override
    public void set(String s) {}  // 擦除后签名与父类不同
}

编译器会生成一个桥接方法:

public void set(Object o) {
    set((String) o);  // 调用实际的set(String)方法
}
  • 协变返回类型
class Parent {
    Object get() { return null; }
}

class Child extends Parent {
    @Override
    String get() { return ""; }  // 返回类型不同
}

编译器生成桥接方法:

public Object get() {
    return get();  // 调用String get()方法
}

Java 的泛型中 super 和 extends 有什么区别?

关键字说明

  • ? 通配符类型
  • extends T> 表示类型的上界,表示参数化类型的可能是T 或是 T的子类
  • super T> 表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至Object

换成白话是这个意思:

List<? extends T> 是说 这个list放的是T或者T的子类型的对象,但是不能确定具体是什么类型,所以可以get(),不能add()(可以add null值)

List<? super T> 是说这个list放的是至少是T类型的对象,所以我可以add T或者T的子类型,但是get得到的类型不确定,所以不能get

extends 示例

static class Food{}

static class Fruit extends Food{}

static class Apple extends Fruit{}

static class RedApple extends Apple{}

List<? extends Fruit> flist = new ArrayList<Apple>();
// complie error:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // only work for null 

List<? extends Frut> 表示 “具有任何从Fruit继承类型的列表”,编译器无法确定List所持有的类型,所以无法安全的向其中添加对象。可以添加null,因为null 可以表示任何类型。所以List 的add 方法不能添加任何有意义的元素,但是可以接受现有的子类型List 赋值。

Fruit fruit = flist.get(0);
Apple apple = (Apple)flist.get(0);

由于,其中放置是从Fruit中继承的类型,所以可以安全地取出Fruit类型。

flist.contains(new Fruit());
flist.contains(new Apple());

在使用Collection中的contains 方法时,接受Object 参数类型,可以不涉及任何通配符,编译器也允许这么调用。

super 示例

List<? super Fruit> flist = new ArrayList<Fruit>();
flist.add(new Fruit());
flist.add(new Apple());
flist.add(new RedApple());

// compile error:
List<? super Fruit> flist = new ArrayList<Apple>();

List<? super Fruit> 表示“具有任何Fruit超类型的列表”,列表的类型至少是一个 Fruit 类型,因此可以安全的向其中添加Fruit 及其子类型。由于List<? super Fruit>中的类型可能是任何Fruit 的超类型,无法赋值为Fruit的子类型Apple的List.

// compile error:
Fruit item = flist.get(0);

因为,List<? super Fruit>中的类型可能是任何Fruit 的超类型,所以编译器无法确定get返回的对象类型是Fruit,还是Fruit的父类Food 或 Object.

详解Java中的泛型(Generics)

公众号:玩转安卓Dev

Java基础

面向对象与Java基础知识

Java集合框架

JVM

多线程与并发

设计模式

Kotlin

Android

项目相关问题

Android基础知识

Android消息机制

Android Binder

View事件分发机制

Android屏幕刷新机制

View的绘制流程

Activity启动

Framework

性能优化

Jetpack&系统View

第三方框架实现原理

计算机网络

算法

Clone this wiki locally