Effective Java 3rd (四)

本文深入探讨Java泛型与枚举的最佳使用方法,包括限定通配符提升API灵活性,结合泛型与可变参数,使用枚举替代整型常量,以及如何运用枚举增强代码的安全性和功能性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

  1. 使用限定通配符来增加 API 的灵活性
  2. 合理地结合泛型和可变参数
  3. 优先考虑类型安全的异构容器
  4. 使用枚举类型替代整型常量
  5. 使用实例属性替代序数
  6. 使用EnumSet替代位属性
  7. 使用EnumMap替代序数索引
  8. 使用接口模拟可扩展的枚举
  9. 注解优于命名模式
  10. 始终使用Override注解

31. 使用限定通配符来增加 API 的灵活性

泛型不是协变,所以有
<? extends E> 代表 E 所有的子类.
<? super E> 代表 E 所有的父类.
对于这个api

public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}

假设我们想要添加一个方法来获取一系列元素,并将它们全部推送到栈上。

// pushAll method without wildcard type - deficient!
public void pushAll(Iterable<E> src) {
	for (E e : src)
	push(e);
}

传入元素E没有问题,但是如果我们想传入E的子类,这个方法显然不行,只能改成,否则无法通过编译:

// Wildcard type for a parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
	for (E e : src)
		push(e);
	}
}

又假如,有一个 Stack<Number> 和 Object 类型的变量。 如果从栈中弹出一个元素并将其存储在该变量中,则需要改成

// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
	while (!isEmpty())
		dst.add(pop());
}

有一个助记符:PECS 代表: producer-extends,consumer-super
    这里的生产者是相对元素来说的,例如popAll方法,dst变量消费pop方法的元素;pushAll方法的e提供一个元素给push方法。
    也可以这么理解,用extends只能get出元素,不能add元素;super反之。

32. 合理地结合泛型和可变参数

调用可变参数的时候会创建一个数组。
当参数化类型的变量引用不属于该类型的对象时会发生堆污染(Heap pollution)[JLS,4.12.2]。 它会导致编译器的自动生成的强制转换失败,违反了泛型类型系统的基本保证。

下面的代码可以通过编译,但是运行时报错。

// Mixing generics and varargs can violate type safety!
static void dangerous(List<String>... stringLists) {
	List<Integer> intList = List.of(42);
	Object[] objects = stringLists;
	objects[0] = intList; // Heap pollution
	String s = stringLists[0].get(0); // ClassCastException
}

这里的代码不会编译错误,是因为语言设计者允许这样,在开发中有很方便,例如 java自带的工具类:
Arrays.asList(T… a) ,Collections.addAll(Collection<? super T> c, T… elements) , EnumSet.of(E first, E…rest) 都允许可变参数。
而运行时异常则是因为最后一行代码会有编译器生成的隐形类型转换,说明类型安全性已经被破坏,将值保存在泛型可变参数数组参数中是不安全的。

看下面这段代码

// UNSAFE - Exposes a reference to its generic parameter array!
static <T> T[] toArray(T... args) {
	return args;
}

static <T> T[] pickTwo(T a, T b, T c) {
	switch(ThreadLocalRandom.current().nextInt(3)) {
		case 0: return toArray(a, b);
		case 1: return toArray(a, c);
		case 2: return toArray(b, c);
	} 
	throw new AssertionError(); // Can't get here
}

public static void main(String[] args) {
	String[] attributes = pickTwo("Good", "Fast", "Cheap");
}

运行这段代码会报ClassCastException,原因在于toArray方法返回的是一个Object数组。说明了泛型可变参数会导致类型丢失。

33. 优先考虑类型安全的异构容器

Class在方法中传递字面类传递编译时和运行时类型信息时,它被称为类型令牌(type token)。

// Typesafe heterogeneous container pattern - API
public class Favorites {
	public <T> void putFavorite(Class<T> type, T instance);
	public <T> T getFavorite(Class<T> type);
}

// Typesafe heterogeneous container pattern - implementation
public class Favorites {
	private Map<Class<?>, Object> favorites = new HashMap<>();
	public<T> void putFavorite(Class<T> type, T instance) {
		favorites.put(Objects.requireNonNull(type), instance);
	} 
	public<T> T getFavorite(Class<T> type) {
		return type.cast(favorites.get(type));
	}
}

此实现中的putFavorite方法的instance是泛型,依赖于T,但是通过原始类型可以轻松破坏map中的value
为了防止这种情况发生,可以改成:
favorites.put(Objects.requireNonNull(type), type.cast(instance));
在运行时存放的map的value中保存与key关联的实例。

令牌无法传List<String>.class

    总之,泛型 API 的通常用法(以集合 API 为例)限制了每个容器的固定数量的类型参数。 你可以通过将类型参数放在键上而不是容器上来解决此限制。 可以使用 Class 对象作为此类型安全异构容器的键。 以这种方式使用的Class 对象称为类型令牌。 也可以使用自定义键类型。

34. 使用枚举类型替代整型常量

枚举类代替常量的原因:
数字常量
1 没有提供类型安全,可以随意传值
2 常量值变更时需要重新编译
3 无法打印有意义的值
字符串常量
1 客户端可能会硬编码
2 打错字符串值引起错误

枚举为每个常量导出实例,是单例,不能继承,不可创建,默认实现了Comparable,Serializable接口。
如果一个枚举是广泛使用的,它应该是一个顶级类; 如果它的使用与特定的顶级类绑定,它应该是该顶级类的成员类。
可以定义一个抽象方法,强制属性重写方法。

// Enum type that switches on its own value - questionable
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// Do the arithmetic operation represented by this constant
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);
	}
}

这个方法可以优化成

// Enum type with constant-specific method implementations
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;}};
	public abstract double apply(double x, double y);
}

如果添加新属性,就不会忘记在switch中添加逻辑了。
结论:
枚举更具可读性,更安全,更强大。

35. 使用实例属性替代序数

枚举都有一个 ordinal 方法,它返回每个枚举常量类型的数值位置(从0开始)。
建议不使用这个方法来返回类似id之类的数值,以后不好修改。

36. 使用 EnumSet 替代位属性

int 枚举模式是利用int进行位运算的模式:

// Bit field enumeration constants - OBSOLETE!
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
	// Parameter is bitwise OR of zero or more STYLE_ constants
	public void applyStyles(int styles) { ... }
}

缺点:
1、如果扩展后超过32位,需要修改客户端代码。
2、可读性差,易造成错误。

使用EnumSet可以避免这个问题,当枚举数量小于64位,底层使用64位的long,超过则用long数组。上面的代码优化为

// EnumSet - a modern replacement for bit fields
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) { ... }
}

EnumSet实现了Set接口,但是不是线程安全的,可用 Collections.unmodifiableSet 改为线程安全的类。

37. 使用 EnumMap 替代序数索引

有时候需要用枚举的下标来引用其他数据结构,如枚举类

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;
	}
}

表示植物的生命周期属于一年生、多年生或者双年生,不建议的做法:

// Using ordinal() to index into an array - DON'T DO THIS!
Set<Plant>[] plantsByLifeCycle =(Set<Plant>[]) 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);
// Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {
	System.out.printf("%s: %s%n",
		Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}

从前面的条目中可以知道,泛型跟数组转换容易造成不安全,建议做法:

// Using an EnumMap to associate data with an enum
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
	plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
	plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);

这种做法更安全、可读、清晰,其底层会以key作为Plant.LifeCycle作为key,下标数量作为object数组的长度,value可以自定义来跟key关联。

在jdk8中可以用lamda来简化上面的代码:

System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));

上面的代码生成的map并不是EnumMap,可以通过这种方式转换:

System.out.println(Arrays.stream(garden).collect(groupingBy(
	p -> p.lifeCycle,() -> new EnumMap<>(LifeCycle.class), toSet())))

38. 使用接口模拟可扩展的枚举

枚举虽然不可扩展,但是可以实现接口,有些接口的功能非常适合用枚举类来实现,例如算数操作。

// Emulated extensible enum using an interface
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;
	}
}

由于枚举接口不可继承,当需要扩展接口功能时,需要定义一个新的枚举类来实现相应的接口。
当有多个枚举类时,可以将其封装在辅助类或静态辅助方法中,方便调用。

39. 注解优于命名模式

**命名模式:使用约定的名字,如前缀或者后缀的方法名称来处理特定的方法。**如JUnit3的"test"开头的方法都会触发单元测试。
缺点:
易拼写错误。
无法确保它们仅用于适当的程序元素。(命名方法中不小心使用了约定的方法名)
没有提供将参数值与程序元素相关联的好的方法。(异常,参数类型关联等)

40. 始终使用 Override 注解

@Override注解可以在重写方法时,方法签名写错的情况下报编译错误,当要覆盖某个方法时建议写上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值