第6章:枚举和注解

第34条:用enum代替int常量

34.1 int枚举模式

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
34.1.1 缺点
  1. 不具有类型安全性
    1. 例如将apple传到想要orange的方法中,编译器不报错
    2. ==、-、+等操作符对apple和orange进行比较,编译器不报错
  2. 当两个int枚举组具有相同名称的常量时,必须加前缀来区分
//例如apple和orange都有HAN这个品种,那么必须加前缀,即APPLE_HAN和ORANGE_HAN这两个变量来区分
  1. int枚举值一旦改变,必须重新编译客户端代码
//因为int枚举为编译时常量
  1. 打印int枚举值时只会打印出数字,同时没有方法可以遍历所有的int枚举值常量,也没有方法能够获取int枚举值的个数

34.2 String枚举模式

  1. 提供了打印上的方便
  2. 性能没有int枚举模式好,因为依赖字符串的比较
  3. 初级程序员可能用错,而编译时发现不了,运行时报错
public static final String APPLE_FUJI = "FUJI";
public static void main(String[] args) {
	//没使用常量的属性名APPLE_FUJI,而是将字符串常量硬编码到客户端代码
	//如果写错了,编译时不报错,运行时可能会有问题
	System.out.println("FUJI1");
}

34.3 枚举类型

  1. Java枚举本质是int值
  2. 枚举值默认被public static final修饰,不允许人为添加
  3. 枚举的构造器默认使用private修饰,因此不能被继承
  4. 客户端无法创建其实例,即枚举类型是实例受控的
  5. 单例模式本质上是单个元素的枚举类型
34.3.1 优点
  1. 类型安全:将Apple的枚举值传给Orange的变量会报错
  2. 不同枚举类型的同名常量可以共处,因为每个枚举类型都有自己的命名空间
  3. 增加、重新排序枚举类型中的常量,无需重新编译客户端代码
//因为不像int枚举模式一样,将常量值编译到了客户端代码中
  1. 可以通过调用枚举值的toString方法,将其打印
  2. 枚举类中可以添加属性和方法
34.3.2 示例
//7. 如果一个枚举类具有普遍适用性,可以将它设计为一个顶层类,例如java.math.RoundingMode,表示十进制小数的舍入模式(四舍五入还是什么),它被用于BigDecimal类,但该API的设计者,还希望程序员你重用这个枚举类,增强自己设计的API与他们设计的API的一致性,因此设计为顶层类
//8. 如果枚举类只被用在特定的顶层类中,那就应该把该枚举类设计为这个顶层类的一个成员类(内部的枚举类)
public enum Planet {
	//5. 但删除一个枚举值时,客户端用到这个枚举值的地方重新编译时会失败,如果不重新编译,执行时也会报错,不会像int枚举模式那样,不报错,但给出错误的结果
	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);
	
	//1. 设计枚举类的初衷,就是希望它是不可变的类,为了实现这一点,应该将成员变量都设置为final
	//2. 最好private修饰,并提供getter方法
	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;
	}
	//6. 如果方法只用在枚举类,或其所在的包中,最好用private或default修饰,除非客户端需要调用该方法,可以使用public、protected
	private void wusihan() {
		
	}

	public static void main(String[] args) {
		//3. 打印一个在地球上1234g的物体,在各个星球上的重量
		double earthWeight = Double.parseDouble("1234");
		double mass = earthWeight / Planet.EARTH.surfaceGravity();
		for (Planet p : Planet.values())
			//4. 打印枚举类对象p时,默认调用其toString方法,而枚举类的toString方法默认返回其枚举值的字符串,也可以覆盖toString方法, 进行修改
			System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
	}
}

34.4 在枚举类中定义根据不同枚举值有不同表现的方法

34.4.1 switch
  1. 代码
public enum Operation {
	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 AssertionError("Unknown op: " + this);
	}
}
  1. 缺点:
    1. throw语句肯定执行不到,但代码必须存在,否则编译报错。因为该句话不存在,系统认为你如果传入的枚举值不在PLUS、MINUS、TIMES、DIVIDE中,就没有返回值,因此编译不通过
    2. 当加入新的枚举值,却没给swtich增加相应的条件,编译通过,但执行报错
34.4.2 特定于常量(枚举值)的方法实现:constant-specific method implementation
  1. 在枚举类中声明一个抽象的apply方法,每个枚举值,用不同的方式覆盖该方法
public enum Operation {
	//1. 该括号后的内容,书中叫做特定于常量的类主体(constant-specific class body),其实constant-specific翻译应该是独特的常量,强调每个常量中的方法实现不同
	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);
}
34.4.3 特定于常量的方法实现与特定于常量的数据结合,从而方便打印
  1. 代码
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public enum 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;
		}
	};
	// 1. 当枚举值不同时,其属性symbol的值也不同,因此叫做特定于常量的数据
	private final String symbol;

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

	@Override
	public String toString() {
		return symbol;
	}

	public abstract double apply(double x, double y);
	//3. Stream 就如同一个高级版本的 迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返
	//4. 以Operation的toString作为key,Operation对象作为value的map
	private static final Map<String, Operation> stringToEnum = Stream.of(values())
			.collect(Collectors.toMap(Object::toString, e -> e));

	//2. fromString方法,可以通过传入枚举值的toString打印的结果,得到该枚举值
	public static Optional<Operation> fromString(String symbol) {
		return Optional.ofNullable(stringToEnum.get(symbol));
	}

	public static void main(String[] args) {
		double x = Double.parseDouble(args[0]);
		double y = Double.parseDouble(args[1]);
		for (Operation op : Operation.values())
			System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
	}
}
  1. 无法将枚举值,通过自己的构造器,将自身放入映射
