你好!我们继续我们关于泛型的系列课程。我们之前对它们是什么以及为什么需要它们有一个大概的了解。今天我们将更多地了解泛型的一些特性以及如何使用它们。我们走吧! 在上一课中,我们讨论了泛型类型和原始类型之间的区别。原始类型是其类型已被删除的泛型类。
文档说,“T - 此 Class 对象建模的类的类型。” 将其从文档语言翻译成普通话,我们了解到对象的类别
List list = new ArrayList();
这是一个例子。这里我们不指明将放置什么类型的对象List
。如果我们尝试创建这样的 aList
并向其中添加一些对象,我们将在 IDEA 中看到警告:
"Unchecked call to add(E) as a member of raw type of java.util.List".
但我们也谈到了泛型只出现在 Java 5 中的事实。到这个版本发布时,程序员已经使用原始类型编写了一堆代码,所以语言的这个特性不能停止工作,并且能够在 Java 中创建原始类型被保留。然而,事实证明这个问题更为普遍。如您所知,Java 代码被转换为一种称为字节码的特殊编译格式,然后由 Java 虚拟机执行。但是如果我们在转换的过程中把类型参数的信息放到字节码中,就会把之前写的代码全部打断,因为Java 5之前是没有类型参数的!使用泛型时,您需要记住一个非常重要的概念。它被称为类型擦除. 这意味着类不包含有关类型参数的信息。此信息仅在编译期间可用,并在运行前被擦除(变得不可访问)。如果您尝试将错误类型的对象放入您的 中List<String>
,编译器将生成错误。这正是该语言的创建者在创建泛型时想要实现的目标:编译时检查。但是当你所有的 Java 代码都变成字节码时,它就不再包含类型参数的信息了。在字节码中,您的List<Cat>
猫列表与字符串没有什么不同List<String>
。在字节码中,没有什么说这cats
是一个对象列表Cat
。此类信息在编译期间会被删除——只有您拥有一个列表这一事实List<Object> cats
才会最终出现在程序的字节码中。让我们看看这是如何工作的:
public class TestClass<T> {
private T value1;
private T value2;
public void printValues() {
System.out.println(value1);
System.out.println(value2);
}
public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
TestClass<T> result = new TestClass<>();
result.value1 = (T) o1;
result.value2 = (T) o2;
return result;
}
public static void main(String[] args) {
Double d = 22.111;
String s = "Test String";
TestClass<Integer> test = createAndAdd2Values(d, s);
test.printValues();
}
}
我们创建了自己的通用TestClass
类。它非常简单:它实际上是 2 个对象的一个小“集合”,在创建对象时立即存储这些对象。它有 2 个T
字段。方法执行的时候createAndAdd2Values()
,传入的两个对象( Object a
andObject b
必须强制转换为T
类型,然后添加到TestClass
对象中。在main()
方法中,我们创建了一个TestClass<Integer>
,即Integer
类型参数替换了Integer
类型参数。我们也是将aDouble
和a传递String
给方法createAndAdd2Values()
。你认为我们的程序会运行吗?毕竟,我们指定Integer
为类型参数,但String
绝对不能将 a 转换为 an Integer
!让我们运行main()
方法和检查。控制台输出:
22.111
Test String
这是出乎意料的!为什么会这样? 这是类型擦除的结果。编译代码时,有关Integer
用于实例化对象的类型参数的 信息已被删除。TestClass<Integer> test
该领域成为TestClass<Object> test
。我们的Double
和String
参数很容易转换为Object
对象(它们没有Integer
像我们预期的那样转换为对象!)并悄悄地添加到TestClass
. 这是类型擦除的另一个简单但非常有启发性的示例:
import java.util.ArrayList;
import java.util.List;
public class Main {
private class Cat {
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(numbers.getClass() == cats.getClass());
}
}
控制台输出:
true
true
看起来我们创建了具有三种不同类型参数的集合—— String
、Integer
和我们自己的Cat
类。但是在转换为字节码的过程中,所有三个列表都变成了List<Object>
,所以当程序运行时它告诉我们在所有三种情况下我们都使用了同一个类。
使用数组和泛型时类型擦除
List
在使用数组和泛型类(例如)时,必须清楚地理解一个非常重要的点。在为您的程序选择数据结构时,您还应该考虑到这一点。泛型受类型擦除的影响。有关类型参数的信息在运行时不可用。相比之下,数组在程序运行时知道并可以使用有关其数据类型的信息。尝试将无效类型放入数组将导致抛出异常:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
控制台输出:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
因为数组和泛型之间存在很大差异,所以它们可能存在兼容性问题。最重要的是,您不能创建通用对象数组,甚至不能创建参数化数组。这听起来有点混乱吗?让我们来看看。例如,您不能在 Java 中执行以下任何操作:
new List<T>[]
new List<String>[]
new T[]
如果我们尝试创建一个List<String>
对象数组,我们会得到一个编译错误,抱怨通用数组创建:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
// Compilation error! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
但是为什么这样做呢?为什么不允许创建这样的数组?这一切都是为了提供类型安全。如果编译器让我们创建这样的通用对象数组,我们可能会给自己带来很多问题。这是 Joshua Bloch 的书“Effective Java”中的一个简单示例:
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42, 65, 44); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
}
让我们想象一下,创建一个数组List<String>[] stringLists
是允许的,并且不会产生编译错误。如果这是真的,那么我们可以做一些事情: 在第 1 行中,我们创建了一个列表数组:List<String>[] stringLists
。我们的数组包含一个List<String>
. 在第 2 行中,我们创建了一个数字列表:List<Integer>
。在第 3 行中,我们将 our 分配List<String>[]
给一个Object[] objects
变量。Java语言允许这样:一个对象数组X
可以存储X
所有子类的对象和对象X
。因此,您可以将任何内容放入Object
数组中。objects()
在第 4 行中,我们将数组 (a )的唯一元素替换List<String>
为 a List<Integer>
。因此,我们将 a 放入一个List<Integer>
仅用于存储的数组中List<String>
对象!我们只有在执行第 5 行时才会遇到错误。ClassCastException
运行时会抛出A。因此,在 Java 中添加了禁止创建此类数组的规定。这让我们避免了这种情况。
我怎样才能绕过类型擦除?
好吧,我们了解了类型擦除。让我们尝试欺骗系统!:) 任务: 我们有一个通用TestClass<T>
类。我们想为这个类编写一个createNewT()
方法来创建并返回一个新T
对象。但这是不可能的,对吧?所有关于T
类型的信息在编译期间都会被擦除,在运行时我们无法确定我们需要创建什么类型的对象。实际上有一种棘手的方法可以做到这一点。您可能还记得 Java 有一个Class
类。我们可以使用它来确定任何对象的类:
public class Main2 {
public static void main(String[] args) {
Class classInt = Integer.class;
Class classString = String.class;
System.out.println(classInt);
System.out.println(classString);
}
}
控制台输出:
class java.lang.Integer
class java.lang.String
但这是我们还没有谈到的一个方面。在 Oracle 文档中,您会看到 Class 类是通用的!
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
Integer.class
不仅仅是Class
,而是Class<Integer>
。对象的类型String.class
不只是Class
,而是 等Class<String>
。如果还是不清楚,可以尝试在前面的示例中添加一个类型参数:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
// Compilation error!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
// Compilation error!
Class<Double> classString2 = String.class;
}
}
现在,利用这些知识,我们可以绕过类型擦除并完成我们的任务! 让我们尝试获取有关类型参数的信息。我们的类型参数将是MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("A MySecretClass object was created successfully!");
}
}
以下是我们如何在实践中使用我们的解决方案:
public class TestClass<T> {
Class<T> typeParameterClass;
public TestClass(Class<T> typeParameterClass) {
this.typeParameterClass = typeParameterClass;
}
public T createNewT() throws IllegalAccessException, InstantiationException {
T t = typeParameterClass.newInstance();
return t;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
MySecretClass secret = testString.createNewT();
}
}
控制台输出:
A MySecretClass object was created successfully!
我们刚刚将所需的类参数传递给了泛型类的构造函数:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
这允许我们保存有关类型参数的信息,防止它被完全删除。结果,我们能够创建一个T
目的!:) 至此,今天的课程就结束了。使用泛型时,您必须始终记住类型擦除。这种变通方法看起来不太方便,但您应该明白泛型在创建时并不是 Java 语言的一部分。此功能可帮助我们创建参数化集合并在编译期间捕获错误,此功能是后来添加的。在包含第一个版本泛型的其他一些语言中,没有类型擦除(例如,在 C# 中)。顺便说一句,我们还没有完成对泛型的研究!在下一课中,您将熟悉泛型的更多特性。现在,最好解决几个任务!:)