初识Java 18-2 泛型

目录

构建复杂模型

类型擦除

C++中的泛型

迁移的兼容性

类型擦除存在的问题

边界的行为

对类型擦除的补偿

创建类型实例

泛型数组


本笔记参考自: 《On Java 中文版》


构建复杂模型

        泛型的一个优点就是,能够简单且安全地创建复杂模型

【例子:生成更复杂的数据结构】

import onjava.Tuple4;

import java.util.ArrayList;

public class TupleList<A, B, C, D>
        extends ArrayList<Tuple4<A, B, C, D>> {
    public static void main(String[] args) {
        TupleList<Vehicle, Amphibian, String, Integer> tl =
                new TupleList<>();
        tl.add(TupleTest2.h());
        tl.add(TupleTest2.h());
        tl.forEach(System.out::println);
    }
}

        程序执行的结果是:

        除此之外,我们也可以利用泛型组合各种各样的“块”,使其最终能够实现强大的功能。下面的例子表示的是一个商店(Store),这个商店中有通道(Aisle)、货架(Shelf)和商品(Product):

【例子:通过泛型构建一个商店模型】

import onjava.Suppliers;

import java.util.ArrayList;
import java.util.Random;
import java.util.function.Supplier;

// 构建商品模型
class Product {
    private final int id;
    private String description;
    private double price;

    Product(int idNumber, String descr, double price) {
        id = idNumber;
        description = descr;
        this.price = price;
        System.out.println(toString());
    }

    @Override
    public String toString() {
        return id + ":" + description +
                ", 价格:" + price + "元";
    }

    public void priceChange(double change) {
        price += change;
    }

    public static Supplier<Product> generator =
            new Supplier<Product>() {
                private Random rand = new Random(47);

                @Override
                public Product get() {
                    return new Product(rand.nextInt(1000),
                            "某件商品", Math.round(
                            rand.nextDouble() * 1000.0) + 0.99);
                }
            };
}

// 构建货架模型
class Shelf extends ArrayList<Product> {
    Shelf(int nProducts) {
        // Suppliers需要由自己进行实现
        Suppliers.fill(this, Product.generator, nProducts);
    }
}

// 构建通道模型
class Aisle extends ArrayList<Shelf> {
    Aisle(int nShelves, int nProducts) {
        for (int i = 0; i < nShelves; i++)
            add(new Shelf(nProducts));
    }
}

class CheckOutStand {
}

class Office {
}

// 最终组合成了一个商店的模型
public class Store extends ArrayList<Aisle> {
    private ArrayList<CheckOutStand> checkouts =
            new ArrayList<>();
    private Office office = new Office();

    public Store(
            int nAisles, int nShelves, int nProducts) {
        for (int i = 0; i < nAisles; i++)
            add(new Aisle(nShelves, nProducts));
    }

    @Override
    public String toString() {
        StringBuffer result = new StringBuffer();
        for (Aisle a : this)
            for (Shelf s : a)
                for (Product p : s) {
                    result.append(p);
                    result.append("\n");
                }
        return result.toString();
    }

    public static void main(String[] args) {
        System.out.println(new Store(3, 2, 2));
    }
}

        程序执行的结果是:

        从Store.toString()方法中可以看出:尽管经过了层层封装,但我们依旧可以方便、安全地管理这些模块。

        这里还需要注意自定义的Suppliers.fill()方法,这个方法的实现会在之后提到。此处的fill()方法可以等价于:

Stream.generate(Product.generator)
    .limit(nProducts)
    .forEach(this::add);

类型擦除

        Java的泛型同样存在着不合理之处。例如,尽管我们可以声明ArrayList.class,但却无法使用ArrayList<Integer>.class

【例子:发现泛型的不合理】

import java.util.ArrayList;

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }
}

        程序执行,会返回true

        输出告诉我们,ArrayList<String>ArrayList<Integer>是相同的类型。但这是有问题的:因为它们的行为并不相同。我们无法将Integer对象放入到ArrayList<String>,却可以把Integer对象放入到ArrayList<Integer>中。

        除此之外,Java的泛型还有一个更麻烦的特性:

