EffectiveJava(5)之枚举和注解

注:本文是《Effective Java》学习的笔记。

Java支持两种特殊用途的引用类型,一种是类,称作枚举类型。另一种是接口,称作注解类型。

34.用enum代替int常量

枚举类型有两个特征,一是它可能的值是有限的且预先定义的,二是枚举值都有一个顺序

枚举之前常常使用int常量来存储常量数值,它存在些许不足。int常量方式没有类型安全性。没有可描述性。int常量是编译时常量,它们的int值会被编译到使用它们的客户端中,如果与int常量关联的值发生了变化,客户端必须重新编译。

而对于字符串常量会导致性能问题,因为它依赖于字符串的比较操作。

 

java枚举类型的基本想法非常简单:这些类通过 public static final 域为每个枚举变量导出了一个实例。枚举类型没有可以访问的构造器,所以它是真正的final类。客户端不能创建枚举类型的实例,也不能对它进行扩展,因此不存在实例,而只存在声明过的枚举常量。换句话说,枚举类型是实例受控的。它们是单例的泛型化,本质上是单元素的枚举。

如下的一个枚举

public enum Fruit {
    BANANA(4.0),APPLE(8.8),ORANGE(6.5);
    private double price;
    private Fruit(double fruitPrice){
        this.price = fruitPrice;
    }

    public static void main(String[] args) {
        System.out.println(BANANA.price + "--"+APPLE.price+"--"+ORANGE.price);
    }
}

编译成字节码文件后再反编译的结果如下。 (没截静态static块) javap -c Fruit  

public final class zy.service.effective.five.Fruit extends java.lang.Enum<zy.service.effective.five.Fruit> {
  public static final zy.service.effective.five.Fruit BANANA;

  public static final zy.service.effective.five.Fruit APPLE;

  public static final zy.service.effective.five.Fruit ORANGE;