//我觉得这个操作是可以的,怀疑作者的本意是想表达,static的这种map,枚举值做不到通过构造器将自身放入,因为枚举构造器压根无法访问枚举的静态域
//新成员,定义一个映射
public Map map = new HashMap();
//构造器
Operation(String symbol) {
	this.symbol = symbol;
	//将自身放入映射,编译不会报错
	map.put("/",this);
}
  1. 枚举构造器不可以访问枚举的静态域,除非该静态域是编译时常量
private static final String name = "handidiao";
private static final String name1 = new String("handidiao");
Operation(String symbol) {
	this.symbol = symbol;
	//这是因为在枚举类中,枚举值必须写在最前面,而枚举值默认由public static final修饰,属于静态域,而静态域的初始化,是按顺序的,也就是说,构造器被调用时,实际上你想使用的其他静态域还没被初始化,因此不允许使用
	//而如果该静态域为编译时常量,由于编译后,该值被直接替换为一个常量,因此可以使用
	System.out.println(name);
	//System.out.println(name1);
	//1. 这导致构造器无法将自身放入 静态的映射中
	//stringToEnum.put("123", DIVIDE);
	//2. 这导致构造器中无法访问其他枚举值
	//System.out.println(TIMES);
}

34.5 策略枚举

实际上这个方案的本质,是为枚举值,传入一个实例(该实例可以是另一个枚举类的枚举值),然后枚举值的方法中,转为调用该实例的方法,可以减少样板代码

  1. 用于解决特定于常量的方法实现,所造成的样板代码的增加
  2. 利用switch语句:新增枚举值,但不维护switch中代码,会造成编译通过,但和自己想要的行为不同
enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
	private static final int MINS_PER_SHIFT = 8 * 60;

	int pay(int minutesWorked, int payRate) {
		int basePay = minutesWorked * payRate;
		int overtimePay;
		switch (this) {
		case SATURDAY:
		case SUNDAY: 
			overtimePay = basePay / 2;
			break;
		default: 
			overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
		}
		return basePay + overtimePay;
	}
}
  1. 特定于常量的方法实现:样板代码过多
enum PayrollDay {
	MONDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
		}
	},
	TUESDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
		}
	},
	WEDNESDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
		}
	},
	THURSDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
		}
	},
	FRIDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
		}
	},
	SATURDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked * payRate / 2;
		}
	},
	SUNDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked * payRate / 2;
		}
	};
	private static final int MINS_PER_SHIFT = 8 * 60;

	int basePay(int minutesWorked, int payRate) {
		return minutesWorked * payRate;
	}

	abstract int overtimePay(int minutesWorked, int payRate);

	int pay(int minutesWorked, int payRate) {
		int basePay = minutesWorked * payRate;
		return basePay + overtimePay(minutesWorked, payRate);
	}
}
  1. 用具体实现替代抽象方法overtimePay以减少样板代码:与switch语句缺点相似,新增枚举值(节假日),如果不覆盖overtimePay方法,可能造成行为错误
enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, 
	//只对SATURDAY、SUNDAY覆盖overtimePay实现
	SATURDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked * payRate / 2;
		}
	},
	SUNDAY {
		@Override
		int overtimePay(int minutesWorked, int payRate) {
			return minutesWorked * payRate / 2;
		}
	};
	private static final int MINS_PER_SHIFT = 8 * 60;

	int basePay(int minutesWorked, int payRate) {
		return minutesWorked * payRate;
	}

	int overtimePay(int minutesWorked, int payRate) {
		return minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
	}

	int pay(int minutesWorked, int payRate) {
		int basePay = minutesWorked * payRate;
		return basePay + overtimePay(minutesWorked, payRate);
	}
}
  1. 策略枚举模式
enum PayrollDay {
	//1. 每当新增一个枚举值,必须指定其使用的策略。更加安全灵活
	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;

	PayrollDay(PayType payType) {
		this.payType = payType;
	}

	int pay(int minutesWorked, int payRate) {
		return payType.pay(minutesWorked, payRate);
	}

	private enum PayType {
		WEEKDAY {
			int overtimePay(int minsWorked, int payRate) {
				return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
			}
		},
		WEEKEND {
			int overtimePay(int minsWorked, int payRate) {
				return minsWorked * payRate / 2;
			}
		};
		abstract int overtimePay(int mins, int payRate);

		private static final int MINS_PER_SHIFT = 8 * 60;

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

34.6 switch中使用枚举值的场景