【例子:泛型代码内部的信息】

import java.util.*;

class Frob {
}

class Fnorkle {
}

class Quark<Q> {
}

class Particle<POSITION, MOMENTUM> {
}

public class LostInformation {
    public static void main(String[] args) {
        List<Frob> list = new ArrayList<>();
        System.out.println(Arrays.toString(
                list.getClass().getTypeParameters()));

        Map<Frob, Fnorkle> map = new HashMap<>();
        System.out.println(Arrays.toString(
                map.getClass().getTypeParameters()));

        Quark<Fnorkle> quark = new Quark<>();
        System.out.println(Arrays.toString(
                quark.getClass().getTypeParameters()));

        Particle<Long, Double> particle = new Particle<>();
        System.out.println(Arrays.toString(
                particle.getClass().getTypeParameters()));
    }
}

        程序执行的结果是:

        Class.getTypeParameters()方法会返回一个由类型变量的对象组成的数组,表示泛型对象声明(所声明)的类型变量。这似乎表示着我们可以获取与泛型参数有关的类型信息。但结果是,我们只能发现作为参数占位符的标识符。

        这就说明:在Java的泛型代码内部,并不存在有关泛型参数类型的可用信息

    而C++等语言是可以在泛型内部获取类型信息的。

        Java的泛型是通过类型擦除实现的。因此在使用泛型时,任何具体的类型信息都会被擦除。在泛型内部,唯一能够知道的事情就是我们在使用这个对象。因此,ArrayList<String>ArrayList<Integer>在运行时都被“擦除”成了它们的原始类型(raw type):ArrayList

C++中的泛型

        Java的设计中有许多参考了C++的元素。因此,二者在参数化类型的语法部分也十分相似:

【例子:C++中的泛型(即模板)】

#include <iostream>
using namespace std;

template <class T>
class Manipulator {
    T obj;

public:
    Manipulator(T x) {
        obj = x;
    }

    void manipulate() {
        obj.f();
    }
};

class HasF {
public:
    void f() {
        cout << "HasF::f()" << endl;
    }
};

int main() {
    HasF hf;
    Manipulator<HasF> manipulator(hf);
    manipulator.manipulate();
}

        编译并执行程序,可得:

        C++编译器会在实例化模板时进行检测。因此,在实例化Manipulator<HasF>时,编译器会发现HasF中存在着方法f()

        接下来再尝试通过Java实现同样的效果:

【例子:在Java中进行尝试】

        首先编写一个HasF类:

public class HasF {
    public void f(){
        System.out.println("HasF.f()");
    }
}

        但接下来的部分却没办法如C++一样书写。若我们尝试调用方法obj.f(),编译器就会提示我们:

因为类型擦除的缘故,编译器不会知道Manipulator<HasF>的类型参数是HasF,因此会认为这种调用是不安全的。若想要调用f(),我们就必须人为规定泛型类的边界,帮助编译器确定符合边界的类型:

public class Manipulator2<T extends HasF> {
    private T obj;

    Manipulator2(T x) {
        obj = x;
    }

    public void manipulator() {
        obj.f();
    }
}

        <T extends HasF>告诉编译器,T的类型必须HasF及其的子类

        在这里,泛型的类型参数被擦除为了其的第一个边界HasF(与之相对的,也存在拥有多重边界的泛型)。编译器会将类型参数替换为擦除后的类型,因此可以说,在这个例子中T被替换成了HasF

    并且,该例子实际上并不需要使用到泛型——可以直接使用更加具体的类型HasF

        注意:当我们希望代码能够跨越多个类型运行时,泛型才会发挥作用(因此,在具有实际价值的泛型代码中,类型参数及其应用往往会比简单的类替换更加复杂)。

(基于以上论点,可以认为<T extends HasF>这种用法存在缺陷。)

        下面的例子展示了更好的一种泛型应用:通过让方法返回类型参数T,可以使泛型返回精确的类型。

