这是一篇面向初学者的原创学习笔记。很多人第一次接触泛型,只知道在集合里写 <String><Integer>,但并不真正明白泛型到底解决了什么问题。本文会从“为什么需要泛型”开始,逐步讲清泛型类、泛型方法、泛型接口、通配符以及 extendssuper 的区别。


一、什么是泛型?

在 Java 里,泛型可以理解成:把类型也当成参数来传递。

以前我们给方法传参数,传的是具体的值;而泛型做的事情,是让类、接口、方法在设计阶段先不把类型写死,而是在使用的时候再决定具体类型。

比如下面这行代码:

ArrayList<String> list = new ArrayList<>();

这里的 String 就是传给集合的“类型参数”。

这意味着:

  • 这个集合里准备存放 String
  • 往里放别的类型会报错
  • 取出来的数据也会自动当成 String 处理

所以泛型不是语法装饰,而是一种类型约束机制


二、为什么要有泛型?

如果没有泛型,很多代码虽然也能写,但会有两个很明显的问题:

  • 类型不安全
  • 取数据时需要强制类型转换

先看一个不用泛型的例子:

import java.util.ArrayList;

public class Test {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("Java");
        list.add(100);

        String s1 = (String) list.get(0);
        String s2 = (String) list.get(1); // 运行时报错

        System.out.println(s1);
        System.out.println(s2);
    }
}

问题在哪?

1. 什么类型都能往里放

因为没有指定集合中元素的类型,所以字符串、整数、对象都能放进去。

2. 取出来必须强转

String s1 = (String) list.get(0);

这种写法很麻烦,而且容易出错。

3. 错误延迟到运行期才暴露

真正危险的是:代码编译时不报错,但运行到这里时才抛出异常。

这类错误排查起来往往比编译错误更麻烦。


三、使用泛型之后有什么变化?

看同样的例子,改成泛型写法:

import java.util.ArrayList;

public class Test {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Java");
        // list.add(100); // 编译报错

        String s = list.get(0);
        System.out.println(s);
    }
}

这时好处很明显:

  • 集合只能存 String
  • 不需要强制类型转换
  • 类型错误在编译阶段就能发现

所以泛型最大的价值可以总结成一句话:

把原本运行时才会暴露的类型问题,尽量提前到编译时解决。


四、泛型的本质怎么理解?

泛型的本质不是“高级语法”,而是“预留类型位置”。

比如你写一个箱子类:

class Box<T> {
    private T value;

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

    public T getValue() {
        return value;
    }
}

这里的 T 可以理解为“暂时还不知道的类型”。

等到真正使用时,再告诉它到底是什么:

Box<String> box1 = new Box<>();
Box<Integer> box2 = new Box<>();

这就表示:

  • box1 中的 T 被替换成 String
  • box2 中的 T 被替换成 Integer

所以你完全可以把泛型理解成:

写代码时先把类型空出来,等使用时再填进去。


五、泛型中的常见字母含义

泛型参数常常写成单个大写字母,这些字母本身没有强制要求,但通常有约定俗成的命名习惯:

  • T:Type,类型
  • E:Element,元素
  • K:Key,键
  • V:Value,值

例如:

class Box<T> {
}

interface List<E> {
}

interface Map<K, V> {
}

这些只是命名习惯,不是语法要求。


六、泛型类

1. 定义泛型类

泛型类就是在类定义时,把类型参数也一起定义出来。

class Box<T> {
    private T data;

