《Effective Java 3rd》读书笔记——泛型

不要使用原生类型

声明中具有一个或多个类型参数的类或者接口就是泛型类或接口。泛型类和接口统称为泛型。

每一种泛型都定义一个原生(态)类型,即不带任何实际类型参数的泛型名称。例如,与List<E>相对应的原生类型是List

private final Collection<Stamp> stamps = ...;

通过这条声明,编译器知道stamps应该只包含Stamp实例,并给予保证。如果使用原生类型,就失掉了泛型在安全性和描述性方面的所有优势。

使用参数化的类型以允许插入任意对象(比如List<Object>)是可行的。原生类型List和参数化的类型List<Object>之间有什么区别呢?
不严格的说,前者逃避了泛型检查,后者明确告诉编译器,它能够持有任意类型的对象。虽然可以将List<String>传递给类型List参数,但是不能将它
传给类型List<Object>的参数。

private static void fun1(List list) {

}

private static void fun2(List<Object> list) {

}

public static void main(String[] args) {
    List<String> list = Lists.newArrayList();
    fun1(list);
    fun2(list);//Error:(24, 14) java: 不兼容的类型: java.util.List<java.lang.String>无法转换为java.util.List<java.lang.Object>
}

也就是说List<String>List<Object>它们没有任何关系,是不同的类型。虽然StringObject的子类,但是不能说List<String>List<Object>的子类!

List<String>是原生类型List的一个子类。如果使用像List这样的原生类型,就会失掉类型安全性,但如果使用像List<Object>这样的参数化类型,则不会。

为了更加具体地进行说明,请参考下面的程序:

private static void unsafeAdd(List list, Object o) {
    list.add(o);//Unchecked call to 'add(E)' as a member of raw type 'java.util.List'
}

public static void main(String[] args) {
    List<String> strings = Lists.newArrayList();
    unsafeAdd(strings,Integer.valueOf(42));
    String s = strings.get(0);//Error! java.lang.Integer cannot be cast to java.lang.String
}

如果运行该段程序,会产生ClassCastException。但是能通过编译。

如果将参数类型改为List<Object>

private static void unsafeAdd(List<Object> list, Object o) {
    list.add(o);
}

public static void main(String[] args) {
    List<String> strings = Lists.newArrayList();
    unsafeAdd(strings,Integer.valueOf(42));//Wrong 1st argument type. Found: 'java.util.List<java.lang.String>', required: 'java.util.List<java.lang.Object>
}

它就无法编译了。因为会进行类型检查。可以避免你犯错。

在不确定或不在乎集合中的元素类型的情况下,你也许会使用原生类型。例如,设想要编写一个方法,它有两个集合,并从中返回它们共有元素的数量。
如果你对泛型不熟悉,可能会编写下面类似的代码:

static int numElementsInCommon(Set s1,Set s2) {
    int result = 0;
    for (Object o1 : s1) {
        if (s2.contains(o1)) {
            result++;
        }
    }
    return result;
}

这个方法可行,但是是很危险的。安全的替代做法是使用无限制的通配符类型(unbounded wildcard type)。
如果要使用泛型,但不确定或不关心实际的类型参数,就可以一个问号代替。例如,泛型Set<E>的无限制通配符类型为Set<?>
这是最普通的参数化Set类型,可以持有任何集合:

static int numElementsInCommon(Set<?> s1,Set<?> s2){ ... }

无限制的通配符类型Set<?>和原生类型Set之间有什么区别呢?通配符类型是安全的,原生类型不安全。但不能将任何元素(除null外)放到Collection<?>中:

private static void collectionTest(Collection<?> collection) {

}

public static void main(String[] args) {
    String s = "s";
    //collectionTest(s);//Error:(41, 24) java: 不兼容的类型: java.lang.String无法转换为java.util.Collection<?>
    List<String> list = Lists.newArrayList();
    collectionTest(list);//而List<String>却可以
}

必须在类文字(class lietral)中使用原生类型。也就是说List.class/String[].class/int.class是合法的,但List<String>.classList<?>.class不合法。

泛型信息在运行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的。

下面是利用泛型来使用instanceof操作符的首选方法:

if(o instanceof Set) {
    Set<?> s = (Set<?>) o;
}

下面给出常见的术语:

[外链图片转存失败(img-hA50YZb8-1566538871190)(_v_images/20190613220139342_18020.png)]

消除非受检的警告

