5. Enumeration and Annotation

Keywords: Java less sublime

Article 30: Replace the int constant with enum

Enumeration type refers to a group of fixed constants constituting legitimate worthwhile type. For example, the seasons of the year, the planets in the solar system or the colors in a deck of cards. In development, we often use static final to define an int constant in classes. There are many shortcomings in this way, and there is no help in type safety and convenience.

Look at some examples of enumeration types and see how they are used.

Planets in the solar system:

public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS     (4.869e+24, 6.052e6),
    EARTH      (5.975e+24, 6.378e6),
    MARS       (6.419e+23, 3.393e6),
    JUPITER    (1.899e+27, 7.149e7),
    SATURN   (5.685e+26, 6.027e7),
    URANUS  (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);
    private final double mass;
    private final double radius;
    private final double surfaceGravity;
    private static final double G = 6.67300E-11;
    Planet(double mass, double radius){
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }
    public double mass(){
        return mass;
    }
    public double radius(){
        return radius;
    }
    public double surfaceGravity(){
        return surfaceGravity;
    }
    public double surfaceWeight(double mass){
        return mass * surfaceGravity;
    }
}
public class WeightTable {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        double earthWeight = 70;
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for(Planet p : Planet.values()){
            System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
        }
    }

}

An example of an operator:

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    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("Unknown op: " + this);
    }
}

The switch mode is obviously not good enough. Let's improve it.

public enum Operation2 {
    PLUS("+")    {double apply(double x, double y){return x + y;}}, 
    MINUS("-") {double apply(double x, double y){return x - y;}}, 
    TIMES("*")  {double apply(double x, double y){return x * y;}}, 
    DIVIDE("/") {double apply(double x, double y){return x / y;}};
    private final String symbol;
    Operation2(String symbol){
        this.symbol = symbol;
    }
    @Override
    public String toString() {
        return symbol;
    }
    private final static Map<String, Operation2> stringToEnum = new HashMap<String, Operation2>();
    static {
        for(Operation2 op : Operation2.values()){
            stringToEnum.put(op.toString(), op);
        }
    }
    public static Operation2 fromString(String symbol){
        return stringToEnum.get(symbol);
    }
    abstract double apply(double x, double y);
    public static void main(String[] args){
        double x = Double.parseDouble("2");
        double y = Double.parseDouble("4");
        for(Operation2 op : Operation2.values()){
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
        }
        Operation2 op = fromString("+");
        System.out.println(op);
    }
}

An example of calculating normal wages and overtime wages:

public enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
    private static final int HOURS_PER_SHIFT = 8;
    double pay(double hoursWorked, double payRate){
        double basePay = hoursWorked * payRate;
        double overtimePay;
        switch(this){
            case SATURDAY: case SUNDAY:
                overtimePay = hoursWorked * payRate / 2;
            default:
                overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                break;
        }
        return basePay + overtimePay;
    }
}

The code is very concise, but from a maintenance point of view, it is very dangerous, also because of switch. If you add a new enumeration but forget to add the corresponding case in the switch statement, the program logic will go wrong.

Revised version:

public enum PayrollDay2 {
    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;
    private PayrollDay2(PayType payType) {
        this.payType = payType;
    }
    double pay(double hoursWorked, double payRate){
        return payType.pay(hoursWorked, payRate);
    }
    private enum PayType{
        WEEKDAY {
            @Override
            double overtimePay(double hrs, double payRate) {
                return hrs <= HOURS_PER_SHIFT ? 0 : (hrs - HOURS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            @Override
            double overtimePay(double hrs, double payRate) {
                return hrs * payRate / 2;
            }
        };
        abstract double overtimePay(double hrs, double payRate);
        private static final int HOURS_PER_SHIFT = 8;
        double pay(double hoursWorked, double payRate){
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }
    }
}

In summary, the advantages of enumeration types over int s are self-evident. Enumeration is much easier to read, safer and more powerful.

 

Article 31: Replacing ordinal numbers with instance fields

The enumeration type has an ordinal method, which ranges the ordinal number of the constant from 0. It is not recommended to use this method because it does not maintain the enumeration well. It should use the instance domain correctly, for example:

public enum Ensemble2 {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), 
    NONET(9), DECTET(10);
    private final int nuberOfMusicians;
    Ensemble2(int size){
        this.nuberOfMusicians = size;
    }
    public int numberOfMusicians(){
        return nuberOfMusicians;
    }
}

 

 

Article 32: Replace the bit field with EnumSet

Bit field representation allows the use of bit operations to effectively perform set operations such as union and intersection first. But bit fields have all the drawbacks of int enumeration, even more. When bitfield is printed in digital form, it is much more difficult to translate bitfield than to translate simple int enumeration constants. Even there is no easy way to traverse all elements represented by bit fields.

public class Text {
    public static final int STYLE_BOLD                   = 1 << 0;    //1
    public static final int STYLE_ITALIC                  = 1 << 1;    //2
    public static final int STYLE_UNDERLINE         = 1 << 3;    //4
    public static final int STYLE_STRIKETHROUGH = 1 << 3;    //8
    
