Java核心技术 泛型程序设计2

6.约束与限制性

Java泛型时需要考虑的一些限制,大多数限制由类型擦除引起。

不能用基本类型实例化类型参数

不能用类型参数代替基本类型。因此没有Pair< double >,只有Pair< Double >。其原因是类型擦除之后,Pair类含有Object类型的域,而Object不能存储double值。

运行时类型查询只适用于原始类型

虚拟机中的对象有一个特定的非泛型类型。因此所有的类型查询只产生原始类型。

if (a instanceof Pair<String>) // Error
if (a instanceof Pair<T>) // Error
Pair<String> p = (Pair<String>) a; // 警告,只能测试a是一个Pair

试图查询一个对象是否属于某个泛型类型时,使用instanceof会得到一个编译器错误,如果使用强制类型转换会得到一个警告。
同样的,getCalss方法总是返回原始类型:

Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass) // 相等

比较结果为true,这是因为两次调用getClass都返回Pair.class。

不能创建参数化类型的数组

Pair<String>[] table = new Pair<String>[10]; // Error

擦除之后table类型是Pair[],可以转换成Object[]:Object[] objarray = table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常:

Pair<String>[] table = new Pair[10];
objarray[0] = "Hello"; // Error,类型是Pair

不过对于泛型类型,擦除会使这种机制无效。

objarray[0] = new Pair<Employee>(); 

能够通过数组存储检查,不过仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。而声明类型为Pair< String >的变量仍是合法的。不过不能用new Pair< String >[10]初始化这个变量。

可以声明通配符类型的数组,然后进行类型转换:

Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];

结果不安全,如果table[0]中存储一个Pair< Employee >,然后对table[0].getFirst()调用一个String方法,会得到一个ClassCastException异常。如果需要收集参数化类型的对象,使用ArrayList< Pair< String > >更加安全有效。

Varargs警告

向参数个数可变的方法传递一个泛型类型的实例:

public static <T> void addAll(Collection<T> coll, T... ts){
	for (t : ts) {coll.add(t);}
}

实际上参数ts是一个数组,为了调用这个方法,Java虚拟机必须建立一个Pair< String >数组,这违反了前面的规则。但这种情况只会得到一个警告,而不是错误。
有两种方法抑制警告,一种是注解@SupperssWarning(“unckecked”)。或者在Java SE 7中,还可以用@SafeVarargs直接标注。

不能实例化类型变量

不能使用像new T(…),new T[…]或T.class这样的表达式中的类型变量。
在Java SE 8之后,最好的解决办法是让调用者提供一个构造器表达式:

Pair<String> p = Pair.makePair(String::new);

public static <T> Pair<T> makePair(Supplier<T> constr) {
    return new Pair<>(constr.get(), constr.get());
}

Supplier< T >是一个函数式接口,表示一个无参数而且返回类型为T的函数。

比较传统的解决方法是反射调用Class.newInstance方法来构造泛型对象。

Pair<String> p = Pair.makePair(String.class);

public static <T> Pair<T> makePair(Class<T> cl) {
    try {
        return new Pair<>(cl.newInstance(), cl.newInstance());
    } catch (Exception e) {
        return null;
    }
}

Class类本身是泛型。例如,String.class是一个Class< String >的实例(事实上,它是唯一的实例)。因此,makePair方法能够推断出pair
的类型。

不能构造泛型数组

就像不能实例化一个泛型实例一样,也不能实例化数组:

public static <T extends Comparable> T[] minmax(T[] a) {
	T[] mm = new T[2]; // Error,类型参数“T”不能直接实例化
	...
}

类型擦除会让这个方法永远构造Comparable[2]数组。
如果数组仅仅做为一个类的私有域,就可以将数组声明为Object[],并且在获取元素时进行类型转换。
自己实现ArrayList类:

public class ArrayList<E> {
    private Object[] emements;
    
    @SuppressWarnings("unchecked")
    public E get(int n) {
        return (E) emements[n];
    }
    
    public void set(int n, E e) {
        emements[n] = e;
    }
}

实际上的ArrayList的实现没有这么清晰:

public class ArrayList<E> {
	private E[] elements;
	public ArrayList() {
		elements = (E[]) new Object[10];
	}
}