用泛型编程时会遇到许多编译器警告:非受检转换警告(unchecked cast warning)、非受检方法调用警告、非受检参数化可变参数类型警告,以及非受检转换警告(unchecked conversion warning)。

有许多非受检警告很容易消除。例如:Set<String> set = new HashSet();

编译器会提醒:Unchecked assignment: 'java.util.HashSet' to 'java.util.Set<java.lang.String>。而消除方法很简单:Set<String> set = new HashSet<>();

而有些警告则非常难以消除。但是你要尽可能地消除每一个非受检警告。这样意味着不会在运行时出现ClassCastException

如果无法消除警告,同时可以证明引起警告的代码是类型安全的,才可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告。

该注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。但是,**应该始终在尽可能小的范围内使用SuppressWarnings注解。
永远不要再整个类上使用它,这样可能会掩盖重要的警告。

如果你发现自己在长度不止一行的方法或构造器中使用了该注解,可以将它移动到一个局部变量的声明中。虽然你必须声明一个新的局部变量,不过这么做是值得的。

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

如果编译该方法,就会产生这条警告:Unchecked cast: 'java.lang.Object[]' to 'T[]'

你可以试着将注解放在整个放上,但是在实践中千万不要这么做,而是声明一个局部变量来保存返回值,并注解其声明。像这样:

public static <T> T[] toArray(T[] a) {
    if (a.length < size) {
        @SuppressWarnings("unchecked")
        T[] result = (T[]) Arrays.copyOf(elementData, size, a.getClass());
        return result;
    }
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

这样方法可以正确地编译,禁止非受检警告的范围也会减到最小。

每当使用SuppressWarnings("unchecked")注解时,都要添加一条注释,说明为什么这么做是安全的。

总之,非受检警告很重要,不要忽略它们。每一条警告都表示可能在运行时抛出ClassCastException。要进最大的努力消除这些警告。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,才可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告,并添加一条注释,说明为什么这么做是安全的。

列表优于数组

数组与泛型相比,有两个重要的不同点。首先,数组是协变的——如果SubSuper的子类型,那么数组类型Sub[]就是Super[]的子类型。

下面的代码片段是合法的:

Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";//抛出ArrayStoreException

但下面的是不合法的:

List<Object> ol = new ArrayList<Long>();//Error:(13, 27) java: 不兼容的类型: java.util.ArrayList<java.lang.Long>无法转换为java.util.List<java.lang.Object>
ol.add("I don't fit in");

我们可以发现,如果使用数组,在运行时才发现问题。而利用列表,编译时就可以发现错误。

数组与泛型第二大区别在于,数组是具体化的。因此数组会在运行时知道和强化它们的元素类型。而泛型是通过擦除来实现的。意味着,泛型只在编译时强化它们的类型信息,
并在运行时擦除。

擦除导致泛型可以与没有使用泛型的代码随意进行互用,为了兼容Java老版本。

数组和泛型不能很好的混合使用。创建泛型、参数化类型或者类型参数的数组是非法的。下面这些数组创建表达式没有一个是合法的:

new List<E>[];
new List<String[];
new E[];

为什么创建泛型数组是非法的?因为它不是类型安全的。
要是它合法,编译器在其他正确的程序中发生的转换就会在运行时失败,并出现一个ClassCastException,这违背了泛型系统提供的基本保证。

List<String>[] stringLists = new List<String>[1];
List<Integer> intList = Lists.newArrayList(4);
Object[] objects = stringLists;
objects[0] = intList;//`List<Integer>`实例保存到了原本声明只包含`List<String>`实例的数组中
String s = stringLists[0].get(0);

Lists来自com.google.common.collect.Lists)

假设第1行是合法的,它创建了一个泛型数组。第2行创建并初始化了一个包含单个元素的List<Integer>。第3行将List<String>数组保存到Object数组变量中,这是合法的。(因为ListObject的子类,数组是协变的。)
第4行将List<Integer>保存到Object数组里唯一的元素中,这是可以的,因为泛型是通过擦除实现的。擦除后是ListList<String>[]擦除后的类型是List[]
但是,当我们将一个List<Integer>实例保存到了原本声明只包含List<String>实例的数组中。
在第5行,从这个数组中获取了唯一的元素。编译器会自动将获取到的元素转换成String,但其实它是一个Integer。因此会得到ClassCastException。为了防止这种情况,第1行必须产生编译时错误。

