前言

大家好,我是程序员梁白开,今天我们聊一聊Java泛型。

在 Java 开发中,你是否经常被ClassCastException困扰?是否厌倦了代码里随处可见的(String)、(Integer)强制类型转换?如果答案是肯定的,那你一定需要深入掌握泛型这个核心特性。泛型不仅能让代码更简洁、更安全,更是进阶 Java 开发的必经之路。今天,我们就从原理到实战,彻底吃透 Java 泛型!


一、什么是泛型?为什么需要泛型?

1. 泛型的定义

泛型(Generic)是 JDK 5 引入的特性,全称参数化类型。它允许我们在定义类、接口和方法时,通过一个类型参数来表示未知类型,在使用时再指定具体的类型。

简单来说:泛型就是将类型由原来的具体类型,变成一个可以动态指定的参数

2. 没有泛型的日子,有多 “痛苦”?

在泛型出现之前,Java 集合类只能存储Object类型的对象。如果我们想存储String类型,需要手动装箱;取出时,又要强制类型转换。

import java.util.ArrayList;
import java.util.List;

public class NoGenericDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        // 存储字符串
        list.add("Java泛型");
        list.add("CSDN博客");
        // 不小心存入一个整数(编译不会报错)
        list.add(100);

        // 取出元素时强制转换,运行时会报错
        for (Object obj : list) {
            String str = (String) obj; // 此处抛出ClassCastException
            System.out.println(str.length());
        }
    }
}

问题分析:

  • 编译期无法检查类型错误,只有运行时才会抛出ClassCastException,风险极高。
  • 每次取出元素都要强制类型转换,代码冗余且易出错。

3. 泛型登场:解决痛点

使用泛型后,我们可以在定义集合时指定元素类型,编译期就能检查类型合法性,彻底告别强制转换!

import java.util.ArrayList;
import java.util.List;

public class GenericDemo {
    public static void main(String[] args) {
        // 指定List只能存储String类型
        List<String> list = new ArrayList<>();
        list.add("Java泛型");
        list.add("CSDN博客");
        // list.add(100); // 编译直接报错,无法添加非String类型

        // 取出元素无需强制转换
        for (String str : list) {
            System.out.println(str.length());
        }
    }
}

泛型的核心优势:

  1. 类型安全:编译期检查类型,避免运行时类型转换异常。
  2. 代码简洁:消除冗余的强制类型转换代码。
  3. 代码复用:一套泛型代码可以适配多种类型,提升代码灵活性。

二、泛型的核心语法

泛型的使用场景主要分为三类:泛型类 / 接口、泛型方法、泛型通配符

1. 泛型类与泛型接口

(1)定义语法
泛型类的定义格式:

public class 类名<T> {
    // 成员变量可以使用泛型类型
    private T data;
    
    // 构造方法可以使用泛型类型
    public 类名(T data) {
        this.data = data;
    }
    
    // 成员方法可以使用泛型类型
    public T getData() {
        return data;
    }
}

其中,是类型参数,T是类型变量,通常使用大写字母表示,常见的有:

  • T:Type(类型)
  • E:Element(元素,常用于集合)
  • K:Key(键,常用于 Map)
  • V:Value(值,常用于 Map)

(2)实战案例:自定义泛型容器

// 泛型类:支持存储任意类型的数据
public class GenericContainer<T> {
    private T value;

    public GenericContainer(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    public static void main(String[] args) {
        // 存储String类型
        GenericContainer<String> strContainer = new GenericContainer<>("Hello Generic");
        System.out.println(strContainer.getValue());

        // 存储Integer类型
        GenericContainer<Integer> intContainer = new GenericContainer<>(2024);
        System.out.println(intContainer.getValue());
    }
}

(3)泛型接口
泛型接口的定义与泛型类类似,实现类可以指定具体类型,也可以继续使用泛型。

// 泛型接口
public interface GenericInterface<T> {
    T getResult();
}

// 实现类指定具体类型:String
public class StringInterfaceImpl implements GenericInterface<String> {
    @Override
    public String getResult() {
        return "实现泛型接口,指定类型为String";
    }
}

// 实现类继续使用泛型
public class GenericInterfaceImpl<T> implements GenericInterface<T> {
    private T data;

    public GenericInterfaceImpl(T data) {
        this.data = data;
    }

    @Override
    public T getResult() {
        return data;
    }
}

2. 泛型方法

泛型方法是指方法本身带有类型参数,它可以定义在普通类中,也可以定义在泛型类中。
(1)定义语法

// 泛型方法的语法:类型参数放在修饰符和返回值之间
public <T> T 方法名(T 参数) {
    // 方法体
    return 参数;
}

(2)实战案例:通用打印方法

public class GenericMethodDemo {
    // 泛型方法:打印任意类型的数组
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        // 打印字符串数组
        String[] strArray = {"Java", "Python", "Go"};
        printArray(strArray);