    public void setData(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

2. 使用泛型类

public class Test {
    public static void main(String[] args) {
        Box<String> box1 = new Box<>();
        box1.setData("你好,泛型");
        System.out.println(box1.getData());

        Box<Integer> box2 = new Box<>();
        box2.setData(200);
        System.out.println(box2.getData());
    }
}

输出:

你好,泛型
200

3. 泛型类的意义

同一个类,不需要重复写多个版本:

  • StringBox
  • IntegerBox
  • DoubleBox

只要写一个 Box<T>,就能适配多种数据类型。

这体现的就是代码复用能力。


七、泛型方法

有时候,不是整个类都需要泛型,而只是某个方法需要更灵活的类型处理。

这时候就可以使用泛型方法。

1. 定义方式

class Util {
    public <T> void printValue(T value) {
        System.out.println(value);
    }
}

这里要特别注意:

public <T> void printValue(T value)

<T> 一定写在返回值类型前面,这表示这个方法自己声明了一个泛型参数。

2. 使用示例

public class Test {
    public static void main(String[] args) {
        Util util = new Util();
        util.printValue("hello");
        util.printValue(123);
        util.printValue(true);
    }
}

输出:

hello
123
true

这说明这个方法能够接收多种类型的数据。

3. 带返回值的泛型方法

class Util {
    public <T> T getData(T value) {
        return value;
    }
}

调用:

Util util = new Util();
String s = util.getData("Java");
Integer n = util.getData(10);

八、泛型接口

接口同样可以使用泛型。

1. 定义泛型接口

interface Data<T> {
    void add(T t);
    T get();
}

2. 实现方式一:实现类直接指定类型

class StringData implements Data<String> {
    private String data;

    @Override
    public void add(String t) {
        this.data = t;
    }

    @Override
    public String get() {
        return data;
    }
}

这种写法表示:这个实现类只处理 String 类型。

3. 实现方式二:实现类继续保留泛型

class DataImpl<T> implements Data<T> {
    private T data;

    @Override
    public void add(T t) {
        this.data = t;
    }

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

这种写法表示:实现类本身也保持通用,使用时再决定具体类型。


九、泛型在集合中的典型使用

泛型最常见的落地场景,就是集合框架。

1. List

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

public class Test {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Python");

        for (String s : list) {
            System.out.println(s);
        }
    }
}

2. Set

import java.util.HashSet;
import java.util.Set;

public class Test {
    public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        set.add(10);
        set.add(20);
        System.out.println(set);
    }
}

3. Map

import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("语文", 95);
        map.put("数学", 100);

        for (String key : map.keySet()) {
            System.out.println(key + "=" + map.get(key));
        }
    }
}

这里:

  • String 是键的类型
  • Integer 是值的类型

十、泛型和 Object 有什么区别?

很多同学会想:

“泛型不就是为了接收不同类型吗?那我直接用 Object 不也一样吗?”

表面上看有点像,但本质完全不同。

1. Object 是“统一接收”

class Box {
    private Object value;

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

    public Object getValue() {
        return value;
    }
}

这种写法确实什么都能存,但取出来时必须强制类型转换。

String s = (String) box.getValue();

2. 泛型是“编译期约束”

class Box<T> {
    private T value;

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

    public T getValue() {
        return value;
    }
}

这种写法不仅更通用,而且更安全。

所以:

  • Object 是宽泛接收,缺点是取出时不安全
  • 泛型是在编译期就把类型限定好

十一、通配符 ? 是什么?

通配符是泛型里一个很重要的知识点。

? 表示:未知类型

也就是说,我知道这里有个泛型,但我暂时不确定它到底是什么具体类型。


十二、无界通配符 <?>

1. 基本含义

<?> 表示任意类型。

public void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

这个方法既能接收 List<String>,也能接收 List<Integer>

printList(new ArrayList<String>());
printList(new ArrayList<Integer>());

2. 为什么不能随便添加元素?

List<?> list = new ArrayList<String>();
// list.add("abc"); // 编译错误
list.add(null);      // 可以

因为你只知道这是“某种类型的集合”,但不知道它具体是什么类型,所以不能安全地往里面放任意对象。


十三、上界通配符 <? extends T>

<? extends T> 表示:

泛型类型必须是 TT 的子类。

例如:

public void showNumbers(List<? extends Number> list) {
    for (Number n : list) {
        System.out.println(n);
    }
}

这个方法可以接收:

  • List<Integer>
  • List<Double>
  • List<Float>

因为这些类型都属于 Number 的子类。

适合理解为“可读”

因为不管具体是 Integer 还是 Double,取出来时都可以按 Number 来接收:

