当前位置: 首页 > news >正文

二十、泛型(4)

本章概要

  • 补偿擦除
    • 创建类型的实例
    • 泛型数组

补偿擦除

因为擦除,我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型:

//无法编译
public class Erased<T> {private final int SIZE = 100;public void f(Object arg) {// error: illegal generic type for instanceofif (arg instanceof T) {}// error: unexpected typeT var = new T();// error: generic array creationT[] array = new T[SIZE];// warning: [unchecked] unchecked castT[] array = (T[]) new Object[SIZE];}
}

有时,我们可以对这些问题进行编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 Class 对象,以在类型表达式中使用它。

例如,由于擦除了类型信息,因此在上一个程序中尝试使用 instanceof 将会失败。类型标签可以使用动态 isInstance()

class Building {
}class House extends Building {
}public class ClassTypeCapture<T> {Class<T> kind;public ClassTypeCapture(Class<T> kind) {this.kind = kind;}public boolean f(Object arg) {return kind.isInstance(arg);}public static void main(String[] args) {ClassTypeCapture<Building> ctt1 =new ClassTypeCapture<>(Building.class);System.out.println(ctt1.f(new Building()));System.out.println(ctt1.f(new House()));ClassTypeCapture<House> ctt2 =new ClassTypeCapture<>(House.class);System.out.println(ctt2.f(new Building()));System.out.println(ctt2.f(new House()));}
}

在这里插入图片描述

编译器来保证类型标签与泛型参数相匹配。

创建类型的实例

试图在 Erased.javanew T() 是行不通的,部分原因是由于擦除,部分原因是编译器无法验证 T 是否具有默认(无参)构造函数。但是在 C++ 中,此操作自然,直接且安全(在编译时检查):

// generics/InstantiateGenericType.cpp
// C++, not Java!template<class T> class Foo {T x; // Create a field of type TT* y; // Pointer to T
public:// Initialize the pointer:Foo() { y = new T(); }
};class Bar {};int main() {Foo<Bar> fb;Foo<int> fi; // ... and it works with primitives
}

Java 中的解决方案是传入一个工厂对象,并使用该对象创建新实例。方便的工厂对象只是 Class 对象,因此,如果使用类型标记,则可以使用 newInstance() 创建该类型的新对象:

import java.util.function.Supplier;class ClassAsFactory<T> implements Supplier<T> {Class<T> kind;ClassAsFactory(Class<T> kind) {this.kind = kind;}@Overridepublic T get() {try {return kind.newInstance();} catch (InstantiationException |IllegalAccessException e) {throw new RuntimeException(e);}}
}class Employee {@Overridepublic String toString() {return "Employee";}
}public class InstantiateGenericType {public static void main(String[] args) {ClassAsFactory<Employee> fe =new ClassAsFactory<>(Employee.class);System.out.println(fe.get());ClassAsFactory<Integer> fi =new ClassAsFactory<>(Integer.class);try {System.out.println(fi.get());} catch (Exception e) {System.out.println(e.getMessage());}}
}

在这里插入图片描述

这样可以编译,但对于 ClassAsFactory<Integer> 会失败,这是因为 Integer 没有无参构造函数。由于错误不是在编译时捕获的,因此语言创建者不赞成这种方法。他们建议使用显式工厂(Supplier)并约束类型,以便只有实现该工厂的类可以这样创建对象。这是创建工厂的两种不同方法:

Suppliers.java

import java.util.*;
import java.util.function.*;
import java.util.stream.*;public class Suppliers {// Create a collection and fill it:public static <T, C extends Collection<T>> Ccreate(Supplier<C> factory, Supplier<T> gen, int n) {return Stream.generate(gen).limit(n).collect(factory, C::add, C::addAll);}// Fill an existing collection:public static <T, C extends Collection<T>>C fill(C coll, Supplier<T> gen, int n) {Stream.generate(gen).limit(n).forEach(coll::add);return coll;}// Use an unbound method reference to// produce a more general method:public static <H, A> H fill(H holder,BiConsumer<H, A> adder, Supplier<A> gen, int n) {Stream.generate(gen).limit(n).forEach(a -> adder.accept(holder, a));return holder;}
}

FactoryConstraint.java

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;class IntegerFactory implements Supplier<Integer> {private int i = 0;@Overridepublic Integer get() {return ++i;}
}class Widget {private int id;Widget(int n) {id = n;}@Overridepublic String toString() {return "Widget " + id;}public staticclass Factory implements Supplier<Widget> {private int i = 0;@Overridepublic Widget get() {return new Widget(++i);}}
}class Fudge {private static int count = 1;private int n = count++;@Overridepublic String toString() {return "Fudge " + n;}
}class Foo2<T> {private List<T> x = new ArrayList<>();Foo2(Supplier<T> factory) {Suppliers.fill(x, factory, 5);}@Overridepublic String toString() {return x.toString();}
}public class FactoryConstraint {public static void main(String[] args) {System.out.println(new Foo2<>(new IntegerFactory()));System.out.println(new Foo2<>(new Widget.Factory()));System.out.println(new Foo2<>(Fudge::new));}
}

在这里插入图片描述

IntegerFactory 本身就是通过实现 Supplier<Integer> 的工厂。 Widget 包含一个内部类,它是一个工厂。还要注意,Fudge 并没有做任何类似于工厂的操作,并且传递 Fudge::new 仍然会产生工厂行为,因为编译器将对函数方法 ::new 的调用转换为对 get() 的调用。

另一种方法是模板方法设计模式。在以下示例中,create() 是模板方法,在子类中被重写以生成该类型的对象:

abstract class GenericWithCreate<T> {final T element;GenericWithCreate() {element = create();}abstract T create();
}class X {
}class XCreator extends GenericWithCreate<X> {@OverrideX create() {return new X();}void f() {System.out.println(element.getClass().getSimpleName());}
}public class CreatorGeneric {public static void main(String[] args) {XCreator xc = new XCreator();xc.f();}
}

在这里插入图片描述

GenericWithCreate 包含 element 字段,并通过无参构造函数强制其初始化,该构造函数又调用抽象的 create() 方法。这种创建方式可以在子类中定义,同时建立 T 的类型。

泛型数组

正如在 Erased.java 中所看到的,我们无法创建泛型数组。通用解决方案是在试图创建泛型数组的时候使用 ArrayList

// generics/ListOfGenerics.javaimport java.util.ArrayList;
import java.util.List;public class ListOfGenerics<T> {private List<T> array = new ArrayList<>();public void add(T item) {array.add(item);}public T get(int index) {return array.get(index);}
}

这样做可以获得数组的行为,并且还具有泛型提供的编译时类型安全性。

有时,仍然会创建泛型类型的数组(例如, ArrayList 在内部使用数组)。可以通过使编译器满意的方式定义对数组的通用引用:

// generics/ArrayOfGenericReference.javaclass Generic<T> {
}public class ArrayOfGenericReference {static Generic<Integer>[] gia;
}

编译器接受此操作而不产生警告。但是我们永远无法创建具有该确切类型(包括类型参数)的数组,因此有点令人困惑。由于所有数组,无论它们持有什么类型,都具有相同的结构(每个数组插槽的大小和数组布局),因此似乎可以创建一个 Object 数组并将其转换为所需的数组类型。实际上,这确实可以编译,但是会产生 ClassCastException

