二十、泛型(4)

news2025/1/23 3:55:42

本章概要

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

补偿擦除

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

//无法编译
public class Erased<T> {
    private final int SIZE = 100;

    public void f(Object arg) {
        // error: illegal generic type for instanceof
        if (arg instanceof T) {
        }
        // error: unexpected type
        T var = new T();
        // error: generic array creation
        T[] array = new T[SIZE];
        // warning: [unchecked] unchecked cast
        T[] 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 T
  T* 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;
    }

    @Override
    public T get() {
        try {
            return kind.newInstance();
        } catch (InstantiationException |
                 IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

class Employee {
    @Override
    public 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>> C
    create(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;

    @Override
    public Integer get() {
        return ++i;
    }
}

class Widget {
    private int id;

    Widget(int n) {
        id = n;
    }

    @Override
    public String toString() {
        return "Widget " + id;
    }

    public static
    class Factory implements Supplier<Widget> {
        private int i = 0;

        @Override
        public Widget get() {
            return new Widget(++i);
        }
    }
}

class Fudge {
    private static int count = 1;
    private int n = count++;

    @Override
    public String toString() {
        return "Fudge " + n;
    }
}

class Foo2<T> {
    private List<T> x = new ArrayList<>();

    Foo2(Supplier<T> factory) {
        Suppliers.fill(x, factory, 5);
    }

    @Override
    public 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> {
    @Override
    X 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.java

import 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.java

class 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.coloradmin.cn/o/1185260.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手机玻璃盖板为什么需要透光率检测

手机盖板&#xff0c;也称为手机壳或保护套&#xff0c;是一种用于保护手机外观和延长使用寿命的装置。它们通常由塑料、硅胶、玻璃或金属等材料制成&#xff0c;并固定在手机外壳上,其中任何一个工序出现差错&#xff0c;都有可能导致手机盖板产生缺陷&#xff0c;例如漏油、透…

维控PLC——LX1S :编程口通讯协议

文章目录 说明通讯帧通讯命令字通讯数据地址维控 LX1S通讯协议举例 说明 该协议适用于维控LX1S系列PLC&#xff0c;关于维控LX2N的协议将在后面描述。 通讯帧 通讯采用ASCII码&#xff0c;校验方式采用和校验。 请求帧格式:报文开始命令字地址&#xff08;有些无&#xff09…

热门的免费报表软件,建议收藏!

目前&#xff0c;随着企业对数据越来越重视&#xff0c;报表软件的应用越来越广泛。企业报表的需求越来越多变&#xff0c;就需要好用的免费报表软件&#xff0c;报表软件必须具备简捷、专业、灵活的特点&#xff0c;这里就给大家测评几款免费报表软件&#xff0c;供大家做参考…

机器学习模板代码(期末考试复习)自用存档

机器学习复习代码 利用sklearn实现knn import numpy as np import pandas as pd from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import GridSearchCVdef model_selection(x_train, y_train):## 第一个是网格搜索## p是选择查找方式:1是欧…

JVM之jps虚拟机进程状态工具

jps虚拟机进程状态工具 1、jps jps&#xff1a;(JVM Process Status Tool)&#xff0c;虚拟机进程状态工具&#xff0c;可以列出正在运行的虚拟机进程&#xff0c;并显示虚拟机执 行主类&#xff08;Main Class&#xff0c;main()函数所在的类&#xff09;的名称&#xff0c…

公司来了个00后,起薪就是18K,不愧是卷王。。。

前言 都在传00后躺平、整顿职场&#xff0c;但该说不说&#xff0c;是真的卷&#xff0c;感觉我都要被卷废了... 前段时间&#xff0c;公司招了一个年轻人&#xff0c;其中有一个是00后&#xff0c;工作才一年多&#xff0c;直接跳槽到我们公司&#xff0c;薪资据说有18K&…

Java自学第6课:电商项目(1)

从本课开始&#xff0c;我们跟着项目一起来敲代码。通过项目来学习Java和Java web 1 开始 首先了解要做什么项目&#xff0c;这里选择B2C电商。 需求分析很重要&#xff0c;所以要了解甲方业务流程。 之后配置开发环境&#xff0c;选择开发工具。 然后就是搭建开发环境&…

【16】c++11新特性 —>弱引用智能指针weak_ptr(1)

定义 std::weak_ptr&#xff1a;弱引用的智能指针&#xff0c;它不共享指针&#xff0c;不能操作资源&#xff0c;是用来监视 shared_ptr 中管理的资源是否存在。 use_count #include <iostream> #include <memory> using namespace std;int main() {shared_ptr…

最终前端后端小程序还有nginx配置

前端 前端 build 代码及其 放置位置 后端 nginx.conf 配置 user root;worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid;events {worker_connections 1024; }http {include /etc/nginx/mime.types;default_type a…

SysML理论知识

概述 由来 长期以来系统工程师使用的建模语言、工具和技术种类很多&#xff0c;如行为图、IDEF0、N2图等&#xff0c;这些建模方法使用的符号和语义不同&#xff0c;彼此之间不能互操作和重用。系统工程正是由于缺乏一种强壮的标准的建模语言&#xff0c;从而限制系统工程师和…

电商API接口多平台全面分类|接入方式|提供测试

l 角色分类 对应角色主要包括&#xff1a; 依次表示公开查询应用、买家应用、卖家应用、商家应用、高级应用、专业应用被授权访问API的角色级别。其中公开查询应用为最低权限集合级别、专业 应用为最高权限集合级别。查、买、卖接口无需审批&#xff0c;仅受默认流量规则限制…

Windows上免费可用的 7 款外置硬盘数据恢复软件

这篇文章全面测试和比较了不同的外部硬盘恢复软件&#xff0c;并选出了其中最好的七个。您将看到有关 7 种数据恢复软件的详细报告&#xff0c;包括价格、支持的平台、目标用户、优缺点、易用性、恢复性能、扫描方法、扫描速度以及许多其他会影响用户体验的标准。第一个软件是最…

α-SRHLA

作者未提供代码

uniapp中picker 获取时间组件如何把年月日改成年月日默认时分秒为00:00:00

如图所示&#xff0c;uniapp中picker组件的日期格式为&#xff1a; 但后端要 2023-11-08 00:00:00格式 如何从2023-11-08转化为 2023-11-08 00:00:00&#xff1a;&#x1f447; const date new Date(e.detail.value);//"2023-11-17" date.setHours(0, 0, 0); // 2…

一文1700字使用Postman搞定各种接口token实战(建议收藏)

现在许多项目都使用jwt来实现用户登录和数据权限&#xff0c;校验过用户的用户名和密码后&#xff0c;会向用户响应一段经过加密的token&#xff0c;在这段token中可能储存了数据权限等&#xff0c;在后期的访问中&#xff0c;需要携带这段token&#xff0c;后台解析这段token才…

微服务中配置文件(YAML文件)和项目依赖(POM文件)的区别与联系

实际上涉及到了微服务架构中的两个重要概念&#xff1a;服务间通信和项目依赖管理。在微服务架构中&#xff0c;一个项目可以通过两种方式与另一个项目建立依赖关系&#xff1a;通过配置文件&#xff08;如YAML文件&#xff09;和通过项目依赖&#xff08;如POM文件&#xff09…

学完这套jmeter结果数据写入到指定文件,涨薪3k没问题

背景&#xff1a; 在日常的功能测试或者性能测试的时候&#xff0c;需要把结果中指定的数据保存到指定的文件&#xff0c;进行使用&#xff0c;发现 jmeter 自带的文件写入&#xff0c;没有自己需要的&#xff0c;默认只支持 XML 的结果保存&#xff0c;那么接下来我们围绕 bea…

【吞噬星空】地球怪兽沦为濒危物种,罗峰出发虬龙星,新剧情开始

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析国漫资讯。 深度爆料&#xff0c;《吞噬星空》国漫第92话最新剧情解析&#xff0c;在银河系的边际&#xff0c;一艘宇宙飞船悄然启程&#xff0c;目的地是远离家乡的虬龙星。罗峰&#xff0c;这位年轻的旅者&#xff0c;…

【算法-链表1】移除链表元素 和 设计链表

今天&#xff0c;带来链表相关算法的讲解。文中不足错漏之处望请斧正&#xff01; 理论基础点这里 移除链表元素 1. 思路 链表的删除和插入&#xff0c;都是改变链接关系。 如图&#xff1a;要删除值为2的结点&#xff0c;要找到其上一个节点&#xff0c;让上一个节点指向值…

利用QT画图像的直方图

1.什么是直方图 直方图是一种图形化展示数据频率分布的方式。它将样本数据分成一系列相邻的区间&#xff0c;统计每个区间内数据所占比例或数量&#xff0c;并用矩形条形图表现出来。直方图可以反映样本数据的分布情况&#xff0c;例如它们的集中趋势、对称性和离散程度等。 …