带你快速看完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));
}
相关文章
|
2月前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
30 0
[Java]泛型
|
2月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
16 1
|
2月前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
27 5
|
3月前
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
43 9
Java——包装类和泛型
|
2月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
19 1
|
2月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
22 2
|
3月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
3月前
|
存储 安全 搜索推荐
Java中的泛型
【9月更文挑战第15天】在 Java 中,泛型是一种编译时类型检查机制,通过使用类型参数提升代码的安全性和重用性。其主要作用包括类型安全,避免运行时类型转换错误,以及代码重用,允许编写通用逻辑。泛型通过尖括号 `&lt;&gt;` 定义类型参数,并支持上界和下界限定,以及无界和有界通配符。使用泛型需注意类型擦除、无法创建泛型数组及基本数据类型的限制。泛型显著提高了代码的安全性和灵活性。
|
2月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
17 0
|
2月前
|
Java
【Java】什么是泛型?什么是包装类
【Java】什么是泛型?什么是包装类
20 0