带你快速看完9.8分神作《Effective Java》—— 泛型篇(二)

简介: 我个人在Java领域也已经学习了近5年,在修炼“内功”的方面也通过各种途径接触到了一些编程规约,例如阿里巴巴的泰山版规约,在此基础下读这本书的时候仍是让我受到了很大的冲激,学习到了很多约定背后的细节问题,还有一些让我欣赏此书的点是,书中对于编程规约的解释让我感到十分受用,并愿意将他们应用在我的工作中,也提醒了我要把阅读JDK源码的任务提上日程。

30 优先考虑泛型方法


静态工具方法尤其适合于泛型化

编写泛型方法类似于编写泛型类:


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


上面的类有两个警告信息:


50.png


如果修复这些警告,要将方法声明修改为声明一个类型参数:


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


这个例子中,类型参数列表是,返回类型是Set

使用上面的方法也很简单:


public static void main(String[] args) {
  Set<String> guys = Set.of("Tom", "Dick", "Harry");
  Set<String> stooges = Set.of("Larry", "Moe", "Curly");
  Set<String> aflCio = union(guys, stooges);
  System.out.println(aflCio);
}

上面代码的运行结果会输出:[Moe, Tom, Harry, Larry, Curly, Dick]


union 方法的一个限制是所有三个集合(输入参数和返回值)的类型必须完全相同。通过使用限定通配符类型(bounded wildcard types)(Set<? extends xxx>),可以使该方法更加灵活。



除此之外,还有递归类型限制(recursive type bound)的概念:通过包含类型参数本身的表达式来限制类型参数。


递归类型限制的一个经典用法和Comparable接口有关:

public interface Comparable<T> {
  int compareTo(T o);
}


类型参数T,可以与实现Comparable<T> 的类型的元素进行比较。例如,String 类实现了Comparable<String>,Integer 类实现了Comparable<Integer>,几乎所有类型都只能与同类型的元素比较。


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

<E extends Comparable <E >> 可以理解为「任何可以与自己比较的类型E」

下面的代码实现了计算最大值的功能:


public static <E extends Comparable<E>> E max(Collection<E> c) {
  if (c.isEmpty())
    throw new IllegalArgumentException("Empty collection");
  E result = null;
  for (E e : c)
    if (result == null || [e.compareTo(result](http://e.compareTo(result)) > 0)
      result = Objects.requireNonNull(e);
  return result;
}


这里更好的选择是返回一个Optional<E>,这一点将在后面说明


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


相对于提供的不可变的类型,有时需要更多的灵活性,在第29条里写了一个Stack的API:


public class Stack<E> {
  public Stack();
  public void push(E e);
  public E pop();
  public boolean isEmpty();
}

如果要添加一个方法来将多个元素放到栈里:

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

如果此时声明了一个Stack<Number>,尝试插入Integer数据就会报错:


Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);

51.png


Java提供了一种特殊的参数化类型——限定通配符类型(bounded wildcard type),pushAll的输入参数类型应该是「E的某个子类型的Iterable接口」,用代码表示就是Iterable<? extends E>


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

假设还想写一个popAll方法,与pushAll方法相对应:popAll方法从栈中弹出每个元素并将元素添加到给定的集合中。


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


假设还想写一个popAll方法,与pushAll方法相对应:popAll方法从栈中弹出每个元素并将元素添加到给定的集合中。


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


假设有一个Stac<Number>Collection<Object> 类型的变量,从栈中弹出一个元素并将其存储在该变量中:

Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);


会得到与第一版pushAll 非常类似的错误:Collection<Object> 不是Collection<Number> 的子类型。popAll的输入参数的类型不应该是「E的集合」,而应该是「E的某个父类型的集合」。使用Collection<? super E>修改上面的代码:


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


所以结论就很明显:为了获得最大的灵活性,对代表生产者和消费者的输入参数使用通配符类型。


PECS

这是一个记忆的口诀,producer-extends,consumer-super:


如果一个参数化类型代表一个T 生产者,使用<? extends T>;如果它代表T 消费者,则使用<? super T>。


Stack 示例中,pushAll方法的src 参数生成栈使用的E 实例,因此src的合适类型为Iterable<? extends E>;popAll方法的dst 参数消费Stack 中的E 实例,因此dst的合适类型是Collection <? super E>。



下面就用一些例子来说明这个口诀如何使用的:


第28条中的Chooser类构造方法


public Chooser(Collection<T> choices)


这个构造方法只使用集合选择来生产类型T的值,所以它的声明应该使用一个extends T的通配符类型:


public Chooser(Collection<? extends T> choices)


30条中的union 方法

public static <E> Set<E> union(Set<E> s1, Set<E> s2)


两个参数s1和s2都是E的生产者:


public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)


注意,返回类型仍然是Set。不要使用限定通配符类型作为返回类型,修改之后的用法如下:


Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);

第30条中的max 方法

public static <T extends Comparable<T>> T max(List<T> list)

public static <T extends Comparable<T>> T max(List<T> list)

修改之后的声明为:


public static <T extends Comparable<? super T>> T max(List<? extends T> list)

public static <T extends Comparable<? super T>> T max(List<? extends T> list)

为了从原来到修改后的声明,需要应用两次PECS,先应用到参数 list,它产生T 实例,所以将类型从List<T>更改为List<? extends T>;最初,T 被指定为继承Comparable<T>,但 Comparable 的 T 消费 T 实例,所以使用Comparable<? super T> 代替 Comparable<T>


通过这个例子也可以得到两条结论:


32 合理地结合泛型和可变参数


可变参数和泛型不能很好的搭配使用,可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象(leaky abstraction):当你调用一个可变参数方法时,会创建一个数组来保存可变参数


当参数化类型的变量引用不属于该类型的对象时会发生堆污染


例如:

static void dangerous(List<String>... stringLists) {
  List<Integer> intList = List.of(42);
  Object[] objects = stringLists;
  objects[0] = intList; // Heap pollution
  String s = stringLists[0].get(0); // ClassCastException
}


此方法没有可⻅的强制转换,但在调用一个或多个参数时抛出ClassCastException异常。所以将值保存在泛型可变参数数组参数中是不安全的。


这个例子引发了一个有趣的问题:为什么声明一个带有泛型可变参数的方法是合法的,当显式创建一个泛型数组是非法的呢?


答案是:具有泛型的可变参数的方法在实践中可能非常有用,例如Arrays.asList(T... a),Collections.addAll(Collection<? super T> c, T... elements),EnumSet.of(E first, E... rest),因此设计人员选择忍受这种不一致。



在Java 7中,增加了@SafeVarargs注解,@SafeVarargs注解表示了作者对该方法是类型安全的承诺,对于每一个带有泛型可变参数的方法,都要用@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(b, c);
  }
  throw new AssertionError(); // Can't get here
}


编译器会生成代码,以创建一个可变参数数组,将两个T实例传递给toArray,toArray方法的返回值是Object[ ],就容易像dangerous一样产生堆污染,例如:


public static void main(String[] args) {
  String[] attributes = pickTwo("Good", "Fast", "Cheap");
}

当运行它时,抛出一个ClassCastException异常,原因是pickTwo返回的值上产生了一个隐藏的String[ ]转换,Object不是String的子类,所以会报错。


所以允许另一个方法访问一个泛型的可变参数数组是不安全的,但有两种情况例外:


将数组传给用@SafeVarargs注解过的方法

将数组传给一个非可变参数的方法,该方法仅计算数组内容部分


下列代码是一个安全使用泛型可变参数的典型例子,将入参合并为一个List:


@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
  List<T> result = new ArrayList<>();
  for (List<? extends T> list : lists)
    result.addAll(list);
  return result;
}

在下列情况下,泛型可变参数方法是安全的:


它没有在可变参数数组中保存任何值

它不会使数组(或克隆)对不可信代码可⻅


SafeVarargs注解只能用在无法被覆盖的方法上,因为不可能保证每个重写方法都是安全的。在Java 8中,该注解仅能用在static和final方法上。



如果不想用SafeVarargs注解,可以采用第28条里面的建议,用一个List参数代替可变参数:


static <T> List<T> flatten(List<List<? extends T>> lists) {
  List<T> result = new ArrayList<>();
  for (List<? extends T> list : lists)
    result.addAll(list);
  return result;
}

可以将此方法与静态工厂方法List.of 结合使用,来允许可变数量的参数:

audience = flatten(List.of(friends, romans, countrymen));


这种方法的优点是编译器可以证明这种方法是类型安全的。


List.of 声明使用了@SafeVarargs 注解


这个技巧也可以用在toArray方法上:


static <T> List<T> pickTwo(T a, T b, T c) {
  switch(rnd.nextInt(3)) {
    case 0: return List.of(a, b);
    case 1: return List.of(a, c);
    case 2: return List.of(b, c);
  }
  throw new AssertionError();
}

main 方法变成这样:

public static void main(String[] args) {
  List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}


生成的代码是安全的,因为它只使用了泛型,没有用到数组。



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


泛型最常用于集合,Set<E>、Map<K, V>等,以及单个元素的容器,ThreadLocal<T>、AtomicReference<T>等,这里有一个限制就是每个容器只能有固定数量的泛型参数。


有时需要更多的灵活性,例如数据库有很多列,如果能安全地访问所有列就好了,其实这个需求借助Map就可以实现,将key进行参数化替代将容器参数化。



以Favorites类为例,它允许其客户端保存和检索任意多种类型的favorite实例:


public class Favorites {
    public <T> void putFavorite(Class<T> type, T instance) {...};
    public <T> T getFavorite(Class<T> type) {...};
}


Class对象将扮演参数化key的一部分,入参的类型变为Class<T>


例如:String.class的类型为Class<String>,Integer.class的类型为Class<Integer>。


