• 初识Java 18-2 泛型


    目录

    构建复杂模型

    类型擦除

    C++中的泛型

    迁移的兼容性

    类型擦除存在的问题

    边界的行为

    对类型擦除的补偿

    创建类型实例

    泛型数组


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


    构建复杂模型

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

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

    1. import onjava.Tuple4;
    2. import java.util.ArrayList;
    3. public class TupleList
    4. extends ArrayList> {
    5. public static void main(String[] args) {
    6. TupleList tl =
    7. new TupleList<>();
    8. tl.add(TupleTest2.h());
    9. tl.add(TupleTest2.h());
    10. tl.forEach(System.out::println);
    11. }
    12. }

            程序执行的结果是:

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

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

    1. import onjava.Suppliers;
    2. import java.util.ArrayList;
    3. import java.util.Random;
    4. import java.util.function.Supplier;
    5. // 构建商品模型
    6. class Product {
    7. private final int id;
    8. private String description;
    9. private double price;
    10. Product(int idNumber, String descr, double price) {
    11. id = idNumber;
    12. description = descr;
    13. this.price = price;
    14. System.out.println(toString());
    15. }
    16. @Override
    17. public String toString() {
    18. return id + ":" + description +
    19. ", 价格:" + price + "元";
    20. }
    21. public void priceChange(double change) {
    22. price += change;
    23. }
    24. public static Supplier generator =
    25. new Supplier() {
    26. private Random rand = new Random(47);
    27. @Override
    28. public Product get() {
    29. return new Product(rand.nextInt(1000),
    30. "某件商品", Math.round(
    31. rand.nextDouble() * 1000.0) + 0.99);
    32. }
    33. };
    34. }
    35. // 构建货架模型
    36. class Shelf extends ArrayList {
    37. Shelf(int nProducts) {
    38. // Suppliers需要由自己进行实现
    39. Suppliers.fill(this, Product.generator, nProducts);
    40. }
    41. }
    42. // 构建通道模型
    43. class Aisle extends ArrayList {
    44. Aisle(int nShelves, int nProducts) {
    45. for (int i = 0; i < nShelves; i++)
    46. add(new Shelf(nProducts));
    47. }
    48. }
    49. class CheckOutStand {
    50. }
    51. class Office {
    52. }
    53. // 最终组合成了一个商店的模型
    54. public class Store extends ArrayList {
    55. private ArrayList checkouts =
    56. new ArrayList<>();
    57. private Office office = new Office();
    58. public Store(
    59. int nAisles, int nShelves, int nProducts) {
    60. for (int i = 0; i < nAisles; i++)
    61. add(new Aisle(nShelves, nProducts));
    62. }
    63. @Override
    64. public String toString() {
    65. StringBuffer result = new StringBuffer();
    66. for (Aisle a : this)
    67. for (Shelf s : a)
    68. for (Product p : s) {
    69. result.append(p);
    70. result.append("\n");
    71. }
    72. return result.toString();
    73. }
    74. public static void main(String[] args) {
    75. System.out.println(new Store(3, 2, 2));
    76. }
    77. }

            程序执行的结果是:

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

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

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

    类型擦除

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

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

    1. import java.util.ArrayList;
    2. public class ErasedTypeEquivalence {
    3. public static void main(String[] args) {
    4. Class c1 = new ArrayList().getClass();
    5. Class c2 = new ArrayList().getClass();
    6. System.out.println(c1 == c2);
    7. }
    8. }

            程序执行,会返回true

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

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

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

    1. import java.util.*;
    2. class Frob {
    3. }
    4. class Fnorkle {
    5. }
    6. class Quark {
    7. }
    8. class Particle {
    9. }
    10. public class LostInformation {
    11. public static void main(String[] args) {
    12. List list = new ArrayList<>();
    13. System.out.println(Arrays.toString(
    14. list.getClass().getTypeParameters()));
    15. Map map = new HashMap<>();
    16. System.out.println(Arrays.toString(
    17. map.getClass().getTypeParameters()));
    18. Quark quark = new Quark<>();
    19. System.out.println(Arrays.toString(
    20. quark.getClass().getTypeParameters()));
    21. Particle particle = new Particle<>();
    22. System.out.println(Arrays.toString(
    23. particle.getClass().getTypeParameters()));
    24. }
    25. }

            程序执行的结果是:

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

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

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

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

    C++中的泛型

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

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

    1. #include
    2. using namespace std;
    3. template <class T>
    4. class Manipulator {
    5. T obj;
    6. public:
    7. Manipulator(T x) {
    8. obj = x;
    9. }
    10. void manipulate() {
    11. obj.f();
    12. }
    13. };
    14. class HasF {
    15. public:
    16. void f() {
    17. cout << "HasF::f()" << endl;
    18. }
    19. };
    20. int main() {
    21. HasF hf;
    22. Manipulator manipulator(hf);
    23. manipulator.manipulate();
    24. }

            编译并执行程序,可得:

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

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

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

            首先编写一个HasF类:

    1. public class HasF {
    2. public void f(){
    3. System.out.println("HasF.f()");
    4. }
    5. }

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

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

    1. public class Manipulator2extends HasF> {
    2. private T obj;
    3. Manipulator2(T x) {
    4. obj = x;
    5. }
    6. public void manipulator() {
    7. obj.f();
    8. }
    9. }

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

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

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

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

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

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

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

    1. public class ReturnGenericTypeextends HasF> {
    2. private T obj;
    3. ReturnGenericType(T x) {
    4. obj = x;
    5. }
    6. public T get() {
    7. return obj;
    8. }
    9. }

    迁移的兼容性

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

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

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

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

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


    类型擦除存在的问题

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

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

    1. class Foo {
    2. T var;
    3. }

    若为它创建一个实例:

    Foo f = new Foo<>();

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

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

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

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

    1. class GenericBase {
    2. private T element;
    3. public void set(T arg) {
    4. element = arg;
    5. }
    6. public T get() {
    7. return element;
    8. }
    9. }
    10. // 使用泛型:
    11. class Derived1 extends GenericBase {}
    12. // 使用原始类型,但未发出警告:
    13. class Derived2 extends GenericBase {}
    14. // 引发错误:
    15. //class Derived3 extends GenericBase {}
    16. public class ErasureAndInheritance {
    17. @SuppressWarnings("unchecked")
    18. public static void main(String[] args) {
    19. Derived2 d2 = new Derived2();
    20. Object obj = d2.get();
    21. d2.set(obj); // d2.set()会引发警告,使用@SuppressWarnings()进行关闭
    22. }
    23. }

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

    @SuppressWarnings("unchecked")

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

            Derived3会引发错误:

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

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


    边界的行为

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

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

    1. import java.lang.reflect.Array;
    2. import java.util.Arrays;
    3. public class ArrayMaker {
    4. private Class kind;
    5. public ArrayMaker(Class kind) {
    6. this.kind = kind;
    7. }
    8. @SuppressWarnings("unchecked")
    9. T[] create(int size) { // 需要使用类型转换
    10. return (T[]) Array.newInstance(kind, size);
    11. }
    12. public static void main(String[] args) {
    13. ArrayMaker stringMaker =
    14. new ArrayMaker<>(String.class);
    15. String[] stringArray = stringMaker.create(9);
    16. System.out.println(Arrays.toString(stringArray));
    17. }
    18. }

            程序执行的结果是:

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

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

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

    【例子:无意义的集合】

    1. import java.util.ArrayList;
    2. import java.util.List;
    3. public class ListMaker {
    4. List create() {
    5. return new ArrayList<>();
    6. }
    7. public static void main(String[] args) {
    8. ListMaker stringMaker = new ListMaker<>();
    9. List stringList = stringMaker.create();
    10. }
    11. }

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

    ---

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

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

    1. import onjava.Suppliers;
    2. import java.util.ArrayList;
    3. import java.util.List;
    4. import java.util.function.Supplier;
    5. public class FilledList extends ArrayList {
    6. FilledList(Supplier gen, int size) {
    7. // 等价于使用Stream.generate(gen)生成size个元素,并装入该类中
    8. Suppliers.fill(this, gen, size);
    9. }
    10. public FilledList(T t, int size) {
    11. for (int i = 0; i < size; i++)
    12. this.add(t);
    13. }
    14. public static void main(String[] args) {
    15. List list = new FilledList<>("Hello", 4);
    16. System.out.println(list);
    17. // 也可以借由Supplier接口进行实现
    18. List ilist = new FilledList<>(() -> 47, 4);
    19. System.out.println(ilist);
    20. }
    21. }

            程序执行的结果是:

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

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

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

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

    对类型擦除的补偿

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

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

            类型标签与instanceof的不同之于,instanceof是静态的检查,若类型被擦除,那么instanceof就会失效。而类型标签可以通过isInstance()提供动态的检查,这使得它可以在泛型中进行使用:

    【例子:使用类型标签】

    1. class Building {
    2. }
    3. class House extends Building {
    4. }
    5. public class ClassTypeCapture {
    6. Class kind;
    7. public ClassTypeCapture(Class kind) {
    8. this.kind = kind;
    9. }
    10. public boolean f(Object arg) {
    11. return kind.isInstance(arg);
    12. }
    13. public static void main(String[] args) {
    14. ClassTypeCapture ctt1 =
    15. new ClassTypeCapture<>(Building.class);
    16. System.out.println(ctt1.f(new Building()));
    17. System.out.println(ctt1.f(new House()));
    18. ClassTypeCapture ctt2 =
    19. new ClassTypeCapture<>(House.class);
    20. System.out.println(ctt2.f(new Building()));
    21. System.out.println(ctt2.f(new House()));
    22. }
    23. }

            程序执行的结果是:

            编译器会确保类型标签能够与泛型参数相匹配。

    创建类型实例

            在Erased.java中执行new T()操作是无法成功的,这有两个原因:①类型擦除和②编译器无法验证T中是否存在无参构造器。但C++却支持这种操作:

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

    1. template<class T> class Foo {
    2. T x; // 字段x
    3. T* y; // 指向T类的指针
    4. public:
    5. Foo() {
    6. // 初始化指针
    7. y = new T();
    8. }
    9. };
    10. class Bar {};
    11. int main()
    12. {
    13. Foo fb;
    14. Foo<int> fi; // 甚至可以使用基本类型
    15. return 0;
    16. }

    ---

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

    【例子:通过newInstance()创建泛型对象】

    1. import java.util.function.Supplier;
    2. class ClassAsFactory implements Supplier {
    3. Class kind;
    4. ClassAsFactory(Class kind) {
    5. this.kind = kind;
    6. }
    7. @Override
    8. public T get() {
    9. try {
    10. return kind.getConstructor().newInstance();
    11. } catch (Exception e) {
    12. throw new RuntimeException(e);
    13. }
    14. }
    15. }
    16. class Employee {
    17. public Employee() {
    18. }
    19. @Override
    20. public String toString() {
    21. return "Employee";
    22. }
    23. }
    24. public class InstantiateGenericType {
    25. public static void main(String[] args) {
    26. ClassAsFactory fe =
    27. new ClassAsFactory<>(Employee.class);
    28. System.out.println(fe.get());
    29. ClassAsFactory fi =
    30. new ClassAsFactory<>(Integer.class);
    31. try {
    32. System.out.println(fi.get());
    33. } catch (Exception e) {
    34. System.out.println(e.getMessage());
    35. }
    36. }
    37. }

            程序执行的结果是:

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

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

    1. import onjava.Suppliers;
    2. import java.util.ArrayList;
    3. import java.util.List;
    4. import java.util.function.Supplier;
    5. class IntegerFactory implements Supplier {
    6. private int i = 0;
    7. @Override
    8. public Integer get() {
    9. return ++i;
    10. }
    11. }
    12. class Widget {
    13. private int id;
    14. Widget(int n) {
    15. id = n;
    16. }
    17. @Override
    18. public String toString() {
    19. return "Widget " + id;
    20. }
    21. public static
    22. class Factory implements Supplier {
    23. private int i = 0;
    24. @Override
    25. public Widget get() {
    26. return new Widget(++i);
    27. }
    28. }
    29. }
    30. class Fudge {
    31. private static int count = 1;
    32. private int n = count++;
    33. @Override
    34. public String toString() {
    35. return "Fudge " + n;
    36. }
    37. }
    38. class Foo2 {
    39. private List x = new ArrayList<>();
    40. Foo2(Supplier factory) {
    41. // 等价于使用Stream.generate(factory)生成5个元素,并装入该类中
    42. Suppliers.fill(x, factory, 5);
    43. }
    44. @Override
    45. public String toString() {
    46. return x.toString();
    47. }
    48. }
    49. public class FactoryConstraint {
    50. public static void main(String[] args) {
    51. System.out.println(
    52. new Foo2<>(new IntegerFactory()));
    53. System.out.println(
    54. new Foo2<>(new Widget.Factory()));
    55. System.out.println(
    56. new Foo2<>(Fudge::new));
    57. }
    58. }

            程序执行的结果是:

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

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

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

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

    1. abstract class GenericWithCreate {
    2. final T element;
    3. GenericWithCreate() {
    4. element = create();
    5. }
    6. // 会在子类中重写的模板方法:
    7. abstract T create();
    8. }
    9. class X {
    10. }
    11. class XCreator extends GenericWithCreate {
    12. @Override
    13. X create() {
    14. return new X();
    15. }
    16. void f() {
    17. System.out.println(
    18. element.getClass().getSimpleName());
    19. }
    20. }
    21. public class CreatorGeneric {
    22. public static void main(String[] args) {
    23. XCreator xc = new XCreator();
    24. xc.f();
    25. }
    26. }

            程序执行的结果是:

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


    泛型数组

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

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

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

    1. import java.util.ArrayList;
    2. import java.util.List;
    3. public class ListOfGenerics {
    4. private List array = new ArrayList<>();
    5. public void add(T item) {
    6. array.add(item);
    7. }
    8. public T get(int index) {
    9. return array.get(index);
    10. }
    11. }

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

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

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

    1. class Generic {
    2. }
    3. public class ArrayOfGenericReference {
    4. static Generic[] gia;
    5. }

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

            答案依旧是否定的:

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

    1. public class ArrayOfGeneric {
    2. static final int SIZE = 100;
    3. static Generic[] gia;
    4. @SuppressWarnings("unchecked")
    5. public static void main(String[] args) {
    6. try {
    7. gia = (Generic[]) new Object[SIZE];
    8. } catch (ClassCastException e) {
    9. System.out.println(e.getMessage());
    10. }
    11. // 运行时会发生类型擦除,得到的是原始类型Generic[]
    12. gia = (Generic[]) new Generic[SIZE];
    13. System.out.println(gia.getClass().getSimpleName());
    14. gia[0] = new Generic<>();
    15. // 发生编译时错误(类型不匹配):
    16. // gia[1] = new Object();
    17. // gia[2] = new Generic();
    18. }
    19. }

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

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

    gia = (Generic[]) new Object[SIZE];

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

            而另一条创建语句:

    gia = (Generic[]) new Generic[SIZE];

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

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

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

    1. public class GenericArray {
    2. private T[] array;
    3. @SuppressWarnings("unchecked")
    4. public GenericArray(int sz) {
    5. array = (T[]) new Object[sz];
    6. }
    7. public void put(int index, T item) {
    8. array[index] = item;
    9. }
    10. public T get(int index) {
    11. return array[index];
    12. }
    13. // 通过返回T[],可以发现其的潜在表现形式:
    14. public T[] rep() {
    15. return array;
    16. }
    17. public static void main(String[] args) {
    18. GenericArray gai = new GenericArray<>(10);
    19. try {
    20. Integer[] ia = gai.rep();
    21. } catch (ClassCastException e) {
    22. System.out.println(e.getMessage());
    23. }
    24. // 可以使用Object数组接受:
    25. Object[] oa = gai.rep();
    26. }
    27. }

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

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

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

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

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

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

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

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

    1. public class GenericArray2 {
    2. private Object[] array;
    3. public GenericArray2(int sz) {
    4. array = new Object[sz];
    5. }
    6. public void put(int index, T item) {
    7. array[index] = item;
    8. }
    9. @SuppressWarnings("unchecked")
    10. public T get(int index) {
    11. return (T) array[index];
    12. }
    13. // 该方法依旧存在问题:为检测的类型转换
    14. @SuppressWarnings("unchecked")
    15. public T[] rep() {
    16. return (T[]) array;
    17. }
    18. public static void main(String[] args) {
    19. GenericArray2 gai =
    20. new GenericArray2<>(10);
    21. for (int i = 0; i < 10; i++)
    22. gai.put(i, i);
    23. for (int i = 0; i < 10; i++)
    24. System.out.print(gai.get(i) + " ");
    25. System.out.println();
    26. try {
    27. Integer[] ia = gai.rep();
    28. } catch (Exception e) {
    29. System.out.println(e);
    30. }
    31. }
    32. }

            程序执行的结果是:

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

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

    ---

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

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

    1. import java.lang.reflect.Array;
    2. public class GenericArrayWithTypeToken {
    3. private T[] array;
    4. @SuppressWarnings("unchecked")
    5. public GenericArrayWithTypeToken(Class type, int sz) {
    6. array = (T[]) Array.newInstance(type, sz);
    7. }
    8. public void put(int index, T item) {
    9. array[index] = item;
    10. }
    11. public T get(int index) {
    12. return array[index];
    13. }
    14. // 依旧会暴露潜在的表达方式:
    15. public T[] rep() {
    16. return array;
    17. }
    18. public static void main(String[] args) {
    19. GenericArrayWithTypeToken gai =
    20. new GenericArrayWithTypeToken<>(
    21. Integer.class, 10);
    22. // 现在可以正常运行:
    23. Integer[] ia = gai.rep();
    24. }
    25. }

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

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

  • 相关阅读:
    ansible常用模块的用法和ansible基于模块方式实现LNMP
    图像识别与处理学习笔记(二)图像增强之频率域处理
    Istio数据面新模式:Ambient Mesh技术解析
    Git指令
    GITEE 基于OAuth2的API V5版本
    python绘制立体玫瑰花
    vue使用pdf-dist实现pdf预览以及水印
    【VMware vCenter】VMware vCenter Server(VCSA) 5.5 版本证书过期问题处理过程。
    kali的安装与配置
    记一次 Sedona(GeoSpark) 空间计算优化
  • 原文地址:https://blog.csdn.net/w_pab/article/details/134443383