《Effective Java》阅读笔记-第六章

发布时间 2023-12-18 15:47:10作者: AliveCats

Effective Java 阅读笔记

第六章 枚举和注解

第 34 条 用 enum 代替 int 常量

int 类型常量或者 String 类型常量作为参数的可读性和可维护性都比较差,甚至 IDE 都不好提示。

Java 中的枚举是完全单例,并且可以有字段、方法,以及实现接口(因为编译之后就是个类,并且自动继承了java.lang.Enum类)。

给枚举实现方法时,如果会根据枚举类型进行不同的处理,不要使用 switch 或者 if 进行判断:

这是一个反例:

枚举反例
public enum BadOperation {
    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 UnsupportedOperationException("unknown op: " + this);
    }
}

如果添加了新的操作方法枚举,很容易在switch时漏掉,这种情况可以在枚举上声明抽象方法,然后针对每个枚举实例去实现抽象方法:

枚举抽象方法

public enum GoodOperation {
    PLUS {
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        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);
}

在后续添加新字段/方法的时候就不会忘记处理(不处理就编译不过):

枚举添加新字段
public enum GoodOperation {
    PLUS("+") {
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        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;

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

    public abstract double apply(double x, double y);

    @Override
    public String toString() {
        return "GoodOperation{" +
                "symbol='" + symbol + '\'' +
                '}';
    }
}

如果枚举中只有部分是特定计算方法,比如周一到周五工资计算是工作日工资计算方式,周六和周日是加班工资计算方式,
那么在定义星期的时候,可以吧计算方式通过构造函数穿进去,计算方式通过内部枚举进行定义,这样虽然啰嗦一些,但是更安全,可以避免添加新的枚举常量时漏掉switch中的定义。

第 35 条 用字段代替枚举序号

每个枚举类的实例都有方法ordinal(),可以获取枚举的序号,从0开始。但是这个序号是根据位置获得的,不要用这个序号作为业务中使用的序号,使用字段来代替这个东西,不然非常不好维护。永远不要根据序号获取值,如果有需要就保存到字段上。

字段代替序号
public enum Ensemble {
    SOLO(1),
    DUET(2),
    // ...
    ;

    private final int numberOfMusicians;

    Ensemble(int numberOfMusicians) {
        this.numberOfMusicians = numberOfMusicians;
    }

    public int getNumberOfMusicians() {
        return numberOfMusicians;
    }
}

第 36 条 用 EnumSet 代替位域

位域:位域(Bit fields)通常是指将一个或多个相关的布尔标志(或状态)打包到单个整数类型中的特定位上。这允许有效地使用内存,因为它避免了使用多个独立的布尔变量,而是将它们存储在一个整数类型中的不同位上。使用位域通常涉及到位操作,例如位与(&)、位或(|)、位取反(~)和位移(<<、>>)

位域例子
public class FlagsExample {
    // 定义一些标志的位置
    private static final int FLAG1 = 1;  // 0001
    private static final int FLAG2 = 2;  // 0010
    private static final int FLAG3 = 4;  // 0100
    private static final int FLAG4 = 8;  // 1000

    // 用一个整数类型的变量来表示一组标志
    private int flags;

    // 设置标志的方法
    public void setFlag1(boolean value) {
        if (value) {
            flags |= FLAG1;
        } else {
            flags &= ~FLAG1;
        }
    }

    public void setFlag2(boolean value) {
        if (value) {
            flags |= FLAG2;
        } else {
            flags &= ~FLAG2;
        }
    }

    // 其他标志的设置方法类似...

    // 检查标志的方法
    public boolean isFlag1Set() {
        return (flags & FLAG1) != 0;
    }

    public boolean isFlag2Set() {
        return (flags & FLAG2) != 0;
    }