当一个类的字面被用在方法中时,被称为类型令牌。


下面是API的用法:


public static void main(String[] args) {
   Favorites f = new Favorites();
    f.putFavorite(String.class, "Java");
    f.putFavorite(Integer.class, 0xcafebabe);
    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.getName());
}


上面的输出结果是:Java cafebabe Favorites


  • Favorites是类型安全的:当请求String时不会返回其他的类型;
  • Favorites也是异构的,所有Key都是不同类型的。

所以Favorites就是一个类型安全的异构容器


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));
    };
}

favorites的Map的value类型是Object,所以Map并不能保证key - value的类型一定相同


getFavorite方法的先从favorites的映射中获得与指定Class对象对应的值,它的类型只是个Object,但我们需要返回一个T,所以使用了cast方法将对象引用动态转换成Class对应的类型


cast方法只检验它的参数是否为Class对象所表示的类型的实例,如果是则返回参数,否则就抛出ClassCastException异常



确保Favorites不违背它的类型约束条件的方式是在插入元素的时候检查一下待插入元素的类型:让putFavorite方法检验instance是否真的是type所表示的类型的实例:


有一些集合包装类采用了同样的技巧,如checkSet、checkedList、checkedMap



此外Favorites类也不能用在不可具体化的类型中,即可以保存最喜爱的String或String[ ],但不能保存List<String>。原因是List<String>.class语法错误,但这也是一件好事,防止List<String>和List<Integer>共享同一个Class 对象(List.class)


有时可能我们要限制可传递给方法的类型。这可以通过有限制的类型令牌(bounded type token)来实现。


Java里面注解API就广泛使用了有限制的类型令牌,例如:


public <T extends Annotation> T getAnnotation(Class<T> annotationType);

入参 annotationType 是表示注解类型的有限值的类型令牌。如果element有这种类型的注解,该方法返回


被注解元素的本质上是一个类型安全的异构容器,key是注解类型


假设有一个Class<?>类型的对象,将它传给getAnnotation时,可以将对象转换成Class<? extends Annotation>,这种转换是unchecked的,所以会有编译时警告。Class提供了一个安全执行这种转换的方法:asSubClass,它将调用它的Class对象转换成入参类的一个子类:


static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
    Class<?> annotationType = null; // Unbounded type token
    try {
        annotationType = Class.forName(annotationTypeName);
    } catch (Exception ex) {
        throw new IllegalArgumentException(ex);
    }
    return element.getAnnotation(
            annotationType.asSubclass(Annotation.class));
}
相关文章
|
23天前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
44 2
|
19天前
|
安全 Java Go
Java&Go泛型对比
总的来说,Java和Go在泛型的实现和使用上各有特点,Java的泛型更注重于类型安全和兼容性,而Go的泛型在保持类型安全的同时,提供了更灵活的类型参数和类型集的概念,同时避免了运行时的性能开销。开发者在使用时可以根据自己的需求和语言特性来选择使用哪种语言的泛型特性。
34 7
|
1月前
|
存储 算法 Java
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)
36 2
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)
|
18天前
|
存储 安全 Java
如何理解java的泛型这个概念
理解java的泛型这个概念
|
23天前
|
存储 缓存 Java
|
24天前
|
安全 Java
【Java 第六篇章】泛型
Java泛型是自J2 SE 1.5起的新特性,允许类型参数化,提高代码复用性与安全性。通过定义泛型类、接口或方法,可在编译时检查类型安全,避免运行时类型转换异常。泛型使用尖括号`&lt;&gt;`定义,如`class MyClass&lt;T&gt;`。泛型方法的格式为`public &lt;T&gt; void methodName()`。通配符如`?`用于不确定的具体类型。示例代码展示了泛型类、接口及方法的基本用法。
10 0
|
24天前
|
Java
【Java基础面试四十五】、 介绍一下泛型擦除
这篇文章解释了Java泛型的概念,它解决了集合类型安全问题,允许在创建集合时指定元素类型,避免了类型转换的复杂性和潜在的异常。
|
24天前
|
Java
【Java基础面试四十四】、 说一说你对泛型的理解
这篇文章解释了Java泛型的概念,它解决了集合类型安全问题,允许在创建集合时指定元素类型,避免了类型转换的复杂性和潜在的异常。
|
1月前
|
Java
【Java】内部类、枚举、泛型
【Java】内部类、枚举、泛型
|
2月前
|
安全 Java
Java进阶之泛型
【7月更文挑战第10天】Java泛型,自Java 5引入,旨在提升类型安全和代码重用。通过泛型,如List&lt;String&gt;,可在编译时捕获类型错误,防止ClassCastException。泛型包括泛型类、接口和方法,允许定义参数化类型,如`class className&lt;T&gt;`,并用通配符&lt;?&gt;、extends或super限定边界。类型擦除确保运行时兼容性,但泛型仅做编译时检查。使用泛型能增强类型安全性,减少强制转换,提高性能。
28 1