你好!我们将讨论 Java 泛型。我必须说你会学到很多东西!不仅这节课,而且接下来的几节课,都将专门讨论泛型。所以,如果您对泛型感兴趣,那么今天是您的幸运日:您将学到很多关于泛型特性的知识。如果没有,请辞职并放松!:) 这是一个非常重要的主题,您需要了解它。让我们从简单的开始:“什么”和“为什么”。
祝你学业成功!:)
什么是 Java 泛型?
泛型是具有参数的类型。创建泛型类型时,您不仅要指定类型,还要指定它将使用的数据类型。我猜你已经想到了最明显的例子:ArrayList!这就是我们通常在程序中创建一个的方式:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> myList1 = new ArrayList<>();
myList1.add("Test String 1");
myList1.add("Test String 2");
}
}
正如您可能猜到的那样,这个列表的一个特点是我们不能把所有东西都塞进去:它只适用于String对象。现在让我们稍微偏离一下 Java 的历史并尝试回答“为什么?”这个问题。为此,我们将编写自己的 ArrayList 类的简化版本。我们的列表只知道如何向内部数组添加数据和从中检索数据:
public class MyListClass {
private Object[] data;
private int count;
public MyListClass() {
this.data = new Object[10];
this.count = 0;
}
public void add(Object o) {
this.data[count] = o;
count++;
}
public Object[] getData() {
return data;
}
}
假设我们希望我们的列表只存储Integer s。我们没有使用通用类型。我们不想在add()方法中包含显式的“instanceof Integer ”检查。如果这样做,我们的整个类将只适用于Integer,我们将不得不为世界上所有其他数据类型编写一个类似的类!我们将依赖我们的程序员,并在代码中留下注释以确保他们不会添加我们不想要的任何内容:
// Use this class ONLY with the Integer data type
public void add(Object o) {
this.data[count] = o;
count++;
}
一位程序员错过了这条评论,无意中将几个字符串放入数字列表中,然后计算它们的总和:
public class Main {
public static void main(String[] args) {
MyListClass list = new MyListClass();
list.add(100);
list.add(200);
list.add("Lolkek");
list.add("Shalala");
Integer sum1 = (Integer) list.getData()[0] + (Integer) list.getData()[1];
System.out.println(sum1);
Integer sum2 = (Integer) list.getData()[2] + (Integer) list.getData()[3];
System.out.println(sum2);
}
}
控制台输出:
300
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at Main.main (Main.java:14)
这种情况最糟糕的部分是什么?当然不是程序员的粗心。最糟糕的是,不正确的代码最终出现在我们程序的重要位置并成功编译。现在我们不会在编写代码时遇到错误,而只会在测试期间遇到错误(这是最好的情况!)。在开发的后期阶段修复错误的成本要高得多——无论是金钱还是时间。这正是泛型给我们带来好处的地方:泛型类可以让不幸的程序员立即检测到错误。该程序根本无法编译!
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> myList1 = new ArrayList<>();
myList1.add(100);
myList1.add(100);
myList1.add ("Lolkek"); // Error!
myList1.add("Shalala"); // Error!
}
}
程序员立即意识到他或她的错误并立即变得更好。顺便说一句,我们不必创建自己的List类来查看此类错误。只需从普通 ArrayList 中 删除尖括号和类型 ( <Integer> )!
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List list = new ArrayList();
list.add(100);
list.add(200);
list.add("Lolkek");
list.add("Shalala");
System.out.println((Integer) list.get(0) + (Integer) list.get(1));
System.out.println((Integer) list.get(2) + (Integer) list.get(3));
}
}
控制台输出:
300
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at Main.main(Main.java:16)
换句话说,即使使用 Java 的“本机”机制,我们也可能会犯这种错误并创建不安全的集合。但是,如果我们将此代码粘贴到 IDE 中,我们会收到警告:“未经检查的调用 add(E) 作为 java.util.List 原始类型的成员”我们被告知添加项目时可能会出错到缺少通用类型的集合。但是“原始类型”这个短语是什么意思呢?原始类型是其类型已被删除的泛型类。换句话说,List myList1是原始类型。与原始类型相反的是泛型类型——带有参数化类型指示的泛型类。例如,List<String> myList1. 您可能会问为什么该语言允许使用原始类型?原因很简单。Java 的创建者在语言中保留了对原始类型的支持,以避免产生兼容性问题。到 Java 5.0 发布时(泛型首次出现在这个版本中),已经有很多代码是使用原始类型编写的。因此,该机制至今仍受支持。我们在课程中多次提到了Joshua Bloch的经典著作《Effective Java》。作为该语言的创造者之一,他在书中 并没有跳过原始类型和泛型类型。该书的第 23 章有一个非常雄辩的标题:“不要在新代码中使用原始类型”,这是您需要记住的。使用泛型类时,切勿将泛型类型转换为原始类型。
通用方法
Java 允许您通过创建所谓的泛型方法来参数化各个方法。这些方法有何帮助?最重要的是,它们的帮助在于它们让您可以使用不同类型的方法参数。如果可以将相同的逻辑安全地应用于不同的类型,那么泛型方法可能是一个很好的解决方案。考虑这是一个非常简单的例子:假设我们有一个名为myList1的列表。我们想从列表中删除所有值并用新值填充所有空白区域。这是我们的类使用泛型方法的样子:
public class TestClass {
public static <T> void fill(List<T> list, T val) {
for (int i = 0; i < list.size(); i++)
list.set(i, val);
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("Old String 1");
strings.add("Old String 2");
strings.add("Old String 3");
fill(strings, "New String");
System.out.println(strings);
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
fill(numbers, 888);
System.out.println(numbers);
}
}
注意语法。它看起来有点不寻常:
public static <T> void fill(List<T> list, T val)
我们在返回类型之前写上<T>。这表明我们正在处理一个泛型方法。在这种情况下,该方法接受 2 个参数作为输入:T 对象列表和另一个单独的 T 对象。通过使用 <T>,我们参数化了方法的参数类型:我们不能传入一个字符串列表和一个整数。一个字符串列表和一个字符串、一个整数列表和一个整数列表、一个我们自己的Cat对象列表和另一个Cat对象——这就是我们需要做的。main ()方法说明了如何使用fill()方法轻松处理不同类型的数据。首先,我们使用带有字符串列表和字符串作为输入的方法,然后使用整数列表和整数。控制台输出:
[New String, New String, New String] [888, 888, 888]
想象一下,如果我们没有泛型方法并且需要30 个不同类的fill()方法的逻辑。我们将不得不为不同的数据类型编写相同的方法 30 次!但是多亏了泛型方法,我们可以重用我们的代码!:)
通用类
您不限于标准 Java 库中提供的通用类 — 您可以创建自己的类!这是一个简单的例子:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.set("Old String");
System.out.println(stringBox.get());
stringBox.set("New String");
System.out.println(stringBox.get());
stringBox.set(12345); // Compilation error!
}
}
我们的Box<T>类是一个泛型类。一旦我们在创建过程中分配了一个数据类型(<T>),我们就不能再在其中放置其他类型的对象。这可以在示例中看到。在创建我们的对象时,我们指出它可以与字符串一起使用:
Box<String> stringBox = new Box<>();
在最后一行代码中,当我们尝试将数字 12345 放入框中时,出现编译错误!就这么简单!我们已经创建了自己的通用类!:) 至此,今天的课程就结束了。但我们并没有与泛型说再见!在接下来的课程中,我们将讨论更多高级功能,所以不要走开!) 为了巩固您所学的知识,我们建议您观看我们的 Java 课程中的视频课程
GO TO FULL VERSION