  1. 之前讲述了对于枚举类而言,switch语句并不适合作为特定于常量的行为的实现
  2. 我理解书中表达意思是,如果一个方法在枚举类A外部,而该方法想根据A的不同枚举值,有不同的逻辑实现,就应该使用swtich,因为这种情况,你没法做到设定一个抽象方法, 不同枚举值用不同的逻辑覆盖它,因为不在一个类里,根本没法覆盖
  3. 例如inverse方法,如果不在枚举类Operation中,而你又希望为该方法传入不同枚举值时,该方法可以返回不同内容
public static Operation inverse(Operation op) {
	switch (op) {
	case PLUS:
		return Operation.MINUS;
	case MINUS:
		return Operation.PLUS;
	case TIMES:
		return Operation.DIVIDE;
	case DIVIDE:
		return Operation.TIMES;
	default:
		throw new AssertionError("Unknown op: " + op);
	}
}

34.7 最佳实践

  1. 枚举在使用时与int常量性能相当,只不过在装载和初始化时,需要更多的时间和空间
  2. 使用枚举的场景:需要一组常量,且这些常量在编译时就知道所有可能的值
    1. 天然枚举类型:行星、一周的天数、棋子的数目
    2. 其他:菜单选项、操作代码、命令行标记
  3. 枚举是为了二进制兼容性设计的
  4. 枚举与int枚举模式比较
    1. 可行强
    2. 安全
    3. 功能强大
  5. 多个枚举值,同时共享相同行为(周一到周五同一个工资计算方法),需考虑策略枚举
  6. 特定于常量的方法优于swtich方法
  7. 大部分枚举不提供构造器和属性,剩下一部分将枚举值与其属性关联,并根据这个属性,提供独特的方法,只有很少一部分将多种行为,与同一个方法关联

35 用实例的属性替代枚举值的ordinal

35.1 使用枚举类的ordinal方法,表示该枚举值关联的int值

  1. 枚举类都继承Enum,拥有ordinal方法,该返回枚举值的索引
  2. Ensemble代码中,枚举值表示演奏音乐的类型,例如SOLO表示独奏、QUARTET表示四重奏
public enum Ensemble {
	SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET;
	//1. 该方法希望返回该演奏类型需要的音乐家数,编写该方法的程序员观察到,正好目前为止,演奏类型所音乐家数,正好是他们的索引+1,因此想使用ordinal方法获取其索引值,再+1
	//2. 这样做有局限性
	//a. 如果想加入一个双四重奏,实际上需要八个人,但索引值为7的位置,已经有了枚举值
	//b. 如果当前的枚举值顺序颠倒,该方法将失效
	//c. 如果想加入一个三四重奏,需要12个音乐家,即为了让该方法生效,其枚举值,应该排在第12位,但目前只有10个枚举值,那么意味着,如果想让该方法生效,你必须补充一个没用的、作为第11个枚举值
	public int numberOfMusicians() {
		return ordinal() + 1;
	}
}

35.2 用实例的属性替代ordinal

  1. 如果枚举值想和一个int值关联,不要使用ordinal方法,而是将这个关联的int值作为枚举值的属性存放
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;
	}
}

35.3 最佳实践

  1. 大多数程序员不需要使用ordinal方法,只在需要设计那种基于枚举的通用数据结构(EnumSet、EnumMap)时才使用,除非你在编写这种数据结构,否则不要使用

36 用EnumSet代替位域

36.1 位域

36.1.1 概念
  1. 位域在本文中指的是一种表示集合的方法,它利用二进制的特性,来表示几个不同整数值的集合,即利用二进制某几个位(域)上的值(位)来表示存放的数据,比如15,在二进制中为1111,那么它可以表示二进制为1000、0100、0010、0001的四个数的集合,即1、2、4、8的集合
  2. 某些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位,为了节省存储空间,出现了位域这种表示方法
36.1.1 使用

例如,在一个系统中,用户一般有查询(Select)、新增(Insert)、修改(Update)、删除(Delete)四种权限,四种权限有多种组合方式,也就是有16中不同的权限状态

  1. 常规
//用四个boolean类型变量来保存每种权限状态
public class Permission {

	// 是否允许查询
	private boolean allowSelect;

	// 是否允许新增
	private boolean allowInsert;

	// 是否允许删除
	private boolean allowDelete;

	// 是否允许更新
	private boolean allowUpdate;

	// 省略Getter和Setter
}
  1. 位域
//用一个二进制数即可,每一位来表示一种权限,0表示无权限,1表示有权限
public class NewPermission {
	// 是否允许查询,二进制第1位,0表示否,1表示是
	public static final int ALLOW_SELECT = 1 << 0; // 0001

	// 是否允许新增,二进制第2位,0表示否,1表示是
	public static final int ALLOW_INSERT = 1 << 1; // 0010

	// 是否允许修改,二进制第3位,0表示否,1表示是
	public static final int ALLOW_UPDATE = 1 << 2; // 0100

	// 是否允许删除,二进制第4位,0表示否,1表示是
	public static final int ALLOW_DELETE = 1 << 3; // 1000

	// 存储目前的权限状态,该值的二进制表示法为1111,即同时拥有四个权限
	private int flag;

	/**
	 *  重新设置权限
	 */
	public void setPermission(int permission) {
		flag = permission;
	}

	/**
	 *  添加一项或多项权限,即书中所说的联合(union)操作
	 */
	public void enable(int permission) {
		flag |= permission;
	}

	/**
	 *  删除一项或多项权限,即书中所说的交集(intersection)操作
	 */
	public void disable(int permission) {
		flag &= ~permission;
	}

	/**
	 *  是否拥某些权限
	 */
	public boolean isAllow(int permission) {
		return (flag & permission) == permission;
	}

	/**
	 *  是否禁用了某些权限
	 */
	public boolean isNotAllow(int permission) {
		return (flag & permission) == 0;
	}

	/**
	 *  是否仅仅拥有某些权限
	 */
	public boolean isOnlyAllow(int permission) {
		return flag == permission;
	}
}
  1. 设置权限时代码比对
//常规
permission.setAllowSelect(true);
permission.setAllowInsert(true);
permission.setAllowUpdate(false);
permission.setAllowDelete(false);

//位域
permission.setPermission(NewPermission.ALLOW_SELECT | NewPermission.ALLOW_INSERT);

36.2 位域的缺点

  1. 因为位域实际上是用整型值的二进制格式,表示一组整型的值,既然是一组整型的值,就一定会具有int枚举常量的所有缺点
  2. 位域以数字形式打印时,翻译位域比翻译int枚举常量更困难
  3. 无法遍历位域中所有元素
  4. 编写API时,必须预测好位域的二进制有多少位,然后根据这个信息给位域选定对应的类型,一般为int/long,一旦选好类型,在没有修改API的情况下,该位域将无法存放超出其位宽度(32/64)的整型值,也就是说上例中位域flag中,最多存放32种状态

36.3 EnumSet替代位域

  1. EnumSet提供了更丰富的功能,和类型安全性
  2. 如果EnumSet中存放的元素少于64个,那么其底层是使用一个long型的位域实现的,其removeAll、retainAll方法,都是利用算法实现的,因此性能与位域性能相同
  3. EnumSet相当于帮你解决了使用位域时,容易产生的错误、以及难以阅读的代码
  4. 代码
import java.util.Set;

public class PermissionEnumSet {
	public enum Authority {
		allowSelect, allowInsert, allowDelete, allowUpdate;
		//此处考虑到客户端可能需要传递一些其他Set的实现,所以没用EnumSet定义
		public void applyStyles(Set<Authority> authorities) {
			//省略业务代码
		}
	}
}

36.4 最佳实践

  1. EnumSet同时具有位域的简洁性、性能优势,与枚举类型的所有优点,应使用EnumSet替代位域
  2. 截止Java9无法创建不可变的EnumSet,可以用Collections.unmodifiableSet封装EnumSet,得到不可变的集合,但性能会受影响

37 不要用ordinal作为Map中的键,或数组中的索引,使用枚举值本身替代

37.1 几种不同方式的区别

import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
//表示植物
class Plant {
	//表示植物的声明周期
	enum LifeCycle {
		//每年,多年,每两年
		ANNUAL, PERENNIAL, BIENNIAL
	}

	final String name;
	final LifeCycle lifeCycle;

	Plant(String name, LifeCycle lifeCycle) {
		this.name = name;
		this.lifeCycle = lifeCycle;
	}

	@Override
	public String toString() {
		return name;
	}

	public static void main(String[] args) {
		//假设有个一个花园(Plant[]),它里面包含着多种植物
		Plant[] garden = new Plant[5];
		garden[0] = new Plant("植物一", Plant.LifeCycle.ANNUAL);
		garden[1] = new Plant("植物二", Plant.LifeCycle.ANNUAL);
		garden[2] = new Plant("植物三", Plant.LifeCycle.PERENNIAL);
		garden[3] = new Plant("植物四", Plant.LifeCycle.PERENNIAL);
		garden[4] = new Plant("植物五", Plant.LifeCycle.BIENNIAL);
		
		//1. ordinal作为数组的下标 
		//a. 数组无法与泛型兼容,会有异常警告
		Set<Plant>[] plantsByLifeCycle = new Set[Plant.LifeCycle.values().length];
		for (int i = 0; i < plantsByLifeCycle.length; i++)
			plantsByLifeCycle[i] = new HashSet<>();
		for (Plant p : garden)
			plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
		//b. 数组(plantsByLifeCycle)不知道它的索引(i)代表什么,必须手工标注(Plant.LifeCycle.values()[i]),打印复杂
		for (int i = 0; i < plantsByLifeCycle.length; i++) {
			System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
		}
		//c. int无法提供类型安全,这里数组下标正常只能放入LifeCycle的枚举值的ordinal,如果你传入了Orange的枚举值的ordinal,那么如果该值大于数组中元素个数,会出现ArrayIndexOutOfBoundsException,如果小于,可能导致打印出的内容和想要的不一致
//		System.out.println(plantsByLifeCycle[5]);

		//2. ordinal作为Map的key:同样打印复杂、无法提供类型安全
		Map<Integer, Set<Plant>> plantsByLifeCycleMap = new HashMap<Integer, Set<Plant>>();
		for (Plant.LifeCycle lc : Plant.LifeCycle.values())
			plantsByLifeCycleMap.put(lc.ordinal(), new HashSet<>());
		for (Plant p : garden)
			plantsByLifeCycleMap.get(p.lifeCycle.ordinal()).add(p);

		//3. ordinal作为Map的key:解决了所有缺点。注意EnumMap的构造器中传入的是该映射中key的类型令牌。内部是使用以ordinal作为索引的数组实现的
		Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycleEnumMap = new EnumMap<>(Plant.LifeCycle.class);
		for (Plant.LifeCycle lc : Plant.LifeCycle.values())
			plantsByLifeCycleEnumMap.put(lc, new HashSet<>());
		for (Plant p : garden)
			plantsByLifeCycleEnumMap.get(p.lifeCycle).add(p);
		System.out.println(plantsByLifeCycleEnumMap);
		
		//4. Stream简化创建EnumMap过程
		//a. groupingBy函数,第一个参数为Map的key的来源,第二个参数为创建出的Map的类型,第三个参数为Map的value的来源
		//b. Stream创建EnumMap与正常创建EnumMap区别:如果花园中只包含一年生和多年生植物,不包含二年生植物,Stream这种方式创建的EnumMap对象中只有两组键值,而正常方式,会有三组
		EnumMap<Plant.LifeCycle, Set<Plant>> plantsByLifeCycleStream = Arrays.stream(garden).collect(Collectors.groupingBy(p -> p.lifeCycle,
				() -> new EnumMap<>(Plant.LifeCycle.class), Collectors.toSet()));
		System.out.println(plantsByLifeCycleStream);

	}
}

37.2 以ordinal作为索引的二维数组的修改方案

  1. ordinal作为索引的二维数组
public enum Phase {
	//固、液、气
	SOLID, LIQUID, GAS;
	public enum Transition {
		MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
		//1. 这个数组中元素个数必须为Phase中枚举值(3)的平方数,即使里面有很多是null,但不可以去掉
		//2. Phase,或Transition中枚举值修改后,这个数组必须重新维护,如果没维护,很有可能报错,或不报错,但和自己想要的结果不一致
		//3. int值不具有类型安全性,一旦用错了int值,会报错、与想要结果不一致
		private static final Transition[][] TRANSITIONS = { 
				{ null, MELT, SUBLIME }, 
				{ FREEZE, null, BOIL },
				{ DEPOSIT, CONDENSE, null } 
		};

		public static Transition from(Phase from, Phase to) {
			return TRANSITIONS[from.ordinal()][to.ordinal()];
		}
	}
}
  1. EnumMap<Phase,Map<Phase,Transaction>>替代
import static java.util.stream.Collectors.*;

import java.util.EnumMap;
import java.util.Map;
import java.util.stream.Stream;

public enum Phase {
	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 Phase from;
		private final Phase to;

		Transition(Phase from, Phase to) {
			this.from = from;
			this.to = to;
		}

		//groupingBy函数用于返回一个收集Map的Collector,第一个对象表示该Map的键的来源,第二个参数表示创建出的Map的类型,第三个参数表示Map中值的来源
		//toMap函数也用于返回一个收集Map的Collector,第一个参数表示该Map的键的来源,第二个参数表示该Map的值的来源,第三个参数用于将并行流中结果进行合并,第四个参数表示创建出的Map的类型
		private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
				.collect(groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class),
						toMap(t -> t.to, t -> t, (x, y) -> y, () -> new EnumMap<>(Phase.class))));

		public static Transition from(Phase from, Phase to) {
			return m.get(from).get(to);
		}
	}
}
  1. 为Phase新增枚举值PLASMA(离子),Transition新增DEIONIZATION(离子变气体)、IONIZATION(气体变离子)
    1. 第一种方案:TRANSITIONS数组需改为一个4*4的数组
    2. 第二种方案
    public enum Phase {
    	SOLID, LIQUID, GAS,
    	//新增
    	PLASMA;
    	public enum Transition {
    		MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), SUBLIME(SOLID, GAS),DEPOSIT(GAS, SOLID),
    		//新增
    		DEIONIZATION(PLASMA,GAS),IONIZATION(GAS,PLASMA);
    		...
    	}
    	...
    }
    

37.3 最佳实践

  1. EnumMap内部是使用ordinal为索引的数组实现的,因此性能与ordinal为索引的数组相同,又避免了其所有缺点,因此尽量使用EnumMap
  2. 尽量不要使用Enum的ordinal方法

38 用接口模拟枚举类的继承

这样做可以达到自己编写枚举类的目的,比如一个方法传入参数为Operation接口,那么你的枚举类,只要实现该接口,就可以传入该方法

38.1 类型安全枚举模式

  1. 类型安全枚举模式
public class Food{
	int size;
	public Food(int size) {
		this.size = size;
	}
	//java5之前,使用这种模式来替代枚举类,这种模式类型安全,且可以被继承
	public static final Food food_1 = new Food(5);
}
  1. 类型安全枚举模式的继承:基本上这种继承后来被证明都不是什么好点子

public class Apple extends Food{

	public Apple(int size) {
		super(size);
	}
	//1. apple_1这个实例,可以转为Food,但Food中的food_1却不可以转为Apple,会造成使用上的混乱
	//2. 没有好的方法直接获得Food和Apple下所有的枚举值(food_1和apple_1)
	public static final Apple apple_1 = new Apple(15);

}
  1. 枚举类的继承:无法做到,因为默认枚举类已经继承了Enum

38.2 模拟枚举继承的效果

  1. 需要模拟枚举继承的效果的使用场景:枚举值表示在机器上的某些操作,比如计算器上的加、减、乘、除
  2. 这种场景下,用户可能需要提供自己的操作,比如原客户端只提供了加减乘除,需要用户自己提供求幂、求余的实现
  3. 模拟枚举继承时,原类型称为基本枚举,自定义的枚举叫做扩展枚举,因为它效果上是基本枚举的一个扩充
//比如你现在有一个方法,需要传入一个枚举类型BasicOperation的枚举值(加减乘除),那么当你想自己定义一个枚举类(求幂、求余)传进来,是做不到的,因为枚举类无法被继承,因此没法自己定义一个ExtendedOperation继承BasicOperation
private static void printWu(BasicOperation a) {
	System.out.println(a);
}
  1. 模拟方案:利用枚举类可以实现接口这一特性
//1. 定义Operation接口
public interface Operation {
	double apply(double x, double y);
}
//2. 原类型BasicOperation实现Operation接口
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;
	}
}
//3. 自定义枚举类型ExtendedOperation,也实现Operation接口
public enum ExtendedOperation 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;

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

	@Override
	public String toString() {
		return symbol;
	}
}
//4. 修改方法签名中,需传入的参数类型成接口类型,那么所有使用原操作(加减乘除)地方,都可以传入一个新的操作(幂、余)
//5. 这样,原本该方法,只能传入枚举类BasicOperation中的枚举值,现也可以传入自定义的枚举类ExtendedOperation中的枚举值
private static void printWu(Operation a) {
	System.out.println(a);
}

38.3 为方法传入扩展枚举的所有枚举值

38.3.1 通过传入类型令牌实现
public static void main(String[] args) {
	double x = Double.parseDouble(args[0]);
	double y = Double.parseDouble(args[1]);
	test(ExtendedOperation.class, x, y);
}
//T extends Enum<T> & Operation,确保T为枚举类型,同时实现了Operation接口
private static <T extends Enum<T> & Operation> void test(Class<T> opEnumType, double x, double y) {
	//getEnumConstants为Class的方法,用于返回该对象的所有枚举值组成的数组
	for (Operation op : opEnumType.getEnumConstants())
		System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
38.3.2 通过传入一个集合
public static void main(String[] args) {
	double x = Double.parseDouble(args[0]);
	double y = Double.parseDouble(args[1]);
	test(Arrays.asList(ExtendedOperation.values()), x, y);
}
//更加灵活,可以将Operation下所有枚举类的枚举值放到一起
//猜测作者的意思是,没法将所有枚举类的枚举值组成一个EnumSet,或EnumMap,传入这个方法,因为不同枚举类下的枚举值,没法组成一个EnumSet
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));
}

38.4 模拟枚举继承的缺点

  1. 基本枚举和扩展枚举的实现,无法共用,必须分别实现Operation中的apply方法
  2. 如果共享代码(apply实现)比较多,可以考虑将这些方法逻辑封装在一个辅助类中,或静态辅助方法中

38.5 Java类库中的应用

  1. Java类库中的java.nio.file.LinkOption枚举类,同时实现了CopyOption、OpenOption接口,想扩展这个枚举类,可以实现CopyOption、OpenOption接口即可

39 注解优先于命名模式

39.1 命名模式

39.1.1 定义
  1. 命名模式,可以理解为,某个工具或框架,需要特殊处理某些程序元素时,要求该程序元素以指定的命名规则来命名,这种设计方案就叫做明明模式
  2. 比如Java4之前,JUNIT,要求使用它的客户端,必须使用test作为其测试方法名称的开头,才能正常运行
39.1.2 命名模式缺点
  1. 文字拼写错误会导致JUNIT会忽略该测试方法,但编译器不会提示:例如将testSafetyOverride不小心写成tsetSafetyOverride
  2. 无法确保其只应用于指定的元素上,例如Annotation可以指定只能加在类上或方法上,而对于JUNIT,可能有客户自定义了一个类,以Test开头,例如TestSafetyMechanisms,希望JUNIT自动测试其内所有方法,但实际上JUNIT根本做不到
  3. 无法为需要特殊处理的程序元素,指定参数,例如Annotation,可以指定成员为一种异常类型,那么我们就可以做到测试方法中,抛出这个指定的异常时,才算测试成功,命名模式虽然也能做到,但需要将方法名定义为testRuntimeException这种格式,然后拆字符串判断,这样代码不雅观,也脆弱,编译器无法检测到你拼写的是否正确,如果拼错了,比如拼成testRtimeException,得等到运行时,才会报错

39.2 Annotation替代命名模式

39.2.1 自定义一个测试框架
  1. Test:注释
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
  1. Sample:被测试的类
//1. 一会要编写一个测试类RunTests,只测试@Test注释的方法,且测试的方法如果未抛异常,计入为成功数(Passed),抛异常时,计入失败数,并打印异常,非static方法时,也计入失败
public class Sample {
	@Test
	public static void m1() {
	} 

	public static void m2() {
	}

	@Test
	public static void m3() { 
		throw new RuntimeException("Boom");
	}

	public static void m4() {
	}
	
