CodeGym /课程 /JAVA 25 SELF /泛型入门

泛型入门

JAVA 25 SELF
第 16 级 , 课程 4
可用

1. 引言

设想你有一个万能盒子,可以放任何东西:苹果、书,甚至玩具。在 Java 中,这个“万能盒子”是一个以最通用类型 Object 存储数据的类。该类型是 Java 中所有其他类的父类,因此任何对象都可以放进一个 Object 类型的变量里。

再想象一个堆满这种盒子的仓库。如果盒子上没有标签,你可以往里面放任何东西,但当需要取出某样东西时——你就得打开盒子猜里面是什么。使用泛型就像一个贴有清晰标签的仓库:“只放苹果”、“只放书”、“只放工具”。你总能知道每个盒子里装的是什么,也不会把书误放到装苹果的盒子里。

乍一看,把数据存放在 Object 中似乎很方便:无需为不同数据类型创建单独的类。但在实践中,这种“通用性”会带来问题:

  1. 很容易犯错。 你可能会不小心把并非期望的对象放进盒子。
  2. 编译器看不出问题。 它会允许你放入任何东西,因为 Object 类型本来就允许。
  3. 不得不手动“解包”。 当你从这种盒子里取出对象时,它仍然是 Object 类型,你必须手动把它转换为需要的类型(这称为类型转换cast)。如果你转换成了错误的类型,程序就会直接以错误结束运行!

我们来看一个简单的示例。

class Box {
    private Object value;

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

    public Object get() {
        return value;
    }
}

现在来使用这个盒子:

Box box = new Box();
box.set("Hello"); // 放入了字符串
String s = (String) box.get(); // 取出字符串,一切正常

box.set(123); // 放入了数字
// 编译器看不出问题...
String t = (String) box.get(); // 程序运行时出错!

如你所见,编译器轻易放过了最终导致错误的代码。只有当程序运行并“崩溃”时我们才发现问题。

2. 解决方案——泛型

Generics(泛型)是解决该问题的一种方式。这是一种特殊语法,允许在编译阶段就把类或方法绑定到具体的数据类型。简单说,这就像盒子上的标签,告诉你:“这个盒子里只能放字符串”或“这个盒子里只能放数字”。

因此,我们获得了类型安全:编译器不会允许把错误的对象放进“盒子”。它会在你运行程序之前检查代码并指出错误。

同一个 Box 类,但现在使用了泛型:

class Box<Type> {
    private Type value;

    public void set(Type value) {
        this.value = value;
    }

    public Type get() {
        return value;
    }
}

这里的 Type类型参数。它是我们自己选择的一个占位名称(通常使用 TEKV),表示:当我们创建 Box 时,会指定它要处理的具体类型,而我会在代码中所有写成 Type 的地方使用该类型。

3. 使用 Generics

当我们创建带泛型的对象时,需要在尖括号 <...> 中指定具体类型。

// 创建了一个只处理字符串的盒子
Box<String> stringBox = new Box<>();
stringBox.set("Hello,世界!"); // OK,放入了字符串
String s = stringBox.get(); // 取出字符串,无需类型转换

stringBox.set(123); // 编译错误!编译器不会允许这样做。

如果我们创建 Box<Integer>,编译器会确保只能往里放数字:

// 创建了一个只处理数字的盒子
Box<Integer> intBox = new Box<>();
intBox.set(42); // OK,放入了数字
Integer number = intBox.get(); // 取出数字,无需类型转换

intBox.set("Hello"); // 编译错误!

现在编译器确切地知道每个盒子里应该是什么类型的数据,并能可靠地保护我们免于出错。

4. 通过示例看其工作方式

泛型不仅可以用于类,还可以用于方法。这能让你编写非常灵活且通用的代码。

示例 1——带泛型的类

假设我们需要一个 Pair 类,它能保存两个相同类型的对象。用泛型可以这样写:

class Pair<T> {
    private T first;
    private T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }
}

用法:

Pair<String> greetings = new Pair<String>("Hello", "世界");
System.out.println(greetings.getFirst() + " " + greetings.getSecond());

Pair<Integer> numbers = new Pair<Integer>(10, 20);
System.out.println(numbers.getFirst() + numbers.getSecond());

在第一种情况下,编译器确信 greetings 中保存的是字符串;在第二种情况下——是数字。