EList<E>List<String>这样的类型称为不可具体化的类型。指其运行时包含的信息比它编译时包含的信息要少。
唯一可具体化的参数化类型是无限制的通配符类型,如List<?>Map<?,?>

当你得到泛型数组创建错误时,**最好的解决办法通常是优先使用集合类型List<E>

总之,数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也是一样。
一般来说,数组和泛型不能很好的混合使用。若是混合起来使用,最好是用列表代替数组。

优先考虑泛型

下面以第7条(消除过期的对象引用)中的简单栈实现为例,来学习下泛型的使用方法:

class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

我们如何把这个类泛型化呢。第一步将类型参数替换所有的Object类型:

class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        E result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

这个类产生了一个错误,如上条所述,不能创建不可具体化类型的数组(E)。解决这个问题有两种方法:第一种,创建一个Object数组,并将它转换为泛型数组(ArrayList中就是这么干的 ,记错了,囧,ArrayList底层是Object[]数组,而不是E[],因为ArrayList是在泛型出来之前就已经写好了吧)。
这样编译器会产生一条警告,整体上来说,不是类型安全的:

@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
}

第二种方法是,将elements域的类型从E []改为Object []。这样会得到另外一种错误由E result = elements[--size];导致的类型不匹配。

可以将这个错误变成一条警告:

public E pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    @SuppressWarnings("unchecked")
    E result = (E) elements[--size];
    elements[size] = null;
    return result;
}

第一种方法可读性更强,且只需转换一次,而第二种方法则是每次读取一个数组元素的时候都需要转换一次。因此,优先使用第一种方法。

有一些泛型限制了可允许的类型参数。例如,以java.util.concurrent.DelayQueue为例,其声明如下:

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

类型参数列表<E extends Delayed> 要求实际的类型参数E必须是Delayed的一个子类型(或者是Delayed本身)。允许DelayQueue的实现利用Delayed#getDelay方法,无序显式地转换。

优先考虑泛型方法

声明类型参数的类型参数列表,处在方法的修饰符及其返回值之间。

public static <E> Set<E> union(Set<E> s1,Set<E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

假设要编写一个恒等函数分发器,如果在每次需要的时候都重新创建一个,这样很浪费,因为它是无状态的。如果Java泛型被具体化了,每个类型都需要一个恒等函数,但是它们被擦除后,就只需要一个泛型单例。

private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
    return (UnaryOperator<T>) IDENTITY_FN;
}

它产生了一条未受检警告,因为UnaryOperator<Object>对于每个T来说并非都是个UnaryOperator<T>。恒等函数很特殊:它返回未被修改的参数,因此我们知道无论T值是什么,用它作为UnaryOperator<T>都是类型安全的。

下面是利用泛型单例作为UnaryFunction<String>UnaryFunction<Number>:

public static void main(String[] args) {
    String [] strings = {"jute","hemp","nylon"};
    UnaryOperator<String> sameString = identityFunction();
    for (String s: strings) {
        System.out.println(sameString.apply(s));
    }

    Number[] numbers = {1,2.0,3L};
    UnaryOperator<Number> sameNumber = identityFunction();
    for (Number n: numbers) {
        System.out.println(sameNumber.apply(n));
    }
}

有许多方法都带有一个实现Comparable接口的元素列表,为了对列表进行排序,并在其中进行搜索等等。要完成这其中的操作,都要求列表中的每个元素能与列表中的其他元素比较。
下面是如何表达这种约束的一个示例:

public static <E extends Comparable<E>> E max(Collection<E> c);

<E extends Comparable<E>>读作针对可以与自身进行比较的每个类型E

利用有限通配符来提升API的灵活性

这一小节是重点

如前面所述,参数化的类型是不可变的。List<String>不是List<Object>的子类型。
有时候,我们需要的灵活性要比不变类型所能提供的更多。比如我们例子中的Stack
假设我们想增加一个方法,让它按顺序将一系列的元素全部放到栈中。第一次尝试如下:

public void pushAll(Iterable<E> src) {
    for (E e : src) {
        push(e);
    }
}

如果Iterable的元素类型与栈类型完全匹配是没什么问题的。但是假如有一个Stack<Number>,并调用了push(intVal),这里的intValInteger类型。
这是可以的,因为IntegerNumber的一个子类。因此从逻辑上来说,下面这个方法应该可行:

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> intergers = Arrays.asList(1,2,3,4,5);
numberStack.pushAll(intergers);//Error:(51, 29) java: 不兼容的类型: java.lang.Iterable<java.lang.Integer>无法转换为java.lang.Iterable<java.lang.Number>

