Java编程思想__泛型(四)

边界处的动作

  • 正是因为有了擦除,我们发现泛型最令人困惑的方面源自这样一个事实,即可以表示没有任何意义的事物。
public class ArrayMaker {

    private Class tClass;

    public ArrayMaker(Class tClass) {
        this.tClass = tClass;
    }

    public T[] create(int size){
        return (T[])Array.newInstance(tClass,size);
    }
    public static void main(String[] args) {
        ArrayMaker arrayMaker=new ArrayMaker<>(String.class);
        String[] strings = arrayMaker.create(9);
        System.out.println(Arrays.toString(strings));
    }
}

//运行结果为

[null, null, null, null, null, null, null, null, null]
  1. 即使 tClass 被存储为 Class ,擦除也意味着它实际将被存储为 Class,没有任何参数。因此, 当你正在使用它时,例如在创建数组 Array.newInstance() 实际上并未拥有 tclass所蕴含的类型信息,因此这不会产生具体的结果,所以必须转型,这将产生一条令你无法满意的警告。
  2. 注意, 对于在泛型中创建数组,使用Array.newInstance()是推荐的方式。
  3. 如果我们要创建一个容器而不是数组,情况就有些不同了:
public class ArrayMaker1 {
    List create(){
        return new ArrayList();
    }
    public static void main(String[] args) {
        ArrayMaker1 maker1 = new ArrayMaker1<>();
        List strings = maker1.create();
        System.out.println(strings);
    }
}

//运行结果
[]
  1. 编译器不会给出任何警告,尽管我们(从擦除中)知道在 create() 内部的 new ArrayList 中的 被移除了 ___在运行时,这个类的内部没有任何 ,因此这看起来毫无意义。但是如果你遵从这种思路,并将这个表达式改为 new ArrayList() , 编译器就会给出警告。
  2. 在本例中,这是否真的毫无意义呢? 如果返回list之前,将某些东西放入其中,就像下面这样,情况又会如何呢?
public class ArrayMark2 {
    List create(T t,int size){
        List list=new ArrayList<>();
        for (int i = 0; i < size; i++) {
            list.add(t);
        }
        return list;
    }

    public static void main(String[] args) {
        ArrayMark2 arrayMark2=new ArrayMark2<>();
        List list = arrayMark2.create("hello", 3);
        System.out.println(list);
    }
}
//运行结果为

[hello, hello, hello]
  1. 即使编译器无法知道有关 create() 中的 T 的任何信息, 但是它仍旧可以在编译期确保你放置到 list 中的对象具有 T 类型,使其适合 ArrayList
  2. 因此, 即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。
  3. 因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界: 即对象进入和离开的地点。这些正是编译器在编译器执行类型检查并插入转型代码的地点。
public class SimpleHolder {
    private Object obj;

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.setObj("Item");
        String obj = (String) holder.getObj();
    }
}
  1. 用 javap -c 反编译 SimpleHolder 类,得到如下内容
 public java.lang.Object getObj();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void setObj(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class generic/SimpleHolder
       3: dup
       4: invokespecial #4                  // Method "":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
  1. setObj() 和 getObj() 方法将直接存储和产生值,而转型是在调用 getObj() 的时候接受检查的。
  2. 现在将泛型合并到上面的代码中:
public class SimpleHolder {
    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }

    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.setObj("Item");
        String obj = holder.getObj();
    }
}

