JavaRush /Java 博客 /Random-ZH /擦除类型

擦除类型

已在 Random-ZH 群组中发布
你好!我们继续我们的仿制药系列讲座。之前,我们大致了解了它是什么以及为什么需要它。今天我们将讨论泛型的一些特性,并探讨使用它们时的一些陷阱。去! 擦除类型 - 1一讲,我们讨论了泛型类型原始类型的区别。如果您忘记了,原始类型是一个泛型类,其类型已从中删除。
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 aObject b我们的类型T,之后它们将被添加到该对象中TestClassmain()在我们创造的方法上TestClass<Integer>,也就是在T我们将拥有的品质上Integer。但同时,createAndAdd2Values()我们将一个数字Double和一个对象传递给该方法String。您认为我们的计划可行吗?毕竟,我们指定为参数类型Integer,但String它肯定不能转换为Integer!让我们运行该方法main()并检查一下。控制台输出: 22.111 测试字符串 意外结果!为什么会发生这种情况? 正是因为类型擦除。Integer在代码编译期间,有关对象的 参数类型的信息TestClass<Integer> test被删除。他变成了TestClass<Object> test。我们的参数毫无问题地转换为Double(而不是像我们预期的那样转换为 !)并悄悄添加到。这是另一个简单但非常说明性的类型擦除示例: StringObjectIntegerTestClass
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 看起来我们已经创建了具有三种不同参数类型的集合 - StringInteger和我们创建的类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是一个泛型类! 擦除类型 - 3文档中说:“T 是这个 Class 对象建模的类的类型。” 如果我们将其从文档语言翻译成人类语言,这意味着对象的类Integer.class不仅仅是Class,而是Class<Integer>。对象的类型string.class不仅仅是ClassClass<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#)。然而,我们对泛型的研究还没有结束!在下一讲中,您将熟悉使用它们的更多功能。与此同时,如果能解决几个问题就好了!:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION