高效Java编程-29. 优先考虑泛型

参数化声明并使用 JDK 提供的泛型类型和方法通常不会太困难。 但编写自己的泛型类型有点困难,但值得努力学习。

考虑条目 7 中的简单堆栈实现:

  
   // Object-based collection - a prime candidate for generics
   public 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; // Eliminate obsolete reference
    return result;
    }

    public boolean isEmpty() {
    return size == 0;
    }

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

这个类应该已经被参数化了,但是由于事实并非如此,我们可以对它进行泛型化。 换句话说,我们可以参数化它,而不会损害原始非参数化版本的客户端。 就目前而言,客户端必须强制转换从堆栈中弹出的对象,而这些强制转换可能会在运行时失败。 泛型化类的第一步是在其声明中添加一个或多个类型参数。 在这种情况下,有一个类型参数,表示堆栈的元素类型,这个类型参数的常规名称是 E(详见第 68 条)。

下一步是用相应的类型参数替换所有使用的 Object 类型,然后尝试编译生成的程序:

  
   // Initial attempt to generify Stack - won't compile!
   public 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; // Eliminate obsolete reference
    return result;
    }
    ... // no changes in isEmpty or ensureCapacity
   }

你通常会得到至少一个错误或警告,这个类也不例外。 幸运的是,这个类只产生一个错误:

  
   Stack.java:8: generic array creation
    elements = new E[DEFAULT_INITIAL_CAPACITY];
    ^

如条目 28 所述,你不能创建一个不可具体化类型的数组,例如类型 E。每当编写一个由数组支持的泛型时,就会出现此问题。 有两种合理的方法来解决它。 第一种解决方案直接规避了对泛型数组创建的禁用:创建一个 Object 数组并将其转换为泛型数组类型。 现在没有了错误,编译器会发出警告。 这种用法是合法的,但不是(一般)类型安全的:

  
   Stack.java:8: warning: [unchecked] unchecked cast
   found: Object[], required: E[]
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    ^

编译器可能无法证明你的程序是类型安全的,但你可以。 你必须说服自己,不加限制的类型强制转换不会损害程序的类型安全。 有问题的数组(元素)保存在一个私有属性中,永远不会返回给客户端或传递给任何其他方法。 保存在数组中的唯一元素是那些传递给 push 方法的元素,它们是 E 类型的,所以未经检查的强制转换不会造成任何伤害。

一旦证明未经检查的强制转换是安全的,请尽可能缩小范围(条目 27)。 在这种情况下,构造方法只包含未经检查的数组创建,所以在整个构造方法中抑制警告是合适的。 通过添加一个注解来执行此操作,Stack 可以干净地编译,并且可以在没有显式强制转换或担心 ClassCastException 异常的情况下使用它:

  
   // The elements array will contain only E instances from push(E).
   // This is sufficient to ensure type safety, but the runtime
   // type of the array won't be E[]; it will always be Object[]!
   @SuppressWarnings("unchecked")
   public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
   }

消除 Stack 中的泛型数组创建错误的第二种方法是将属性元素的类型从 E[] 更改为 Object[]。 如果这样做,会得到一个不同的错误:

  
   Stack.java:19: incompatible types
   found: Object, required: E
    E result = elements[--size];
    ^

可以通过将从数组中检索到的元素转换为 E 来将此错误更改为警告:

  
   Stack.java:19: warning: [unchecked] unchecked cast
   found: Object, required: E
    E result = (E) elements[--size];
    ^

因为 E 是不可具体化的类型,编译器无法在运行时检查强制转换。 再一次,你可以很容易地向自己证明,不加限制的转换是安全的,所以可以适当地抑制警告。 根据条目 27 的建议,我们只在包含未经检查的强制转换的分配上抑制警告,而不是在整个 pop 方法上:

  
   // Appropriate suppression of unchecked warning
   public E pop() {
    if (size == 0)
    throw new EmptyStackException();

    // push requires elements to be of type E, so cast is correct
    @SuppressWarnings("unchecked") E result =
    (E) elements[--size];

    elements[size] = null; // Eliminate obsolete reference
    return result;
   }

两种消除泛型数组创建的技术都有其追随者。 第一个更可读:数组被声明为 E[] 类型,清楚地表明它只包含 E 实例。 它也更简洁:在一个典型的泛型类中,你从代码中的许多点读取数组;第一种技术只需要一次转换(创建数组的地方),而第二种技术每次读取数组元素都需要单独转换。 因此,第一种技术是优选的并且在实践中更常用。 但是,它确实会造成堆污染(heap pollution)(详见第 32 条):数组的运行时类型与编译时类型不匹配(除非 E 碰巧是 Object)。 这使得一些程序员非常不安,他们选择了第二种技术,尽管在这种情况下堆的污染是无害的。

下面的程序演示了泛型 Stack 类的使用。 该程序以相反的顺序打印其命令行参数,并将其转换为大写。 对从堆栈弹出的元素调用 String  toUpperCase 方法不需要显式强制转换,而自动生成的强制转换将保证成功:

  
   // Little program to exercise our generic Stack
   public static void main(String[] args) {
    Stack<String> stack = new Stack<>();
    for (String arg : args)
    stack.push(arg);
    while (!stack.isEmpty())
    System.out.println(stack.pop().toUpperCase());
   }

上面的例子似乎与条目 28 相矛盾,条目 28 中鼓励使用列表优先于数组。 在泛型类型中使用列表并不总是可行或可取的。 Java 本身生来并不支持列表,所以一些泛型类型(如 ArrayList)必须在数组上实现。 其他的泛型类型,比如 HashMap,是为了提高性能而实现的。

绝大多数泛型类型就像我们的 Stack 示例一样,它们的类型参数没有限制:可以创建一个 Stack<Object>, Stack<int[]>Stack<List<String>> 或者其他任何对象的 Stack 引用类型。 请注意,不能创建基本类型的堆栈:尝试创建 Stack<int>  Stack<double> 将导致编译时错误。 这是 Java 泛型类型系统的一个基本限制。 可以使用基本类型的包装类(详见第 61 条)来解决这个限制。

有一些泛型类型限制了它们类型参数的允许值。 例如,考虑 java.util.concurrent.DelayQueue,它的声明如下所示:

  
   class DelayQueue<E extends Delayed> implements BlockingQueue<E>

类型参数列表(<E extends Delayed>)要求实际的类型参数 E  java.util.concurrent.Delayed 的子类型。 这使得 DelayQueue 实现及其客户端可以利用 DelayQueue 元素上的 Delayed 方法,而不需要显式的转换或 ClassCastException 异常的风险。 类型参数 E 被称为限定类型参数。 请注意,子类型关系被定义为每个类型都是自己的子类型[JLS,4.10],因此创建 DelayQueue<Delayed> 是合法的。

总之,泛型类型比需要在客户端代码中强制转换的类型更安全,更易于使用。 当你设计新的类型时,确保它们可以在没有这种强制转换的情况下使用。 这通常意味着使类型泛型化。 如果你有任何现有的类型,应该是泛型的但实际上却不是,那么把它们泛型化。 这使这些类型的新用户的使用更容易,而不会破坏现有的客户端(条目 26)。

文章列表

更多推荐

更多
  • Spark编程-结构化流式编程指南 概述,简单例子,编程模型,使用 Dataset 和 DataFrame 的API,连续处理,额外信息,基本概念,处理 Eventtime 和 Late Data,faulttolerance 语义,创建流式 DataFrame 和流式
  • Spark编程-20 Spark 配置Spark 属性,Environment Variables环境变量,Configuring Logging配置 Logging,Overriding configuration directory覆盖配置目录,Inhe
  • Spark编程-在Mesos上运行Spark 运行原理,安装 Mesos,连接 Spark 到 Mesos,Mesos 运行模式,Mesos Docker 支持,集成 Hadoop 运行,使用 Mesos 动态分配资源,配置,故障排查和调试,从源码安装,第三方软件包,验证,上传 S
  • Spark编程-Running Spark on YARN 启动 Spark on YARN,准备,配置,调试应用,在安全集群中运行,添加其他的 JARs,配置外部的 Shuffle Service,用 Apache Oozie 来运行应用程序,Kerberos 故障排查,使用 Spark Hi
  • Spark编程-Spark 调优 数据序列化,内存调优,其它考虑,,内存管理概论,确定内存消耗,优化数据结构,序列化 RDD 存储,GC优化,并行级别,Reduce任务内存使用,广播大变量,数据局部性, 由于大多数Spark计算都在内存中,所以集群中的任何资源(C
  • Spark编程-Spark Standalone模式 安装 Spark Standalone 集群,手动启动一个集群,集群启动脚本,提交应用程序到集群中,启动 Spark 应用程序,Resource Scheduling资源调度,监控和日志,与 Hadoop 集成,配置网络安全端口,高可用
  • Spark编程-Monitoring and Instrumentation Web 界面,Metrics,高级工具,事后查看,REST API,环境变量,Spark配置选项,API 版本控制策略, 有几种方法来监视 Spark 应用程序:Web UI,metrics 和外部工具。 Web 界面每
  • Spark编程-Spark提交任务Submitting Applications 打包应用依赖,用 sparksubmit 启动应用,Master URLs,从文件中加载配置,高级的依赖管理,更多信息, 在 script in Spark的 `bin` 目录中的`spark-submit` 脚本用与在集群上启动
  • Spark编程-作业调度 概述,跨应用调度,应用内调度,动态资源分配,公平调度资源池,资源池默认行为,配置资源池属性,配置和部署,资源分配策略,优雅的关闭Executor(执行器),概述Spark 有好几计算资源调度的方式。首先,回忆一下 [集群
  • Spark编程-Spark 概述 安全,下载,运行示例和 Shell,在集群上运行,进一步学习链接, Apache Spark 是一个快速的,通用的集群计算系统。它对 Java,Scala,Python 和 R 提供了的高层 API,并有一个经优化的支持通用执行图
  • 近期文章

    更多
    文章目录

      推荐作者

      更多