二十、泛型(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 中以处理注解。” 我发现此技术在人们对于舒适度的看法方面存在一些不一致之处;有些人强烈喜欢本章前面介绍的工厂方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/165210.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

kafka可视化工具

Offset Explorer kafka可视化工具

前端-选中DOM定位源代码

用到的工具&#xff1a;react-dev-inspector 使用流程 根据react-dev-inspector文档进行配置 安装 yarn add --dev react-dev-inspector配置&#xff1a;在根目录下配置Inspector import { createRoot } from react-dom/client import { Inspector } from react-dev-inspe…

C/C++轻量级并发TCP服务器框架Zinx-游戏服务器开发006:基于redis查找玩家姓名+游戏业务实现总结

文章目录 1 Redis的安装与API的使用1.1 安装目录及环境变量1.2 设置远程客户端连接和守护进程1.3 启动redis1.4 Hiredis API的使用1.5 我的动态库和头文件 2 Redis的使用2.1 初始化时候2.2 结束的时候 3 测试4 Makefile5 游戏业务总结 1 Redis的安装与API的使用 1.1 安装目录及…

Geotrust证书

GeoTrust是著名的证书颁发机构DigiCert的品牌。GeoTrustSSL产品在Internet上提供从基本域名验证到扩展验证SSL标准支持的最高级验证的安全性。 GeoTrust OV&#xff08;组织验证&#xff09;证书验证域所有权和组织的存在。在颁发证书之前&#xff0c;会检查该组织在公共数据库…

5G边缘计算网关的功能及作用

5G边缘计算网关具有多种功能。 首先&#xff0c;它支持智能云端控制&#xff0c;可以通过5G/4G/WIFI等无线网络将采集的数据直接上云&#xff0c;实现异地远程监测控制、预警通知、报告推送和设备连接等工作。 其次&#xff0c;5G边缘计算网关可以采集各种数据&#xff0c;包…

JSP通用材料收集归档系统eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 JSP 通用材料收集归档系统是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&#xff0c…

leetcode做题笔记226. 翻转二叉树

给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1]示例 2&#xff1a; 输入&#xff1a;root [2,1,3] 输出&#xff1a;[2,3,1]示例 3&#x…

MSSQL 配置ORACLE ​链接服务器

在有些场景&#xff0c;我们需要整合其他异构数据库的数据。我们可以使用代码去读取&#xff0c;经过处理后&#xff0c;再将数据保存到MSSQL数据库中。如果数据量比较大&#xff0c;但处理的逻辑并不复杂的情况下&#xff0c;这种方式就不是最好的办法。这时可以使用使用链接服…

如何使用 NFTScan NFT API 在 Arbitrum 网络上开发 Web3 应用

Arbitrum 是以太坊的 Layer 2 扩容方案&#xff0c;为以太坊面临的高 gas 费和网络拥堵问题&#xff0c;提供了一个解决方案。作为 Layer 1 的以太坊基础层受每秒只能验算 15 笔交易的限制&#xff0c;在目前以太坊使用需求庞大的情况下&#xff0c;局限了以太坊的可扩展性。Ar…

访问者模式

详情可参考&#xff1a;https://zhuanlan.zhihu.com/p/380161731 意图&#xff1a;主要将数据结构与数据操作分离。 适用于&#xff1a;系统中有稳定的数据结构&#xff0c;且数据结构的功能经常发生变化。 双分派&#xff1a;我的理解是两次多态操作&#xff0c;动态获取对象…

Kotlin HashMap entries.filter过滤forEach

Kotlin HashMap entries.filter过滤forEach fun main(args: Array<String>) {val hashMap HashMap<String, Int>()hashMap["a"] 1hashMap["b"] 2hashMap["c"] 3println(hashMap)hashMap.entries.filter {println("filter $…

20.8 OpenSSL 套接字SSL传输文件

有了上面的基础那么传输文件的实现就变得简单了&#xff0c;在传输时通常我们需要打开文件&#xff0c;并每次读入1024个字节的数据包&#xff0c;通过SSL加密传输即可&#xff0c;此处的文件传输功能在原生套接字章节中也进行过详细讲解&#xff0c;此处我们还是使用原来的密钥…