Effective Java 学习笔记--36-38条 枚举类型

目录

枚举之前--int常量

枚举类型

枚举类型的特性

自定义方法和域的设置

实例的统一行为

实例的不同行为

switch语句

抽象方法

公共接口

策略枚举

用实例域代替默认序数

枚举集合

EnumSet

EnumMap


枚举类型用于定义一组固定的命名常量,在枚举类型中定义的每一个枚举常量都是枚举类型的一个实例,而且这个实例是不可变的。

枚举之前--int常量

枚举类型是Java 5版本引入的,在引入之前通常使用int常量来表示枚举类型,每一个int常量代表一个类型实例:

public class FruitMarket {
    public static final int APPLE = 0;
    public static final int ORANGE = 1;
    public static final int BANANA = 2;
    public static final int PINEAPPLE = 3;

}

int常量的确能够轻松的列举出有限的枚举,但是有几个问题:

一是int常量没有安全性可言,只要是int值相同,不同类型的枚举都视为一样:

class FruitMarket {
    public static final int APPLE = 0;
    public static final int ORANGE = 1;
    public static final int BANANA = 2;
    public static final int PINEAPPLE = 3;

}

class VegetableMarket{
    public static final int CARROT = 0;
    public static final int CABBAGE = 1;
    public static final int TOMATO = 2;
    public static final int CUCUMBER = 3;

}

public class Application{

    public static void main(String[] args){
        VegetableMarket.CARROT == FruitMarket.APPLE;
    }

}

二是Int常量作为静态常量在编译时就会被赋值,一旦常量做了改变(或者有了新增),就要重新编译文件,而且要重新检视应用了这些常量的Java文件。

三是Int常量没有丰富的专用方法,比如遍历所有的枚举、将具体的常量值映射到对应的String标识等等,都要通过循环遍历和switch语句来实现。

四是Int常量受限于Integer类型,无法顺畅的加入一些枚举特有的方法。

但是Int常量的优势在于其存储的轻量化,所以如果是需要轻量化的枚举(比如只需要作为简单的标识作用),Int常量还是可以考虑的。 

枚举类型

枚举类型的特性

Java的枚举类型对于枚举的使用做了针对性的设计,同时配置了齐全的功能。

一是通过公有的静态final域为每一个枚举常量导出一个枚举实例。

二是没有可以外部访问的构造器,进一步保证了外部无法通过构造器来新增枚举实例,但是可以允许有私有构造器,将枚举实例与相关的数据对应起来:

public enum Planet{
    MERCURY(3.302e+23, 2.439e6),
    VENUS(4.869e+24, 6.052e6),
    EARTH(5.972e+24, 6.378e6),
    MARS(6.419e+23, 3.396e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN(5.685e+26, 6.052e7),
    URANUS(8.681e+25, 2.556e7),
    NEPTUNE(1.002e+25, 1.188e6);

    private double mass;
    private double radius;
    private double surfaceGravity;
    private final static double G = 6.67300E-11;

    Planet(double mass, double radius){
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G*mass/(radius*radius);
    }

}

//这里将Planet实例与其质量(mass)、半径(radius)和重力常量对应起来。

三是枚举类提供了丰富的内置方法,其中绝大部分都是继承自java.lang.Enum基类(除了values()方法)

  • name()

    • 返回枚举常量的名称,即定义枚举时使用的标识符。
    • 例如,对于枚举 Day.MONDAYname() 方法将返回 "MONDAY"
  • ordinal()

    • 返回枚举常量在其枚举类型中的位置索引(从0开始)。
    • 例如,对于枚举 Day.MONDAYordinal() 方法将返回 0,因为它是第一个枚举常量。
  • toString()

    • 返回枚举常量的名称,与 name() 方法相同。
    • 默认情况下,toString() 方法返回枚举常量的名称。
  • compareTo(E other)

    • 比较两个枚举常量的顺序。
    • 参数 other 是同一枚举类型的另一个枚举常量。
    • 返回值表示当前枚举常量与 other 枚举常量的相对顺序。
  • valueOf(Class<E> enumType, String name)

    • 静态方法,用于根据枚举类型的名称获取枚举常量。
    • 如果名称匹配枚举类型中的一个常量,则返回该枚举常量;否则,抛出 IllegalArgumentException
  • values()

    • 静态方法,返回枚举类型的数组,包含该枚举类型的所有枚举常量。
    • 这个方法可以用来遍历枚举类型的全部常量。

可以看到绝大部分方法都是继承了Object类和实现Comparable与Serializable接口所实现的方法。

自定义方法和域的设置

实例的统一行为

枚举类型由于不是基本数据类型,所以在构建的时候可以添加自定义方法和域,这些方法和域可以实现将各种支持性数据和它对应的常量对应起来(比如将Apple实例与其图片、介绍等内容关联起来),自定义域的定义的在前面说私有构造器的时候已经举过相关例子了,这里把自定义的方法扩展一下:

public enum Planet{
    MERCURY(3.302e+23, 2.439e6),
    VENUS(4.869e+24, 6.052e6),
    EARTH(5.972e+24, 6.378e6),
    MARS(6.419e+23, 3.396e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN(5.685e+26, 6.052e7),
    URANUS(8.681e+25, 2.556e7),
    NEPTUNE(1.002e+25, 1.188e6);

    private final double mass;
    private final double radius;
    private final double surfaceGravity;
    private final static double G = 6.67300E-11;

    Planet(double mass, double radius){
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G*mass/(radius*radius);
    }

    public double getMass(){
        return mass;
    }

    public double getRadius(){
        return radius;
    }

    public double surfaceWeight(double mass){
        return mass*surfaceGravity;
    }
}

这里作者强调由于枚举类本身是不可变的,因此所有的域都要改成final,而且要做好封装,能私有化的尽量都私有化,通过公有接口来调用。

实例的不同行为

比如四则运算,每一个枚举实例的运算规则都不相同,这种实例的不同行为可以有多种实现方式:

switch语句
public enum Operation{
    PLUS, MINUS, TIMES, DIVIDE;


    public double apply(double x, double y){
        switch(this){
            case PLUS: return x+y;
            case MINUS: return x-y;
            case TIMES: return x*y;
            case DIVIDE: return x/y;
        
        }

        throw new AssertionError("Unknow op:"+this);
    }

}

//由switch语句来实现apply方法

这种方法比较简洁,但是有几个问题:

一是必须声明throw方法,不然无法通过编译,但是枚举类保证了实例的范围,所以这段代码是永远不会被执行的。

二是一旦新增了枚举常量,就需要记得给switch方法中再增加一个case。

抽象方法
package Operation;

public enum BasicOperation{
    PLUS("+"){
        public double apply(double x, double y){
            return x + y;
        }
    },

    MINUS("-"){
        public double apply(double x, double y){
            return x-y;
        }
    },

    MULTIPLY("*"){
        public double apply(double x, double y){
            return x * y;
        }
    },

    DIVIDE("/"){
        public double apply(double x, double y){
            return x / y;
        }
    };

    public abstract double apply(double x, double y);//在枚举类中设置抽象方法,然后让各个实例来自行实现。 

    private final String symbol;

    BasicOperation(String symbol){
        this.symbol = symbol;
    }

    @Override
    public String toString(){
       return symbol;
    }
}

这个方法将公共方法抽象到了类中,实现了高内聚,但是还可以进一步的抽象为公共接口。

公共接口
package Operation;

//将apply方法抽象到接口当中
public interface Operation {
    public double apply(double x, double y);
}

//具体的枚举类通过实现接口来调用接口方法apply
public enum BasicOperation implements Operation{
    PLUS("+"){
        public double apply(double x, double y){
            return x + y;
        }
    },

    MINUS("-"){
        public double apply(double x, double y){
            return x-y;
        }
    },

    MULTIPLY("*"){
        public double apply(double x, double y){
            return x * y;
        }
    },

    DIVIDE("/"){
        public double apply(double x, double y){
            return x / y;
        }
    };

    private final String symbol;

    BasicOperation(String symbol){
        this.symbol = symbol;
    }

    @Override
    public String toString(){
       return symbol;
    }
}

公共接口的方式进一步提升了抽象的层次,使得任何枚举类都可以通过实现接口来调用相关方法,这种方式适合新建另一个枚举类的时候使用(比如新建一个AdvancedOperation类实现了幂运算和开方运算)。

策略枚举

策略枚举是一种较为特殊的方式,它适用的场景是对于枚举实例设置一个新的分类方式,并基于这个分类方式实行差异化的方法,比如把周一到周日分为工作日和休息日,其中工作日超过八小时的工作时间会产生加班工资,在休息日所有工作都产生加班工资:

package PayrollDay;

public class PayrollDay {
    MONDAY(PayType.WEEKDAY),TUESDAY(PayType.WEEKDAY),WEDNESDAY(PayType.WEEKDAY),THURSDAY(PayType.WEEKDAY),FRIDAY(PayType.WEEKDAY),SATURDAY(PayType.WEEKEND),SUNDAY(PayType.WEEKEND);

    private final PayType, payType;

    GoodPayrollDay(PayType payType){
        this.payType = payType;
    }

    int pay(int minutesWorked, int payRate){
        return payType.pay(minutesWorked, payRate);
    }
    
}
//这里将PayTYpe设置为新的分类方式,并且作为基本枚举类PayrollDay的一个域映射到每一个实例上,而pay的实现方法都定义在PayTYpe中

enum PayType{
    WEEKDAY{
        int overtimePay(int minutes, int PayRate){
            return minutes <= MINS_PER_SHIFT ? 0 : (minutes - MINS_PER_SHIFT) * PayRate/2;
        };
    },
    WEEKEND{int overtimePay(int minutes, int PayRate){
        return minutes * PayRate/2;
        }
    };

    abstract int overtimePay(int minutes, int PayRate);
    private static final int MINS_PER_SHIFT = 8 * 60; 

    int pay(int minutes, int PayRate){
        int basePay = minutes*PayRate;

        return basePay + overtimePay(minutes, PayRate);
    }
}

用实例域代替默认序数

枚举类有默认实现的ordinal()方法来返回每个枚举常量在类型中对应的数字位置,但是这个数字并不是程序员可以直接控制的,而且没有实际的意义,因此作者建议自行定义关联值并将它保存在一个实例域中:

package Ensemble;

public enum ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10);

    private int numberOfMusicians;

    ensemble(int size){
        this.numberOfMusicians = size;
    }
    public int numberOfMusicians(){
        return numberOfMusicians;
    }
}

枚举集合

EnumSet

EnumSet是同一个枚举类常量的集合,在枚举类出现之前的int常量时期,往往使用位域操作来实现int常量的集合运算:

public class text {
    public static final int STYLE_BOLD = 1<<0;
    public static final int STYLE_ITALIC = 1<<1;
    public static final int STYLE_UNDERLINE = 1<<2;
    public static final int STYLE_STRIKETHROUGH = 1<<3;

    public static int applyStyle(int style){
        return style;
    };
}

这里由int常量来表示枚举,由于int有32位,这意味着可以使用int类型来表示最多32中不同的状态,并且通过or运算来做并集(比如将0001和1000合并成为1001)。

但是这种方式有很大的问题,一是它继承了int常量所有的缺陷,二是难以遍历位域运算形成的集合,三是在编写API的时候,必须先预测最多需要多少位,同时要选择合适的类型(如果特性较多可能要使用long类型),不然会导致溢出。

所以EnumSet可以完美解决上述的缺陷,它实现了Set接口的集合处理方法,使得对于集合的各种处理功能更加丰富。

public class Text{
    public enum Style{BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

    publi void applyStyles(Set<Style> styles){};
}


Text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

EnumMap

EnumMap即以枚举常量为key的Map,在实现EnumMap之前一般通过序数索引来构建枚举实例为key对应的Map类型,而序数索引一般由ordinal方法实现。

package Plant;

import java.util.HashSet;
import java.util.Set;

public class BadApplication {
    public static void main(String[] args) {
        Set<Plant> garden = new HashSet<>();
        garden.add(new Plant("Violets", Plant.LifeCycle.ANNUAL));
        garden.add(new Plant("Radishes", Plant.LifeCycle.PERENNIAL));
        garden.add(new Plant("Apple", Plant.LifeCycle.BIENNIAL));
        garden.add(new Plant("Pears", Plant.LifeCycle.BIENNIAL));
        garden.add(new Plant("Grapes",Plant.LifeCycle.ANNUAL));

        Set<Plant>[] plantByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
        for(int i = 0; i < Plant.LifeCycle.values().length; i++){
            plantByLifeCycle[i] = new HashSet<>();
        }

        for(Plant p : garden){
            plantByLifeCycle[p.getLifeCycle().ordinal()].add(p);
        }

        for(int i=0; i<plantByLifeCycle.length; i++){
            System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantByLifeCycle[i]);
        }
    }

}

这方法首先数组与泛型合用时程序需要进行未受检的转换,不然会在编译是出告警;二是ordinal()方法返回的序数和plantByLifeCycle序数的对应需要人工进行核对,程序是不保证的。

而EnumMap可以很好的解决这个问题:

public class Application {
    public static void main(String[] args) {
        Set<Plant> garden = new HashSet<>();
        garden.add(new Plant("Violets", Plant.LifeCycle.ANNUAL));
        garden.add(new Plant("Radishes", Plant.LifeCycle.PERENNIAL));
        garden.add(new Plant("Apple", Plant.LifeCycle.BIENNIAL));
        garden.add(new Plant("Pears", Plant.LifeCycle.BIENNIAL));
        garden.add(new Plant("Grapes",Plant.LifeCycle.ANNUAL));
        Map<Plant.LifeCycle, List<Plant>> plantByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
        for(Plant.LifeCycle lc: Plant.LifeCycle.values()){
            plantByLifeCycle.put(lc, new ArrayList<Plant>());
        }

        for(Plant p: garden){
            plantByLifeCycle.get(p.getLifeCycle()).add(p);
        }


        for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
            System.out.println(lc+": ");
            for (Plant plant : plantByLifeCycle.get(lc)) {
                System.out.println(plant);
            }
            System.out.println("\n");
        }
    }

这里有的读者会质疑EnumMap和普通Map相比在起初构建时并无显著不同,那它的优势在哪里:

  1. 类型安全EnumMap 的键必须是某个枚举类型的枚举值。这意味着在编译时就会检查键的有效性,从而避免了运行时可能出现的 ClassCastException
  2. 高效EnumMap 内部使用数组实现,因此在查找枚举键时非常高效。这是因为枚举类型的枚举值数量是固定的,而且是已知的,因此 EnumMap 可以预先计算出每个枚举值的位置。
  3. 有序EnumMap 会按照枚举值的声明顺序来维护键的顺序。这意味着当你遍历 EnumMap 时,键的顺序是确定的,这有助于简化代码逻辑。

除了上述方法之外,作者还展示了如何使用Stream的形式来实现EnumMap:

System.out.println(Arrays.stream(garden).collect(groupingBy(p->p.lifeCycle, ()->new EnumMap<>(LifeCycle.class), Collector.toSet())));

区别在于后一种方式可以仅包含garden涉及的枚举类型,而第一种的key中会列示所有的枚举常量。 

