Tips

《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。

在这里第一时间翻译成中文版。供大家学习分享之用。

37. 使用EnumMap替代序数索引

有时可能会看到使用ordinal方法(条目 35)来索引到数组或列表的代码。 例如,考虑一下这个简单的类来代表一种植物:

class Plant {
    enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
    final String name;
    final LifeCycle lifeCycle;     Plant(String name, LifeCycle lifeCycle) {
        [this.name](http://this.name) = name;
        this.lifeCycle = lifeCycle;
    }     @Override public String toString() {
        return name;
    }
}

现在假设你有一组植物代表一个花园,想要列出这些由生命周期组织的植物(一年生,多年生,或双年生)。为此,需要构建三个集合,每个生命周期作为一个,并遍历整个花园,将每个植物放置在适当的集合中。一些程序员可以通过将这些集合放入一个由生命周期序数索引的数组中来实现这一点:

// Using ordinal() to index into an array - DON'T DO THIS!

Set<Plant>[] plantsByLifeCycle =

    (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];

for (int i = 0; i < plantsByLifeCycle.length; i++)

    plantsByLifeCycle[i] = new HashSet<>();

for (Plant p : garden)

    plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);

// Print the results

for (int i = 0; i < plantsByLifeCycle.length; i++) {

    System.out.printf("%s: %s%n",

        Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);

}

这种方法是有效的,但充满了问题。 因为数组不兼容泛型(条目 28),程序需要一个未经检查的转换,并且不会干净地编译。 由于该数组不知道索引代表什么,因此必须手动标记索引输出。 但是这种技术最严重的问题是,当你访问一个由枚举序数索引的数组时,你有责任使用正确的int值; int不提供枚举的类型安全性。 如果你使用了错误的值,程序会默默地做错误的事情,如果你幸运的话,抛出一个ArrayIndexOutOfBoundsException异常。

有一个更好的方法来达到同样的效果。 该数组有效地用作从枚举到值的映射,因此不妨使用Map。 更具体地说,有一个非常快速的Map实现,设计用于枚举键,称为java.util.EnumMap。 下面是当程序重写为使用EnumMap时的样子:

// Using an EnumMap to associate data with an enum

Map<Plant.LifeCycle, Set<Plant>>  plantsByLifeCycle =

    new EnumMap<>(Plant.LifeCycle.class);

for (Plant.LifeCycle lc : Plant.LifeCycle.values())

    plantsByLifeCycle.put(lc, new HashSet<>());

for (Plant p : garden)

    plantsByLifeCycle.get(p.lifeCycle).add(p);

System.out.println(plantsByLifeCycle);

这段程序更简短,更清晰,更安全,运行速度与原始版本相当。 没有不安全的转换; 无需手动标记输出,因为map键是知道如何将自己转换为可打印字符串的枚举; 并且不可能在计算数组索引时出错。 EnumMap与序数索引数组的速度相当,其原因是EnumMap内部使用了这样一个数组,但它对程序员的隐藏了这个实现细节,将Map的丰富性和类型安全性与数组的速度相结合。 请注意,EnumMap构造方法接受键类型的Class对象:这是一个有限定的类型令牌(bounded type token),它提供运行时的泛型类型信息(条目 33)。

通过使用stream(条目 45)来管理Map,可以进一步缩短以前的程序。 以下是最简单的基于stream的代码,它们在很大程度上重复了前面示例的行为:

// Naive stream-based approach - unlikely to produce an EnumMap!

System.out.println(Arrays.stream(garden)

        .collect(groupingBy(p -> p.lifeCycle)));

这个代码的问题在于它选择了自己的Map实现,实际上它不是EnumMap,所以它不会与显式EnumMap的版本的空间和时间性能相匹配。 为了解决这个问题,使用Collectors.groupingBy的三个参数形式的方法,它允许调用者使用mapFactory参数指定map的实现:

// Using a stream and an EnumMap to associate data with an enum

System.out.println(Arrays.stream(garden)

        .collect(groupingBy(p -> p.lifeCycle,

() -> new EnumMap<>(LifeCycle.class), toSet())));

这样的优化在像这样的示例程序中是不值得的,但是在大量使用Map的程序中可能是至关重要的。

基于stream版本的行为与EmumMap版本的行为略有不同。 EnumMap版本总是为每个工厂生命周期生成一个嵌套map类,而如果花园包含一个或多个具有该生命周期的植物时,则基于流的版本才会生成嵌套map类。 因此,例如,如果花园包含一年生和多年生植物但没有两年生的植物,plantByLifeCycle的大小在EnumMap版本中为三个,在两个基于流的版本中为两个。

你可能会看到数组索引(两次)的数组,用序数来表示从两个枚举值的映射。例如,这个程序使用这样一个数组来映射两个阶段到一个阶段转换(phase transition)(液体到固体表示凝固,液体到气体表示沸腾等等):

// Using ordinal() to index array of arrays - DON'T DO THIS!

public enum Phase {

    SOLID, LIQUID, GAS;

    public enum Transition {

        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

        // Rows indexed by from-ordinal, cols by to-ordinal

        private static final Transition[][] TRANSITIONS = {

            { null,    MELT,     SUBLIME },

            { FREEZE,  null,     BOIL    },

            { DEPOSIT, CONDENSE, null    }

        };

        // Returns the phase transition from one phase to another

        public static Transition from(Phase from, Phase to) {

            return TRANSITIONS[from.ordinal()][to.ordinal()];

        }

    }

}

这段程序可以运行,甚至可能显得优雅,但外观可能是骗人的。 就像前面显示的简单的花园示例一样,编译器无法知道序数和数组索引之间的关系。 如果在转换表中出错或者在修改PhasePhase.Transition枚举类型时忘记更新它,则程序在运行时将失败。 失败可能是ArrayIndexOutOfBoundsExceptionNullPointerException或(更糟糕的)沉默无提示的错误行为。 即使非空条目的数量较小,表格的大小也是phase的个数的平方。

同样,可以用EnumMap做得更好。 因为每个阶段转换都由一对阶段枚举来索引,所以最好将关系表示为从一个枚举(from 阶段)到第二个枚举(to阶段)到结果(阶段转换)的map。 与阶段转换相关的两个阶段最好通过将它们与阶段转换枚举相关联来捕获,然后可以用它来初始化嵌套的EnumMap

// Using a nested EnumMap to associate data with enum pairs

public enum Phase {

   SOLID, LIQUID, GAS;

   public enum Transition {

      MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),

      BOIL(LIQUID, GAS),   CONDENSE(GAS, LIQUID),

      SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

      private final Phase from;

      private final Phase to;

      Transition(Phase from, Phase to) {

         this.from = from;

         [this.to](http://this.to) = to;

      }

      // Initialize the phase transition map

      private static final Map<Phase, Map<Phase, Transition>>

        m = Stream.of(values()).collect(groupingBy(t -> t.from,

         () -> new EnumMap<>(Phase.class),

         toMap(t -> [t.to](http://t.to), t -> t,

            (x, y) -> y, () -> new EnumMap<>(Phase.class))));

      public static Transition from(Phase from, Phase to) {

         return m.get(from).get(to);

      }

   }

}

初始化阶段转换的map的代码有点复杂。map的类型是Map<Phase, Map<Phase, Transition>>,意思是“从(源)阶段映射到从(目标)阶段到阶段转换映射。”这个map的map使用两个收集器的级联序列进行初始化。 第一个收集器按源阶段对转换进行分组,第二个收集器使用从目标阶段到转换的映射创建一个EnumMap。 第二个收集器((x, y) -> y))中的合并方法未使用;仅仅因为我们需要指定一个map工厂才能获得一个EnumMap,并且Collectors提供伸缩式工厂,这是必需的。 本书的前一版使用显式迭代来初始化阶段转换map。 代码更详细,但可以更容易理解。

现在假设想为系统添加一个新阶段:等离子体或电离气体。 这个阶段只有两个转变:电离,将气体转化为等离子体; 和去离子,将等离子体转化为气体。 要更新基于数组的程序,必须将一个新的常量添加到Phase,将两个两次添加到Phase.Transition,并用新的十六个元素版本替换原始的九元素阵列数组。 如果向数组中添加太多或太少的元素或者将元素乱序放置,那么如果运气不佳:程序将会编译,但在运行时会失败。 要更新基于EnumMap的版本,只需将PLASMA添加到阶段列表中,并将IONIZE(GAS, PLASMA)DEIONIZE(PLASMA, GAS)添加到阶段转换列表中:

// Adding a new phase using the nested EnumMap implementation

public enum Phase {

    SOLID, LIQUID, GAS, PLASMA;

    public enum Transition {

        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),

        BOIL(LIQUID, GAS),   CONDENSE(GAS, LIQUID),

        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),

        IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);

        ... // Remainder unchanged

    }

}

该程序会处理所有其他事情,并且几乎不会出现错误。 在内部,map的map是通过数组的数组实现的,因此在空间或时间上花费很少,以增加清晰度,安全性和易于维护。

为了简便起见,上面的示例使用null来表示状态更改的缺失(其从目标到源都是相同的)。这不是很好的实践,很可能在运行时导致NullPointerException。为这个问题设计一个干净、优雅的解决方案是非常棘手的,而且结果程序足够长,以至于它们会偏离这个条目的主要内容。

总之,使用序数来索引数组很不合适:改用EnumMap。 如果你所代表的关系是多维的,请使用EnumMap <...,EnumMap <... >>。 应用程序员应该很少使用Enum.ordinal(条目 35),如果使用了,也是一般原则的特例。

Effective Java 第三版——37. 使用EnumMap替代序数索引的更多相关文章

  1. Effective Java 第三版——36. 使用EnumSet替代位属性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  2. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

  3. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  4. effective Java 第三版学习笔记

    创建对象类型的 1,静态工厂方法代替构造器 静态工厂方法有名称,不容易混乱他的作用 不必再每次调用他的时候创建实例,创建实例的代价是高的,可以重复利用缓存的对象 静态工厂甚至能返回子类对象,例如在接口 ...

  5. Effective Java 第三版——38. 使用接口模拟可扩展的枚举

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  6. Effective Java 第三版——46. 优先考虑流中无副作用的函数

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  7. Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——7. 消除过期的对象引用

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. acdrem1083 人民城管爱人民 DP

    思路:d(i, 0)表示从节点i到达大运村的最短路径,d(i, 1)表示从节点i到达大运村的次短路径. 1.最短路:当做DAG处理即可. 2.次短路:假设当前在u点处,下一个节点是v.v到终点的最短路 ...

  2. HDU5992 - Finding Hotels

    原题链接 Description 给出个二维平面上的点,每个点有权值.次询问,求所有权值小于等于的点中,距离坐标的欧几里得距离最小的点.如果有多个满足条件的点,输出最靠前的一个. Solution 拿 ...

  3. JAVA在不确定具体 Annotation 类型时,获得注解参数

       package com.lzw.demo; @SpringBootApplication public class DemoApplication { public static void ma ...

  4. CDN中前端层的复制

    前端层的复制是为了提高静态内容分发的性能和可扩展性.将静态内容的分发转由边缘服务器来完成是为了解决可扩展性的问题,因为这样做可以避免出现在对等点和广域网链路处的网络拥堵风险,而这两个地方的拥堵是网络延 ...

  5. 简化的CDN架构分析

    CDN架构的设计目标是通过复制系统资源(即Web服务器)的方式来获得高性能和高扩展性,为了能确保在海量内容下可以稳定提供高性能的服务.系统资源的复制可以在本地和地理两个尺度上进行.如果是本地复制则响应 ...

  6. 在windows XP系统下编译和使用ffmpeg

    最近在做流媒体开发这一块,在服务器端,所用的live555不支持mp4,avi等视频容器格式,所以打算运用ffmpeg来进行扩展.将MP4文件先运用ffmpeg进行解析,解析成live555所支持的基 ...

  7. 工作中常用的linux命令(1)

    1.cd :进入一个目录,例如进入/home/admin目录:cd /home/admin 2.pwd :查看当前所在目录:如图: 3.ls :列出当前目录下的所有文件: 4.ll :列出当前目录下的 ...

  8. Windows 7 蓝屏原因

    Windows 7 蓝屏 Microsoft (R) Windows Debugger Version 6.11.0001.404 X86 Copyright (c) Microsoft Corpor ...

  9. CSS3之background-origin

    1.属性简介 background-origin:border|content|padding|!important border:从border区域开始显示背景: content:从content区 ...

  10. [Err] 1172 - Result consisted of more than one row

    1 错误描述 [Err] 1172 - Result consisted of more than one row Procedure execution failed 1172 - Result c ...