//从 getObj() 返回之后的转型消失了, 但是我们还知道传递给setObj() 的值在编译器会接受检查,下面是相关字节码。

 public T getObj();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void setObj(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return
public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class generic/GenericHolder
       3: dup
       4: invokespecial #4                  // Method "":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
  1. 所产生的的字节码是相同的。对进入setObj() 的类型检查是不需要的,因为这将由编译器执行。
  2. 而对从 getObj() 返回的值进行转型仍旧是需要的,但这与你自己必须执行的操作是一样的___此处它将由编译器自动插入,因此你写入和读取的代码的噪声将更小。
  3. 由于所产生的 getObj() 和 setObj() 的字节码相同,所以在泛型中的所有动作都发生在边界处___对传递进来的值进行额外的编译期检查,并插入对传递出去值的转型。这有助于澄清对擦除的混淆,记住: 边界就是发生动作的地方

  

擦除的补偿

  • 正如我们看到的,擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作。
public class Erased {
    
    private final int SIZE=100;
    
    static void f(Object obj){
        //下面这段代码是错误的 
        if (obj instanceof  T){  
            T t=new T();
            T [] array=new T[SIZE];
            T[] arrays=(T)Object[SIZE]; //未经检查的警告
        }
    }
}
  1. 偶尔可以绕过这些问题来编程,但是有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显式地传递你的类型的 Class 对象,以便你可以在类型的表达式中使用它。
  2. 例如,在前面实例中对使用 instanceof 的尝试最终失败了,因为其类型信息已经被擦除了。如果引入类型标签,就可以转而使用动态的 isInstance();
class Building{}

class House extends Building{}

public class Erased {
    Class tClass;
    public Erased(Class tClass) {
        this.tClass = tClass;
    }
    boolean f(Object obj){
        return tClass.isInstance(obj);
    }
    public static void main(String[] args) {
        //1, 创建泛型 为 Building类
        Erased erased=new Erased<>(Building.class);
        System.out.println(erased.f(new House()));
        System.out.println(erased.f(new Building()));

        //2,创建泛型为 House类
        Erased erased1=new Erased<>(House.class);
        System.out.println(erased1.f(new Building()));
        System.out.println(erased1.f(new House()));
    }
}

//运行结果为

true
true
false
true
  1. 编译器将确保类型标签可以匹配泛型参数。

 

创建类型实例

  • 在 Erased.java(编译错误的版本中) 中对创建一个 new T() 的尝试讲将无法实现,部分原因是因为擦除,而另一部分原因就是因为编译器不能验证T具有默认(无参)构造器。
  • Java中的解决方案是传递一个工厂对象,并用它来创建新的实例。最便利的工厂对象就是Class 对象,因此如果使用类型标签,那么你就可以使用 newInstance() 来创建类型的新对象。
public class ClassAsFactory {
    T t;
    public ClassAsFactory(Class tClass) {
        try {
            t = tClass.newInstance();
        } catch (Exception e) {
           throw new RuntimeException(e);
        }
    }
}

class Employee{}

class InstantiateGenericType{
    public static void main(String[] args) {
        ClassAsFactory employeeClassAsFactory=new ClassAsFactory<>(Employee.class);
        System.out.println("ClassAsFactory succeeded");

        try {
            ClassAsFactory classAsFactory=new ClassAsFactory<>(Integer.class);
        }catch (Exception e){
            System.out.println(" ClassAsFactory failed");
        }
    }
}

//运行结果为
ClassAsFactory succeeded
ClassAsFactory failed
  1. 这可以编译,但是会因 ClassAsFactory 而失败,因为Integer 没有任何默认的构造器。因为这个错误不是在编译期间捕获的,所以Sun 的伙计们对这种方式并不赞成,他们建议使用显式的工厂,并将限制其类型,使得只能接受实现了这个工厂的类。
interface Factory{
    T create();
}

//创建对象工厂
class Foo2{
    private T t;
    public > Foo2(F factory){
        t=factory.create();
    }
}

class IntegerFactory implements Factory{

    @Override
    public Integer create() {
        return new Integer(0);
    }
}

class Widget{
    static class TestFactory implements Factory{
        @Override
        public Widget create() {
            return new Widget();
        }
    }
}


public class FactoryConstraint {
    public static void main(String[] args) {
        new Foo2<>(new IntegerFactory());
        new Foo2<>(new Widget.TestFactory());
    }
}
  1. 注意,这确实只是传递 Class 的一种变体。两种方式都传递了工厂对象,Class 碰巧是内建的工厂对象,而上面的方式创建了一个显式的工厂对象,但是你却获得了编译期检查。
  2. 另一种方式是模板方法设计模式。在下面的示例中, get() 是模板方法,而create() 在子类中定义的,用来生成子类类型的对象。
public abstract class GenericWithCreate {
    final T element;

     GenericWithCreate() {
        this.element = create();
    }

    abstract T create();
}

class X{}

class Creator extends GenericWithCreate{

    @Override
    X create() {
        return new X();
    }

    void f(){
        System.out.println(element.getClass().getSimpleName());
    }
}

class CreatorGeneric{
    public static void main(String[] args) {
        Creator creator = new Creator();
         creator.f();
    }
}

//运行结果为
X

 

 

泛型数组

  • 正如你在 Erased.java中所见(Erased 类中出现错误的版本),不能创建泛型数组。一般的解决方案是在任何要创建泛型数组的地方都使用 ArrayList
public class ListOfGenerics {

    private List lists=new ArrayList<>();
    
    void add(T t){
        lists.add(t);
    }
    
    T get(int index){
        return lists.get(index);
    }
}
  1. 这里你将获取数组的行为,以及由泛型提供的编译器的类型安全。
  2. 有时,你仍旧希望创建泛型类型的数组(例如,ArrayList 内部使用的是数组)。有趣的是,可以按照编译器喜欢的方式来定义一个引用,例如
class Generic{}

class ArrayOfGenericReference{
    static Generic [] gia;
}
  1. 编译器将接受这个程序,而不会产生任何警告。但是,永远都不能创建这个确切类型的数组(包括类型参数),因此这有一点令人困惑。既然所有数组无论它们持有的类型如何,都具有相同的结构(每个数组槽位的尺寸和数组的布局),那么看起来你应该能够创建一个 Object 数组,并将其转型为所希望的数组类型。事实上这可以编译,但是不能运行,它将产ClassCaseException。
public class ArrayOfGeneric {
    static final int SIZE = 100;
    static Generic[] gia;

    public static void main(String[] args) {
        //compiles produces classCastException
        //编译产生 classCastException
        //gia=(Generic[])new Object[SIZE];

        //运行时类型是原始(擦除)类型
        gia = (Generic[]) new Generic[SIZE];
        System.out.println(gia.getClass().getSimpleName());

        gia[0] = new Generic();

        
        //编译时错误
        // gia[1]=new Object();

        //在编译时发现类型不匹配
        //gia[2]=new Generic();

    }
}

//运行结果为

Generic[]
  1. 问题在意数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此,即使 gia 已经被转型为 Generic[] ,而这个信息只存于编译期。
  2. 在运行时,它仍旧是Object数组,而这将引发问题。成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。
public class GenericArray {

    private T [] array;

    public GenericArray(int size) {
        array= (T[]) new Object[size];
    }

    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) {
        GenericArray genericArray =new GenericArray<>(5);
        //ClassCastException
        //Integer [] integers=genericArray.rep();

        //下面这个是没问题的
        java.lang.Object [] objects=genericArray.rep();
    }
}
  1. 与前面相同,我们并不能声明 T[] array=new T[size],因此我们创建了一个对象数组,然后对其转型。
  2. rep() 方法将返回 T[] ,它在 main() 中将用于  genericArray ,因此如果调用它,并尝试着将结果作为 Integer [] 引用来捕获,就会得到 ClassCastException ,这还是因为实际的运行时类型是 Object[]。
  3. 因为有了擦除,数组在运行时类型就只能是 Object[] ,如果我们立即将其转型为 T[] ,那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。
  4. 正因为这样,最好是在集合内部使用 Object[] ,然后当你使用数组元素时,添加一个对T 的转型。让我们看看这是如何运作的如下。