Number n = list.get(0);

但一般不能安全添加元素:

// list.add(10); // 编译错误

因为编译器不知道它到底是 List<Integer> 还是 List<Double>


十四、下界通配符 <? super T>

<? super T> 表示:

泛型类型必须是 TT 的父类。

例如:

public void addIntegers(List<? super Integer> list) {
    list.add(10);
    list.add(20);
}

这个方法可以接收:

  • List<Integer>
  • List<Number>
  • List<Object>

适合理解为“可写”

因为无论这个集合具体是 IntegerNumber 还是 Object,往里面放 Integer 一定是安全的。

但读取时只能按 Object 看待:

Object obj = list.get(0);

因为你没法确定它实际声明成了哪个父类类型。


十五、怎么记 extends 和 super?

这是泛型里最经典、最容易混淆的一部分。

你可以记住下面这句口诀:

PECS:Producer Extends,Consumer Super

意思是:

  • 如果一个参数主要是“提供数据给你读取”,用 extends
  • 如果一个参数主要是“接收你写进去的数据”,用 super

换成更好理解的话就是:

  • extends:偏读取
  • super:偏写入

十六、泛型擦除是什么?

Java 泛型还有一个必须知道的概念,叫做类型擦除

意思是:

泛型主要在编译阶段起作用,到了运行阶段,具体的泛型类型信息会被擦除。

看例子:

ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();

System.out.println(list1.getClass() == list2.getClass());

输出是:

true

这说明在运行时,它们本质上还是同一个 ArrayList 类。

所以泛型并不是运行时真正保留了完整的类型信息,而是主要帮助编译器做类型检查。


十七、泛型的注意事项

1. 泛型不能使用基本数据类型

错误写法:

ArrayList<int> list = new ArrayList<>();

正确写法:

ArrayList<Integer> list = new ArrayList<>();

因为泛型只能使用引用类型。

所以要学会基本类型和包装类的对应关系:

  • int -> Integer
  • double -> Double
  • char -> Character

2. 静态方法中如果要使用泛型,必须单独声明

错误写法:

class Demo<T> {
    public static void show(T t) {
    }
}

正确写法:

class Demo {
    public static <T> void show(T t) {
        System.out.println(t);
    }
}

3. 泛型不支持直接创建泛型数组

例如下面这种写法通常不允许:

T[] arr = new T[10];

4. List<Object> 不能接收 List<String>

错误写法:

List<Object> list = new ArrayList<String>();

这是很多初学者容易误解的地方。

Java 中泛型之间默认没有这样的继承关系。

如果你想接收任意泛型类型,应该使用:

List<?> list = new ArrayList<String>();

十八、泛型的核心价值总结

如果要把整篇内容浓缩成最重要的几句话,那就是:

1. 泛型的本质

把类型参数化。

2. 泛型的最大作用

提高类型安全,减少强制类型转换。

3. 泛型最常见的场景

集合框架。

4. 通配符的核心理解

  • <?>:任意类型
  • <? extends T>:上界,偏读
  • <? super T>:下界,偏写

十九、最后总结

泛型是 Java 中非常重要的“进阶基础”。

它表面上看只是尖括号里的一个类型,实际上背后体现的是 Java 对“类型安全”和“代码复用”的支持。

你可以把它记成下面这几句话:

  • 泛型不是装饰,而是约束
  • 泛型不是为了写起来花哨,而是为了让代码更安全
  • 泛型最重要的价值,是把类型错误提前到编译阶段

当你真正理解泛型后,后面再学集合、Stream、Lambda、框架源码时,会轻松很多。


二十、练习题

练习 1

定义一个泛型类 Container<T>,包含一个属性 data,并提供 setData()getData() 方法。

练习 2

定义一个泛型方法,传入任意类型参数并输出。

练习 3

定义一个泛型接口 Message<T>,再写一个实现类来实现它。

练习 4

分别解释下面三种写法的含义:

List<?>
List<? extends Number>
List<? super Integer>

练习 5

思考:为什么 List<Object> 不能直接接收 List<String>

更多推荐