    public void applyStyles(int styles){}
}

The java.util package provides EnumSet classes to effectively represent multiple sets of values extracted from a single enumeration type. This class implements the Set interface, providing rich functionality, type security, and interoperability that can be derived from any other Set implementation. But in the internal implementation, each EnumSet content is represented as a bit vector. If the underlying enumeration type has 64 or fewer elements -- most of them. The entire EnumSet is represented by a single long, so its performance is better than that of the upper domain. Batch processing, such as removeAll and retainAll, is implemented using bit algorithms. It's just like the manual replacement of bit domains. But you can avoid errors and less elegant code when you do it manually, because EnumSet does the hard work for you. `

//EnumSet - a modern replacement for bit fields
public class Text2 {
  public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH };

  //Any Set could be passed in, but EnumSet is clearly best
  public void applyStyles(Set<Style> styles) { 
      System.out.println(styles);
  }

  public void test() {
      applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
  }
}

In short, because enumeration types are used in Set sets, there is no reason to express them in bit fields.

 

Article 33: Replace ordinal index with EnumMap

Sometimes we use ordinal () to index the code of an array. This method is feasible, but there are hidden problems, such as incompatibility between arrays and generics, undetected transformation of programs, insertion of ordinal numbers, security of enumeration types, easy use of wrong values, etc. Some examples show that:

public class Herb {
    //Indicates annual, perennial or biennial plants.
    public enum Type { ANNUAL, PERENNIAL, BIENNIAL }
    
    private final String name;
    private final Type type;
    Herb(String name, Type type){
        this.name = name;
        this.type = type;
    }
    @Override
    public String toString() {
        return name;
    }
    
    public static void main(String[] args){
        //Using ordinal() to index an array - Don't do this
        Herb[] garden = {new Herb("plant1", Type.ANNUAL), new Herb("plant2", Type.BIENNIAL), new Herb("plant3", Type.PERENNIAL), 
                                   new Herb("plant4", Type.PERENNIAL), new Herb("plant5", Type.ANNUAL), new Herb("plant6", Type.BIENNIAL)};
//        @SuppressWarnings("unchecked")
//        Set<Herb>[] herbsByType = (Set<Herb>[])new Set[Herb.Type.values().length];
//        for(int i = 0; i < herbsByType.length; i++){
//            herbsByType[i] = new HashSet<Herb>();
//        }
//        for(Herb h : garden){
//            herbsByType[h.type.ordinal()].add(h);
//        }
//        for(int i = 0; i < herbsByType.length; i++){
//            System.out.printf("%s : %s%n", Herb.Type.values()[i], herbsByType[i]);
//        }
        
        //Using an EnumMap to associate data with an enum
        Map<Herb.Type, Set<Herb>> herbsByType2 = new EnumMap<>(Herb.Type.class); 
        for(Herb.Type t : Herb.Type.values()){
            herbsByType2.put(t, new HashSet<Herb>());
        }
        for(Herb h : garden){
            herbsByType2.get(h.type).add(h);
        }
        System.out.println(herbsByType2);
    }
}
public enum Phase2 {
    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 Phase2 src;
        private final Phase2 dst;
        Transition(Phase2 src, Phase2 dst){
            this.src = src;
            this.dst = dst;
        }
        private static final Map<Phase2, Map<Phase2, Transition>> m = new EnumMap<>(Phase2.class);
        static {
            for(Phase2 p : Phase2.values()){
                m.put(p, new EnumMap<Phase2, Transition>(Phase2.class));
            }
            for(Transition trans : Transition.values()){
                m.get(trans.src).put(trans.dst, trans);
            }
        }
        public static Transition from(Phase2 src, Phase2 dst){
            return m.get(src).get(dst);
        }
    }
}

In short, it's better not to index arrays by ordinal numbers, but by EnumMap.

 

Rule 34: Simulating scalable enumerations with interfaces

An example of operation has been written before:

/**
 * Addition, subtraction, multiplication and division enumeration
 * Created by yulinfeng on 8/20/17.
 */