        // 打印整数数组
        Integer[] intArray = {1, 2, 3, 4, 5};
        printArray(intArray);

        // 打印布尔数组
        Boolean[] boolArray = {true, false, true};
        printArray(boolArray);
    }
}

注意:泛型方法的类型参数必须放在static等修饰符之后,返回值之前。

3. 泛型通配符:?的妙用

在使用泛型时,我们经常会遇到类型不确定的场景,此时可以使用泛型通配符?来表示任意类型。通配符主要分为三类:

  • 无界通配符:<?>
  • 上界通配符:<? extends 父类>
  • 下界通配符:<? super 子类>

(1)无界通配符 <?>
表示可以接收任意类型的泛型实例,通常用于读取操作。

import java.util.ArrayList;
import java.util.List;

public class WildcardDemo {
    // 无界通配符:打印任意类型的List
    public static void printList(List<?> list) {
        for (Object obj : list) {
            System.out.print(obj + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        strList.add("A");
        strList.add("B");
        printList(strList);

        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
        printList(intList);
    }
}

(2)上界通配符 <? extends T>
表示只能接收T 类型或 T 的子类的泛型实例,遵循 “只能读,不能写” 原则(因为无法确定具体的子类类型)。

import java.util.ArrayList;
import java.util.List;

// 父类
class Fruit {}
// 子类
class Apple extends Fruit {}
class Banana extends Fruit {}

public class UpperBoundWildcard {
    // 上界通配符:只能接收Fruit及其子类的List
    public static void printFruit(List<? extends Fruit> list) {
        for (Fruit fruit : list) {
            System.out.println(fruit.getClass().getSimpleName());
        }
    }

    public static void main(String[] args) {
        List<Apple> appleList = new ArrayList<>();
        appleList.add(new Apple());
        printFruit(appleList); // 合法

        List<Banana> bananaList = new ArrayList<>();
        bananaList.add(new Banana());
        printFruit(bananaList); // 合法

        // List<String> strList = new ArrayList<>();
        // printFruit(strList); // 编译报错:String不是Fruit的子类
    }
}

(3)下界通配符 <? super T>
表示只能接收T 类型或 T 的父类的泛型实例,遵循 “只能写,不能读” 原则(读取时只能向上转型为 Object)。

import java.util.ArrayList;
import java.util.List;

public class LowerBoundWildcard {
    // 下界通配符:只能接收Apple及其父类的List
    public static void addApple(List<? super Apple> list) {
        list.add(new Apple()); // 可以添加Apple对象
    }

    public static void main(String[] args) {
        List<Fruit> fruitList = new ArrayList<>();
        addApple(fruitList); // 合法:Fruit是Apple的父类

        List<Apple> appleList = new ArrayList<>();
        addApple(appleList); // 合法

        // List<Banana> bananaList = new ArrayList<>();
        // addApple(bananaList); // 编译报错:Banana不是Apple的父类
    }
}

三、泛型的 “坑”:类型擦除

这是泛型最容易被忽略的核心知识点!Java 的泛型是编译期特性,在运行时会进行类型擦除—— 即 JVM 在运行时并不知晓泛型类型的存在,所有的泛型信息都会被擦除。

1. 类型擦除的原理

泛型的类型擦除规则:

  • 对于无界泛型(如List<?>),类型参数会被擦除为Object。
  • 对于有界泛型(如List<? extends Fruit>),类型参数会被擦除为上界类型(Fruit)。

示例代码:

public class TypeErasureDemo {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();

        // 类型擦除后,strList和intList的实际类型都是ArrayList
        System.out.println(strList.getClass() == intList.getClass()); // 输出 true
    }
}

结论:泛型在运行时不存在,List和List在 JVM 中都是ArrayList类型。

2. 类型擦除带来的限制

由于类型擦除的存在,泛型有以下几个常见限制:

  1. 不能使用基本数据类型作为类型参数
  2. 不能创建泛型数组
  3. 不能在静态方法中使用泛型类的类型参数
  4. 不能使用instanceof判断泛型类型

四、泛型的实战应用场景

  1. 集合框架:这是泛型最常用的场景,List、Map<K,V>等都是泛型的典型应用。
  2. 自定义工具类:例如通用的Result返回结果类,适配不同的数据类型。`
  3. DAO 层通用接口:例如BaseDao,实现对任意实体类的增删改查,提升代码复用性。

五、总结

泛型是 Java 中提升代码安全性和简洁性的核心特性,掌握它的关键在于:

  1. 理解泛型的本质是参数化类型,核心作用是编译期类型检查。
  2. 区分泛型类、泛型方法、泛型通配符的使用场景,尤其是上界和下界通配符的区别。
  3. 牢记类型擦除的原理,避开泛型的常见陷阱。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