这里的强制类型E[]是一个假象,而类型擦除使其无法察觉。

由于minmax方法返回T[]数组,使得这一技术无法施展,如果掩盖这个类型会有运行时错误结果:

public static <T extends Comparable> T[] minmax(T... a) {
	Object[] mm = new Object[2];
	...
	return (T[]) mm; // 编译时警告
}

调用该方法时候,编译时不会有任何警告。当Object[]引用赋值给Comparable[]变量时,将会发生ClassCastException异常。

在这种情况下,最好让用户提供一个数组构造器表达式,构造器表达式String::new指示一个函数,给定所需的长度,会构造一个指定长度的String数组:

String[] ss = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");

public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
    T[] mm = constr.apply(2);
    return mm;
}

比较老式的方法是利用反射,调用Array.newInstance:

public static <T extends Comparable> T[] minmax(T... a) {
    T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
    return mm;
}

ArrayList类的toArray方法,需要生成一个T[]数组,但没有成分类型,因此有两种不同的形式:
Object[] toArray()
T[] toArray(T[] result)
第二个方法接收一个数组参数。如果数组足够大,就是用数组。否则,用result的成分类型构造一个足够大的新数组。

泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。

public class Singleton<T> {
    private static T singleInstance; //Error
    
    public static T getSingleInstance() { //Error
        if (singleInstance == null) {
            return singleInstance;
        }
    }
}

类型擦除后,只剩下Singleton类,它只包含一个singleInstance域。因此,禁止使用带有类型变量的静态域和方法。

不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展Throwable都是不合法的。
catch子句不能使用类型变量。

try{
} catch (T e) { // Error,无法捕获类型变量
}

不过,在异常规范中使用类型变量是允许的:

public static <T extends Throwable> void doWork(T t) throws T {
    try {
        
    } catch (Throwable realCause) {
        t.initCause(realCause);
        throw t;
    }
}

可以消除对受检异常的检查

Java异常处理的一个基本原则是,必须为所有受检异常提供一个处理器。不过可以利用泛型消除这个限制:

public class Block {
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
    throw (T) e;
}
}

被调用该方法:

try {
} catch(Throwable t) {
	Block.<RuntimeException>throwAs(t);
}

编译器会认为t是一个非受检异常,会把所有异常都转化为编译器所认为的非受检异常。

public abstract class Block {
    public abstract void body() throws Exception;
    public Thread toThread() {
        return new Thread() {
            @Override
            public void run() {
                try {
                    body();
                } catch (Throwable t) {
                    Block.<RuntimeException>throwAs(t);
                }
            }
        };
    }
    @SuppressWarnings("unchecked")
    public static <T extends Throwable> void throwAs(Throwable e) throws T {
        throw (T) e;
    }
    public static void main(String[] args) {
        new Block() {
            @Override
            public void body() throws Exception {
                Scanner in = new Scanner(new File("ququx"), "UTF-8");
                while (in.hasNext()) {
                    System.out.println(in.next());
                }
            }
        }.toThread().start();
    }
}

正常情况下,必须捕获线程run方法中的所有受检异常,把他们包装到非受检异常中,因为run方法声明为不抛出任何受检异常。
不过这里并没有做包装,只是抛出异常,并哄骗编译器,让它认为这不是一个受检异常。

注意擦除后的冲突

假设将equals方法添加到Pair类中,方法擦除后boolean equals(T)就是boolean equals(Object)与Object.equals方法发生冲突。
补救的方法是重新命名引发错误的方法。要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。

下述代码是非法的:

class Employee implements Comparable<Employee> {...}
class Manager extends Employee implements Comparable<Manager> {...} //Error

Manager实现了同一接口的不同参数化。
实现了Comaprable< X >的类可以获得一个桥方法:
public int comparaTo(Object ohter){return compareTo((x) other);}
对于不同类型的X不能有两个这样的方法。

7.泛型类型的继承规则

Pair< Manager >并不是Pair< Employee >:

Manager[] topHonchos = ...;
Pair<Employee> result = ArrayAlg.minmax(topHonchos );  // Error

无论S与T有什么联系,通常Pair< S >与Pair< T >没有什么联系。

Pair<Manager> managerBuddies = new Pair(ceo, cfo);
Pair<Employee> employeeBuddies = managerBuddies; // 不合法的,假设可以
employeeBuddies.setFirst(lowlyEmployee);

