洛阳霞光建设网站,cpanel 安装wordpress,微网站开发微网站建设,网站建设 淘宝描述psd目录
构建复杂模型
类型擦除
C中的泛型
迁移的兼容性
类型擦除存在的问题
边界的行为
对类型擦除的补偿
创建类型实例
泛型数组 本笔记参考自#xff1a; 《On Java 中文版》 构建复杂模型 泛型的一个优点就是#xff0c;能够简单且安全地创建复杂模型。
【例子中的泛型
迁移的兼容性
类型擦除存在的问题
边界的行为
对类型擦除的补偿
创建类型实例
泛型数组 本笔记参考自 《On Java 中文版》 构建复杂模型 泛型的一个优点就是能够简单且安全地创建复杂模型。
【例子生成更复杂的数据结构】
import onjava.Tuple4;import java.util.ArrayList;public class TupleListA, B, C, Dextends ArrayListTuple4A, B, C, D {public static void main(String[] args) {TupleListVehicle, 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());}Overridepublic String toString() {return id description , 价格 price 元;}public void priceChange(double change) {price change;}public static SupplierProduct generator new SupplierProduct() {private Random rand new Random(47);Overridepublic Product get() {return new Product(rand.nextInt(1000),某件商品, Math.round(rand.nextDouble() * 1000.0) 0.99);}};
}// 构建货架模型
class Shelf extends ArrayListProduct {Shelf(int nProducts) {// Suppliers需要由自己进行实现Suppliers.fill(this, Product.generator, nProducts);}
}// 构建通道模型
class Aisle extends ArrayListShelf {Aisle(int nShelves, int nProducts) {for (int i 0; i nShelves; i)add(new Shelf(nProducts));}
}class CheckOutStand {
}class Office {
}// 最终组合成了一个商店的模型
public class Store extends ArrayListAisle {private ArrayListCheckOutStand 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));}Overridepublic 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但却无法使用ArrayListInteger.class。
【例子发现泛型的不合理】
import java.util.ArrayList;public class ErasedTypeEquivalence {public static void main(String[] args) {Class c1 new ArrayListString().getClass();Class c2 new ArrayListInteger().getClass();System.out.println(c1 c2);}
} 程序执行会返回true。 输出告诉我们ArrayListString和ArrayListInteger是相同的类型。但这是有问题的因为它们的行为并不相同。我们无法将Integer对象放入到ArrayListString却可以把Integer对象放入到ArrayListInteger中。 除此之外Java的泛型还有一个更麻烦的特性
【例子泛型代码内部的信息】
import java.util.*;class Frob {
}class Fnorkle {
}class QuarkQ {
}class ParticlePOSITION, MOMENTUM {
}public class LostInformation {public static void main(String[] args) {ListFrob list new ArrayList();System.out.println(Arrays.toString(list.getClass().getTypeParameters()));MapFrob, Fnorkle map new HashMap();System.out.println(Arrays.toString(map.getClass().getTypeParameters()));QuarkFnorkle quark new Quark();System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));ParticleLong, Double particle new Particle();System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));}
} 程序执行的结果是 Class.getTypeParameters()方法会返回一个由类型变量的对象组成的数组表示泛型对象声明所声明的类型变量。这似乎表示着我们可以获取与泛型参数有关的类型信息。但结果是我们只能发现作为参数占位符的标识符。 这就说明在Java的泛型代码内部并不存在有关泛型参数类型的可用信息。 而C等语言是可以在泛型内部获取类型信息的。 Java的泛型是通过类型擦除实现的。因此在使用泛型时任何具体的类型信息都会被擦除。在泛型内部唯一能够知道的事情就是我们在使用这个对象。因此ArrayListString和ArrayListInteger在运行时都被“擦除”成了它们的原始类型raw typeArrayList。
这种通过类型擦除实现的泛型有时也被称为第二类泛型类型
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;ManipulatorHasF manipulator(hf);manipulator.manipulate();
} 编译并执行程序可得 C编译器会在实例化模板时进行检测。因此在实例化ManipulatorHasF时编译器会发现HasF中存在着方法f()。 接下来再尝试通过Java实现同样的效果
【例子在Java中进行尝试】 首先编写一个HasF类
public class HasF {public void f(){System.out.println(HasF.f());}
} 但接下来的部分却没办法如C一样书写。若我们尝试调用方法obj.f()编译器就会提示我们 因为类型擦除的缘故编译器不会知道ManipulatorHasF的类型参数是HasF因此会认为这种调用是不安全的。若想要调用f()我们就必须人为规定泛型类的边界帮助编译器确定符合边界的类型
public class Manipulator2T 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 ReturnGenericTypeT extends HasF {private T obj;ReturnGenericType(T x) {obj x;}public T get() {return obj;}
} 迁移的兼容性 注意类型擦除并不是一项语言特性。它是Java在实现泛型时使用的一种必要的折中因为泛型并不是这门语言与生俱来的一部分。 因此Java中的泛型并没有将类型参数具体化成第一类实体的能力。 因为类型擦除泛型类型被视同第二类类型处理这使得其无法在一些重要的上下文中得到使用泛型类型只会在静态类型检查时存在之后程序会将泛型类型擦除成它们的非泛型上界。 在Java 5之前存在许多编写完毕的非泛型的库。库是一门语言重要的组成部分无法被轻易抛弃。因此Java的泛型设计必然需要保证向后兼容性和迁移兼容性。前者保证原有的数据依旧合法后者则需要协调泛化的程序与非泛化的库反之亦然。
||| 至于类型擦除是否是一种好的手段就只能靠时间来验证了。 类型擦除存在的问题 类型擦除在非泛化代码和泛化代码之间构建起了一座桥梁泛型得以在不破坏现有库的情况下加入Java。 然而这种做法是有代价的。泛型代码无法用于需要显式引用运行时类型的操作例如类型转换、instanceof操作以及new表达式。在编写泛型代码时我们只是看起来掌握了参数的类型信息。就比如现在有一个泛型类
class FooT {T var;
}
若为它创建一个实例
FooCat f new Foo();
尽管不论是直观的理解或是语法本身带来的暗示都在说明T已经被替换成了Cat。遗憾的是泛型内部的T已经只是一个Object。 泛型擦除像是一个边界在边界里面的成员会被擦除成原始的类型。只有在进出边界时它们才会被转换成对应的类型。 另外因为类型擦除和迁移兼容性Java中泛型的使用并非是强制性的。
【例子不强制的泛型使用】
class GenericBaseT {private T element;public void set(T arg) {element arg;}public T get() {return element;}
}// 使用泛型
class Derived1T extends GenericBaseT {}// 使用原始类型但未发出警告
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 ArrayMakerT {private ClassT kind;public ArrayMaker(ClassT kind) {this.kind kind;}SuppressWarnings(unchecked)T[] create(int size) { // 需要使用类型转换return (T[]) Array.newInstance(kind, size);}public static void main(String[] args) {ArrayMakerString stringMaker new ArrayMaker(String.class);String[] stringArray stringMaker.create(9);System.out.println(Arrays.toString(stringArray));}
} 程序执行的结果是 在这个例子中尽管kind看起来会获得一个具体的ClassT但当进入类型擦除的边界时T就会消失。换言之kind中存储的只是一个无意义的Class它无法生成一个具体的结果因此我们还需要使用到类型转换并且会产生警告。 在create()方法中使用到的Array.newInstance()是在泛型中创建数组的推荐方法。 我们也可以使用泛型创建集合而不是数组
【例子无意义的集合】
import java.util.ArrayList;
import java.util.List;public class ListMakerT {ListT create() {return new ArrayList();}public static void main(String[] args) {ListMakerString stringMaker new ListMaker();ListString 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 FilledListT extends ArrayListT {FilledList(SupplierT 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) {ListString list new FilledList(Hello, 4);System.out.println(list);// 也可以借由Supplier接口进行实现ListInteger 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 ClassTypeCaptureT {ClassT kind;public ClassTypeCapture(ClassT kind) {this.kind kind;}public boolean f(Object arg) {return kind.isInstance(arg);}public static void main(String[] args) {ClassTypeCaptureBuilding ctt1 new ClassTypeCapture(Building.class);System.out.println(ctt1.f(new Building()));System.out.println(ctt1.f(new House()));ClassTypeCaptureHouse 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允许创建泛型的类型实例】
templateclass T class Foo {T x; // 字段xT* y; // 指向T类的指针
public:Foo() {// 初始化指针y new T();}
};class Bar {};int main()
{FooBar fb;Fooint fi; // 甚至可以使用基本类型return 0;
}
--- 而Java则需要使用工厂设计方法来创建新的实例。Class就是一个方便的工厂对象将其作为类型标签我们能够在Java中实现类似上例的功能
【例子通过newInstance()创建泛型对象】
import java.util.function.Supplier;class ClassAsFactoryT implements SupplierT {ClassT kind;ClassAsFactory(ClassT kind) {this.kind kind;}Overridepublic T get() {try {return kind.getConstructor().newInstance();} catch (Exception e) {throw new RuntimeException(e);}}
}class Employee {public Employee() {}Overridepublic String toString() {return Employee;}
}public class InstantiateGenericType {public static void main(String[] args) {ClassAsFactoryEmployee fe new ClassAsFactory(Employee.class);System.out.println(fe.get());ClassAsFactoryInteger 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 SupplierInteger {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 SupplierWidget {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 Foo2T {private ListT x new ArrayList();Foo2(SupplierT factory) {// 等价于使用Stream.generate(factory)生成5个元素并装入该类中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));}
} 程序执行的结果是 Foo2类用于调用各种工厂方法生成实例。这里展示了三种创建工厂的方式
IntegerFactory本身就是一个实现了SupplierInteger的工厂Widget包含了一个作为工厂的内部类Fudge不执行任何类似工厂的操作但编译器会自动将Fudge::new转变成对get的调用。 除此之外还有另一种设计模式模板方法。将方法在子类中进行重写用来生成对应类型的对象
【例子使用模板方法生成泛型实例】
abstract class GenericWithCreateT {final T element;GenericWithCreate() {element create();}// 会在子类中重写的模板方法abstract T create();
}class X {
}class XCreator extends GenericWithCreateX {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有唯一的无参构造器这样就可以要求任何所有这个类的程序员必须通过我们规定的方式初始化这个类。另一边create()方法将类的创建逻辑交付给了子类实现这使得该方法的返回值可以在子类中得到更具体的定义。 泛型数组 正如之前所看到的我们无法直接在泛型中创建泛型数组 一个直接的方法是使用集合来代替数组
【例子使用集合替代数组】
import java.util.ArrayList;
import java.util.List;public class ListOfGenericsT {private ListT array new ArrayList();public void add(T item) {array.add(item);}public T get(int index) {return array.get(index);}
} 这样我们就获得了数组的行为并且得到了泛型提供的编译时类型检查。 但如果确实有使用泛型数组的必要那么可以尝试使用一个泛型引用通过将这个引用指向一个数组可以变相满足编译器的规定
【例子将引用指向数组】
class GenericT {
}public class ArrayOfGenericReference {static GenericInteger[] gia;
} 因为类型擦除这个数组实际上没有具体的类型无论指定的泛型参数是什么数组都会具有相同的结构和大小。这看上去有点像Object那么我们是否可以将一个Object类型的数组转换成目标数组 答案依旧是否定的
【例子无法对Object数组进行转型】
public class ArrayOfGeneric {static final int SIZE 100;static GenericInteger[] gia;SuppressWarnings(unchecked)public static void main(String[] args) {try {gia (GenericInteger[]) new Object[SIZE];} catch (ClassCastException e) {System.out.println(e.getMessage());}// 运行时会发生类型擦除得到的是原始类型Generic[]gia (GenericInteger[]) new Generic[SIZE];System.out.println(gia.getClass().getSimpleName());gia[0] new Generic();// 发生编译时错误类型不匹配// gia[1] new Object();// gia[2] new GenericDouble();}
} 程序执行的结果是输出已经过折叠 数组的类型在它们被创建的时候才会确定下来因此转型信息GenericInteger[]也只会存在于编译时。语句
gia (GenericInteger[]) new Object[SIZE];
能得到的只会是Object数组这就会导致问题。 而另一条创建语句
gia (GenericInteger[]) new Generic[SIZE];
对一个被擦除类型的数组进行强制类型转换得到了成功。这也是唯一可以成功创建泛型数组的方式。 以此类推下面是一个更加复杂的例子
【例子更复杂的泛型数组尝试】
public class GenericArrayT {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) {GenericArrayInteger 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 GenericArray2T {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) {GenericArray2Integer 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 GenericArrayWithTypeTokenT {private T[] array;SuppressWarnings(unchecked)public GenericArrayWithTypeToken(ClassT 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) {GenericArrayWithTypeTokenInteger gai new GenericArrayWithTypeToken(Integer.class, 10);// 现在可以正常运行Integer[] ia gai.rep();}
} 尽管还是需要抑制警告但在这个例子中数组在运行时是精确的T[]类型了。 然而在Java的源代码中也存在着许都使用Object数组转型为参数化类型的操作对其编译甚至会产生警告……因此Java的库代码难以作为我们自己编写代码时的范例