这里补充一下Stream的用法解析:

  1. 使用 Stream API:使用 Arrays.stream(garden) 将列表转换为 Stream。
  2. 分组:使用 Collectors.groupingBy 方法按 Plant 的 lifeCycle 属性进行分组。
    1. Plant::getLifeCycle:指定分组键的提取器。
    2. () -> new EnumMap<>(LifeCycle.class):指定创建 EnumMap 的工厂方法。
    3. Collectors.toSet():指定收集器,将每个分组的结果收集到 Set 中。
  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《Effective Java第三版》是由Joshua Bloch所著的一本Java编程指南。这本书是基于第二版的更新版本,目的是给Java程序员提供一些最佳实践和经验,以编写高效、可维护和可靠的Java代码。 这本书共分为15个章节,每个章节都讲解了一个与Java开发有关的重要主题。比如,章节一讲述了使用静态工厂方法代替构造器的优点,章节二则介绍了如何用Builder模式来构建复杂的对象。此外,书中还提及了Java对象的等价性、覆盖equals方法和hashCode方法、避免创建不必要的对象、使用泛型、枚举、lambda表达式等等。 《Effective Java第三版》通过具体的代码示例和清晰的解释来说明每个主题的关键概念,使读者能够更好地理解和应用。此外,书中还提供了一些实用的技巧和技术,例如避免使用原始类型、尽量使用接口而非类来定义类型等。 总的来说,这本书提供了很多实用的建议和技巧,可以帮助Java开发者写出高质量的代码。无论是初学者还是有经验的开发者,都可以从中受益匪浅。无论你是打算从头开始学习Java编程,还是已经有一定经验的开发者,这本书都是值得推荐的读物。 ### 回答2: 《Effective Java 第三版》是由Joshua Bloch 所著的一本Java编程指南,是Java程序员必读的经典之作。该书共包含90个目,涵盖了各种Java编程的最佳实践和常见问题的解决方法。 本书分为多个部分,每个部分都侧重于一个特定的主题。作者探讨了Java编程中的各种问题和挑战,并提供了解决方案和建议。这些建议包括如何选择和使用合适的数据结构和算法,如何设计高效的类和接口,如何处理异常和错误,以及如何编写可读性强的代码等等。 《Effective Java 第三版》还关注了Java编程中的性能优化和安全性问题。作者强调了遵循Java语言规范、使用标准库、防范常见安全漏洞等重要原则。此外,本书还介绍了Java 8及其后续版本的新特性和用法,如Lambda表达式、流式编程和Optional类等。 这本书的特点之一是每个目都独立于其他目,可以单独阅读和理解。每个目开头都有一个简洁的总结,让读者能够快速掌握主要观点。此外,书中还有大量的示例代码和解释,帮助读者更好地理解和运用所学知识。 总的来说,《Effective Java 第三版》是一本非常实用和全面的Java编程指南。它适用于各个层次的Java程序员,无论是初学者还是有经验的开发人员,都可以从中获得宝贵的经验和知识。无论是编写高质量的代码、优化性能还是确保安全性,这本书都是一本不可或缺的参考书籍。 ### 回答3: 《Effective Java 第3版(中文版)》是由 Joshua Bloch 所著的一本关于使用 Java 编程语言的指南书。该书是对 Java 语言的最佳实践的详尽描述,为中高级 Java 开发人员提供了许多实用的建议和技巧。 该书的主要内容包括Java 语言的优雅编程风格、类和接口的设计、Lambda 表达式和流的使用、泛型、异常和并发编程等方面的最佳实践。 在《Effective Java 第3版(中文版)》中,许多传统的 Java 开发中的陷阱、常见错误和不良习惯都得到了深入的剖析和解答。它不仅提供了可供开发人员参考的示例代码,还解释了为什么某种方式是有问题的,以及如何更好地进行改进。 该书的深度和广度非常适合正在努力提高 Java 编程技能的开发人员。它涵盖了多个关键领域,为读者提供了在实际项目中解决常见问题的方法和思路。 此外,《Effective Java 第3版(中文版)》还介绍了最新版本的一些特性和改进。例如,它详细说明了如何正确地使用 Java 8 中新增的 Lambda 表达式和流,以及如何充分利用 Java 9、10 和 11 中的新功能。 总之,这本书是 Java 开发人员必备的指南之一。通过深入理解和应用书中的实践建议,读者可以更加高效地编写、优化和维护 Java 代码。无论是想提升职业技能还是在项目中减少错误和问题,这本《Effective Java 第3版(中文版)》都是一本非常有帮助的参考书。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值