实际上编译不通过,报了不兼容的类型错误。因为Iterable<Integer>不是Iterable<Number>的子类。

幸运的是,Java提供了一种特殊的参数化类型,称为有限制的通配符类型(bounded wildcard type),它可以处理类似的问题。
pushAll的输入参数类型不应该为EIterable接口,而应该是**E的某个子类型的Iterable接口**,通配符类型Iterable<? extends E>正是这个意思。

每个类型都是自身的子类型

public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}

这样修改上面的代码就没有问题了。现在想要编写一个与该方法相对应的popAll方法。popAll方法从栈中弹出每个元素,并添加到指定的集合中。
初次尝试编写的popAll方法可能像下面这样:

public void popAll(Collection<E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}

还是和上面一样的问题,只能用于目标集合的元素类型与栈的类型相同。假设你有一个Stack<Number>Object类型的变量。

Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = Lists.newArrayList();
numberStack.popAll(objects);//Error:(68, 28) java: 不兼容的类型: java.util.Collection<java.lang.Object>无法转换为java.util.Collection<java.lang.Number>

这次通配符类型同样体用了一种解决方法。popAll的输入参数类型不应该为E的集合,而应该是E的某种超类的集合。有一个通配符类型符合此意:
Collection<? super E>

public void popAll(Collection<? super E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}

为了获得最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型

生产者用extends,消费者用super。 PECS: producer-extends, consumer-super。 这里的参照物是自身。比如上面dst,它用于接收数据,因此它为消费者,所以用super

我们的Stack例子汇总,pushAllsrc参数产生E实例供Stack使用,因此为生产者,相应的类型为? extends E

还可以这么理解,因为这是一个类型为E的栈,它应该能存放E的子类,所以pushAll中的参数应该为? extends E

popAlldst参数通过Stack消费E实例,类型为? super E

上一节例子中的public static <E> Set<E> union(Set<E> s1,Set<E> s2),其中s1s2作为生产者产生E的实例,可以这样改造为:public static <E> Set<E> union(Set<? extends E> s1,Set<? extendsE> s2)。返回类型仍然为Set<E>不要用通配符类型作为返回类型

Set<Integer> ints = Sets.newHashSet(1, 3, 5);
Set<Double> doubles = Sets.newHashSet(2.0, 4.0, 6.0);
Set<Number> numbers = union(ints, doubles);
System.out.println(numbers);//[2.0, 4.0, 1, 3, 5, 6.0]

这里的ENumber,而IntegerDouble都继承自它。

接下来,我们来改造下max方法:public static <E extends Comparable<E>> E max(List<E> list)

public static <E extends Comparable<? super E>> E max(List<? extends E> list) {
    if (list == null || list.size() == 0) {
        throw new IllegalArgumentException();
    }
    E max = list.get(0);
    for (int i = 1; i < list.size(); i++) {
        if (list.get(i).compareTo(max) > 0) {
            max = list.get(i);
        }
    }
    return max;
}

我们应用PECS转换了两次。首先应用到参数list。它产生E实例,因此改为List<? extends E> list。更灵活的是运用到类型参数E

最初E被指定用来扩展Comparable<E>,但是Ecomparable#compareTo()消费E实例,产生表示顺序的整数值。因此,参数化类型Comparable<E>被有限制通配符类型Comparable<? super E>取代。
记住所有的comparablecomparator始终是消费者,因此使用时始终应该是Comparable<? super E>优于Comparable<E>。对于Comparator接口也一样。

下面看这个强大的max方法的示例:

List<java.sql.Date> dates = Lists.newArrayList();
max(dates);

我们看下java.sql.Date的定义:public class Date extends java.util.Date{...},它没有实现Comparable<java.util.Date>,而是继承java.util.Datejava.util.Date实现了Comparable<Date>。也就是说,java.sql.Date示例除了可以与其他java.sql.Date实例比较,还可以与任何java.util.Date实例比较。 也就是说,用通配符支持那些不直接实现Comparable接口而是继承自实现了该接口的类型。

类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或另一个进行声明。

public static <E> void swap(List<E> list,int i,int j);
public static void swap(List<?> list, int i, int j);