示例 2——泛型方法

我们可以编写一个可用于任何数据类型的方法。

class Utils {
    // <T> 在 void 前表示该方法将使用类型参数 T
    public static <T> void printTwice(T value) {
        System.out.println(value);
        System.out.println(value);
    }
}

现在我们可以用任何数据类型来调用该方法,它都会以相同方式工作:

Utils.printTwice("Java");
Utils.printTwice(123);
Utils.printTwice(3.14);

5. 泛型的优势

泛型不仅仅是语法糖,而是能解决实际开发问题的强大工具。我们来看三个关键优势。

类型安全是泛型的首要优势。如果没有泛型,编译器无法检查你是否在“通用”类和方法中正确地使用了数据类型。诸如把字符串放进“数字盒子”之类的错误,只有在程序运行时才会暴露,程序会突然以异常 ClassCastException “崩溃”。使用泛型后,编译器会严格检查类型。它会确保你在 Box<String> 中只放字符串,而在 Box<Integer> 中只放数字。如果你尝试做错,编译器会立即指出错误,使你在运行前就能修复。这让代码更加可靠、可预测。

代码更简洁(无需多余的类型转换)。回想一下我们没有泛型的“万能”盒子:Box box = new Box(); box.set("Hello"); String s = (String) box.get();。每次从盒子里取出对象,你都得写 (String)(Integer) 等等。有了泛型,这种需要就消失了。编译器已经知道内部是什么类型,并会自动为你处理:Box<String> stringBox = new Box<>(); stringBox.set("Hello"); String s = stringBox.get();。这不仅让代码更短,也显著提升了可读性与易用性。

灵活性与代码复用。泛型允许创建真正通用的类和方法,它们能在不丢失类型安全的前提下与不同数据类型配合。例如,Box<T> 可以用来存储字符串、数字、你自己的类(StudentCar)——几乎任何东西!你无需分别编写 StringBoxIntegerBoxStudentBox。只需实现一个通用的 Box<T>,在创建对象时指定所需类型即可。这能减少代码量,避免重复,使程序更加模块化、更灵活。

6. 泛型的限制

尽管有诸多优势,泛型仍有一些必须了解的重要限制。

基本类型(primitives)。你不能把基本类型(例如 intdoubleboolean 等)用作类型参数。例如,代码 Box<int> intBox = new Box<>(); 会导致编译错误。必须改用它们的“包装类”(IntegerDoubleBoolean)。Box<Integer> intBox = new Box<>();。Java 编译器可以在基本类型与包装类之间自动转换(intInteger 互转)——该机制称为自动装箱/拆箱(autoboxing/unboxing)

类型擦除(Type Erasure)。这是 Java 中泛型实现方式的关键特性。其思想是:Java 编译器只在编译期间使用泛型信息。在检查并确认你的代码类型安全后,它会擦除所有类型参数信息。例如,Box<String>Box<Integer> 在已编译代码(字节码)中会呈现为简单的 Box。这意味着对于 Java 虚拟机(JVM)来说,它们成为同一个类型。

这对你意味着什么?你不能创建泛型数组,例如 new Box<String>[10],也不能把 instanceof 与泛型一起使用,去检查某个对象是否是 instanceof Box<String>。这是一个更复杂的话题,但理解它对于深入学习 Java 很重要。此时只需记住:泛型本质上是编译器用来检查代码的“标签”,当程序准备运行时,这个标签会被拿掉。

1
任务
JAVA 25 SELF, 第 16 级, 课程 4
已锁定
旧仓库:通用但无类型信息的箱子 📦
旧仓库:通用但无类型信息的箱子 📦
1
任务
JAVA 25 SELF, 第 16 级, 课程 4
已锁定
现代仓库:贴有精确标签的“智能”箱子 🏷️
现代仓库:贴有精确标签的“智能”箱子 🏷️
1
任务
JAVA 25 SELF, 第 16 级, 课程 4
已锁定
关系管理系统:通用数据对 🖇️
关系管理系统:通用数据对 🖇️
1
任务
JAVA 25 SELF, 第 16 级, 课程 4
已锁定
开发者工具:通用回显 💬
开发者工具:通用回显 💬
1
调查/小测验
嵌套类和内部类第 16 级,课程 4
不可用
嵌套类和内部类
嵌套类和内部类
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION