Effective Java(五)

五、枚举和注解

1. 用enum代替int常数

        枚举类型是指由一组固定的常量组成合法值的类型,在编程语言没有引入枚举之前,表示枚举类型的常用模式是声明一组具名的int常量,每个类型成员一个常量:

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

        这种方法称作int枚举模式,它存在着诸多不足:

  • 类型安全性问题  可能会传递错误的值
  • 没有自己的命名空间  一般只能通过前缀的形式区分
  • 采用int枚举模式的程序十分脆弱  int枚举是编译时常量,被编译到使用它们的客户端中,如果枚举常量值发生了变化,客户端必须重新编译才行
  • 无法提供便利的方法打印信息  int枚举的打印信息只是数字

        String枚举模式是int枚举模式的变体,虽然它可以提供可打印的字符串,但存在性能及书写时的安全性问题。

        Java1.5开始,提供了枚举类型

public enum Apple {
    FUJI,
    PIPPIN,
    GRANNY_SMITH
}

         枚举类型不仅可以避免int枚举模式和String枚举模式的缺点,还可以提供许多额外的好处:

(1)提供编译时的类型安全
        如果声明一个参数的类型为枚举类型Apple,就可以保证,被传递到该参数上的任何非null的对象引用一定属于三个有效的Apple之一。试图传递类型错误的值时,会导致编译错误。
(2)每个枚举类型都有自己的命名空间
        枚举类是独立的类型,有自己的命名空间,可以增加或者重新排列枚举类型中的常量。
(3)可提供便利的打印信息
        通过toString(),可以将枚举转换成可打印的字符串。
(4)允许添加任意的方法和域,并实现任意的接口
        枚举是一种类型,可以拥有自己的方法和域,并实现接口。

        枚举的缺点:
        装载和初始化枚举时会有空间和时间的成本

        在枚举中添加域和方法的动机:

  • 想将数据与它的常量关联起来
  • 添加方法增强枚举类型功能

        如果一个枚举具有普适性,就应该成为一个顶层类;如果它只是被用在一个特定的顶层类中,就应该成为该顶层类的一个成员类

        在枚举类中添加方法时,这些方法是枚举常量共有的,但有时每个常量都会关联本质上完全不同的行为,可以使用特定于常量的方法实现来完成。它的实现过程如下:

  • 在枚举类型中声明一个抽象的方法
  • 在特定常量的类主体中,用具体的方法实现抽象方法
enum Operation {
    PLUS {
        @Override
        double apply(double x, double y) {
            return x + y;
        }
    },
    
    MINUS {
        @Override
        double apply(double x, double y) {
            return x - y;
        }
    },
    
    TIMES {
        @Override
        double apply(double x, double y) {
            return x * y;
        }
    },
    
    DIVIDE {
        @Override
        double apply(double x, double y) {
            return x / y;
        }
    };

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

2. 用实例域代替序数

        所有的枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数组位序。
        依赖ordinal()返回的枚举常量序数会使得代码极难维护。因为枚举常量可能会进行重新排序,也可能会添加新的枚举常量。

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

//不当的使用方式
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;
    
    //依赖ordinal()返回与枚举常量关联的值
    public int numberOfMusicians() {
        return ordinal() + 1;
    }
}
//推荐的使用方式
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), TRIPLE_QUARTET(12);
    
    private final int numberOfMusicians;
    
    Ensemble(int size) {
        this.numberOfMusicians = size;
    }
    
    public int numberOfMusicians() {
        return numberOfMusicians;
    }
}

3. 用EnumSet代替位域

        如果一个枚举类型的元素主要用在集合中,可能会使用int枚举模式:

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 << 2;     //4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; //8
    
    public void applyStyles(int styles) {
        ...
    }
}

        这种表示法让你用or位运算符将几个常量合并到一个集合中,这个集合称作位域

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

        位域表示法也允许利用位操作,执行像交集、并集这样的集合操作。但位域具有int枚举常量所有的缺点,甚至更多。位域以数字形式打印时,翻译位域比翻译int枚举常量要困难的多,遍历位域表示的所有元素也相当不容易。

        Set是一种集合,只能向其中添加不重复的对象,enum也要求其成员都是唯一的,看起来也具有集合的行为,但不能从enum中删除/添加元素。Java1.5引入了EnumSet替代传统的基于int枚举类型的位域集合,它表示从单个枚举类型中提取多个枚举值的集合
        EnumSet是与enum类型一起使用的专用Set类型,EnumSet中的所有元素都必须来自同一个enum。

        使用EnumSet代替位域后的代码更加简短、更加清楚、更加安全:

public class Text {
    public enum Style {
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }
    
    public void applyStyles(Set<Style> styles) {
        ...
    }
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

4. 用EnumMap代替序数索引

        有时候,会见到利用枚举常量的序数作为数组下标来索引数组的代码,对应映射关系如下图所示:

        Enum序数作为数组索引,这种方法的确可行,但是隐藏着很多问题:

  • 数组不能与泛型兼容,使其使用受限
  • 数组不知道它的索引代表着什么,需要手工标注
  • 错误的索引值会引发数组越界异常

        Java1.5版本引入了EnumMap类型,它是一种特殊的Map,它要求其中的key必须来自一个enum,使用enum实例作为键在EnumMap中进行各种操作。EnumMap在运行速度方面可以与数组相媲美,它在内部实现中使用了数组,但是它对程序员隐藏了实现细节,它具有Map的丰富功能、类型安全,以及数组的快速访问。映射关系如下图:

        最好不要用序数来索引数组,而要使用EnumMap。
        应用程序的程序员在一般情况下都不使用Enum.ordinal()。 

public class Herb {
	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) {
		Herb[] garden = { new Herb("Basil", Type.ANNUAL),
				new Herb("Carroway", Type.BIENNIAL),
				new Herb("Dill", Type.ANNUAL),
				new Herb("Lavendar", Type.PERENNIAL),
				new Herb("Parsley", Type.BIENNIAL),
				new Herb("Rosemary", Type.PERENNIAL) };

		// Using an EnumMap to associate data with an enum - Page 162
		Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(
				Herb.Type.class);
		for (Herb.Type t : Herb.Type.values())
			herbsByType.put(t, new HashSet<Herb>());
		for (Herb h : garden)
			herbsByType.get(h.type).add(h);
		System.out.println(herbsByType);
	}
}

5. 用接口模拟可伸缩的枚举

        枚举类型不可扩展,但有时又需要枚举类型具备可伸缩的特性,一种好的方法就是利用接口:

public interface Operation {
    double apply(double x, double y);
}
public enum BasicOperation implements Operation {
    PLUS("+")  {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-")  {
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*")  {
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/")  {
        @Override
        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;
    }
}
public enum ExtendedOperation implements Operation {
    EXP("^") {
        @Override
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        @Override
        public double apply(double x, double y) {
            return x % y;
        }
    };
    
    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    
    @Override
    public String toString() {
        return symbol;
    }
}

        只要API是被写成采用接口类型(Operation)而非实现(BasicOperation),那么在可以使用基础操作的任何地方,都可以使用新的操作。


public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        //test(ExtendedOperation.class, x, y);  //方式一
        test(Arrays.asList(ExtendedOperation.values()), x, y);  //方式二
    }
    
    //方式一
    private static <T extends Enum<T> & Operation> void test(Class<T> opSet, double x, double y) {
        for (Operation op : opSet.getEnumConstants()) {
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
        }
    }

    //方式二
    private static void test(Collection<? extends Operation> opSet, double x, double y) {
        for (Operation op : opSet) {
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
        }
    }
}

6. 注解优先于命名模式

        Java1.5版本之前,一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理。例如,JUnit4之前原本要求测试方法要以test作为开头。这种方法可行,但有几个很严重的缺点:

  • 文字拼写错误会导致失败,且没有任何提示。
  • 无法确保它们只用于相应的程序元素上。如将某个类称作testSafetyMechanisms,希望JUnit可以自动地测试它的所有方法,而不管类中的方法名字是什么。虽然JUnit不会出错,但也不会执行测试。
  • 没有提供将参数值与程序元素关联起来的好方法。

        注解很好地解决命名模式的所有问题,因此,Java1.5版本后,JUnit4使用注解代替命名模式,重新实现了整个测试框架,使之更加强大、易用。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ExceptionTest {
    Class<? extends Exception>[] value();
}

class Sample {
    @ExceptionTest( { IndexOutOfBoundsException.class,
         NullPointerException.class})
    public static void doublyBad() {
        //List<String> list = new ArrayList<>();
        List<String> list = null;
        list.add(5,null);
    }
}

public class RunTest {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName("Sample");
        for(Method m : testClass.getDeclaredMethods()) {
            if(m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try{
                    m.invoke(null);
                }catch (InvocationTargetException ite) {
                    //Throwable exc = ite.getTargetException();
                    Throwable exc = ite.getCause();
                    Class<? extends Exception>[] excTypes 
                        = m.getAnnotation(ExceptionTest.class).value();
                    for(Class<? extends Exception> excType : excTypes) {
                        if(excType.isInstance(exc)) {
                            excType.newInstance().printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

7. 坚持使用Override注解

        Override注解只能用在方法声明中,它表示被注解的方法声明覆盖(重写)了超类型中的一个方法声明。坚持使用这个注解,可以防止一大类的非法错误。这类错误基本上都是由于不小心而造成的,使用Override注解后,编译器会做自动检查,可以避免这类无意识的错误。

        使用Override注解可以有效防止覆盖方法时的错误。

        例如:想要在String中覆盖equals方法

//这是方法重载,将产生编译错误
@Override
public boolean equals(String obj) {
    ....
}

//覆盖
@Override
public boolean equals(Object obj) {
    ....
}

8. 用标记接口定义类型

        标记接口是没有包含方法声明的接口,它只是标明一个类实现了具有某种属性的接口。例如,通过实现Serializable接口,表明类的实例可以被序列化。

标记接口胜过标记注解的两大优点:

  • 标记接口定义的类型是由被标记类的实例实现的,允许在编译时发现标记接口的使用错误。
  • 标记接口可以被更加精确地进行锁定,它可以用来标记某类特殊接口的实现。

标记注解胜过标记接口的两大优点:

  • 它可以通过默认的方式添加一个或者多个注解类型元素,给已被使用的注解类型添加更多信息。
  • 它是更大的注解机制的一部分,在那些支持注解作为编程元素的框架中具有一致性。

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

  • 如果标记是用到程序元素而不是类或接口,要使用注解;
  • 如果标记只应用给类和接口,就该优先使用接口。
发布了51 篇原创文章 · 获赞 53 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_34519487/article/details/104239855
今日推荐