【例子:更好的泛型使用】

public class ReturnGenericType<T extends HasF> {
    private T obj;

    ReturnGenericType(T x) {
        obj = x;
    }

    public T get() {
        return obj;
    }
}

迁移的兼容性

        注意:类型擦除并不是一项语言特性。它是Java在实现泛型时使用的一种必要的折中,因为泛型并不是这门语言与生俱来的一部分。

    因此,Java中的泛型并没有将类型参数具体化成第一类实体的能力。

        因为类型擦除,泛型类型被视同第二类类型处理,这使得其无法在一些重要的上下文中得到使用:泛型类型只会在静态类型检查时存在,之后,程序会将泛型类型擦除成它们的非泛型上界

        在Java 5之前,存在许多编写完毕的非泛型的库。库是一门语言重要的组成部分,无法被轻易抛弃。因此,Java的泛型设计必然需要保证向后兼容性迁移兼容性。前者保证原有的数据依旧合法,后者则需要协调泛化的程序与非泛化的库(反之亦然)。

||| 至于类型擦除是否是一种好的手段,就只能靠时间来验证了。


类型擦除存在的问题

        类型擦除在非泛化代码和泛化代码之间构建起了一座桥梁,泛型得以在不破坏现有库的情况下加入Java。

        然而这种做法是有代价的。泛型代码无法用于需要显式引用运行时类型的操作,例如类型转换、instanceof操作以及new表达式。在编写泛型代码时,我们只是看起来掌握了参数的类型信息。就比如,现在有一个泛型类:

class Foo<T> {
    T var;
}

若为它创建一个实例:

Foo<Cat> f = new Foo<>();

尽管不论是直观的理解或是语法本身带来的暗示,都在说明T已经被替换成了Cat。遗憾的是,泛型内部的T已经只是一个Object

    泛型擦除像是一个边界,在边界里面的成员会被擦除成原始的类型。只有在进出边界时,它们才会被转换成对应的类型。

        另外,因为类型擦除和迁移兼容性,Java中泛型的使用并非是强制性的。

【例子:不强制的泛型使用】

class GenericBase<T> {
    private T element;

    public void set(T arg) {
        element = arg;
    }

    public T get() {
        return element;
    }
}

// 使用泛型:
class Derived1<T> extends GenericBase<T> {}

// 使用原始类型,但未发出警告:
class Derived2 extends GenericBase {}

// 引发错误:
//class Derived3 extends GenericBase<?> {}

public class ErasureAndInheritance {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Derived2 d2 = new Derived2();
        Object obj = d2.get();
        d2.set(obj); // d2.set()会引发警告,使用@SuppressWarnings()进行关闭
    }
}

        Derived2继承了GenericBase,而未使用泛型参数。编译器没有在这里给出警告,直到进行编译时,在d2.get()才显现出来。若要关闭警告,可以使用Java提供的注解:

@SuppressWarnings("unchecked")

这一注解应该被放置于触发警告的类上。

        Derived3会引发错误:

编译器需要的是一个原始的基类,而我们却提供了一个带有<?>的泛型。

    在Java中,使用类型参数就意味着我们需要管理边界。这使得Java泛型并没有完全发挥其应有的灵活性。


边界的行为

        类型擦除使得泛型会表现出一些无意义的行为:

【例子:无意义的泛型行为】

import java.lang.reflect.Array;
import java.util.Arrays;

public class ArrayMaker<T> {
    private Class<T> kind;

    public ArrayMaker(Class<T> kind) {
        this.kind = kind;
    }

    @SuppressWarnings("unchecked")
    T[] create(int size) { // 需要使用类型转换
        return (T[]) Array.newInstance(kind, size);
    }