显然最后一句是合法的,但是employeeBuddies和managerBuddies引用同样的对象。现在将CFO和一个普通员工组成一对,这对于Pair< Manager >来说不太可能。
对于数组来说,数组具有特别的保护,如果将一个低级别雇员存储到employeeBuddies[0],虚拟机将会抛出ArrayStoreException:

Manager[] managerBuddies = { ceo, cfo };
Employee[] employeeBuddies = managerBuddies; // OK

永远可以将参数化类型转换为一个原始类型。例如Pair< Employee >是原始类型Pair的一个子类型。在与遗留代码衔接时,这个转换非常必要。

Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies; // OK
rawBuddies.setFirst(new File("...")); // 编译时警告

当使用getFirst获得外来对象并赋值给Manager变量时,与通常一样,会抛出ClassCastException异常。这里失去的只是泛型程序设计提供的附加安全性。

泛型类可以扩展或实现其他的泛型类。例如Arraylist< T >类实现List< T >接口,一个ArrayList< Manager >可以被转换为一个List< Manager >,但是ArrayList< Manager >不是一个ArrayList< Employee>或List< Employee>。在这里插入图片描述

8.通配符类型

通配符概念

通配符类型中,允许类型参数变化:
Pair< ? extends Employee >表示任何泛型Pair类型,它的类型参数是Employee的子类。

public static void printBuddies(Pair<Employee> p){...}

不能将Pair< Manager >传递给这个方法。解决方案很简单,使用通配符类型,类型< Manager >是Pair< ? extends Employee >的子类型:

public static void printBuddies(Pair<? extends Employee> p){...}

在这里插入图片描述
通配符不会引起破坏:

Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wi1dcardBuddies.setFirst(1owlyEnployee); // 编译时错误

类型Pair< ? extends Employee >,其方法看起来是:
? extends Employee getFirst()
void setFirst(? extends Employee)
这样将不能调用setFirst,编译器只知道需要某个Employee的子类型,但不知道具体是什么类型。它拒绝传递任何特定类型。
但将getFirst的返回值赋值给一个Employee的引用完全合法。

通配符的超类型限定

超类型限定(supertype bound)
? super Manager,这个通配符限制为Manager的所有超类型。
void setFirst(? super Manager)
? super Manager getFirst()
setFirst不能接受类型为Employee或Object的参数,只能传递Manager类型的对象,或者某个子类型对象。另外如果调用getFirst,不能保证返回对象的类型,只能把它赋值给一个Object。

public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
    if (a.length == 0) {
        return;
    }
    Manager min = a[0];
    Manager max = a[0];
    for (int i = 1; i < a.length; i++)
    {
        if (min.getBonus() > a[i].getBonus()) {
            min = a[i];
        }
        if (max.getBonus() < a[i].getBonus()) {
            max = a[i];
        }
    }
    result.setFirst(min);
    result.setSecond(max);
}

直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

在这里插入图片描述

超类型限定的另一种应用,由于Comaprable接口本身也是一个泛型类型,那可以把ArrayAIg类的min方法声明为:

public static <T extends Comparable<T>> T min(T[] a)

如果计算一个String数组的最小值,T就是String类型的,而String是Comparable< String >的子类型。但是LocalDate实现了ChronoLocalDate,而ChronoLocalDate扩展了Comparable< ChronoLocalDate >。因此Local实现的是Comparable< ChronoLocalDate >而不是Comparable< LocalDate >。
这种情况下,超类型可以:

public static <T extends Conparable<? super T>> T min(T[] a) ...

现在compareTo方法写成int compareTo(? super T),有可能被声明为使用类型T的对象,也有可能使用T的超类型。无论如何,传递一个T类型的对象给compareTo方法都是安全的。

子类型限定的另一个常见用法是作为一个函数式接口的参数类型。
Collection接口有一个方法:

default boolean removeIf(Predicate<? super E> filter)

这个方法会删除所有满足给定谓词条件的元素。

ArrayList<Employee> staff = ...;
Predicate<Object> oddHashCode = obj -> obj.hashCode() % 2 != 0;
staff.removeIf(oddHashCode );