public enum Operation {
    PLUS {
        double apply(double x, double y) {
            return x + y;
        }
    },
    MIUS {
        double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        double apply(double x, double y) {
            return x * y;
        }
    },
    DEVIDE {
        double apply(double x, double y) {
            return x / y;
        }
    };

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

This method is also good, but it is not a good method from the point of view of software development extensibility. Software extensibility is not to modify the original code. At this time, the interface is needed. Modify the example above:

public interface IOperation {
    double apply(double x, double y);
}
public enum ExtendedOperation implements IOperation{
    EXP("^"){
        public double apply(double x, double y){
            return Math.pow(x, y);
        }
    };
    private final String symbol;
    private ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override
    public String toString() {
        return symbol;
    }
    public static void main(String[] args){
        double x = Double.parseDouble("2");
        double y = Double.parseDouble("3");
    }
}

So when we need to expand operator enumeration, we just need to re-implement the IOperation interface. This makes the code scalable, but there is a minor drawback in doing so, that is, it cannot be inherited from one enumeration type to another.

 

Article 35: Annotations take precedence over naming patterns

There are several serious drawbacks in using naming patterns to indicate that some program elements need to be specially handled through a tool or framework:

1. Writing errors can lead to failure without any hints.

2. It is not possible to ensure that they are only used on the corresponding program elements

3. It cannot provide a good way to associate parameter values with program elements.

Examples of annotations are given:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {

}
public class Sample {
    @Test 
    public static void m1(){
        System.out.println("aaaxxx");
    }
    public static void m2(){}
    @Test
    public static void m3(){
        throw new RuntimeException("Boom");
    }
    @Test 
    public void m4(){}
    @Test 
    public static void m5(){
        System.out.println("aaaxxx2");
    }
    @Test
    public static void m6(){
        throw new RuntimeException("Boom2");
    }
}
public class RunTests {

    public static void main(String[] args) throws Exception{
        int tests = 0;
        int passed = 0;
        String className = "chapter6.Sample";
        Class testClass = Class.forName(className);
        for(Method m : testClass.getDeclaredMethods()){
            if(m.isAnnotationPresent(Test.class)){
                tests++;
                try{
                    m.invoke(null);
                    passed++;
                }catch (InvocationTargetException wrappedExc){
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                }catch(Exception exc){
                    System.out.println("INVALID @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }

}

The main test method obtains relevant annotations through reflection technology and tests related methods.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception> value();
}
public class Sample2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1(){
        int i = 0;
        i = i / i;
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m2(){
        int[] a = new int[0];
        int i = a[1];
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m4(){
    }
    
    public static void main(String[] args) throws Exception{
        int tests = 0;
        int passed = 0;
        String className = "chapter6.Sample2";
        Class testClass = Class.forName(className);
        for(Method m : testClass.getDeclaredMethods()){
            if(m.isAnnotationPresent(ExceptionTest.class)){
                tests++;
                try{
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception %n", m);
                }catch (InvocationTargetException wrappedExc){
                    Throwable exc = wrappedExc.getCause();
                    Class<? extends Exception> excType = m.getAnnotation(ExceptionTest.class).value();
                    if(excType.isInstance(exc)){
                        passed++;
                    }else{
                        System.out.printf("Test %s failed: expected %s, got %s %n", m, excType.getName(), exc);
                    }
                }catch(Exception exc){
                    System.out.println("INVALID @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

The above example is similar to the code that handles Test annotations, but at different points, this code extracts the value of the annotation parameter and uses it to verify that the exception thrown by the Test is of the correct type.

In summary, most of our programmers do not need to define annotation types, but we should use the annotation types provided by the platform.

 

Article 36: Stick to the override annotation

For traditional programmers, override annotations are the most important of all annotation types. It indicates that the annotated method declaration overrides a declaration in a superclass. If you insist on using this annotation, you can prevent a large class of illegal errors. Let's take an example of a double letter pair.

public class Bigram {
    private final char first;
    private final char second;
    public Bigram(char first, char second){
        this.first = first;
        this.second = second;
    }

//    public boolean equals(Bigram b){
//        return b.first == first && b.second == second;
//    }
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Bigram)){
            return false;
        }
        Bigram b = (Bigram) obj;
        return b.first == first && b.second == second;
    }
    public int hashCode(){
        return 31 * first + second;
    }
    public static void main(String[] args){
        Set<Bigram> s = new HashSet<>();
        for(int i = 0; i < 10; i++){
            for(char ch = 'a'; ch <= 'z'; ch++){
                s.add(new Bigram(ch, ch));
            }
        }
        System.out.println(s.size());
    }
}

As you can see from the code, the annotated equals method does not cover the superclass method correctly. If you insist on using override annotations, code validation will be performed, and no such error will occur.

 

Article 37: Defining Types with Marked Interfaces

A tag interface is an interface that does not contain method declarations, but simply specifies that a class implements an interface with certain attributes, such as a Serializable interface that indicates that it can be instantiated. In some cases, it may be better to use tag annotations than tag interfaces, but there are two points mentioned in the book that tag interfaces outperform tag annotations:

1) The types defined by tag interfaces are implemented by instances of tagged classes; tag annotations do not define such types. // It's a little hard to understand. I hope the God will explain it more popularly.

2) Although tag annotations can lock classes, interfaces, and methods, they are for all, and tag interfaces can be locked more accurately.

In addition, the book also mentions that tag annotations are better than tag interfaces: they can tag program elements rather than classes and interfaces, and they can add more information to tags in the future.

Posted by shapiro125 on Tue, 14 May 2019 21:52:49 -0700