    public static void main(String[] args) {
        ArrayMaker<String> stringMaker =
                new ArrayMaker<>(String.class);
        String[] stringArray = stringMaker.create(9);
        System.out.println(Arrays.toString(stringArray));
    }
}

        程序执行的结果是:

        在这个例子中,尽管kind看起来会获得一个具体的Class<T>,但当进入类型擦除的边界时,<T>就会消失。换言之,kind中存储的只是一个无意义的Class,它无法生成一个具体的结果,因此我们还需要使用到类型转换(并且会产生警告)。

    在create()方法中使用到的Array.newInstance(),是在泛型中创建数组的推荐方法。

        我们也可以使用泛型创建集合(而不是数组):

【例子:无意义的集合】

import java.util.ArrayList;
import java.util.List;

public class ListMaker<T> {
    List<T> create() {
        return new ArrayList<>();
    }

    public static void main(String[] args) {
        ListMaker<String> stringMaker = new ListMaker<>();
        List<String> stringList = stringMaker.create();
    }
}

        在create()内部的new ArrayList<>()方法中没有使用<T>,并且在运行时<T>也会被移除。这使得这个集合似乎并没有具体意义,但若使用new ArrayList(),依旧会引发警告。

---

        但我们依旧可以通过一些方式进行有意义的调用:

【例子:有意义的泛型集合】

import onjava.Suppliers;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class FilledList<T> extends ArrayList<T> {
    FilledList(Supplier<T> gen, int size) {
        // 等价于使用Stream.generate(gen)生成size个元素,并装入该类中
        Suppliers.fill(this, gen, size);
    }

    public FilledList(T t, int size) {
        for (int i = 0; i < size; i++)
            this.add(t);
    }

    public static void main(String[] args) {
        List<String> list = new FilledList<>("Hello", 4);
        System.out.println(list);

        // 也可以借由Supplier接口进行实现
        List<Integer> ilist = new FilledList<>(() -> 47, 4);
        System.out.println(ilist);
    }
}

        程序执行的结果是:

        虽然在this.add()方法中,编译器无法知道任何关于T的信息,但我们依旧可以在编译时确保放入FilledList中的是类型T。因此,尽管存在类型擦除,编译器依旧可以确保类型在使用方法内部的一致性。

        现在,泛型运行时的关键就指向的边界——对象进入和离开方法体的临界点。编译器在这里执行类型检查,插入类型转换。可以观察下面两个类之间的区别:

        这两个类之间唯一的区别就是它们是否使用了泛型。现在可以通过反编译指令(javap -c)来观察它们的字节码:

        可以发现,非泛型类和泛型类在这里得到的字节码完全相同。在main()中调用set()方法时,编译器自动插入了类型转换,并且get()的类型转换仍然存在。从这里可以得出一个结论:泛型所有的行为都发生在边界,包括输入值的检查、类型转换等。

对类型擦除的补偿

        类型擦除会使得我们在一些操作上受掣肘:

        尽管我们有时可以绕过这些问题,但有些问题总是需要泛型来解决。此时可以使用类型标签来补偿类型擦除带来的损失:我们可以在类型表达式中显示地为所使用的类型传入一个Class对象

        类型标签与instanceof的不同之于,instanceof是静态的检查,若类型被擦除,那么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.java中执行new T()操作是无法成功的,这有两个原因:①类型擦除和②编译器无法验证T中是否存在无参构造器。但C++却支持这种操作:

【例子:C++允许创建泛型的类型实例】

template<class T> class Foo {
    T x; // 字段x
    T* y; // 指向T类的指针
public:
    Foo() {
        // 初始化指针
        y = new T();
    }
};

class Bar {};

int main()
{
    Foo<Bar> fb;
    Foo<int> fi; // 甚至可以使用基本类型
    return 0;
}

---

        而Java则需要使用工厂设计方法来创建新的实例。Class就是一个方便的工厂对象,将其作为类型标签,我们能够在Java中实现类似上例的功能:

【例子:通过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.getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

class Employee {
    public 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());
        }
    }
}

        程序执行的结果是:

        在该例中,我们尝试创建Integer的实例,结果却失败了。这是因为Integer不存在无参构造器。这个错误不会在编译时被发现,也因此上例的方式并不被推荐。更好的方式是使用显式工厂,同时限制能够传入的类型:

【例子:创建工厂,生成泛型实例】

import onjava.Suppliers;

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) {
        // 等价于使用Stream.generate(factory)生成5个元素,并装入该类中
        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));
    }
}

        程序执行的结果是:

        Foo2类用于调用各种工厂方法,生成实例。这里展示了三种创建工厂的方式:

  1. IntegerFactory:本身就是一个实现了Supplier<Integer>的工厂;
  2. Widget:包含了一个作为工厂的内部类;
  3. Fudge:不执行任何类似工厂的操作,但编译器会自动将Fudge::new转变成对get的调用。

        除此之外,还有另一种设计模式:模板方法。将方法在子类中进行重写,用来生成对应类型的对象:

【例子:使用模板方法生成泛型实例】

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有唯一的无参构造器,这样就可以要求任何所有这个类的程序员,必须通过我们规定的方式初始化这个类。另一边,create()方法将类的创建逻辑交付给了子类实现,这使得该方法的返回值可以在子类中得到更具体的定义。


泛型数组

        正如之前所看到的,我们无法直接在泛型中创建泛型数组:

一个直接的方法是使用集合来代替数组:

【例子:使用集合替代数组】

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);
    }
}

        这样我们就获得了数组的行为,并且得到了泛型提供的编译时类型检查。

        但如果确实有使用泛型数组的必要,那么可以尝试使用一个泛型引用,通过将这个引用指向一个数组,可以变相满足编译器的规定:

【例子:将引用指向数组】

class Generic<T> {
}

public class ArrayOfGenericReference {
    static Generic<Integer>[] gia;
}

        因为类型擦除,这个数组实际上没有具体的类型,无论指定的泛型参数是什么,数组都会具有相同的结构和大小。这看上去有点像Object,那么我们是否可以将一个Object类型的数组转换成目标数组?

        答案依旧是否定的:

【例子:无法对Object数组进行转型】

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());
        }

        // 运行时会发生类型擦除,得到的是原始类型Generic[]
        gia = (Generic<Integer>[]) new Generic[SIZE];
        System.out.println(gia.getClass().getSimpleName());
        gia[0] = new Generic<>();
        // 发生编译时错误(类型不匹配):
        // gia[1] = new Object();
        // gia[2] = new Generic<Double>();
    }
}

        程序执行的结果是(输出已经过折叠)

        数组的类型在它们被创建的时候才会确定下来,因此转型信息Generic<Integer>[]也只会存在于编译时。语句:

gia = (Generic<Integer>[]) new Object[SIZE];

能得到的只会是Object数组,这就会导致问题。

        而另一条创建语句:

gia = (Generic<Integer>[]) new Generic[SIZE];

对一个被擦除类型的数组进行强制类型转换得到了成功。这也是唯一可以成功创建泛型数组的方式

        以此类推,下面是一个更加复杂的例子:

【例子:更复杂的泛型数组尝试】

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];
    }

    // 通过返回T[],可以发现其的潜在表现形式:
    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());
        }

        // 可以使用Object数组接受:
        Object[] oa = gai.rep();
    }
}

        程序执行的结果是(输出已经过折叠)

        显然,这里的gai也被类型擦除影响,其在运行时的实际类型也变成了Object

        之前也提到过,通过@SuppressWarnings("unchecked")可以抑制编译器发出警告,否则会出现这样的警告:

        为了获取更加详细的信息,可以在编译时添加-Xlint:unchecked选项。而如果这么做,就会得到如下的信息:

    若认为报出的警告并不影响程序运行,就可以使用注解关闭警告,因为警告在一些时候也会成为不必要的噪声。

        因为类型擦除,在上例中我们只能得到Object[]。若此时立刻将其转变为T[],就会丢失数组的实际类型,这可能会让一些潜在错误有机可乘。

        一个可能的替代方法是在泛型类内部使用Object数组,而在边界处执行类型转换:

【例子:在边界上执行类型转换】

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;
    }

    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);
        }
    }
}

        程序执行的结果是:

        这么做依旧需要抑制警告。但比上一个例子更好的一点在于,现在get()方法能够正确地进行类型转换了。而不好的一点在于,rep()方法依旧无法将Object[]转型为T[]。这里就可以得出一个结论:底层的数组类型是无法更改的,这个类型只能是Object[]

    在泛型类内部使用Object[]的另一个好处是,让程序员花费更少的精力来处理数组的运行时类型。

---

        既然底层的数组无法更改,那么我们还可以换一个思路。通过类型标记,我们可以直接创建一个目标数组的实例:

【例子:使用类型标记创建数组实例】

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];
    }

    // 依旧会暴露潜在的表达方式:
    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> gai =
                new GenericArrayWithTypeToken<>(
                        Integer.class, 10);

        // 现在可以正常运行:
        Integer[] ia = gai.rep();
    }
}

        尽管还是需要抑制警告,但在这个例子中,数组在运行时是精确的T[]类型了。

    然而,在Java的源代码中,也存在着许都使用Object数组转型为参数化类型的操作,对其编译甚至会产生警告……(因此,Java的库代码难以作为我们自己编写代码时的范例)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

广告机/商业显示屏_基于MT878安卓主板方案

安卓主板在广告机领域扮演着重要的角色。无论是在商场、车站、酒店、电梯、机场还是高铁站&#xff0c;LED广告机广泛应用&#xff0c;并通过不同方式进行播放和管理。 广告机/商业显示屏_基于MT878安卓主板方案 基于MT8788安卓主板方案的广告机采用了联发科MT8788八核芯片方案…

力扣.面试题 04.06. 后继者(java 树的中序遍历)

Problem: 面试题 04.06. 后继者 文章目录 题目描述思路解题方法复杂度Code 题目描述 设计一个算法&#xff0c;找出二叉搜索树中指定节点的“下一个”节点&#xff08;也即中序后继&#xff09;。 如果指定节点没有对应的“下一个”节点&#xff0c;则返回null。 思路 由于题…

msvcp140.dll是什么?msvcp140.dll丢失的有哪些解决方法

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“msvcp140.dll丢失”。这个错误通常会导致某些应用程序无法正常运行。为了解决这个问题&#xff0c;我们需要采取一些措施来修复丢失的msvcp140.dll文件。本文将详细介绍5个解决msvcp140.dl…

visual studio 如何建立 C 语言项目

安装这个 模块。 新建 空项目 创建完成 写demo 点击运行&#xff1a;

计算机毕业设计 基于微信小程序的“共享书角”图书借还管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

经典中的经典之字符串

前言&#xff1a;前段时间发烧了&#xff0c;所以耽误了很多事情&#xff0c;一直没有更新&#xff0c;多穿点衣服&#xff0c;感冒不好受。 接下来有时间就会陆续更新一些基础的算法题&#xff0c;题目都很经典&#xff0c;大家可以先尝试着做&#xff0c;再看 解析。 第一…

7.HTML中列表标签

7.列表标签 7.1无序列表&#xff08;重点&#xff09; 表格是用来显示数据的&#xff0c;那么列表就是用来布局的。 列表最大的特点就是整齐&#xff0c;整洁&#xff0c;有序&#xff0c;他作为布局会更加自由和方便&#xff0c; 根据使用的情景不同&#xff0c;列表可分为三…

改进YOLOv5 | C3模块改动篇 | 轻量化设计 |骨干引入动态卷积|CondConv

🗝️YOLOv5实战宝典--星级指南:从入门到精通,您不可错过的技巧   -- 聚焦于YOLO的 最新版本, 对颈部网络改进、添加局部注意力、增加检测头部,实测涨点 💡 深入浅出YOLOv5:我的专业笔记与技术总结   -- YOLOv5轻松上手, 适用技术小白,文章代码齐全,仅需 …

2017年全国硕士研究生入学统一考试管理类专业学位联考数学试题——解析版