  public static zy.service.effective.five.Fruit[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lzy/service/effective/five/Fruit;
       3: invokevirtual #2                  // Method "[Lzy/service/effective/five/Fruit;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lzy/service/effective/five/Fruit;"
       9: areturn

  public static zy.service.effective.five.Fruit valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class zy/service/effective/five/Fruit
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;

       6: checkcast     #4                  // class zy/service/effective/five/Fruit
       9: areturn
 

枚举类型保证了编译时的类型安全。

有一种好的方法可以将不同的行为与每个枚举常量关联起来。在枚举类中声明一个抽象方法。在枚举实例中重写这个方法。改成下面这样。你也可以不用抽象abstract 就可以不必须重写。默认为枚举类中的方法体。

BANANA{
    @Override
    public double plusPrice(double a, double b) {
        return 0;
    }
},apple{};

public double plusPrice(double a,double b){
    return a+b;
}

枚举可以嵌套枚举来实现需求。例如算工时,周末算加班。

public enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    SATURDAY(Paytype.WEEKEND), SUNDAY(Paytype.WEEKEND);
    private final Paytype paytype;

    PayrollDay(Paytype paytype) { //传周末的入口。
        this.paytype = paytype;
    }

    PayrollDay() {   //默认是工作日   不加空参构造就得在实例上都添加paytype的类型。
        this(Paytype.WEEKDAY);
    }
    int pay(int mins,int payRate){     //调用计算好的对应的pay方法的值。
        return paytype.pay(mins, payRate);
    }

    private enum Paytype {    //区分周末计算方法和工作日的计算方法的枚举。最终使用pay方法返回值    策略枚举
        WEEKDAY {
            @Override
            int overtimePay(int mins, int payRate) {
                return 0;
            }
        }, WEEKEND {
            @Override
            int overtimePay(int mins, int payRate) {
                return 0;
            }
        };

        abstract int overtimePay(int mins, int payRate);

        int pay(int mins, int payRate) {    
            int basePay = mins * payRate;
            return basePay + overtimePay(mins, payRate);
        }
    }
}

枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为。

public static Paytype inverse(PayrollDay payrollDay){
    switch (payrollDay){
        case MONDAY:return MONDAY.paytype;
        case ......
        default:throw new AssertionError("unknown payrollDay " +payrollDay);
    }
}

枚举使用场景:每当需要一组固定常量,并且在编译时就知道其成员的时候,就应该使用枚举。枚举类型中的常量集并不一定要始终保持不变。

35.用实例域代替序数

避免使用ordinal() 这个方法我们知道是用来返回枚举实例下标的。但是要是顺序一换就凉了。

永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。

public enum Week {
    MONDAY(1),FRIDAY(5);
    private int number;
    Week(int number){
        this.number = number;
    }
    public int getNumber(){
        return number;
    }
}

36.用EnumSet代替位域

用OR位运算将几个常量合并到一个集合中,称作位域。

例如  2|4  == 6  位域有很多缺点。 因为要翻译成二进制再计算就很烦。还要预算位数来选择存储类型。

EnumSet类有效地表示从单个枚举类型中提取的多个值的多个集合。  使用位向量来编写的。

正是因为枚举类型要用在集合中,所以没有理由用位域来表示它。EnumSet内部用位操作,所有性能绝对够用。

public static void enumSetTest(Set<PayrollDay> days){
    days.forEach(s-> System.out.println(s.name()));
}
public static void main(String[] args) {
    enumSetTest(EnumSet.of(PayrollDay.MONDAY,PayrollDay.FRIDAY));
}

可以看到EnumSet可以工厂构造。只需传入相同泛型的元素即可。因为他是个抽象类不能new

EnumSet集位域的简洁和性能优势及枚举类型的所有优点于一身。

但是EnumSet有一个缺点,无法创建不可变的EnumSet 可以用Collections.unmodifiableSet将EnumSet封装起来。

这里有一个EnumSet的例子:https://www.cnblogs.com/swiftma/p/6044718.html

例子讲的大概是:统计 哪一天没有一个人上班,哪一天只有一个人上班。通过 Day周1-7枚举加上劳动者工作具体信息使用EnumSet进行筛选。这一应用场景。

37.用EnumMap代替序数索引

最好不要用序数也就是 ordinal 来当作枚举数组的索引。  下面时EnumMap的几个实例变量。

private final Class<K> keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
private transient int size = 0;

举个用到EnumMap的例子,用来统计每个瓶子的规格的数量。

@Getter
@Setter
@AllArgsConstructor
public class Bottle{
    private double price;
    private Type type;
    enum Type {
        SMALL,MID,BIG;   //规格枚举
    }
    public static Map<Type,Integer> getBottleNum(List<Bottle> bottles){ //计算规格的方法,相当于分组
        final Map<Type,Integer> map = new EnumMap<>(Type.class);
        bottles.forEach(bottle -> {
            Type type = bottle.getType();   //当前杯子的规格
            Integer num = map.get(type);    //当前规格的数量
            if(Objects.nonNull(num)){
                map.put(type, num+1);
            }else{
                map.put(type, 1);
            }
        });
        return map;
    }
    public static void main(String[] args) {
        Bottle bottle = new Bottle(12,Type.SMALL);   //测试数据
        Bottle bottle1 = new Bottle(13,Type.SMALL);
        Bottle bottle2 = new Bottle(14,Type.BIG);
        List<Bottle> list = new ArrayList<>();
        list.add(bottle);
        list.add(bottle1);
        list.add(bottle2);
        Map<Type, Integer> map = getBottleNum(list);
        System.out.println(map);                     //按照枚举定义的顺序输出。{SMALL=2, BIG=1}
    } 
}

如上用stream + lambda 按照 Type 分组也可以做到。只不过 value存储的是一个集合,而不是一个Integer

EnumMap内部有两个数组,长度相同,一个表示所有可能的键,一个表示对应的值,值为null表示没有该键值对,键都有一个对应的索引,根据索引可直接访问和操作其键和值,效率很高。 

38.用接口模拟可扩展的枚举

由于接口不能扩展,所以使用接口来扩展枚举就成了解决方式。

虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型来对它进行模拟。

public enum ExtendEnum implements ExtendInterface{
    ONE("first"){
        @Override
        public void say() {
            System.out.println(1);
        }
    },TWO("second"),THREE("third");
    private String name;
    ExtendEnum(String name){
        this.name = name;
    }
    @Override
    public void say() {   //这里重写则实例可以默认成这个方法,或者直接实例重写。like ONE
        
    }
}

39.注解优先于命名模式

命名模式就是一个规则,比如以test开头的函数可以作为Junit单元测试 。可想而知很不方便,而且易出错。总之不要这么规则一个东西。

而注解则是灵活的。比如你向单元测试。方法上面加个@Test注解来完成。

 

关于注解我记载在这篇博客上了:https://blog.csdn.net/finalheart/article/details/86552207

 

40.坚持使用Override注解

在你想要覆盖超类声明的每个方法声明中使用Override注解。

例如  equals方法。注意它的参数类型是Object 如果不是Object而是别的的话就是重载而不是重写了。

public boolean equals(Object obj) {}

而如果你知道这个就是在重写父类,那么就添加上@Override注解。这样编辑器会帮助你检查这个错误。

很不错的建议。

41.用标记接口定义类型

标记接口是不包含方法声明的接口,它只是指明一个类实现了具有某种属性的接口。例如serializable等。

标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。

标记接口胜过标记注解的另一个优点是,他们可以被更加精确的进行锁定。

标记注解胜过标记接口的最大优点在于,他们是更大的注解机制的一部分。

如果你发现自己在编写的是目标为ElementType.TYPE的标记注解类型,就要花点时间考虑清楚,它是否真的应该为注解类型,想想标记接口是否会更加合适。

对类和接口的标记用标记接口,对其他的可以使用标记注解。

 

 

参考:https://www.cnblogs.com/swiftma/p/6044672.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章