public class GenericArray2 {

    private Object[] array;

    public GenericArray2(int size) {
        array=new Object[size];
    }

    public void put(int index,T item){
        array[index] =item;
    }

    public T get(int index){
        return (T) array[index];
    }

    public T[] rep(){
        return (T[]) array;
    }

    public static void main(String[] args) {
        GenericArray2 integerGenericArray2=new GenericArray2<>(5);

        for (int i = 0; i < 5; i++) {
            integerGenericArray2.put(i,i);
        }

        for (int i = 0; i < 5; i++) {
            Integer integer = integerGenericArray2.get(i);
            System.out.println(integer);
        }

        try {
            Integer[] integers = integerGenericArray2.rep();
        }catch (Exception e){
            System.out.println("异常了");
        }
    }
}

//运行结果为

0
1
2
3
4
异常了
  1. 初看起来,这好像没有多大变化,只是转型挪了地方。但是现在的内部是Object[] 而不是 T[] ,当get() 方法被调用时,它将对象转型为 T ,这实际上是正确的类型,因此这是安全的。
  2. 然而,如果你调用 rep() ,它还是尝试着将 Object[] 转型为 T[] ,这仍旧是不正确的,将在编译器产生警告,在运行时产生异常。因此,没有任何方式可以推翻底层的数据类型,它只能是 Object[] 。在内部将 array 当做Object[] 而不是T [] 处理的优势是: 我们不太可能忘记这个数组的运行时类型,从而以外地引入缺陷(尽管大多数也可能是所有这类缺陷都可以在运行时快速地探测到)
  3. 对于新代码,应该传递一个类型标记。在这种情况下, GenericArray 看起来会像下面这样。
public class GenericArrayWithTypeToken {

    private T[] array;

    public GenericArrayWithTypeToken(Class tClass,int size){
        array= (T[]) Array.newInstance(tClass,size);
    }

    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 integers=new GenericArrayWithTypeToken<>(Integer.class,5);
        Integer[] rep = integers.rep();
        //这个是没有错误的
    }
}
  1. 类型Class 被传递到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。
  2. 一旦我们获得了实际类型,就可以返回它,并获得想要的结果,就像在 main() 中看到的那样。该数组的运行时类型是确切类型T []。

 

你可能感兴趣的:(java编程思想)