文章目录 2017 级考研管理类联考数学真题解析一、问题求解&#xff08;本大题共 5 小题&#xff0c;每小题 3 分&#xff0c;共 45 分&#xff09;下列每题给出 5 个选项中&#xff0c;只有一个是符合要求的&#xff0c;请在答题卡上将所选择的字母涂黑。真题&#xff08;2017-…

基于Qt的UDP通信、TCP文件传输程序的设计与实现——QQ聊天群聊

&#x1f64c;秋名山码民的主页 &#x1f602;oi退役选手&#xff0c;Java、大数据、单片机、IoT均有所涉猎&#xff0c;热爱技术&#xff0c;技术无罪 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; 获取源码&#xff0c;添加WX 目录 前言一…

词向量笔记

一、词向量的世界排名比赛MTEB MTEB: Massive Text Embedding Benchmark 二、词向量的维数 词向量的维数d1一般取20~500之间

数据结构与算法编程题11

已知两个链表A和B分别表示两个集合&#xff0c;其元素递增排列。 请设计算法求出A与B的交集&#xff0c;并存放于A链表中。 a: 1, 2, 2, 4, 5, 7, 8, 9, 10 b: 1, 2, 3, 6, 7, 8 #include <iostream> using namespace std;typedef int Elemtype; #define ERROR 0; #defin…

基于鹈鹕算法优化概率神经网络PNN的分类预测 - 附代码

基于鹈鹕算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于鹈鹕算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于鹈鹕优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

Linux文件

目录 一、基本概念 二、研究进程和被打开文件的关系 &#xff08;一&#xff09;w方式 &#xff08;二&#xff09;a方式 三、认识系统接口&#xff0c;操作文件 &#xff08;一&#xff09;认识文件描述符 &#xff08;二&#xff09;举例 &#xff08;三&#xff09;…

分类预测 | Matlab实现基于PSO-PNN粒子群算法优化概率神经网络的数据分类预测

分类预测 | Matlab实现基于PSO-PNN粒子群算法优化概率神经网络的数据分类预测 目录 分类预测 | Matlab实现基于PSO-PNN粒子群算法优化概率神经网络的数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现基于PSO-PNN粒子群算法优化概率神经网络的数据…

基于材料生成算法优化概率神经网络PNN的分类预测 - 附代码

基于材料生成算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于材料生成算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于材料生成优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

Java基础(程序控制结构篇)

Java的程序控制结构与C语言一致&#xff0c;分为顺序结构、选择结构&#xff08;分支结构&#xff09;和循环结构三种。 一、顺序结构 如果程序不包含选择结构或是循环结构&#xff0c;那么程序中的语句就是顺序的逐条执行&#xff0c;这就是顺序结构。 import java.util.Sc…

linux之进程地址空间

文章目录 1.进程地址空间回顾1.1进程地址空间划分1.2验证进程地址空间划分1.简单划分2.完整划分 2.初探进程地址空间2.1初看现象2.2Makefile的简便写法 3.进程地址空间详解3.1地址空间是什么?3.2地址空间的设计/由来3.3空间区域划分3.4如何理解地址空间?3.5解释3.2的&#x1…

在中国企业出海的大浪潮下,亚马逊云科技提供遍及全球的基础设施和技术支持

中国技术出海是中国企业更高层次更高质量的全球化。在人类文明发展史上&#xff0c;凝聚中国古人智慧结晶的造纸术、印刷术、火药、指南针等&#xff0c;曾为中国技术出海写下过浓墨重彩的一笔。在今天&#xff0c;如金山办公、店匠科技、ADVANCE.AI等公司又以技术立业&#xf…

最受欢迎的猫罐头有那些?精选的5款热门猫罐头推荐!

新手养猫很容易陷入疯狂购买的模式&#xff0c;但有些品牌真的不能乱买&#xff01;现在的大环境不太好&#xff0c;我们需要学会控制自己的消费欲望&#xff0c;把钱花在刀刃上&#xff01;现在宠物市场真的很内卷&#xff0c;很多品牌都在比拼产品的数据和营养成分。很多铲屎…