	//2. 其实可以通过编写一个注解处理器(annotation processor),来指定编译器限制Test注释只能用在static方法上,本例中在运行时实现这个功能
	@Test
	public void m5() {
	} 

	public static void m6() {
	}

	@Test
	public static void m7() { 
		throw new RuntimeException("Crash");
	}

	public static void m8() {
	}
}
  1. RunTests:测试主方法
//打印结果
//public static void Sample.m3() failed: java.lang.RuntimeException: Boom
//Invalid @Test: public void Sample.m5()
//public static void Sample.m7() failed: java.lang.RuntimeException: Crash
//Passed: 1, Failed: 3

import java.lang.reflect.*;

public class RunTests {
	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(Test.class)) {
				tests++;
				try {
					m.invoke(null);
					passed++;
				//InvocationTargetException只能获取反射调用方法时,方法中产生的异常
				} catch (InvocationTargetException wrappedExc) {
					//该方法可以从InvocationTargetException中获取真正在方法中抛出的异常
					Throwable exc = wrappedExc.getCause();
					System.out.println(m + " failed: " + exc);
				} catch (Exception exc) {
					//实现了非static方法,执行失败
					//非static方法的m5方法,被反射调用时,因为后面参数为null,即调用该方法的对象为null,会报空指针,根本无法进入方法中,因此不会被InvocationTargetException获取,即Exception异常
					//static方法由于不需要调用的对象,因此即使invoke后第一个参数为null,也不会抛出异常
					System.out.println("Invalid @Test: " + m);
				}
			}
		}
		System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
	}
}
39.2.2 控制测试方法在抛出指定一种异常时打印
  1. ExceptionTest:注释
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
	Class<? extends Throwable> value();
}
  1. Sample2:被测试的类
//1. 一会要修改测试类RunTests,只测试@ExceptionTest注释的方法,且测试的方法如果抛ArithmeticException异常,计入为成功数(Passed),抛其他异常或不抛异常时,计入失败数,并打印异常,非static方法时,也计入失败
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 m3() {
	}
}
  1. RunTests:测试主方法修改的内容
//打印结果:
//Test public static void Sample2.m3() failed: no exception
//Test public static void Sample2.m2() failed: expected //java.lang.ArithmeticException, got //java.lang.ArrayIndexOutOfBoundsException: 1
//Passed: 1, Failed: 2

if (m.isAnnotationPresent(ExceptionTest.class)) {
	tests++;
	try {
		m.invoke(null);
		System.out.printf("Test %s failed: no exception%n", m);
	} catch (InvocationTargetException wrappedEx) {
		Throwable exc = wrappedEx.getCause();
		Class<? extends Throwable> 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);
	}
}

39.2.3 控制测试方法在抛出某些异常之一时打印
  1. ExceptionTest:注释
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
	//注意此处异常由Throwable改为了Exception,缩小了范围
	Class<? extends Exception>[] value();
}
  1. Sample2:被测试类新增方法
//这样注释,doublyBad方法抛出IndexOutOfBoundsException和NullPointerException时都计入为成功数。大括号表示数组
@ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class })
public static void doublyBad() {
	List<String> list = new ArrayList<>();
	list.addAll(5, null);
}
  1. RunTests:测试主方法修改
if (m.isAnnotationPresent(ExceptionTest.class)) {
	tests++;
	try {
		m.invoke(null);
		System.out.printf("Test %s failed: no exception%n", m);
	} catch (Throwable wrappedExc) {
		Throwable exc = wrappedExc.getCause();
		int oldPassed = passed;
		Class<? extends Exception>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
		for (Class<? extends Exception> excType : excTypes) {
			if (excType.isInstance(exc)) {
				passed++;
				break;
			}
		}
		if (passed == oldPassed)
			System.out.printf("Test %s failed: %s %n", m, exc);
	}
}
39.2.4 Java8的重复注释

提升源码可读性,看起来好像将同一个注释的多个实例应用到同一个程序元素上,但在声明和处理注释时,会增加样板代码,且容易出错

  1. ExceptionTest:注释
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
//指定该注释允许重复注释,且指定其容器为注释ExceptionTestContainer
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
	Class<? extends Exception> value();
}
  1. ExceptionTestContainer:注释的容器
import java.lang.annotation.*;

//容器的Retention需要大于等于其包含的注释的Retention,Target也要符合规定,否则编译会报错
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
	//容器里面只包含一个value属性存放ExceptionTest数组
	ExceptionTest[] value();
}
  1. Sample2:被测试类修改doublyBad方法
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
	List<String> list = new ArrayList<>();
	list.addAll(5, null);
}
  1. RunTests:测试主方法修改
//1. 重复的注释,使用时,底层会产生一个合成注释ExceptionTestContainer,getAnnotationsByType方法掩盖了这事实,而isAnnotationPresent却暴露了这个问题
//2. isAnnotationPresent(ExceptionTest.class)方法, 对于使用重复注解的方法doublyBad,会返回false,而对于使用了一遍该注释的方法m2,会返回true
//3. isAnnotationPresent(ExceptionTestContainer.class)正好相反
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
	tests++;
	try {
		m.invoke(null);
		System.out.printf("Test %s failed: no exception%n", m);
	} catch (Throwable wrappedExc) {
		Throwable exc = wrappedExc.getCause();
		int oldPassed = passed;
		//4. getAnnotationsByType方法,无论是否使用了重复注释,都会将所有的ExceptionTest注释返回
		ExceptionTest[] excTests = m.getAnnotationsByType(ExceptionTest.class);
		for (ExceptionTest excTest : excTests) {
			if (excTest.value().isInstance(exc)) {
				passed++;
				break;
			}
		}
		if (passed == oldPassed)
			System.out.printf("Test %s failed: %s %n", m, exc);
	}
}