public class ArrayOfGeneric {static final int SIZE = 100;static Generic<Integer>[] gia;@SuppressWarnings("unchecked")public static void main(String[] args) {try {gia = (Generic<Integer>[]) new Object[SIZE];} catch (ClassCastException e) {System.out.println(e.getMessage());}// Runtime type is the raw (erased) type:gia = (Generic<Integer>[]) new Generic[SIZE];System.out.println(gia.getClass().getSimpleName());gia[0] = new Generic<>();//- gia[1] = new Object(); // Compile-time error// Discovers type mismatch at compile time://- gia[2] = new Generic<Double>();}
}

在这里插入图片描述

问题在于数组会跟踪其实际类型,而该类型是在创建数组时建立的。因此,即使 gia 被强制转换为 Generic<Integer>[] ,该信息也仅在编译时存在(并且没有 **@SuppressWarnings ** 注解,将会收到有关该强制转换的警告)。在运行时,它仍然是一个 Object 数组,这会引起问题。成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换。

让我们看一个更复杂的示例。考虑一个包装数组的简单泛型包装器:

public class GenericArray<T> {private T[] array;@SuppressWarnings("unchecked")public GenericArray(int sz) {array = (T[]) new Object[sz];}public void put(int index, T item) {array[index] = item;}public T get(int index) {return array[index];}// Method that exposes the underlying representation:public T[] rep() {return array;}public static void main(String[] args) {GenericArray<Integer> gai = new GenericArray<>(10);try {Integer[] ia = gai.rep();} catch (ClassCastException e) {System.out.println(e.getMessage());}// This is OK:Object[] oa = gai.rep();}
}

在这里插入图片描述

和以前一样,我们不能说 T[] array = new T[sz] ,所以我们创建了一个 Object 数组并将其强制转换。

rep() 方法返回一个 T[] ,在主方法中它应该是 gaiInteger[],但是如果调用它并尝试将结果转换为 Integer[] 引用,则会得到 ClassCastException ,这再次是因为实际的运行时类型为 Object[]

如果再注释掉 **@SuppressWarnings ** 注解后编译 GenericArray.java ,则编译器会产生警告:

GenericArray.java uses unchecked or unsafe operations.
Recompile with -Xlint:unchecked for details.

在这里,我们收到了一个警告,我们认为这是有关强制转换的。

但是要真正确定,请使用 -Xlint:unchecked 进行编译:

GenericArray.java:7: warning: [unchecked] unchecked cast    array = (T[])new Object[sz];                 ^  required: T[]  found:    Object[]  where T is a type-variable:    T extends Object declared in class GenericArray 1 warning

确实是在抱怨那个强制转换。由于警告会变成噪音,因此,一旦我们确认预期会出现特定警告,我们可以做的最好的办法就是使用 **@SuppressWarnings ** 将其关闭。这样,当警告确实出现时,我们将进行实际调查。

由于擦除,数组的运行时类型只能是 Object[] 。 如果我们立即将其转换为 T[] ,则在编译时会丢失数组的实际类型,并且编译器可能会错过一些潜在的错误检查。因此,最好在集合中使用 Object[] ,并在使用数组元素时向 T 添加强制类型转换。让我们来看看在 GenericArray.java 示例中会是怎么样的:

public class GenericArray2<T> {private Object[] array;public GenericArray2(int sz) {array = new Object[sz];}public void put(int index, T item) {array[index] = item;}@SuppressWarnings("unchecked")public T get(int index) {return (T) array[index];}@SuppressWarnings("unchecked")public T[] rep() {return (T[]) array; // Unchecked cast}public static void main(String[] args) {GenericArray2<Integer> gai =new GenericArray2<>(10);for (int i = 0; i < 10; i++) {gai.put(i, i);}for (int i = 0; i < 10; i++) {System.out.print(gai.get(i) + " ");}System.out.println();try {Integer[] ia = gai.rep();} catch (Exception e) {System.out.println(e);}}
}

在这里插入图片描述

最初,看起来并没有太大不同,只是转换的位置移动了。没有 **@SuppressWarnings ** 注解,仍然会收到“unchecked”警告。但是,内部表示现在是 Object[] 而不是 T[] 。 调用 get() 时,它将对象强制转换为 T ,实际上这是正确的类型,因此很安全。但是,如果调用 rep() ,它将再次尝试将 Object[] 强制转换为 T[] ,但仍然不正确,并在编译时生成警告,并在运行时生成异常。

因此,无法破坏基础数组的类型,该基础数组只能是 Object[] 。在内部将数组视为 Object[] 而不是 T[] 的优点是,我们不太可能会忘记数组的运行时类型并意外地引入了bug,尽管大多数(也许是全部)此类错误会在运行时被迅速检测到。

对于新代码,请传入类型标记。在这种情况下,GenericArray 如下所示:

import java.lang.reflect.Array;public class GenericArrayWithTypeToken<T> {private T[] array;@SuppressWarnings("unchecked")public GenericArrayWithTypeToken(Class<T> type, int sz) {array = (T[]) Array.newInstance(type, sz);}public void put(int index, T item) {array[index] = item;}public T get(int index) {return array[index];}// Expose the underlying representation:public T[] rep() {return array;}public static void main(String[] args) {GenericArrayWithTypeToken<Integer> gai =new GenericArrayWithTypeToken<>(Integer.class, 10);// This now works:Integer[] ia = gai.rep();}
}

类型标记 Class 被传递到构造函数中以从擦除中恢复,因此尽管必须使用 **@SuppressWarnings ** 关闭来自强制类型转换的警告,但我们仍可以创建所需的实际数组类型。一旦获得了实际的类型,就可以返回它并产生所需的结果,如在主方法中看到的那样。数组的运行时类型是确切的类型 T[]

不幸的是,如果查看 Java 标准库中的源代码,你会发现到处都有从 Object 数组到参数化类型的转换。例如,这是ArrayList 中,复制一个 Collection 的构造函数,这里为了简化,去除了源码中对此不重要的代码:

public ArrayList(Collection c) {size = c.size();elementData = (E[])new Object[size];c.toArray(elementData);
}

如果你浏览 ArrayList.java 的代码,将会发现很多此类强制转换。当我们编译它时会发生什么?

Note: ArrayList.java uses unchecked or unsafe operations
Note: Recompile with -Xlint:unchecked for details.

果然,标准库会产生很多警告。如果你使用过 C 语言,尤其是使用 ANSI C 之前的语言,你会记住警告的特殊效果:发现警告后,可以忽略它们。因此,除非程序员必须对其进行处理,否则最好不要从编译器发出任何类型的消息。

Neal Gafter(Java 5 的主要开发人员之一)在他的博客中指出,他在重写 Java 库时是很随意、马虎的,我们不应该像他那样做。Neal 还指出,他在不破坏现有接口的情况下无法修复某些 Java 库代码。因此,即使在 Java 库源代码中出现了一些习惯用法,它们也不一定是正确的做法。当查看库代码时,我们不能认为这就是要在自己代码中必须遵循的示例。

请注意,在 Java 文献中推荐使用类型标记技术,例如 Gilad Bracha 的论文《Generics in the Java Programming Language》,他指出:“例如,这种用法已广泛用于新的 API 中以处理注解。” 我发现此技术在人们对于舒适度的看法方面存在一些不一致之处;有些人强烈喜欢本章前面介绍的工厂方法。

相关文章:

  • 【PTE-day06 文件上传】
  • Spring boot集成sentinel限流服务
  • react typescript @别名的使用
  • 【面经】讲一下线程池的参数和运行原理
  • Flutter IOS 前后台切换主题自动变化的问题
  • 鸿蒙列表,类似于安卓的RecyclerView
  • 虚拟机Linux-Centos系统网络配置常用命令+Docker 的常用命令
  • 2023年11月编程语言流行度排名
  • 写论文中的心得记录
  • AVL树 c语言版本 插入部分
  • 01-基于IDEA,Spring官网,阿里云官网,手动四种方式创建SpringBoot工程
  • 5分钟Python安装实战(MAC版本)
  • MapReduce:大数据处理的范式
  • Naocs配置中心配置映射List、Map、Map嵌套List等方式
  • Chatgpt人工智能对话源码系统分享 带完整搭建教程
  • 《Java编程思想》读书笔记-对象导论
  • 【mysql】环境安装、服务启动、密码设置
  • 【知识碎片】第三方登录弹窗效果
  • fetch 从初识到应用
  • Java 实战开发之spring、logback配置及chrome开发神器(六)
  • LeetCode18.四数之和 JavaScript
  • Mocha测试初探
  • MYSQL 的 IF 函数
  • React+TypeScript入门
  • React16时代,该用什么姿势写 React ?
  • spring学习第二天
  • 阿里云爬虫风险管理产品商业化,为云端流量保驾护航
  • 半理解系列--Promise的进化史
  • 使用Tinker来调试Laravel应用程序的数据以及使用Tinker一些总结
  • 微信小程序开发问题汇总
  • 阿里云ACE认证学习知识点梳理
  • 容器镜像
  • ​什么是bug?bug的源头在哪里?
  • #Linux(帮助手册)
  • #pragma预处理命令
  • #我与Java虚拟机的故事#连载05:Java虚拟机的修炼之道
  • $.ajax()参数及用法
  • (Bean工厂的后处理器入门)学习Spring的第七天
  • (二)Eureka服务搭建,服务注册,服务发现
  • (二)Pytorch快速搭建神经网络模型实现气温预测回归(代码+详细注解)
  • (一一四)第九章编程练习
  • .net core 6 集成和使用 mongodb
  • .net core Swagger 过滤部分Api
  • .net项目IIS、VS 附加进程调试
  • ::什么意思
  • ??myeclipse+tomcat
  • @EnableAsync和@Async开始异步任务支持
  • @select 怎么写存储过程_你知道select语句和update语句分别是怎么执行的吗?
  • [22]. 括号生成
  • [BZOJ1053][HAOI2007]反素数ant
  • [CDOJ 1343] 卿学姐失恋了
  • [CUDA 学习笔记] CUDA kernel 的 grid_size 和 block_size 选择
  • [NodeJS]NodeJS基于WebSocket的多用户点对点即时通讯聊天
  • [noip模拟]计蒜姬BFS
  • [OLEDB] 目前还找找不到处理下面错误的办法