你好!我们继续我们的仿制药系列讲座。之前,我们大致了解了它是什么以及为什么需要它。今天我们将讨论泛型的一些特性,并探讨使用它们时的一些陷阱。去! 上一讲,我们讨论了泛型类型和原始类型的区别。如果您忘记了,原始类型是一个泛型类,其类型已从中删除。
List list = new ArrayList();
这是一个例子。这里我们没有指定什么类型的对象将被放置在我们的List
. 如果我们尝试创建一个对象List
并向其中添加一些对象,我们将在 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
为Object b
我们的类型T
,之后它们将被添加到该对象中TestClass
。main()
在我们创造的方法上TestClass<Integer>
,也就是在T
我们将拥有的品质上Integer
。但同时,createAndAdd2Values()
我们将一个数字Double
和一个对象传递给该方法String
。您认为我们的计划可行吗?毕竟,我们指定为参数类型Integer
,但String
它肯定不能转换为Integer
!让我们运行该方法main()
并检查一下。控制台输出: 22.111 测试字符串 意外结果!为什么会发生这种情况? 正是因为类型擦除。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) {
//ошибка компиляции! 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 行中,我们创建一个 Sheets 数组List<String>[] stringLists
。我们的数组包含一个List<String>
. 在第 2 行,我们创建了一个数字列表List<Integer>
。在第 3 行,我们将数组分配List<String>[]
给一个变量Object[] objects
。Java 语言允许您这样做:X
您可以将对象X
和所有子类的对象放入对象数组中Х
。因此,Objects
您可以将任何内容放入数组中。在第 4 行,我们将数组的单个元素替换objects (List<String>)
为 list List<Integer>
。结果,我们放入List<Integer>
了数组,该数组仅用于存储List<String>
!只有当代码到达第5行时我们才会遇到错误。程序执行过程中会抛出异常ClassCastException
。因此,Java 语言中引入了创建此类数组的禁令——这使我们能够避免此类情况。
如何绕过类型擦除?
好吧,我们已经了解了类型擦除。让我们尝试欺骗系统吧!:) 任务: 我们有一个通用类TestClass<T>
。我们需要在其中创建一个方法createNewT()
来创建并返回一个类型为 的新对象Т
。但这是不可能做到的,对吧?所有关于类型的信息Т
都会在编译过程中被删除,而在程序运行时,我们将无法找出我们需要创建什么类型的对象。事实上,有一种棘手的方法。你可能还记得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是一个泛型类! 文档中说:“T 是这个 Class 对象建模的类的类型。” 如果我们将其从文档语言翻译成人类语言,这意味着对象的类Integer.class
不仅仅是Class
,而是Class<Integer>
。对象的类型string.class
不仅仅是Class
、Class<String>
等。如果还不清楚,请尝试在前面的示例中添加类型参数:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
//ошибка компиляции!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
//ошибка компиляции!
Class<Double> classString2 = String.class;
}
}
现在,利用这些知识,我们可以绕过类型擦除并解决我们的问题!让我们尝试获取有关参数类型的信息。它的角色将由类扮演MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("Объект секретного класса успешно создан!");
}
}
以下是我们在实践中使用我们的解决方案的方式:
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();
}
}
控制台输出:
Объект секретного класса успешно создан!
我们只需将所需的类参数传递给泛型类的构造函数:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
因此,我们保存了有关参数类型的信息并防止其被删除。结果,我们能够创建一个对象T
!:) 今天的讲座到此结束。使用泛型时始终要记住类型擦除。这看起来不太方便,但您需要了解,泛型在创建时并不是 Java 语言的一部分。这是后来添加的功能,可以帮助我们创建类型化集合并在编译阶段捕获错误。从版本 1 开始就存在泛型的其他一些语言没有类型擦除(例如 C#)。然而,我们对泛型的研究还没有结束!在下一讲中,您将熟悉使用它们的更多功能。与此同时,如果能解决几个问题就好了!:)
GO TO FULL VERSION