第一个使用类型参数E,第二个使用无限制的通配符。在公共API中,第二种更好一些,因为它更简单。如果类型参数只在方法声明中出现一次,就可以用通配符取代它。

将第二种声明用于swap方法会有一个问题,下面这个实现不能编译:

public static void swap(List<?> list, int i, int j) {
    list.set(i,list.set(j,list.get(i)));//Error:(45, 39) java: 不兼容的类型: java.lang.Object无法转换为capture#1
}

问题在于list的类型为List<?>,不能把null之外的任何值放到List<?>中。有一种方式可以实现这个方法:

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

谨慎并用泛型和可变参数

非具体化类型是指其运行时代码信息比编译时少。所有的泛型和参数类型都是非具体化的。如果一个方法声明其可变参数为非具体化类型,编译器就会产生一条警告。

当一个参数化类型的变量指向一个不是该类型的对象时,会产生堆污染。举个例子:

//产生警告:Possible heap pollution from parameterized vararg type
static void dangerous(List<String>... stringLists) {
    List<Integer> ints = Lists.newArrayList(42);
    Object[] objects = stringLists;
    objects[0] = ints;//堆污染
    String s = stringLists[0].get(0);//ClassCastException
}

我们来调用一下:

public static void main(String[] args) {
    List<String> list = Lists.newArrayList("test");
    dangerous(list);
}

结果为:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at com.java.effective.generics.MyTest.dangerous(MyTest.java:19)
	at com.java.effective.generics.MyTest.main(MyTest.java:24)

String s = stringLists[0].get(0);//ClassCastException该行代码有一个不可见的转换,这个转换失败证明类型安全已经受到了危机,因此将值保存在泛型可变参数数组参数中是不安全的

为什么显示创建泛型数组是非法的,用泛型可变参数声明方法确实合法的呢?答案是,带有泛型可变参数或参数化类型的方法在实践中用处很大,因此Java容忍这一矛盾的存在。实际中,Java中有好几个这样的方法:
Arrays.asList(T ... a)Collections.addAll(Collection<? super T> c,T ... elements),这些类库方法是类型安全的。

Java7中,可以通过@SafeVarargs注解告诉编译器,这段代码是类型安全的,不要发出上面的告警。

那么如何确保安全的?泛型数据时在调用方法时创建的,用来保存可变参数。如果该方法没有在数组中保存任何值(写数组),也不对数组的引用转义,那么它就是安全的。换句话说,
如果可变参数数组只用来将数量可变的参数从调用程序传递到方法,那么该方法就是安全的。

注意,不要在可变参数的数组中保存任何值,这可能会破坏类型安全性。

static <T> T[] toArray(T ... args) {
    return args;
}

这个方法只是返回其可变参数数组,看起来没什么危险,但它实际上很危险。这个数组的类型,由传到方法的参数的编译时类型类决定的,
编译器没有足够信息去准确的决定。因为该方法返回其可变参数数组,它会将堆污染传到调用堆栈上。

下面举个例子:

static <T> T[] pickTwo(T a,T b, T c) {
    switch (ThreadLocalRandom.current().nextInt(3)) {
        case 0: return toArray(a,b);
        case 1: return toArray(a,c);
        case 2: return toArray(a,c);
    }
    throw new AssertionError();
}

编译这个方法时,编译器会产生代码,创建一个可变参数数组,并将两个T实例传到toArray
这些代码配置了一个类型为Object的数组,这是确保能够保存这些实例的最具体的类型。
toArray方法只是将这个数组返回给pickTwo,反过来也将它返回给其调用程序,因此pickTwo始终都会
返回一个类型为Object的数组。

public static void main(String[] args) {
    String [] attributes = pickTwo("Good","Fast","Cheap");//Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
}

在运行的时候,它抛出了类型转换异常。虽然它看起来没有任何可见的转换,但是返回的时候产生了一个隐藏的String[]转换。

这说明了:允许另一个方法访问一个泛型可变参数数组是不安全的

泛型可变参数方法在下列条件下是安全的:

  1. 它没有在可变参数数组中保存任何值
  2. 它没有对不被信任的代码开放该数组

33#优先考虑类型安全的异构容器

泛型最常用于集合,如Set<E>Map<K,V>,一个Set只有一个类型参数,表示它的元素类型;一个Map有两个类型参数,表示它的键和值类型。