希望传入一个Predicate< Object >,而不只是Predicate< Employee >。super通配符可以是愿望成真。

无限定通配符

Pair< ? >:
? getFirst()
void setFirst(?)
getFirst的返回值只能给一个Object。setFirst方法不能被调用,甚至不能用Object调用,可以调用setFirst(null)。
Pair< ? >和Pair本质的不同在于:可以用任意Object对象调用原始Pair类的setObject方法。

它对于许多简单的操作非常有用,测试一个pair是否包含一个null引用,它不需要实际的类型:

public static boolean hasNulls(Pair<?> p) {
	return p.getFirst() == null || p.getSecond() == null;
}

通过将hasNulls转换成泛型方法,可以避免使用通配符类型:

public static <T> boolean hasNulls(Pair<T> p)

但是通配符的版本可读性更强。

通配符捕获

通配符不是类型变量,不能在代码中使用?做为一种类型:

? t = p.getFirst(); // Error

需要一个辅助方法swapHelper:

public static <T> void swapHelper(Pair<T> p) {
	T t = p.getFirst();
	p.setFirst(p.getSecond());
	p.setSecond(t);
}

public static void swap(Pair<?> p) { 
	swapHelper(p); 
}

swapHelper方法的参数T捕获通配符。它不知道是哪种类型的通配符,但是这是一个明确的类型,并且< T >swapHelper的定义在T指出类型时才有明确的含义。
通配符捕获只有在许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、确定的类型。例如ArrayList< Pair< T > >中的T永远不能捕获ArrayList< Pair < ? > >中的通配符。数组列表可以保存两个Pair< ? >分别针对?的不同类型。

public class PairTest3 {
    public static void main(String[] args) {
        Manager ceo = new Manager("Gus Greedy", 800000, 2019, 12, 03);
        Manager cfo = new Manager("Sid Sneaky", 600000, 2019, 12, 03);
        Pair<Manager> buddies = new Pair<>(ceo, cfo);
        printBuddies(buddies);
        ceo.setBonus(1000000);
        cfo.setBonus(500000);
        Manager[] managers = {ceo, cfo};
        Pair<Employee> result = new Pair<>();
        minmaxBonus(managers, result);
        System.out.println("第一:" + result.getFirst().getName() + ",第二:" + result.getSecond().getName());
        maxminBonus(managers, result);
        System.out.println("第一:" + result.getFirst().getName() + ",第二:" + result.getSecond().getName());
    }
    public static void printBuddies(Pair<? extends Employee> p) {
        Employee first = p.getFirst();
        Employee second = p.getSecond();
        System.out.println(first.getName() + " 和 " + second.getName() + "是朋友。");
    }
    public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
        if (a.length == 0) {
            return;
        }
        Manager min = a[0];
        Manager max = a[0];
        for (int i = 1; i < a.length; i++) {
            if (min.getBonus() > a[i].getBonus()) {
                min = a[i];
            }
            if (max.getBonus() < a[i].getBonus()) {
                max = a[i];
            }
        }
        result.setFirst(min);
        result.setSecond(max);
    }
    public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
        minmaxBonus(a, result);
        PairAlg.swapHelper(result);
    }
}
class PairAlg {
    public static boolean hasNulls(Pair<?> p) {
        return p.getFirst() == null || p.getSecond() == null;
    }
    public static void swap(Pair<?> p) {
        swapHelper(p);
    }
    public static <T> void swapHelper(Pair<T> p) {
        T t = p.getFirst();
        p.setFirst(p.getSecond());
        p.setSecond(t);
    }
}

9.反射和泛型

泛型Class类

Class类是泛型的,String.class实际上是一个Class< String >类的对象(唯一对象)。

使用Class< T >参数进行类型匹配

public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException, IllegaAccessException {
	return new Pair<>(c.newInstance(), c.newInstance());
}

调用makePair(Employee.class),Employee.class是类型Class< Employee >的一个对象。makePair方法的类型参数T同Employee匹配,并且编译器可以推断出这个方法将返回一个Pair< Employee >。

虚拟机中的泛型类型信息

Java泛型的卓越特性之一是在虚拟机中泛型类型的擦除。但擦除的类仍然保留一些泛型祖先的微弱记忆。

public static <T extends Comparable<? super T>> T min (T[] a)