    // 其他标志的检查方法类似...
}

使用位域不仅可读性不好,容易出错,而且需要提前计算flag数量,确定使用int还是lang,而且超过64个之后也不再支持(int为32个)。

EnumSet可以很好的解决,例子:

Bad Example
public class Text {
    public static final int STYLE_BLOD      = 1 << 0; // 1
    public static final int STYLE_ITALIC    = 1 << 1; // 2
    public static final int STYLE_UNDERLINE = 1 << 2; // 4
    // ...

    // 根据静态常量进行位运算获取最终style
    // 比如 text.applyStytle(STYLE_BLOD | STYLE_ITALIC)
    public void applyStytle(int styles) {
        // ...
    }
}

位运算非常容易出现错误,并且不美观,唯一的优点就是内存占用少。

Good Example
public class Text {
    // 使用枚举代替
    public static enum Style {BLOD, ITALIC, UNDERLINE;}

    public void applyStytle(Set<Stytle> styles) {
        // ...
    }
}

修改成枚举之后可以通过EnumSet.of(Style.BLOD, Style.ITALIC)来快速创建枚举 set 集合。

第 37 条 用 EnumMap 代替序号索引

道理和上一条类似,如非必要,永远不要使用ordinal()

如果要表达的关系是多维的就用EnumMap<..., EnumMap<...>>表示。

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

枚举不方便继承,如果需要扩展枚举,就需要通过接口来实现。

比如上面例子中的Operation枚举,把抽象方法改为接口,可以更方便的进行扩展:

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

// 然后枚举类实现接口
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;
        }
    },
    TIMES("*") {
        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 "BasicOperation{" +
                "symbol='" + symbol + '\'' +
                '}';
    }
}

这样扩展的枚举只需要实现Operation接口就行。

接受枚举的地方也需要修改,第一种方式是通过传递 Class 对象,就可以获取该枚举下的所有示例:

public <T extends Enum<T> & Operation> void useEnumMethod(Class<T> enumClass, double x, double y) {
    for (T enumConstant : enumClass.getEnumConstants()) {
        enumConstant.apply(x, y);
    }
}

或者仅传输一个实例:

public <T extends Enum<T> & Operation> void useEnumMethod(T opation, double x, double y) {
    opation.apply(x, y);
}

泛型<T extends Enum<T> & Operation>限定了只能是实现了 Operation 接口的枚举类。

第二种方式是传递 Operation 集合:

public void useEnumMethodByCollection(Collection<? extends Operation> operations, double x, double y) {
    for (Operation operation : operations) {
        operation.apply(x, y);
    }
}

这种方式相对更灵活一些。

第 39 条 注解优先于命名模式

命名模式(naming pattern)就是根据名称进行处理,一般是工具或者框架需要进行特殊操作时使用。比如 Java 4 发行之前,JUnit 要求 test 作为方法名的开头。

这种方式的限制很多,注解显然是更好的解决方法,用注解进行操作可比用名字好多了。

第 40 条 坚持使用 Override 注解

重写父类方法时使用 Override 注解可以在编译期就发现很多错误,而且现在重写方法IDE中直接用快捷键就会自动加上,为什么不用呢?

第 41 条 用标记接口定义类型

标记接口(marker interface)就是不包含任何方法的接口,仅作为一个标记,比如Serializable接口。
同事还有标记注解,比如弃用注解@Deprecated

标记接口相对标记注解而言优点:

  1. 接口的类型实例就是被标记的类型,枚举没有具体的实例类型,因此可以在编译期就发现错误,而不是推迟到运行时。
  2. 被接口标记的类型可以更精准的指定类型。因为接口可以继承,所以可以为特定接口进行标记,而注解如果要标记类型,那么所有的类和接口都允许被标记。

标记注解最大的优点就是:不仅可以标记类和接口,还可以标记方法、参数等。而且可以标记一次或多次,并且逐渐添加丰富的信息。

  • 注解可以标记一次

标记接口和标记注解的选择:

  • 如果标记类、接口、枚举,那就用接口
  • 如果是方法、字段等非类、非接口的地方,就用注解

这本书的中文版真是狗屎翻译