39.3 最佳实践

  1. 应用注释替代命名模式
  2. 一般只有平台框架程序员才自定义注释,但所有程序员都应使用Java平台提供的预定义的注释,同时应该考虑IDE或静态分析工具提供的注释,这些注释可以提升由这些工具所提供的诊断信息的质量,但他们尚未标准化,当你一旦换了工具,或出现标准,那么需要有大量的工作要做

40 坚持使用Override注释

40.1 @Override使用

import java.util.HashSet;
import java.util.Set;

public class Bigram {
	private final char first;
	private final char second;

	public Bigram(char first, char second) {
		this.first = first;
		this.second = second;
	}
	//1. 该方法并没有对Object的equals构成重写,因为方法名和形参列表并不完全相同,实际上是与Object的equals构成了重载
	//4. 对该方法使用@Override注释,如果该方法未能重写某方法,编译器就会报错,这样程序员可以立即意识到自己犯的错误
	public boolean equals(Bigram b) {
		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++)
				//2. Set去重时,是根据equals和hashCode完全相同,才会被去掉,但用的是Object的equals方法
				s.add(new Bigram(ch, ch));
		//3. 因此最后实际上插入的不是26个字母,而是260个
		System.out.println(s.size());
	}
}

40.2 最佳实践

  1. 非抽象类继承/实现抽象类/接口
    1. 重写抽象方法:无需使用@Override注释(例如接口中的default方法、类中的非抽象方法),因为编译器在发现你没有对抽象方法提供具体实现时,会自动报错
    2. 重写非抽象方法:使用@Override
  2. 抽象类/接口继承/实现抽象类/接口
    1. 无论重写的是抽象还是非抽象方法,都应提供@Override注释,例如Set接口对于Collection接口,不想添加任何的新功能,你对该接口的所有重写的方法,都使用@Override,可以确保你没有新增功能
  3. 大部分IDE在你选择重写某个方法时,会为你自动加上@Override注释
  4. 大部分IDE甚至可以启用代码检查功能,这个功能会检查你的代码:当代码覆盖了超类方法,但是没有使用@Override注释时,会产生警告,防止你无意识的覆盖

41 用标记接口定义类型

41.1 标记接口

  1. 标记接口:一种内部没有任何方法的接口,它只是为了表明它标记(实现它)的类,拥有某些限制,或表明它标记的类的实例,可以被其他类中的某个方法正确处理。例如标记接口Serializable表示它标记的类的实例,可以被ObjectOutputStream.writeObject方法正确处理
//以下为JDK中ObjectOutputStream.writeObject方法中,对该标记接口处理的部分
if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
            cl.getName() + "\n" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}
  1. 由上面例子可以看出,标记接口实际上和标记注释功能类似,都可以为工具或框架,特殊处理某些程序元素时,提供帮助

41.2 标记接口与标记注释区别

  1. 标记接口可以在编译期被发现问题,而标记注释必须等到运行时
//例如ObjectOutputStream的writeObject方法,可以改写成如下方式,这样如果传入方法的参数没有实现标记接口Serializable,编译无法通过
public final void writeObject(Serializable obj)
  1. 标机接口可以被更加精确的锁定:例如可以只标记实现了某个接口的类/接口,这种标记接口叫做有限制的标记接口。而标记注释,一旦将target设置为ElementType.TYPE,那么它可以标记所有的类或接口
//例如Marker只想标记(被实现)那些,实现了Foo接口的接口/类,拥有特殊的功能,此时你可以用Marker继承Foo接口,这样实际上,只要用Marker标记的类,就一定也实现/继承了Foo接口
public interface Marker extends Foo{
	
}

//对类Han进行标记,标记它拥有特殊功能,Han实际上被动的继承了Foo接口,Marker也就达到了目的,即其只能标记,实现Foo接口的类/接口
public class Han implements Marker{

}

//又比如Set,也可以近似(因为它里面定义了方法,且改动了Collection的方法合约,比如Set的add方法不允许放入重复数据)看做这种"有限制的标记接口",它标记的类/接口,必须需实现Collection接口

41.3 最佳实践

  1. 如果你想编写的类型中,不需要任何方法,应该使用标记接口
  2. 如果想要标记程序元素,而不是接口/类,使用标记注释
  3. 如果在一个重度使用标记注释的框架中,因为已经提供了处理标记注释的相关的功能,应该直接用这些标记注释来进行标记
  4. 当你编写一个target为ElementType.TYPE的标记注释时,应该仔细考虑是否能用标记接口替代
发布了32 篇原创文章 · 获赞 0 · 访问量 935

猜你喜欢

转载自blog.csdn.net/hanzong110/article/details/103110931