擦除为public static Comparable min(Comparable[] a)
可以使用反射API来确定:
•这个泛型方法有一个叫做 T 的类型参数。
•这个类型参数有一个子类型限定, 其自身又是一个泛型类型。
•这个限定类型有一个通配符参数。
•这个通配符参数有一个超类型限定。
•这个泛型方法有一个泛型数组参数。

为了表达泛型类型声明,使用java.lang.reflect 包中提供的接口 Type。这个接口包含下列子类型:
•Class 类,描述具体类型。
•TypeVariable 接口,描述类型变量(如 T extends Comparable< ? super T >)
•WildcardType 接口, 描述通配符 (如?super T)
•ParameterizedType 接口, 描述泛型类或接口类型(如 Comparable< ? super T >)
•GenericArrayType 接口,描述泛型数组(如 T[ ])
下图给出继承层次,最后4个子类型是接口,虚拟机将实例化实现这些接口的适当的类。
在这里插入图片描述

public class GenericReflectionTest {
    public static void main(String[] args) {
        String name;
        if (args.length > 0) {
            name = args[0];
        } else {
            try (Scanner in = new Scanner(System.in)) {
                System.out.println("请输入类名:");
                name = in.next();
            }
        }
        try {
            Class<?> cl = Class.forName(name);
            printClass(cl);
            for (Method m : cl.getDeclaredMethods()) {
                printMethod(m);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    public static void printClass(Class<?> cl) {
        System.out.print(cl);
        printTypes(cl.getTypeParameters(), "<", ", ", ">", true);
        Type sc = cl.getGenericSuperclass();
        if (sc != null) {
            System.out.print(" extends ");
            printType(sc, false);
        }
        printTypes(cl.getGenericInterfaces(), " implements", ", ", "", false);
        System.out.println();
    }
    public static void printMethod(Method m) {
        String name = m.getName();
        System.out.print(Modifier.toString(m.getModifiers()));
        System.out.print(" ");
        printTypes(m.getTypeParameters(), "<", ", ", ">", true);
        printType(m.getGenericReturnType(), false);
        System.out.print(" ");
        System.out.print(name);
        System.out.print("(");
        printTypes(m.getGenericParameterTypes(), "", ", ", "", false);
        System.out.println(")");
    }
    public static void printTypes(Type[] types, String pre, String sep, String suf, boolean isDefinition) {
        if (pre.equals(" extends ") && Arrays.equals(types, new Type[] { Object.class })) {
            return;
        }
        if (types.length > 0) {
            System.out.print(pre);
        }
        for (int i = 0; i < types.length; i++) {
            if (i > 0) {
                System.out.print(sep);
            }
            printType(types[i], isDefinition);
        }
        if (types.length > 0) {
            System.out.print(suf);
        }
    }
    public static void printType(Type type, boolean isDefinition) {
        if (type instanceof Class) {
            Class<?> t = (Class<?>) type;
            System.out.print(t.getName());
        } else if (type instanceof TypeVariable) {
            TypeVariable<?> t = (TypeVariable<?>) type;
            System.out.print(t.getName());
            if (isDefinition) {
                printTypes(t.getBounds(), " extends "," & ", "", false);
            }
        } else if (type instanceof WildcardType) {
            WildcardType t = (WildcardType) type;
            System.out.print("?");
            printTypes(t.getUpperBounds(), " extends "," & ", "", false);
            printTypes(t.getLowerBounds(), " extends "," & ", "", false);
        } else if (type instanceof ParameterizedType) {
            ParameterizedType t = (ParameterizedType) type;
            Type owner = t.getOwnerType();
            if (owner != null) {
                printType(owner, false);
                System.out.print(".");
            }
            printType(t.getRawType(), false);
            printTypes(t.getActualTypeArguments(), "<", ", ", ">", false);
        } else if (type instanceof GenericArrayType) {
            GenericArrayType t = (GenericArrayType) type;
            System.out.print("");
            printType(t.getGenericComponentType(), isDefinition);
            System.out.print("[]");
        }
    }
}
请输入类名:
chapter8.section2.Pair
class chapter8.section2.Pair<T> extends java.lang.Object
public T getFirst()
public void setFirst(T)
public T getSecond()
public void setSecond(T)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值