Java 1.5发行版本新增了两个引用类型家族:枚举类型(Enumerate类)和注解类型(Annotation接口)。
第三十条、用enum代替int常量
-
枚举类型是指由一组固定的常量组成合法值的类型。替代之前的具名的int常量。
public static final int APPLE_FUJI = 0;
这种方式称作int枚举模式,存在诸多的不足:
程序十分脆弱,因为int枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的int发生了变化,客户端就必须重新编译,否则,程序行为会变得不确定性。(还有个变体是String枚举模式) -
java的枚举类型是功能十分齐全的类,本质上是int值。
基本想法是:通过公有的静态final域为每个枚举常量导出实例的类。枚举类型是实例受控的,它们是单例的泛型化,本质上是单元素的枚举。枚举提供了编译时的类型安全,如果声明一个参数的类型为Apple,就可以保证,被传到该参数上的任何非null的对象引用一定属于三个有效的Apple值之一。试图传递类型错误的值时,会导致编译时错误。
public enum Apple{FUJI,PIPPIN,GERNNY_SMITH}
包含同名变量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。 -
除了弥补int枚举常量的不足,枚举类型还允许添加任意的方法和域(近似于类),并实现任意的接口。它们提供了所有Object方法的高级实现,实现了Comparable和Serializable接口。我们为啥要将方法或者域加入到枚举类型中?将数据与它的常量关联起来。可以用任何适当的方法来增强枚举类型。
/** * Created by laneruan on 2017/7/10. * 一个枚举类型的例子 * 为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。 */ public class EnumPlanet { public enum Planet{ MERCURY(3.302e+23,2.439e6), VENUS(4.8693+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 static void main(String[] args){ double earthWeight = Double.parseDouble(args[0]); double mass = earthWeight/Planet.EARTH.surfaceGravity(); for (Planet p : Planet.values()){ System.out.println("Weight on " + p +" is "+p.surfaceWeight(mass)); } } }
与枚举常量关联的有些行为,可能只需要用在定义了枚举的类或者包中,这种行为最好被实现成私有的或者包级私有的方法。
如果一个枚举具有普遍适用性,它就应该成为一个顶层类。如果它是被用在一个特定的顶层类中,它就应该成为该顶层类的一个成员类。 -
特定于常量的方法实现:将不同的行为与每个枚举常量关联起来。
public 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), }
-
什么时候应该使用枚举类型?
每当需要一组固定常量的时候。包括:天然的枚举类型;在编译的时候就知道其所有可能值的集合。 -
总结:与int常量相比,枚举类型的优势不言而喻:易读,更加安全。功能更加强大。许多枚举都不需要显式的构造器或者成员,但许多其他枚举类型则受益于“每个常量与属性的关联”以及“提供行为受这个属性影响的方法”。
第三十一条、用实例域代替序数
许多枚举天生就与一个单独的int值相关联,所有的枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数字位置。可以试着从序数中得到关联的int值。最好避免使用ordinal方法。
永远不要根据枚举的序数导出与它想关联的值,而是要将它保存在一个实例域中:
public enum Ensemble{
SOLO(1),DUET(2),TRIO(3),QUARTET(4),QUINTET(5),
SETET(6),SEPET(7),OCTET(8),NONET(9),DECTET(10);
private final int numberOfMusicians;
Ensemble(int size){
this.numberOfMusicians = size;
}
public int numberOfMusicians(){return numberOfMusicians;}
// public int numberOfMusicians(){return ordinal()+1;}
}
第三十二条、用EnumSet代替位域
需要传递多组常量集时,java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取多个值的多个集合,这个类实现了Set接口,提供了丰富的功能。
class Text{
public enum Style {BOLD,ITALIC,UNDERLINE,STRIKETHROUGH}
public void applyStyles(Set<Style> styles){}
}
text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));
总结:正是因为枚举类型要用在集合中,所以没有理由用位域来表示他。
第三十三条、用EnumMap来代替序数索引
最好不要用序数来索引数组,而要使用EnumMap。
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 假設有個香草的數組,表示一座花園中的植物,想要按照类型(一年生、两年生、多年生)
* 进行组织之后将这些植物列出来。
* 注意的是:EnumMap采用了键类型的Class对象构造器:这是一个有限制的类型令牌,
* 提供了运行时的泛型信息。
*/
public class EnumMapHerb {
public enum Type{ANNUAL,PERENNIAL,BIENNIAL}
private final String name;
private final Type type;
EnumMapHerb(String name,Type type){
this.name = name;
this.type = type;
}
@Override
public String toString(){
return name;
}
public static void main(String[] args){
EnumMapHerb[] garden = null;
Map<Type,Set<EnumMapHerb>> herbByType =
new EnumMap<Type, Set<EnumMapHerb>>(EnumMapHerb.Type.class);
for(EnumMapHerb.Type t :EnumMapHerb.Type.values()){
herbByType.put(t,new HashSet<EnumMapHerb>());
}
for(EnumMapHerb h:garden){
herbByType.get(h.type).add(h);
}
}
}
第三十四条、用接口模拟可伸缩的枚举
-
以操作码opcode为例:它的元素表示在某种机器上的那些操作。有时候,要尽可能地让API的用户提供他们自己的操作,这样可以有效地扩展API提供的操作集。可以利用枚举类型来实现这种效果:
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 symbol; } }
虽然枚举类型BasicOperaion不是可扩展的,但是接口类型是可扩展的,你可以定义另一个枚举类型,它实现这个接口,并用这个新类型的实例代替基本类型。
public enum ExtendOperation implements Operation{ EXP("^"){ public double apply(double x,double y){ return Math.pow(x,y); } }, REMAINDER("%"){ public double apply(double x,double y){ return x % y; } }; private final String symbol; ExtendOperation(String symbol){ this.symbol = symbol; } @Override public String toString(){ return symbol; } }
以下是该类型的使用:
public static void main(String[] args){ double x = Double.parseDouble(args[0]); double y = Double.parseDouble(args[1]); test(ExtendOperation.class,x,y); } //很复杂的声明确保了对象既表示枚举又表示Operation的子类型 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)); } }
第二种使用方法是使用Collection<? extends Operation>,这是个有限制的通配符类型:
public static void main(String[] args){ double x = Double.parseDouble(args[0]); double y = Double.parseDouble(args[1]); test(Arrays.asList(ExtendOperation.values()),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)); } }
这样得到的代码不复杂且更灵活:允许操作者将多个实现类型的操作合并到一起。
- 总结:虽然无法编写可扩展的枚举类型,但可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。
第三十五条、注解优先于命名模式(Naming Pattern)
-
Java 1.5之前一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理。这种模式缺点很明显:文字拼写错误很容易发生且难以发觉;无法确保它们只用于相应的程序元素上;没有提供将参数值与程序元素相关联起来的好方法。
-
注解很好地解决了这些问题:
假设想要定义一个注解类型RunTests来指定简单的测试,它们自动运行,并在抛出异常时失败:import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) //表明注解应在运行时保留,元注解 @Target(ElementType.METHOD) //表明只有在方法中声明才是合法的,元注解 public @interface RunTests { }
-
现实应用中的Test注解,称作标记注解(marker annotation)。
-
总结:大多数程序员不需要定义注解类型,所有的程序员都应该使用Java平台所提供的预定义的注解类型,还要考虑使用IDE或者静态分析工具所提供的任何注解。
第三十六条、坚持使用@Override
注解
-
Override注解只能在方法声明时使用,表示被注解的方法声明覆盖了超类中的一个声明。坚持使用这一注解,可以防止一大类的非法错误。
-
现代的IDE提供了坚持使用
@Override
的另一种理由:IDE具有自动检查功能,称作代码检验(code inspection),当有一个方法没有@Override
注解却覆盖了超类方法时,IDE会产生一条警告提醒你警惕无意识的覆盖。 -
总结:如果你想要的每个方法声明中使用Override注解来覆盖超类声明,编译器就可以替你防止大量的错误。但有一个例外:在具体的类中,不必标注你确信覆盖了抽象方法声明的方法。
第三十七条、用标记接口定义类型
-
标记接口(Marker Interface)是没有包含方法声明的接口,只是指明(或者标明)一个类实现了具有某种属性的接口。例:Serializable接口,通过实现这个接口,类表明它的实例可以被写到ObjectOutputStream(即“被序列化”)
-
标记注解和标记接口:
- 标记接口的优点:
- 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。这个类型允许你在编译时捕捉在使用标记注解的情况要到运行时才能捕捉到的错误。
- 他们可以更加精确地进行锁定。
- 标记注解的优点:
- 它可以通过默认的方式添加一个或者多个注解类型元素,给已被使用的注解类型添加更多的信息。随着时间的迁移,简单的标记注解可以演变成更加丰富的注解类型,这在标记接口中是不可能的;
- 它们是更大的注解机制的一部分。因此,标记注解在那些支持注解作为编程元素之一的框架中同样具有一致性。
- 标记接口的优点:
-
何时使用标记注解和标记接口?
如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接可以用来实现或者扩展接口。如果标记只应用在类和接口,思考下:我要编写一个或者多个只接受有这种标记的方法吗?如果是这样,优先使用标记接口。如果不是,再思考下:是否要永远限制这个标记只用于特殊接口的元素?如果是,则优先使用标记接口。 最后选择使用标记注解。
-
总结:标记接口和标记注解都有用处,如何选择见上。
网友评论