但是,有时候需要更多的灵活性。例如,数据库的行可以有任意数量的列,如果能以类型安全的方式访问所有列就好了。
幸运的是,有一种方法可以很容易做到。这种方法就是将键(key)进行参数化而不是将容器(container)参数化。然后将参数化的键提交给容器来插入或获取值。

下面以Favorites类为例,它允许其客户端从任意数量的其他类中,保存并获取一个最喜爱的实例。
Class对象充当参数化键的部分。类Class被泛型化了。类的类型从字面量上来看不再只是简单的Class,而是Class<T>
例如String.class属于Class<String>类型,Integer.class属于Class<Integer>类型。
当一个类的字面量被用在方法上,来传达编译时和运行时的类型信息时,就被称为类型令牌(type token)

public class Favorites {
    private Map<Class<?>,Object> favorites = new HashMap<>();
    
    public <T> void putFavorite(Class<T> type,T instance) {
        favorites.put(Objects.requireNonNull(type),instance);    
    }
    
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

下面是一个示例程序:

public static void main(String[] args) {
    Favorites f = new Favorites();
    f.putFavorite(String.class,"Java");
    f.putFavorite(Integer.class,0xcafebabe);//java 魔数
    f.putFavorite(Class.class,Favorites.class);
    String favoriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);
    Class<?> favoriteClass = f.getFavorite(Class.class);
    System.out.printf("%s %x %s%n",favoriteString,favoriteInteger,favoriteClass.getSimpleName());\\%n用于生成基于平台的换行符
}

打印是

Java cafebabe Favorites

Favorites是类型安全的,当你向它请求String的时候不会返回一个Integer。同时它也是异构(heterogeneous)的:和普通映射不一样,它的所有键都是不同类型的。

因此,我们将Favorites称为类型安全的异构容器。

favorites Map的值类型只是Object,也就是说,Map并不能保证键和值之间的类型关系。即不能保证每个值都为它的键所表示的类型。

Favorites类有两种局限性要注意。首先,恶意的客户端就可以很轻松地破坏Favorites实例的类型安全,只要以它的原生态使用Class对象。
但会造成客户端在编译时产生未受检的警告。确保Favorites永远不违背它的类型约束条件的方式是,让putFavorite方法检验
instance是否真的是type所表示的类型的实例。

public <T> void putFavorite(Class<T> type,T instance) {
    favorites.put(Objects.requireNonNull(type),type.cast(instance));
}

Java中有一些集合包装类采用了同样的技巧。如checkedSetcheckedList等等。

Favorites类的第二种局限性在于它不能用在不可具体化类型中。也就是不能保存List<String>,原因在于无法为List<String>获得一个Class对象:List<String>.class是个语法错误。

Favorites使用的类型令牌是无限制的,getFavoriteputFavorite接收任何Class对象。可以通过有限制类型参数或有限制通配符来限制可以表示的类型。

如:

<T extends Annotation> T getAnnotation(Class<T> annotationClass);

参数annotationClass是一个表示注解类型的有限制的类型令牌。如果元素有这种类型的注解,该方法就将它返回;如果没有,则返回null。被注解的元素本质上是个类型安全的异构容器,
容器的键属于注解类型。

假设你有个类型为Class<?>的对象,并且想将它传给一个需要有限制的类型令牌的方法,如getAnnotation。可以将对象转换成Class<? extends Annotation>,但是这种转换是非受检的。
Class提供了一个安全(且动态)地执行这种转换的方法——asSubclass,它将调用它的Class对象转换成用其参数表示的类的一个子类。如果转换成功,该方法返回
它的参数;如果失败,抛出ClassCastException

static Annotation getAnnotation(AnnotatedElement element,String annotationTypeName) {
    Class<?> annotationType = null;
    try {
        annotationType = Class.forName(annotationTypeName);
    } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException(e);
    }
    return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}

asSubclass的实现如下:

public <U> Class<? extends U> asSubclass(Class<U> clazz) {
    if (clazz.isAssignableFrom(this)) //判断clazz是否为this代表的类的父类(或者是否为同一个类)
        return (Class<? extends U>) this;
    else
        throw new ClassCastException(this.toString());
}

作为总结,回答上面关于数据库的问题,可以用一个DatabaseRow类型表示一个数据库行容器,用泛型Column<T>作为它的键。

发布了131 篇原创文章 · 获赞 38 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/yjw123456/article/details/93666945
今日推荐