API: Application Programming Interface
简单示例
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
优势
静态工厂方法有名称, 更容易使用和阅读. 当一个类需要多个带有相同签名的构造器时, 使用静态工厂方法代替构造器, 仔细的选择名称以便突出静态工厂方法之间的区别.
不必在每次调用的时候都创建一个新对象, 重复利用缓存起来或预先构建好的实例.
可以返回原返回类型的任何子类型的对象, 选择返回对象的类时有了更大的灵活性.比如java.util.Collections(集合工具类) 总是返回以List接口的类型, 但具体的子类型是多变的.
返回的对象的类可以随着每次调用而发生变化, 这取决于静态工厂方法的参数值.
方法返回的对象所属的类, 在编写包含该静态工厂方法的类时可以不存在.常用于服务提供者框架(Service Provider Framework).比如JDBC
服务提供者框架: 多个服务提供者实现一个服务, 系统为服务提供者的客户端提供多个实例,并把它们从多个实现中解耦出来.
三个重要组件: 服务接口(Service Interface), 这是提供者实现的; 提供者注册API(Provider Registration API), 这是提供者用来注册实现的; 服务访问API(Service Access API), 这是客户端用来获取服务的实例.
第四个组件: 服务提供者接口(Service Provider Interface) 是可选择的, 表示产生服务接口之实例的工厂对象.如果没有服务提供者接口, 实现就通过反射方式进行实例化.
对于JDBC来说, Connection就是服务接口的一部分, DriverManager.registerDriver是提供者注册API, DriverManager.getConnection是服务访问API, Driver是服务提供者接口.
缺点
静态工厂方法惯用名称
重叠构造器模式
/
* 营养成分
*
* @author lzlg
* 2021/3/13 19:07
*/
public class NutritionFacts {
// 必填
private final int servingSize;
// 必填
private final int servings;
// 可选
private final int calories;
// 可选
private final int fat;
// 可选
private final int sodium;
// 可选
private final int carbohydrate;
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
可行, 但当有许多参数的时候, 代码会很难编写, 可读性差, 容易出错.
JavaBeans模式
/
* 营养成分
*
* @author lzlg
* 2021/3/13 19:07
*/
public class NutritionFacts {
// 必填, 无默认值
private int servingSize;
// 必填, 无默认值
private int servings;
// 可选
private int calories = 0;
// 可选
private int fat = 0;
// 可选
private int sodium = 0;
// 可选
private int carbohydrate = 0;
public NutritionFacts() {
}
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
创建实例容易,代码易读.但有严重的缺点: JavaBean 可能处于不一致的状态, 无法仅仅通过检验构造器参数的有效性来保证一致性(比如一个参数会影响另一个参数的取值范围);把类做成不可变的可能性不复存在, 需要付出额外的努力确保线程安全.
建造者(Builder)模式
不直接生成需要的对象, 让客户端利用所有必要的参数调用构造器(或静态工厂), 得到一个Buider对象, 然后在Builder对象上调用类似setter的方法, 来设置每个相关的可选参数.
/
* 营养成分
*
* @author lzlg
* 2021/3/13 19:07
*/
public class NutritionFacts {
// 必填, 无默认值
private final int servingSize;
// 必填, 无默认值
private final int servings;
// 可选, 有默认值
private final int calories;
// 可选, 有默认值
private final int fat;
// 可选, 有默认值
private final int sodium;
// 可选, 有默认值
private final int carbohydrate;
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static class Builder {
// 必填, 无默认值
private final int servingSize;
// 必填
private final int servings;
// 可选, 有默认值
private int calories = 0;
// 可选, 有默认值
private int fat = 0;
// 可选, 有默认值
private int sodium = 0;
// 可选, 有默认值
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
注意: NutritionFacts是不可变的(构造器private), 所有的默认参数都单独放在一个地方. Builder设值方法返回Builder对象本身, 以便把调用链接起来, 得到一个流式的API.
NutritionFacts cocaCola = new Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
代码容易编写, 易于阅读, 模拟了具名的可选参数.可在builder的构造器和方法中校验参数的有效性.
Builder模式也适用于类层次结构
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
/
* 披萨
*
* @author lzlg
* 2021/3/13 19:34
*/
public abstract class Pizza {
final Set<Topping> toppings;
public Pizza(Builder<?> builder) {
this.toppings = builder.toppings.clone();
}
public enum Topping {
HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
}
// Builder是泛型(generic type)
abstract static class Builder<T extends Builder<T>> {
// 带有递归类型参数(recursive type parameter)
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// 模拟的self()类型
protected abstract T self();
}
}
/
* 披萨子类,纽约风味披萨
*
* @author lzlg
* 2021/3/13 19:42
*/
public class NyPizza extends Pizza {
private final Size size;
public NyPizza(Builder builder) {
super(builder);
this.size = builder.size;
}
public enum Size {
SMALL, MEDIUM, LARGE
}
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = size;
}
@Override
public NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
}
/
* 披萨子类,半月型披萨
*
* @author lzlg
* 2021/3/13 19:42
*/
public class CalZone extends Pizza {
private final boolean sauceInside;
public CalZone(Builder builder) {
super(builder);
this.sauceInside = builder.sauceInside;
}
public static class Builder extends Pizza.Builder<Builder> {
// 默认值
private boolean sauceInside = false;
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override
public CalZone build() {
return new CalZone(this);
}
@Override
protected Builder self() {
return this;
}
}
}
NyPizza.Builder的build() 方法返回 NyPizza, CalZone.Builder的build() 方法返回 CalZone, 在方法中, 子类方法声明返回超级类中声明的返回类型的子类型, 被称为协变返回类型(covariant return type), 无需转换类型就能使用该构建器.
NyPizza pizza = new NyPizza.Builder(NyPizza.Size.SMALL) .addTopping(Topping.SAUSAGE).addTopping(Topping.ONION).build();
CalZone calZone = new CalZone.Builder()
.addTopping(Topping.HAM).sauceInside().build();
优势
Builder模式可有多个可变参数, 可多次调用同一个方法(如addTopping)集中到一个域(EnumSet toppings)中.
Builder模式十分灵活, 可用单个Builder构建多个对象. builder的参数可以在调用build方法来创建对象期间进行调整, 也可以随着不同的对象而改变.
Builder模式可以自动填充某些域, 例如每次创建对象时自动增加序列号.
缺点
创建对象时必须先创建Builder, 在十分注重性能的情况下, 就成问题了.
比重叠构造器模式更加冗长, 只有在很多参数时使用(4个或更多).
Singleton是指仅仅被实例化一次的类, 通常用来代表一个无状态的对象, 如函数对象, 或者本质上唯一的对象.使类称为Singleton会使它的客户端测试变得十分困难.
实现Singleton的方法
1.使用公共final域实现Singleton
/
* 使用公共final域实现Singleton
*
* @author lzlg
* 2021/3/14 13:18
*/
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {
}
public void leaveTheBuilding() {
}
}
提醒: 享有特权的客户端可借助AccessibleObject.setAccessible方法, 通过反射机制调用私有构造器, 如果需要抵御这种攻击, 则可修改构造器, 在创建第二个实例时抛出异常.
优势: API很清楚的表明这个类是Singleton, 公有的静态域是final的, 并且更简单.
2.使用静态工厂方法实现Singleton
/
* 使用静态工厂方法实现Singleton
*
* @author lzlg
* 2021/3/14 13:18
*/
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {
}
public static Elvis getInstance() {
return INSTANCE;
}
public void leaveTheBuilding() {
}
}
优势: 提供了灵活性, 在不改变API的前提下, 可以改变该类是否为Singleton的想法;如果应用程序需要可以编写一个泛型Singleton工厂;可以通过方法引用作为提供者,比如Elvis::instance就是一个Supplier.
Singleton的序列化: 仅仅加上implements Serializable是不够的, 必须声明所有的实例域是瞬时的(transient)的, 并提供一个readResolve方法.否则每次反序列化, 都会创建一个新的实例.
private Object readResolve() {
return INSTANCE;
}
3.声明一个包含单个元素的枚举类型
public enum Elvis {
INSTANCE;
public void leaveTheBuilding(){}
}
这种方法更加简洁, 无偿提供了序列号机制, 绝对防止多次实例化.
单元素的枚举类型经常成为实现Singleton的最佳方法
有些工具类(java.util.Arrays, java.lang.Math)不希望被实例化.在缺少显示构造器的情况下, 编译器会自动提供一个公有的无参构造器.
将类做成抽象类来强制该类不可被实例化是行不通的, 该类可以被子类化, 并且该子类也可被实例化.
让类包含一个私有的构造器, 它就不能被实例化.副作用是使一个类不能被子类化.
public class UtilityClass {
private UtilityClass() {
throw new AssertionError();
}
}
有许多类会依赖一个或多个底层的资源.实现方法:
把类实现为静态工具类:
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {}
public static boolean isValid(String word) {...}
public static List<String> suggestions(String typo) {...}
}
把类实现为Singleton:
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {}
public static INSTANCE = new SpellChecker(...);
public static boolean isValid(String word) {...}
public static List<String> suggestions(String typo) {...}
}
以上都不理想, 因为它们都是假定只有一本字典可用.
静态工具类和Singleton类不适合于需要引用底层资源的类.
满足多个字典的需求的最简单模式是: 当创建一个新的实例时, 就将资源传入到构造器中, 这是依赖注入(dependency injection)的一种形式.
pubic class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public static boolean isValid(String word) {...}
public static List<String> suggestions(String typo) {...}
}
另一种变体: 将资源工厂传给构造器, 工厂是可以被反复调用来创建类型实例的一个对象, 这类工厂具体表现为工厂方法(Factory Method)模式. 在java8中增加的接口Supplier最适合用于表示工厂.带有Supplier的方法, 通常应该限制输入工厂的类型参数使用有限制的通配符类型(bounded wildcard type).
Mosaic create(Supplier<? extends Tile> tileFactory) {...}
依赖注入极大的提升了类的灵活性, 可重用性和可测试性.
一般来说, 做好能重用单个对象, 而不是在每次需要的时候就创建一个相同功能的新对象.
反面例子:
String s = new String("hello"); // DON'T DO THIS !
该语句每次被执行的时候都创建一个新的String实例, 是不必要的, 参数("hello")本身就是一个String实例, 功能方面等同于构造器创建的对象.
改进的版本:
String s = "hello";
只用了一个String实例,可以保证在同一台虚拟机中运行的代码, 只要它们包含相同的字符串字面常量, 该对象就会被重用.
同时提供了静态工厂方法和构造器的不同类, 优先使用静态工厂方法, 以避免创建不必要的对象.
有些对象创建的成本比其他对象高的多, 可以把这些对象缓存下来重用.
// 检查一串字符是否是罗马数字
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$")
}
虽然String.matches方法最易于查看一个字符串是否与正则表达式相匹配, 但并不适合在注重性能的情形中重复使用.因为方法内部为正则表达式创建了一个Pattern实例, 却只用了一次.创建Pattern实例的成本很高, 因此需要将正则表达式编译成一个有限状态机(finite state machine).
为了提示性能, 应该显式地将正则表达式编译成一个Pattern实例(不可变), 让它成为类初始化的一部分, 并将它缓存起来, 每当调用isRomainNumerical方法时重用.
import java.util.regex.Pattern;
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
如果RomanNumerals初始化了, 但是方法没有被调用, 那就没有必要初始ROMAN域, 可通过在第一次调用时延迟初始化(lazily initializing)ROMAN域, 但不建议这样做.这样会使代码更加复杂,无法提高性能.
适配器对象: 它把功能委托给一个后备对象, 从而为后备对象提供一个可用替代的接口.由于适配器除了后备对象外, 没有其他的状态信息, 所以针对某个给定的对象的特定适配器而言, 它不需要创建多个适配器实例.
例如: Map接口的keySet方法返回Map对象的Set视图, 其中包含所有Map的key.对于给定的Map对象, 每次调用keySet方法都返回同样的Set实例.虽然返回的Set实例一般是可改变的, 但所有返回的对象在功能上是等同的: 当其中一个返回对象发生变化的时候, 所有其他的返回对象也要发生变化, 因为是同一个Map实例支撑的, 所以创建keySet视图对象的多个实例没有害处,却没有必要,也没好处.
自动装箱:自动装箱使得基本类型和装箱基本类型之间的差别模糊起来, 但并没有完全消除, 语义上有微妙的差别, 在性能上有比较明细的差别.
private static long sum() {
// Long sum = 0L; // 使用Long会创建多余的Long实例
long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
要优先使用基本类型而不是装箱基本类型, 当心无意识的自动装箱.
小对象:小对象的构造器只做很少量的显式工作, 所以小对象的创建和回收动作是廉价的, 特别在现在的JVM实现上更是如此.通过创建附加的对象, 提升程序的清晰性, 简洁性和功能性.
对象池:除非对象池中的对象是重量级的, 否则通过维护对象池来避免创建对象非好的做法.正确使用对象池的典型对象示例是数据库链接池.一般而言, 维护自己的对象池必定会把代码弄得很乱, 同时增加内存占用, 还会损害性能.现代的JVM实现具有高度优化的垃圾回收器, 其性能很容易就会超过轻量级对象池的性能.
保护性拷贝:在提倡使用保护性拷贝的时候, 因重用对象而付出的代价要远远大于因创建重复对象而付出的代价.
栈的实现例子:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return element[--size];
}
// 清空弹出栈对象的过期引用
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = element[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
以上程序有个内存泄漏:
如果栈先增长, 然后再收缩, 那么栈中弹出来的对象将不会当作垃圾回收, 即使使用栈的程序不再引用这些对象, 它们也不会回收, 因为栈内部维护着对这些对象的过期引用(obsolete reference),所谓的过期引用是指永远也不会再解除的引用.
清空对象引用: 是一种例外, 不是一种规范行为.消除过期引用的最好方法是让包含该引用的变量结束其生命周期.
只要类是自己管理内存, 程序员就应该警惕内存泄漏问题.
内存泄漏:
一个常见来源是缓存: 一旦你把对象引用放到缓存中, 就很容易被遗忘掉, 从而使它不再有用之后很长时间仍留在缓存中.如果你实现这样的缓存: 只要在缓存之外存在对某个项的键的引用, 该项就有意义, 那可使用WeekHashMap代表缓存, 当缓存中的项过期后, 它们就会自动被删除.记住只有当所要缓存项的生命周期使由该键的外部引用而不是由值决定时, WeekHashMap才有用处.
更为常见的情形: 缓存项的生命周期是否有意义并不是很容易确定, 随着时间的推移, 其中的项变得越来越没有价值, 这种情况下, 缓存应该时不时清除掉没用的项.清除工作可以由一个后台线程(可能是ScheduledThreadPoolExecutor)来完成, 或者可在给缓存添加新条目时顺便进行清理.LinkedHashMap类利用它的removeEldestEntry方法可以容易实现添加新条目时顺便进行清理的功能.对于更加复杂的缓存, 必须直接使用java.lang.ref
一个常见来源是监听器和其他回调: 如果你实现了一个API, 客户端在这个API中注册回调, 却没有显式地取消注册.确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用, 例如直接它们保存为WeekHashMap中的键.
终结(finalizer)方法通常是不可预测的, 也是很危险的, 一般情况下是不需要的.
清除方法没有终结方法那么危险, 但仍然不可预测, 运行缓慢, 一般情况下也是不必要的.
终结方法和清除方法不能保证会被即使执行, 注重时间(time-critical)的任务不应该由终结方法和清除方法来完成,比如关闭已打开的文件.
及时地执行终结方法和清除方法是垃圾回收算法地主要功能, 在不同的JVM实现中会大相径庭.
为类提供终结方法, 可能会随意地延迟其实例回收过程, 终结方法线程的优先级比应用程序的其他线程的优先级要低得多.
Java语言规范不保证终结方法和清除方法会被及时执行, 同时也不保证它们会被执行.永远不应该依赖终结方法或清除方法来更新重要的持久状态.
如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程会被终止, 可能使对象处于破坏的状态, 如果另一个线程企图使用被破坏的对象,则可能发生任何不确定的行为.异常发生在终结方法中, 不会打印出栈轨迹, 甚至警告都不会打印出来,清除方法没有这个问题,因为使用清除方法的一个类库在控制它的线程.
使用终结方法和清除方法有非常严重的性能损失.
终结方法有严重的安全问题: 为终结方法攻击(finalizer attack)打开了类的大门.从构造器抛出的异常足以防止对象继续存在,有了终结方法,这一点就做不到了
合理用法
1.当资源的所有者忘记调用它的close方法时, 终结方法或清除方法可充当安全网.需考虑这种保护是否值得付出这样的代价.
2.本地对等体(native peer)是一个本地(非java)的对象, 普通对象通过本地方法委托给一个本地对象.因为本地对等体不是一个普通对象, 垃圾回收器不会知道它, 当它的java对等体回收时候, 它不会被回收.如果本地对等体没有关键资源, 且性能可以接受的话, 清除方法或终结方法是执行这项任务的最合适工具.
关闭资源
让类实现Closeable方法, 要求客户端在每个实例不再需要时调用close方法, 利用try-with-resources确保终止.细节: 该实例必须记录下自己是否已经被关闭.
import sun.misc.Cleaner;
import java.io.Closeable;
/
* 清除资源示例
*
* @author lzlg
* 2021/3/14 16:22
*/
public class Room implements Closeable {
private static class State implements Runnable {
int numJunkPiles;
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
@Override
public void run() {
System.out.println("Cleaning room");
numJunkPiles = 0;
}
}
private final State state;
private final Cleaner cleaner;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleaner = Cleaner.create(this, state);
}
@Override
public void close() {
cleaner.clean();
}
}
测试:
/
* 使用try-with-resources
* @author lzlg
* 2021/3/14 16:38
*/
public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("Goodbye");
}
}
}
打印结果:
Goodbye
Cleaning room
public static void main(String[] args) {
Room myRoom = new Room(7);
System.out.println("Goodbye");
}
打印结果:
Goodbye
State实现了Runnable接口,它的run方法最多被Cleaner调用一次.
两种情况会触发run方法调用:
1.通过调用Room的close方法触发, 从而触发cleaner的clean方法, 然后触发run.
2.如果到了Room应该被垃圾回收时, 客户端还没调用close方法, 清除方法就会(希望如此,不一定)调用state的run方法.
关键是State实例没有引用它的Room实例, 如果引用了, 会造成循环, 阻止Room实例被垃圾回收(以及防止被自动清除). 因此State必须是一个静态的嵌套类, 因为非静态嵌套类包含了对外围实例的引用.不建议使用lambda, 因为很容易捕捉到对外围对象的引用.
根据经验try-finally语句是确保资源被适时关闭的最佳方法:
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
br.readLine();
} finally {
br.close();
}
}
但一旦涉及到多个资源, 就会一团糟:
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
在try和finally块中的代码都会抛出异常, 如果底层的物理设备异常, 那调用readLine方法就会抛出异常, 同样的调用close方法也会出现异常.这种情况下, 第二个异常完全抹除了第一个异常.
使用try-with-resources时必须先实现Closeable接口. 代码变得简洁易懂, 更容易找bug.
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
}
}
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
处理必须关闭的资源时, 始终优先考虑用try-with-resources, 而不是try-finally.
不覆盖equals方法的情况:
需要覆盖equals方法的情况:
如果类具有自己特有的逻辑相等概念(不同于对象等同的概念),且超类没有覆盖equals方法.这些类通常属于值类(value class),但枚举类型这种值类不需要覆盖equals方法(符合条件1).
覆盖equals方法的通用约定:
对称性:
import java.util.Objects;
/
* 区分大小写的字符串
*
* @author lzlg
* 2021/3/27 16:06
*/
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if (o instanceof String) {
return s.equalsIgnoreCase((String) o);
}
return false;
}
// 违法了对称性
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
System.out.println(cis.equals(s)); // 返回true
System.out.println(s.equals(cis)); // 返回false, 因为String的equals方法并不知道不区分大小写的字符串.
}
}
一旦违法equals约定,当其他的对象面对你的对象时, 你完全不知道这些对象的行为会怎么样.
解决方法:
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString && s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
传递性:
/
* @author lzlg
* 2021/3/27 16:21
*/
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
Point p = (Point) o;
return p.x == this.x && p.y == this.y;
}
}
import java.awt.*;
/
* @author lzlg
* 2021/3/27 16:23
*/
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
如果ColorPoint完全不提供equals方法,而直接从Point中继承而来, 在equals做比较的时候color域的信息就被忽略了, 虽然不违反equals约定, 但明显无法接受.
如果添加了equals方法:
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
return super.equals(o) && ((ColorPoint) o).color == this.color;
}
public static void main(String[] args) {
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp)); // 返回true
System.out.println(cp.equals(p)); // 返回false
}
但这样会造成普通点和有色点出现对称性问题.
修改equals方法:
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
if (!(o instanceof ColorPoint)) {
return o.equals(this);
}
return super.equals(o) && ((ColorPoint) o).color == this.color;
}
public static void main(String[] args) {
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp)); // 返回true
System.out.println(cp.equals(p)); // 返回true
}
public static void main(String[] args) {
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.println(p1.equals(p2)); // 返回true
System.out.println(p2.equals(p3)); // 返回true
System.out.println(p1.equals(p3)); // 返回false
}
这样做确实提供了对称性, 但却牺牲了传递性.这种方法还会导致无限递归的问题, 如果Point还有子类SmellPoint, 它们各自都带有这种equals方法, 那么myColorPoint.equals(mySmellPoint)的调用会抛出StackOverflowError异常.
如何解决:
这是面向对象语言中关于等价关系的一个基本问题, 我们无法在扩展可实例化的类的同时,即增加新的值组件,同时又保留equals约定.
不再让ColorPoint扩展Point类, 而是加入一个私有的Point域(复合优先于继承), 以及一个公有的视图方法.
import java.awt.*;
/
* @author lzlg
* 2021/3/27 16:23
*/
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
this.point = new Point(x, y);
this.color = color;
}
// 公有的视图方法
public Point asPoint() {
return this.point;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(this.point) && cp.color.equals(this.color);
}
}
但是注意: 你可以在一个抽象类的子类中增加新的组件且不违反equals约定, 因为不可能创建超类的实例.
一致性:
如果两个对象相等,除非其中的对象被修改了, 否则就必须始终保证相等.
不要使equals方法依赖于不可靠的资源,比如java.net.URL中的equals方法依赖于URL中主机IP地址的比较, 将一个主机名变成IP地址可能需要访问网络,随着时间的推移,就不能确保会产生相同的结果,即有可能IP地址发生了改变.
非空性:
if (o == null) {
return false;
}
上诉非空检查是非必要的, 如果使用了instanceof操作符,第一个操作数为null,则不管第二个操作符是什么类型,都会返回false.
实现高质量equals方法:
使用==操作符检查参数是否为这个对象的引用, 是一种性能优化, 如果比较操作很昂贵, 则值得这么做.
使用instanceof操作符检查参数是否为正确的类型.
把参数转换成正确的类型
对于该类中的每个关键(significant)域,检查参数中的域是否与该对象中给对于的域相匹配.
对于不是float也不是double的基本类型域, 可以使用==操作符比较,
对于对象引用域, 可以递归调用equals方法.
对于float域, 使用Float.compare(float,float)方法;
对于double域,使用Double.compare(double,double)方法.
对于float和double域也可使用equals方法比较,但涉及到自动装箱,性能会下降.
有些对象引用域包含null是合法的,为了避免NullPointException异常,则使用静态方法Objects.equals(Object,Object)来检查这类域的等同性.
对于有些类域的比较要比简单的等同性测试复杂得多, 如果是这种情况, 可能希望保留该域的一个范式(canonical form), 这样equals方法根据这些范式进行低开销的比较,对于不可变类最为合适,如果对象发生变化,就必须使其范式保持最新.
域的比较顺序可能会影响equals的性能,
为了获得最佳性能, 应该最先比较最有可能不一致的域,或者开销最低的域,最理想的情况是两个条件都满足的域.
不该比较那些不属于对象逻辑状态的域, 比如用于同步操作的Lock域.
也不需要比较衍生(derived field)域,因为这些域可由关键域计算获得.如果衍生域代表了整个对象的综合描述, 比较这个域可节省在比较失败时去比较实际数据需要的开销.
告诫:
每个覆盖了equals方法的类中, 都必须覆盖hashCode方法, 否则会违法hashCode的通用约定, 从而导致该类型无法结合所有基于散列的集合一起正常工作, 如HashMap和HashSet.
Object的hashCode规范:
没有覆盖hashCode方法违反的是第二条: 相等的对象必须具有相等的散列码(hash code).
计算散列函数:
一个好的散列函数通常倾向于为不相等的对象产生不相等的散列码.理想情况下, 散列函数应该把集合中不相等的实例均匀地分布到所有可能的int值上.
简单的接近这种理想情况的办法:
声明一个int变量并命名为result, 将它初始化为对象中第一个关键域的散列码c.
对象中剩下的每一个关键域f, 都进行以下操作:
为该域计算int类型的散列码c: 如果该域是基本类型, 则使用对应装箱类型的hashCode方法; 如果该域是一个对象引用, 并且该类的equals方法通过递归地调用equals地方式来比较这个域, 则同样为这个域递归地调用hashCode.如果需要更复杂地比较, 则为该域计算一个范式(canonical representation), 然后针对这个范式调用hashCode; 如果为null, 则返回0;
如果该域是个数组, 则要把每个元素根据上述地规则计算一个散列码, 并按照下面的公式把散列码c合并到result中:
result = 31 * result + c;
按照上面的公式, 把计算出的散列码c合并到result中
返回result
在散列码的计算过程中, 可以把衍生域排除在外,必须排除equals比较计算中没有用到的域.
上述的乘法部分使得散列值依赖于域的顺序, 如果一个类包含多个相似的域, 这样的乘法运算会产生一个更好的散列函数.之所以使用31, 因为是一个奇素数, 且有很好的特性, 即用移位和减法来代替乘法, 可以得到更好的性能: 31 * i == (i << 5) - i.
Objects类有一个静态方法hash, 带有任意数量的对象, 并为它们返回一个散列码.运行速度较慢, 因为会引发数组的创建, 以便传入数目可变的参数, 如果参数中有基本类型, 还需装箱和拆箱.
如果一个类是不可变的, 并且计算散列码的开销比较大, 应考虑把散列码缓存在对象内部.如果这种类型的对象会被用作散列键(hash key), 则应该在创建实例的时候计算散列码, 否则可以选择延迟初始化(lazily initialize)散列码, 即一直到hashCode方法被第一次调用的时候才初始化.
不要试图从散列码计算中排除一个对象的关键域来提高性能.在选择忽略的域中, 这些实例的区别非常大, 散列函数会把这些实例映射到极少数的散列码上, 原本以线性级时间运行的程序, 将以平方级的时间运行.
不要对hashCode方法的返回值做出具体的规定, 因此调用方法的客户端无法理所应当地依赖该规定, 这样可为修改散列函数提供灵活性.否则会严格地限制了在未来版本中改进散列函数的能力.
提供好的toString实现可使类用起来更加舒适, 使用了这个类的系统更加易于调试.
toString方法应该返回对象中包含的所有值得关注的信息.如果对象太大, 或者对象中包含的状态信息难以用字符串来表达, toString应返回一个摘要信息.
toString是否在文档中指定返回值的格式.对于值类, 建议这么做, 好处是: 可被用作一种标准的明确的适合人阅读的对象表示法.如果指定了格式, 最好在提供一个相匹配的静态工厂或构造器, 以便程序员可以很容易在对象及字符串表示法之间来回切换.不足之处: 如果该类被广泛使用, 一旦指定格式就必须始终如一地坚持这种格式.不指定格式可以保留灵活性.
无论是否指定格式, 都应该在文档中明确地表明你的意图.
无论是否指定格式, 都为toString返回值中包含的所有信息提供一种可以通过编程访问的途径.(比如getter/setter方法)
在静态工具类中编写toString方法是没有意义的.
在所有子类共享通用字符串表示法的抽象类中, 一定要编写一个toString方法.大多数集合实现中的toString方法都是继承自抽象的集合类.
Cloneable接口(没有包含任何方法)的目的是作为对象的一个mixin(混合)接口, 表明这样的对象允许克隆.决定了Object中受保护的clone方法实现的行为: 如果一个类实现了Cloneable接口, Object的clone方法返回该对象的逐域拷贝, 否则会抛出CloneNotSupportedException异常.
实现Cloneable接口的类为了提供一个功能适当的公有的clone方法.无需调用构造器就可创建对象.
如何实现Cloneable接口:
它的超类都提供了行为良好的clone方法, 首先调用super.clone方法, 由此得到的对象将是原始对象功能完整的克隆.
如果类的clone返回实例不是通过super.clone方法获得, 而是通过调用构造器获得, 但是该类的子类调用了super.clone方法, 得到的对象就会拥有错误的类, 并阻止clone方法的子类的正常工作.如果final类覆盖clone方法, 该规定可忽略, 因为final类不能有子类.如果final类的clone方法没有调用super.clone方法, 这个类没有理由去实现Cloneable接口, 因为它不依赖于Object的克隆实现的行为.
不可变的类永远都不应该提供clone方法.
Java支持协变返回类型(covariant return type), 覆盖方法的返回类型可以是被覆盖方法的返回类型的子类.
对super.clone的方法应该包含在一个try-catch块中, 因为Object声明其clone方法抛出CloneNotSupportedException, 这是一个受检查异常(checked exception).
如果对象中包含的域引用了可变的对象.对象被克隆后, 该域引用的对象和被克隆对象的域引用的对象一致,修改原始的实例会破坏被克隆对象中的约束条件.
clone方法是另一个构造器, 必须确保它不会伤害到原始的对象, 并确保正确地创建被克隆对象中的约束条件.
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
在数组上调用clone返回的数组, 其编译时的类型与被克隆的类型相同, 这是复制数组的最佳习惯做法.
如果elements域是final的, 则上诉代码不能工作, 因为clone方法是被禁止给final域赋新值的.Cloneable架构与引用可变对象的final域的正常用法是不相兼容的, 除非在原始对象和克隆对象之间可安全的共享此可变对象.为了使类成为可克隆的, 可能有必要从某些域中去掉final修饰符.
递归调用clone有时还不够, 虽然被克隆对象有它自己的数组, 但是这个数组的引用的对象与原始对象是一样的, 从而很容易引起克隆对象和原始对象中不确定的行为, 必须单独的进行拷贝.
/
* @author lzlg
* 2021/3/28 15:15
*/
public class MyHashTable implements Cloneable {
private Entry[] buckets = new Entry[10];
@Override
protected MyHashTable clone() {
try {
MyHashTable result = (MyHashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++) {
result.buckets[i] = buckets[i].deepCopy();
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
private static class Entry {
final Object key;
Object value;
Entry next;
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
}
MyHashTable.Entry的deepCopy方法递归调用自身, 在散列桶很长的时候, 会造成栈溢出.使用迭代(iteration)代替递归(recursion).
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next) {
p.next = new Entry(p.next.key, p.next.value, p.next.next);
}
return result;
}
克隆复杂对象的最后一种方法: 先调用super.clone方法, 然后把结果对象中的所有域都设置成初始状态, 然后调用高层的方法重新产生对象的状态.虽然做法简单合理且优美的clone方法, 但运行起来通常没有直接操作对象及其克隆对象的内部状态的clone方法快, 与整个Cloneable架构是对立的, 完全抛弃了Cloneable架构基础的逐域对象复制的机制.
clone方法不应该在构造的过程中, 调用可覆盖的方法, 如果调用了一个在子类中被覆盖的方法, 那么该方法所在的子类有机会修正它在克隆对象中的状态之前, 该方法就会被执行, 这样很有可能会导致克隆对象和原始对象之间的不一致.
Object的clone方法被声明为可抛出CloneNotSupportedException异常, 但覆盖版本的clone方法可忽略这个声明,公有的clone方法应该省略throws声明.
为继承设计类有两种选择, 无论哪种选择, 都不该实现Cloneable接口.选择模拟Object的行为,实现一个功能相当的受保护的clone方法, 应该被声明抛出CloneNotSupportedException异常, 这样可使子类具有实现和不实现Cloneable接口的自由;选择不去实现一个有效的clone方法, 并防止子类去实现它.
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
如果编写线程安全的类准备实现Cloneable接口, 它的clone方法必须得到严格的同步.
简而言之: 所有实现了Cloneable接口的类都应该覆盖clone方法, 并且是公有的方法, 它的返回类型是类本身.该方法应该先调用super.clone方法, 然后修正任何需要修正的域.
对象拷贝的更好办法是提供一个拷贝构造器或拷贝工厂:
// 拷贝构造器
public Yum(Yum yum) {...}
// 拷贝工厂
public static Yum newInstance(Yum yum) {...}
基于接口的拷贝构造器和拷贝工厂(准确的叫法是转换conversion构造器和转换工厂), 允许客户选择拷贝的实现类型.
类实现了Comparable接口, 就表明它的实例具有内在的排序关系(natural ordering). 一旦实现Comparable接口, 可以跟许多泛型算法(algorithm)以及依赖于该接口的集合实现进行协作.
public interface Comparable<T> {
public int compareTo(T o);
}
compareTo方法的通用约定:
将这个对象和指定的对象进行比较,当该对象小于, 等于或大于指定对象的时候, 分别返回一个负整数, 零或者正整数, 如果由于指定对象的类型而无法与该对象进行比较, 则抛出ClassCastException异常.
sgn(expression)表示数学中的signum函数, 根据表达式的值为负值, 零和正值, 分别返回-1, 0和1
违反compareTo方法的约定的类会破坏其他依赖于比较关系的类, 比如TreeSet和TreeMap.
无法在用新的值组件扩展可实例化的类时, 同时保持compareTo方法的约定, 除非愿意放弃面向对象的抽象优势.想给实现了Comparable接口的类增加值组件,不要扩展类,而是要编写一个不相关的类,其中包含第一个类的实例,然后提供一个视图方法返回这个实例.
如果遵守第4条规则, 那么由compareTo方法所施加的顺序关系被认为与equals一致, 反之则不一致, 但如果一个有序集合包含了该类的元素, 这个集合就可能无法遵守相应集合的通用约定.因为这些接口的通用约定是按照equals方法来定义的.
比如BigDecimal类, 它的compareTo方法和equals方法不一致, 创建一个空的HashSet实例, 并添加new BigDecimal("1.0") 和new BigDecimal("1.00") 这个集合包含两个元素, 然而使用TreeSet进行添加时, TreeSet只包含一个运算,因为这两个实例通过compareTo方法比较是相等的.
在compareTo方法中使用关系操作符< 和 > 是非常烦琐的, 且容易出错, 不再建议使用. 建议使用对应装箱类的compare方法进行比较.
如果一个类有多个关键域, 那么你必须从最关键的域开始, 逐步进行到所有的重要域.
Java8比较构造方法:
comparator construction methods
import java.util.Comparator;
// 静态导入
import static java.util.Comparator.comparingInt;
/
* @author lzlg
* 2021/3/28 16:23
*/
public class PhoneNumber implements Comparable<PhoneNumber> {
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
private final Short areaCode, prefix, lineNum;
public PhoneNumber(Short areaCode, Short prefix, Short lineNum) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNum = lineNum;
}
@Override
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
}
comparingInt带有一个键提取器函数(key extractor function), 将一个对象引用映射到一个类型为int的键上, 并根据这个键返回一个对实例进行排序的比较器.
thenComparingInt是Comparator实例上的一个方法, 带有一个类型为int的键提取器函数.Comparator具有全套的构造方法,对应基本类型long和double都有对应的comparingInt和thenComparingInt. int版本可用于short, double版本可用于float.
compareTo或者compare偶尔会依赖于两个值之间的区别:
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
千万不要使用该方法, 很容易造成整数溢出.
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
// 使用Integer自带的compare方法
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
// 或者改为函数式接口
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(Object::hashCode);
设计良好的组件会隐藏所有的实现细节, 把API与实现清晰地隔离开来, 组件之间通过API进行通信, 一个模块不需要知道其他模块的内部工作情况, 这个概念叫做信息隐藏(information hiding)或封装(encapsulation).可以有效的解除组成系统各个组件之间的耦合关系, 即解耦(decouple), 使这些组件可以独立地开发, 测试, 优化, 使用, 理解和修改.组件可并行开发, 减轻维护的负担, 可分析组件的性能, 进行针对性的优化, 提高了软件的可重用性.
尽可能地使每个类或者成员不被外界访问.
公有类的实例域决不能是公有的.包含公有可变域的类通常不是线程安全的.
常量公有静态域, 名称由大写字母组成, 单词之间使用下划线隔开.
让类具有公有的静态final数组域, 或者返回这种域的访问方法, 这是错误的.因为非零的数组是可变的.正确的做法:
公有数组变成私有的, 并增加一个公有的不可变列表:
private static final Thing[] PRIVATE_VALUES = {...}
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
公有数组变成私有的, 添加公共方法, 返回私有数组的一个拷贝:
private static final Thing[] PRIVATE_VALUES = {...}
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
如果类可以在它所在的包之外进行访问, 就提供访问方法. 以保留将来改变该类的内部表示法的灵活性.
如果类是包级私有的, 或是私有的嵌套类, 直接暴露它的数据域并没有本质的错误.
公有类永远都不应该暴露可变的域.
不可变类使指实例不能被修改的类, 每个实例中包含的所有信息都必须在创建该实例的时候就提供, 并在对象的整个生命周期内固定不变.
为了使类成为不可变, 要遵循下面5条规则:
/
* 不可变类举例: 表示一个复数
*
* @author lzlg
* 2021/4/3 14:58
*/
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public double realPart() {
return this.re;
}
public double imaginaryPart() {
return this.im;
}
public Complex plus(Complex c) {
return new Complex(this.re + c.re, this.im + c.im);
}
public Complex minus(Complex c) {
return new Complex(this.re - c.re, this.im - c.im);
}
public Complex times(Complex c) {
return new Complex(this.re * c.re - this.im * c.im,
this.re * c.im - this.im * c.re);
}
public Complex divideBy(Complex c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex((this.re * c.re + this.im * c.im) / tmp,
(this.im * c.re - this.re * c.im) / tmp);
}
@Override
public int hashCode() {
return 31 * Double.hashCode(this.re) + Double.hashCode(this.im);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Complex)) {
return false;
}
Complex c = (Complex) obj;
return Double.compare(c.re, this.re) == 0 && Double.compare(c.im, this.im) == 0;
}
@Override
public String toString() {
return "(" + this.re + " + " + this.im + "i)";
}
}
注意这些算术运算如何创建并返回新的Complex实例, 而不是修改这个实例.被称为函数(functional)方法, 这些方法返回了一个函数的结果, 这些函数对操作数进行运算但并不修改它.
不可变对象只有一种状态, 即被创建时的状态.
不可变对象本质上是线程安全的, 它们不要求同步, 可以被自由地共享.永远不需要进行保护性拷贝(defensive copy).对于频繁用到的值, 可以提供公有的静态final常量.
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
不仅可以共享不可变对象, 甚至可以共享它们的内部信息, 如BigInteger的negate方法返回新的BigInteger实例, 数值是一样的, 但符号相反, 无需拷贝int数组, 新建的BigInteger实例也指向原始实例的同一个内部数组.
不可变对象为其他对象提供了大量的构件.
不可变对象无偿地提供了失败的原子性.
不可变对象唯一的缺点是: 对于每个不同的值都需要一个单独的对象, 创建这些对象代价可能很高.你执行一个多步骤的操作, 并且每个步骤都会产生一个新的对象, 除了最后的结果外, 其他的对象最终会被丢弃, 此时性能问题会显露出来, 解决方法:
使类保持不可变的其他方法:
让类的所有构造器变为私有的或者包级私有的, 并添加公有的静态工厂方法来代替公有的构造器.
对于BigInteger和BigDecimal, 由于不是final类(可以有子类), 如果你在编写的一个类, 它的安全性依赖于BigInteger或者BigDecimal参数的不可变性, 就必须检查, 以确定这个参数是否为真正的BigInteger或者BigDecimal, 而不是不可信任子类的实例, 如果是后者, 就必须假设它可能是可变的前提下进行保护性拷贝:
public static BigInteger safeInstance(BigInteger val) {
return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray());
}
并非所有的域都必须是final的.没有一个方法能够对对象的状态产生外部可见(externally visible)的改变, 许多不可变的类有一个或多个非final域, 它们在第一次被请求执行这些计算的时候, 把一些昂贵的计算结果缓存在这些域中.如果再次请求同样的计算, 就直接返回这些缓存的值, 因为对象是不可变的, 保证了这些计算如果被再次执行, 会产生同样的结果.
除非有很好的理由要让类成为可变的类, 否则它就应该是不可变的.如果类不能被做成不可变的,仍然应该尽可能限制它的可变性,除非有令人信服的理由要使域变成非final的, 否则要使每个域都是private final的.
构造器应该创建完全初始化的对象, 并建立起所有的约束关系.不要在构造器或静态工厂之外再提供公有的初始化方法, 也不应该提供重新初始化方法.
继承打破了封装性, 子类依赖于其超类中特定功能的实现细节, 超类的实现有可能随着发行版本的不同而变化, 子类可能会遭到破坏.
/
* 这个类看起来合理, 但不能正常工作
*
* @author lzlg
* 2021/4/3 16:38
*/
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}
public static void main(String[] args) {
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount()); // 返回6
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
以上例子: 我们期望getAddCount返回3, 但实际上返回6, 因为HashSet内部是addAll方法是基于它的add方法来实现的, 这种自用性是实现细节, 不是承诺, 不能保证在Java平台的所有实现中都保持不变.
如果重新实现超类的方法(重写addAll方法,遍历集合c,添加到集合中), 这些超类的方法可能是自用的, 容易出错, 耗时, 降低性能,这做法并不总是可行的, 因为超类无法访问对于子类来说是私有的域, 所以有些方法就无法实现.
子类可以在超类的后续发行版本中获得新的方法.如果超类增加了新的方法, 但子类覆盖了所有能够添加元素的方法(添加元素有先决条件), 很可能超类调用该新方法给子类添加非法的元素(添加元素无先决条件).
如果子类不覆盖方法, 而是添加新的方法.在超类的后续版本中, 如果出现和子类签名相同但返回类型不同的方法, 那子类就无法通过编译.
以上的问题都可以使用 复合(composition) 解决, 现有的类变成了新类的一个组件.新类的每个实例方法都可以调用被包含的现有类实例中的方法, 并返回它的结果, 被成为转发(forwarding), 新类中的方法被成为转发方法, 不依赖于现有类的实现细节.
/
* 转发类
*
* @author lzlg
* 2021/4/3 17:04
*/
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
@Override
public int size() {
return s.size();
}
@Override
public boolean isEmpty() {
return s.isEmpty();
}
@Override
public boolean contains(Object o) {
return s.contains(o);
}
@Override
public Iterator<E> iterator() {
return s.iterator();
}
@Override
public Object[] toArray() {
return s.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean remove(Object o) {
return s.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
@Override
public void clear() {
s.clear();
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public boolean equals(Object obj) {
return s.equals(obj);
}
@Override
public String toString() {
return s.toString();
}
}
/
* 使用转发方法的InstrumentedHashSet
*
* @author lzlg
* 2021/4/3 16:38
*/
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedHashSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
因为每一个InstrumentedHashSet实例都把另一个Set实例包装起来了, 所以InstrumentedHashSet类被称为包装类(wrapper class), 这也是Decorator(修饰者)模式.
包装类不适合于回调框架, 在回调框架中, 对象把自身的引用传递给其他的对象, 用于后续的调用.因为被包装起来的对象不知道它外面的包装对象, 所以它传递一个指向自身的引用, 回调时避开了外面的包装对象, 被称为SELF问题.
当子类真正是超类的子类时, 才适合使用继承, 对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该扩展A.
如果在适合使用复合的地方使用了继承, 则会不必要地暴露实现细节, 这样得到的API会把你限制在原始的实现上, 永远限定了类的性能.由于暴露了这些细节, 客户端就有可能直接访问这些内部细节.
专门为继承而设计且具有良好说明的类:
对于为了继承而设计的类, 唯一的测试方法就是编写子类.3个子类通常就足以测试一个可扩展的类.
为了允许继承, 构造器绝不能调用可被覆盖的方法, 无论间接调用还是直接调用.超类的构造器在子类的构造器之前运行, 所以子类中覆盖版本的方法将会在子类的构造器运行之前先被调用.
在一个为了继承而设计的类中实现Cloneable或者Serializable接口, 因为clone和readObject方法在行为上类似于构造器, 所以无论是clone还是readObject都不可以调用可覆盖的方法, 不管是直接或间接的形式.
在一个为了继承而设计的类中实现Serializable接口, 并且该类有一个readResolve或者writeReplace方法, 就必须使readResolve或者writeReplace成为受保护的方法.
可以机械的消除可覆盖方法的自用特性, 而不改变它的行为.将每个可覆盖方法的代码体移到一个私有的辅助方法中, 并且让每个可覆盖的方法调用它的私有辅助方法.
抽象类, Java只支持单继承, 作为类型定义受到了限制.
现有的类可以很容易被更新, 以实现新的接口, 无法更新现有的类来扩展新的抽象类.
接口是定义mixin(混合类型)的理想选择.称为mixin, 因为它允许任选的功能可被混合到类型的主要功能中.
接口允许构造非层次结构的类型框架.
接口使得安全地增强类地功能称为可能.
通过对接口提供一个抽象地骨架实现(skeletal implementation)类, 可以把接口和抽象类的优点结合起来.接口负责定义类型, 或许还提供一些缺省方法(default method),而骨架实现类则负责实现除基本类型接口方法之外, 剩下的非基本类型接口方法.(模板方法模式)
按照惯例, 骨架实现类被称为AbstractInterface, 这里的Interface是指所实现接口的名字.
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<Integer>() {
@Override
public Integer get(int index) {
return a[index];
}
@Override
public Integer set(int index, Integer element) {
int oldVal = a[index];
a[index] = element;
return oldVal;
}
@Override
public int size() {
return a.length;
}
};
}
以上例子, 骨架实现可以使程序员非常容易地提供他们自己的接口实现, 该例子是个Adapter.
骨架实现类: 为抽象类提供了实现上的帮助, 但又不强加 抽象类作为类型定义时 所特有的限制.对于接口的大多数实现来讲, 扩展骨架实现类是个很显然的选择, 但不是必须的. 如果预置的类无法扩展骨架实现类, 这个类始终能手工实现这个接口, 同时这个类本身仍然受益于接口中出现的任何缺省方法.骨架实现类有助于接口的实现, 实现了这个接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上, 这个内部私有类扩展了骨架实现类.这种方法被称作模拟多重继承(simulated multiple inheritance).
注意不能为Object方法提供缺省方法
因为骨架实现类是为了继承的目的而设计的, 所以应该提供好的文档.
骨架实现上有个小小的不同, 就是简单实现(simple implementation),比如AbstractMap.SimpleEntry.
Java8增加了缺省方法, 目的是允许给现有的接口添加方法, 但还是充满风险.
Java类库的缺省方法是高品质的实现, 大多数情况下能正常使用, 但是并非每一个可能的实现的所有变体, 始终都可以编写出一个缺省方法.比如Collection接口的removeIf接口, Apache的SynchronizedCollection会继承该实现, 但缺省实现不知道同步这回事, 也无权访问包含该锁定对象的域, 如果客户在SynchronizedCollection上调用removeIf方法,同时另一个线程对该集合进行修改, 就会导致ConcurrentModificationException或其他异常行为.
有了缺省方法, 接口的现有实现不会出现编译时错误或警告, 运行却失败的情况.但建议尽量避免利用缺省方法在现有的接口上添加新的方法, 除非有特殊需要.然而在创建接口时, 用缺省方法提供标准的方法实现是非常方便的, 简化了实现接口的任务.
缺省方法不支持从接口中删除方法, 也不支持修改现有方法的签名.
尽管缺省方法现在是Java平台的组成部分, 但谨慎设计接口仍然是至关重要的.
发布程序之前, 测试每个新的接口尤其重要, 程序员应该以不同的方法实现每一个接口, 不应少于三种实现,编写多个客户端程序, 利用每个新接口的实例来执行不同的任务进行测试.
当类实现接口时, 接口就充当可以引用这个类的实例的类型.类实现了接口, 表明客户端可以对这个类的实例实施某些动作.为了任何其他目的而定义接口都是不恰当的.
常量接口模式是对接口的不良使用:
导出常量的合理方案:
有时可能会遇到带有两种甚至多种风格的实例的类, 并包含表示实例风格的标签(tag)域:
// 千万不要这样做
class Figure {
enum Shape {RECTANGLE, CIRCLE};
final Shape shape;
double length;
double width;
double radius;
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}
标签类的缺点:
改造为类层次:
abstract class Figure {
abstract double area();
}
class Circle extend Figure {
final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extend Figure {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
double area() {
return length * width;
}
}
类层次的好处可以用来反映类型之间本质上的层次关系, 有助于增强灵活性, 有助于更好地进行编译时类型检查.类层次可反映出正方形是一种特殊矩形的事实:
class Square extend Rectangle {
Square(double side) {
super(side, side);
}
}
嵌套类(nested class)是指定义在另一个类内部的类, 存在的目的应该只是为它的外围类(enclosing class)提供服务, 如果嵌套类将来可能会用于其他的某个环境中, 就应该是顶层类.
嵌套类有四种: 静态成员类(static member class), 非静态成员类(nonstatic member class), 匿名类(anonymous class)和局部类(local class). 除了第一种, 其他三种都成为内部类(inner class).
静态成员类:
最简单的一种类, 最好把它看作是普通的类, 只是碰巧被声明在另一个类内部而已.可以访问外围类的所有成员, 包括那些声明为私有的成员, 与其他静态成员一样, 同样遵守可访问性规则.
常见用法: 作为公有的辅助类, 只有与它的外部类一起使用才有意义.
静态成员类和非静态成员类的区别:
私有静态成员类的另一个常见用法是代表外围类所代表的对象组件, 比如Map实现的内部都有一个Entry对象, 虽然每个entry都与一个Map关联, 但是entry上的方法并不需要访问Map, 使用非静态成员类来表示entry是浪费的.
非静态成员类:
常见用法: 定义一个Adapter, 它允许外部类的实例被看作是另一个不相关类的实例.如Set和List集合接口的实现往往使用非静态成员类来实现它们的迭代器(iterator).
如果声明成员类不要求访问外围实例, 就要始终把修饰符static放在它的声明中.如果省略了static修饰符, 则每个实例都包含一个额外的执行外围对象的引用, 保存这份引用要消耗时间和空间, 并且会导致外围实例符合垃圾回收时却仍然得以保留, 由此造成的内存泄漏是灾难性的.
匿名类:
匿名类不与其他的成员一起被声明, 而是在使用的同时被声明和实例化, 匿名类可以出现在代码中任何允许存在表达式的地方.
当且仅当匿名类出现在非静态的环境中时, 它才有外围实例, 即使出现在静态的环境中, 也不可能拥有任何静态成员, 而是拥有常数变量(constant variable), 常数变量是final基本类型, 或者被初始化成常量表达式的字符串域.
匿名类出现在表达式中, 必须保持简短, 否则会影响程序的可读性.
匿名类是动态创建小型函数对象和过程对象的最佳方式, 现在会优先选择lambda, 另一种常见用法是在静态工厂方法的内部(20条的intArrayAsList方法)
局部类:
在任何可以声明局部变量的地方, 都可以声明局部类, 遵守同样的作用域规则.
局部类可以有名字, 可被重复使用. 只有当局部类在非静态环境中定义的时候, 才有外围实例, 也不能包含静态成员.必须非常简单, 以便不会影响可读性.
虽然Java编译器允许在一个源文件中定义多个顶级类, 但这么做没有什么好处, 只会带来巨大的风险.因为在一个源文件中定义多个顶级类, 可能导致给一个类提供多个定义, 哪个定义被用到, 取决于源文件被传给编译器的顺序.
正确做法: 把顶级类分别放入独立的源文件.如果一定要把多个顶级类放进一个源文件中, 就要考虑使用静态成员类, 以此代替将这两个类分到独立的源文件中去.
声明一个或多个类型参数的类或接口, 就是泛型类或接口.
每一种泛型定义一组参数化的类型, 构成格式为:先是类或接口的名称, 接着用尖括号(<>)把对应于泛型形式类型参数的实际类型参数列表括起来.
每一种泛型都定义一个原生态类型(raw type), 即不带任何实际类型参数的泛型名称.(为了兼容以前)
使用原生态类型, 就失掉了泛型在安全性和描述性方面的所有优势.
泛型出现的时候, Java平台存在大量没有使用泛型的代码, 为让这些代码合法, 并能够与使用泛型的新代码互用, 原生态类型必须合法才能将参数化类型的实例传递给那些被设计成使用普通类型的方法, 这种需求称为移植兼容性(migration compatibility), 促成了支持原生态类型, 以及利用擦除(erasure)实现泛型的决定.
List和List<Object.>的区别:
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0);
}
上述使用原生态类型的代码, 编译时会收到警告, 运行时会出现ClassCastException异常.如果使用List<Object.>则在编译的时候就无法通过.
在不确定或者不在乎集合中的元素类型的情况下, 会使用原生态类型, 但不安全, 安全的替代做法是使用无限制的通配符类型(unbounded wildcard type).如果使用泛型, 不确定或者不关心实际的类型参数, 可以使用一个问号代替.例如 Set<E.>的无限制通配符类型为Set<?>
无限制通配符Set<.?>和原生态类型Set的区别: 通配符类型是安全的, 原生态类型不安全; 可以将任何元素放进使用原生态类型的集合中, 容易破坏该集合的类型约束条件, 但不能将任何元素(除了null之外)放到Collection<?>中.
必须在类文字(class literal)中使用原生态类型, 规范不允许使用参数化类型(允许数组类型和基本类型).List.class, String[].class和int.class是合法的, 但是List<String.class>和List<?>.class则不合法.
由于泛型信息可以在运行时被擦除, 因此在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的.用无限制通配符类型代替原生态类型, 对instanceof操作符的行为不会产生任何影响, 这种情况下<>和?多余了, 利用泛型来使用instanceof操作符的首选方法:
if (o instanceof Set) {
Set<?> s = (Set<?>) o;
...
}
使用泛型编程时, 要尽可能消除每一个非受检警告.如果无法消除警告, 同时可以证明引起警告的代码是类型安全的, 只有在这种情况下, 才可以使用一个@SuppressWarnings("unchecked")注解来禁止这条警告.
SuppressWarnings注解可以用在任何粒度的级别中, 应该始终在尽可能小的范围内使用该注解.永远不要在整个类上使用SuppressWarnings, 这么做可能会掩盖重要的警告.
ArrayList上的toArray方法:
public <T> T[] toArray(T[] a) {
if (a.length < size)
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// 可以将SuppressWarnings注解放在方法上, 但实践中千万不要这么做
// 而应该声明一个局部变量来保存返回值, 并注解其声明
public <T> T[] toArray(T[] a) {
if (a.length < size) {
@SuppressWarnings("unchecked")
T[] result = (T[]) Arrays.copyOf(elementData, size, a.getClass());
return result;
}
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// 这个方法可以正确地编译, 禁止非受检警告的范围减到最小
每当使用@SuppressWarnings("unchecked")注解时, 应该添加一条注释, 说明为什么这么做是安全的.
数组与泛型相比, 有两个重要的不同点:
数组是协变(covariant)的, 如果Sub是Super的子类型, 那么数组类型Sub[]就是Super[]的子类型
泛型是可变的(invariant)的, 对于任意两个不同的类型Type1和Type2, List<Type1.>既不是List<Type2.>的子类型, 也不是List<Type2.>的超类型.
// 下面的代码是合法的, 但运行时才能发现错误
Object[] objArray = new Long[1];
objArray[0] = "I don't fit in";
// 下面的代码是非法的,无法通过编译的
List<Object> ol = new ArrayList<Long>();
ol.add("I don't fit in");
数组是具体化的, 因此数组会在运行时知道和强化它们的元素类型, 如果企图将String保存到Long数组中, 就会得到一个ArrayStoreException异常.
泛型是通过擦除来实现的, 泛型只在编译时强化它们的类型信息,并在运行时丢弃(或擦除)它们的元素类型信息, 擦除就是使泛型可以与没有使用泛型的代码随意进行互用, 以确保Java 5中平滑过渡到泛型.
数组和泛型不能很好的混合使用, 创建泛型, 参数化类型或者类型参数的数组是非法的, List<E.>[], new List<String.>[], new E[]都不是合法的.编译时会导致泛型数组创建错误.
像E, List<E.>和List<String.>这样的类型称作不可具体化(nonreifiable)的类型, 不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型.唯一可具体化的参数化类型是无限制的通配符类型, 例如List<?>和Map<?, ?.>.
import java.util.Collection;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
/
* 原生态类型
*
* @author lzlg
* 2021/4/10 18:46
*/
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
this.choiceArray = choices.toArray();
}
public Object choose() {
Random r = ThreadLocalRandom.current();
return choiceArray[r.nextInt(choiceArray.length)];
}
}
/
* 使用泛型和数组
*
* @author lzlg
* 2021/4/10 18:46
*/
public class Chooser<T> {
private final T[] choiceArray;
@SuppressWarnings("unchecked")
public Chooser(Collection<T> choices) {
this.choiceArray = (T[]) choices.toArray();
}
public T choose() {
Random r = ThreadLocalRandom.current();
return choiceArray[r.nextInt(choiceArray.length)];
}
}
/
* 使用泛型和列表
*
* @author lzlg
* 2021/4/10 18:46
*/
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
this.choiceList = new ArrayList<>(choices);
}
public T choose() {
Random r = ThreadLocalRandom.current();
return choiceList.get(r.nextInt(choiceList.size()));
}
}
编写自己的Stack:
/
* 不使用泛型的栈
*
* @author lzlg
* 2021/4/10 18:54
*/
public class MyStack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
public MyStack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
第一种变更为泛型的方法:
/
* 使用泛型的栈
*
* @author lzlg
* 2021/4/10 18:54
*/
public class MyStack<E> {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private E[] elements;
private int size = 0;
@SuppressWarnings("unchecked")
public MyStack() {
// 不能创建不能具体化类型的数组
// this.elements = new E[DEFAULT_INITIAL_CAPACITY];
// 第一种方法,创建Object数组,并把它转换为泛型数组类型,并添加@SuppressWarnings("unchecked")注解
// 编译器不可能证明你的程序是类型安全的,但你可以,你自己必须确保未受检的转换不会危及程序的类型安全性
// 相关的数组(elements)保存在一个私有的域中, 永远不会被返回到客户端或者其他方法.
this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
第二种变更为泛型的方法:
/
* 使用泛型的栈
*
* @author lzlg
* 2021/4/10 18:54
*/
public class MyStack<E> {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
public MyStack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
E result = (E) elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
这两种方法各有所长, 第一种方法的可读性更强,数组被声明为E[]类型清楚的表明它只包含E实例, 更简洁.第一种方法只需转换一次(创建数组的时候), 而第二种方法则是每次读取一个数组元素都需要转换一次. 第一种方法优先, 在实践中更常用, 但是它会导致堆污染(heap pollution)(数组运行时类型与它的编写时类型不匹配).
有一些泛型限制了可允许的类型参数值:
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
类型参数列表(<.E extends Delayed.>)要求实际的类型参数E必须是java.util.concurrent.Delayed的一个子类型, 类型参数E被称作有限制的类型参数(bounded type parameter).
简单的泛型方法例子:
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
有时可能需要创建一个不可变但又适用于许多不同类型的对象.由于泛型是通过擦除实现的, 可以给所有必要的类型参数使用单个对象, 但是需要编写一个静态工厂方法, 让它重复地给每个必要的类型参数分发对象, 这种模式称作泛型单例工厂(generic singleton factory), 常用于函数对象.
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
通过某个包含该类型参数本身的表达式来限制类型参数是允许的, 这就是递归类型限制(recursive type bound).
有许多方法都带有一个实现Comparable接口的元素列表, 为了对列表进行排序, 并在其中进行搜索, 计算出它的最小值或最大值, 要完成这其中的任何一项操作, 都要求列表中的每个元素能够与列表中的每个其他元素相比较.
public static <E extends Comparable<E>> E max(Collection<E> c);
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty()) {
throw new IllegalArgumentException("Empty Collection");
}
E result = null;
for (E e : c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requiredNonNull(e);
}
}
return result;
}
public class Stack<E> {
public Stack(){...}
public void push(E e) {...}
public E pop() {...}
public boolean isEmpty() {...}
}
增加一个方法, 让它按顺序将一系列元素全部放到栈中, 第一次尝试如下:
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
假如一个Stack<Number.>, 并且调用了push(intVal), 这里intVal是Integer类型, 因为Integer是Number的一个子类型, 因此下面的方法逻辑上是可行的:
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
但会编译错误, 因为参数化类型是不可变的.
使用有限制的通配符类型, pushAll的入参类型不应该为E的Iterable接口, 而应该为E的某个子类型的Iterable接口, 通配符类型Iterable<? extends E>就是这个意思.
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
这样上述的numberStack.pushAll(integers)就可以正确的编译, 并且类型安全.
对应的popAll方法:
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
假设有一个Stack<Number.>和Object类型的集合, 如果从栈中弹出一个元素, 并保存到Object类型的集合中, 因此下面的方法逻辑上是可行的:
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
但会编译错误, 因为参数化类型是不可变的.Collection<Object.>不是Collection<Number.>的子类型.popAll的输入参数类型不应该为E的集合, 而应该为E的某种超类的集合, 应使用Collection<? super E>通配符:
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
这样就可保证numberStack.popAll(objects);编译正确.
为了获得最大限度地灵活性, 要在表示生产者或者消费者地输入参数上使用通配符类型.
PECS: 表示 producter-extends, consumer-super
如果参数化类型表示一个生产者T, 就使用<? extends T>; 如果它表示一个消费者T, 就使用<? super T>.
不要用通配符类型作为返回类型., 除了为用户提供额外的灵活性外, 还会强制用户在客户端代码中使用通配符类型.
// 初始声明
public static <T extends Comparable<T>> T max(List<T> list) {...}
// 修改为以下更灵活的API
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {...}
comparable始终是消费者, 使用时始终应该是Comparable<? super T>优先于Comparable<T.>, 同样的comparator接口也一样.
List<ScheduledFuture<?>> scheduledFutures = ...;
原来的初始声明无法应用到这个列表的原因在于: java.util.concurrent.ScheduledFuture没有实现Comparable<ScheduledFuture.>接口, 相反它是扩展Comparable<Delayed.>接口的Delayed接口的子接口, 所以ScheduledFuture实例并非只能与其他ScheduledFuture实例相比较, 它可以与任何Delayed实例相比较.需要通配符支持那些不直接实现Comparable(或Comparator)而是扩展实现了该接口的类型.
类型参数和通配符之间具有双重性, 许多方法都可以利用其中一个或者另一个进行声明:
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
在公共API中,第二种更好一些, 因为更简单.一般来说, 如果类型参数只在方法声明中出现一次, 就可以用通配符取代它.如果是无限制的类型参数, 就用无限制的通配符取代, 如果是有限制的类型参数, 就用有限制的通配符取代.
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
上述代码编译错误, 因为不能把除null之外的任何值放入到List<?>中, 可编写一个私有的辅助方法来捕获通配符类型, 为了捕捉类型, 辅助方法必须是一个泛型方法:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
swapHelper知道list是一个List<E.>, 因此它知道从这个列表中取出的任何值均为E类型, 并且知道将任何E类型的值放入列表是安全的.这个做法允许我们导出比较好的基于通配符的声明,同时在内部利用更加复杂的泛型方法.
可变参数的作用在于让客户端能够将可变数量的参数传给方法, 但这是个技术露底(leaky abstration), 当调用一个可变参数方法时, 会创建一个数组用来存放可变参数, 这个数组是一个实现细节, 是可见的. 因此当可变参数有泛型或者参数化类型时, 编译警告信息就会产生混乱.
当一个参数化类型的变量指向一个不是该类型的对象时, 会产生堆污染(heap pollution), 导致编译器的自动生成转换失败, 破坏了泛型系统的基本保证.
// 说明代码示范(警告: 不能使用)
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42); // java 9中的写法
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0); // ClassCastException
}
将值保存在泛型可变参数数组参数中是不安全的.
显示创建泛型数组是非法的, 用泛型可变参数声明方法是合法的, 带有泛型可变参数或者参数化类型的方法在实践中用处很大, java的设计者选择容忍这矛盾的存在.比如Arrays.asList(T...a)
Java7中添加了SafeVarargs注解, 让带泛型vararg参数的方法的设计这能够自动禁止客户端的警告, 本质上SafeVarargs注解是通过方法的设计者做出承诺, 声明这是类型安全的.
如何确保SafeVarargs注解方法的安全:
方法没有在可变数组中保存任何值(可能破坏类型安全), 也不允许对可变数组的引用转义(可能导致不被信任的代码访问数组), 那么它就是安全的. 如果可变参数数组只用来将数量可变的参数从调用程序传到方法(这才是可变参数的目的), 那么该方法就是安全的.
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();
}
public static void main(String[] args) {
String[] attributes = pickTwo("Good", "Fast", "Cheap");
}
toArray方法返回的是Object[]类型的数组, 编译器在pickTwo的返回值上产生了一个隐藏的String[]转换, 但转换失败了.
允许另一个方法访问一个泛型可变参数数组是不安全的.
安全使用泛型可变参数的范例:
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
确定何时应用@SafeVarargs注解: 对于每一个带有泛型可变参数或者参数化类型的方法, 都要用@SafeVarargs进行注解.
泛型可变参数方法在下列条件下是安全的:
SafeVarargs注解只能用在无法被覆盖的方法上, 因为它不能确保每个可能覆盖的方法都是安全的, 在java8中, 该注解只在静态方法和final实例方法中才是合法的, Java9中它在私有的实例方法上也合法了.
如果不想使用SafeVarargs注解, 可以用List参数代替可变参数:
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
List<String> result = flatten(List.of(List.of("friends"), List.of("romans")));
这种做法的优势在于编译器可以证明该方法是类型安全的.
static <T> List<T> pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return List.of(a, b);
case 1: return List.of(a, c);
case 2: return List.of(b, c);
}
throw new AssertionError();
}
public static void main(String[] args) {
List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}
数据库的行可以有任意数量的列, 如果能以类型安全的方式访问所有列就好了, 有种方法可以做到这一点: 将键(key)进行参数化, 而不是将容器(container)参数化, 然后将参数化的键提交给容器来插入或者获取值, 用泛型系统来确保值得类型与它的键相符.
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/
* @author lzlg
* 2021/4/11 12:26
*/
public class Favorites {
// Class<?>中的?不属于通配符类型的Map类型, 而是Map的键的类型, 每个键都可以有一个不同的参数化类型, 异构就是从这里来的
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorties(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorites(Class<T> type) {
// 利用Class的cast方法,将对象引用动态地转换成了Class对象所表示的类型.
return type.cast(favorites.get(type));
}
}
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorites(String.class, "Java");
f.putFavorites(Integer.class, 0xcafebabe);
f.putFavorites(Class.class, Favorites.class);
String str = f.getFavorites(String.class);
Integer intNum = f.getFavorites(Integer.class);
Class clz = f.getFavorites(Class.class);
System.out.printf("%s %x %s%n", str, intNum, clz.getName());
}
Favorites实例是类型安全的, 同时它是异构的(heterogeneous), 不像普通的映射, 它的所有键都是不同类型的, 因此Favorites称为类型安全的异构容器(typesafe heterogeneous container).
Class的cast方法是Java的转换操作符的动态模拟, 它只检验它的参数是否为Class对象所表示的类型的实例, 如果是,则返回参数, 否则抛出ClassCastException异常.
Favorites有两种局限性: 首先恶意的客户端可以很轻松地破坏Favorites实例的类型安全, 只要以它的原生态形式(raw form)使用Class对象.比如可以利用原生类型的HashSet将String放进HashSet<Integer.>中.确保Favorites永不违背它的类型约束条件的方式是, 让putFavorites方法检验instance是否真的是type所表示的类型的实例:
public <T> void putFavorties(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
java.util.Collections中的一些集合包装类采用了同样的技巧,称作checkedList, checkedSet, checkedMap.
第二个局限性: 它不能用在不可具体化的类型中.比如不能保存List<String.>, 因为无法为List<String.>获得一个Class对象.List<Integer.>和List<String.>共用一个List.class对象.对于该局限性, 目前没有完全令人满意的解决办法.
Favorites使用的类型令牌是无限制的, 有时可能需要限制那些可以传给方法的类型, 可以通过有限制的类型令牌(bounded type token)来实现.利用有限制类型参数或者有限制通配符来限制可以表示的类型.
注解API广泛利用了有限制的类型令牌:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
假设你有一个类型为Class<?>的对象, 并且想将它传给一个需要有限制的类型令牌的方法, 你可以将对象转换成Class<? extends Annotation>, 但是这种转换是非受检的, 会产生编译时警告.类Class提供一个安全(且动态)地执行这种转换的实例方法asSubclass, 它将调用它的Class对象转换成用其参数表示的类的一个子类, 如果转换成功, 方法返回它的参数, 否则抛出ClassCastException异常.
static Annotation getAnnotation(AnnotationElement element, String annotationTypeName) {
Class<?> annotationType = null;
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
int枚举模式: 用一组int常量来表示枚举类型, 其中每一个int常量表示枚举类型的一个成员:
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
// java没有为int枚举组提供命名空间, 所以使用命名前缀APPLE_
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
该模式有很多不足:
同样的有String枚举模式, 虽然为常量提供了可打印的字符串, 但会导致初级的用户把字符串常量硬编码到客户端代码中,而不是使用常量字段名.而且会导致性能问题, 依赖于字符串的比较操作.
Java枚举类型: 本质上是int值, Java枚举类型的基本想法: 类通过公有的静态final域为每个枚举常量导出一个实例.
枚举类型没有可访问的构造器, 它是真正的final类, 客户端不能创建枚举类型的实例, 也不能对它进行扩展.枚举类型是实例受控的, 是单例的泛型化, 本质上是单元素的枚举.
优点:
枚举类型的例子:
/
* 太阳系的8颗行星
*
* @author lzlg
* 2021/5/1 18:07
*/
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),
;
// 引力常量G
private static final double G = 6.67300E-11;
// 质量
private final double mass;
// 半径
private final double radius;
// 表面重力
private final double surfaceGravity;
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
this.surfaceGravity = G * mass / (radius * radius);
}
public double mass() {
return this.mass;
}
public double radius() {
return this.radius;
}
public double surfaceGravity() {
return this.surfaceGravity;
}
// 计算质量为mass的所受的重力
public double surfaceWeight(double mass) {
return mass * surfaceGravity;
}
}
Planet有一个静态的values方法, 按照声明顺序返回它的值数组, toString方法返回每个枚举值的声明名称.
四大运算:
/
* 四大运算
*
* @author lzlg
* 2021/5/1 18:30
*/
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则不能进行编译
throw new AssertionError("Unknown op: " + this);
}
}
上面代码可用, 但很脆弱, 如果添加了新的常量, 却忘记给switch添加相应的条件, 可以编译, 但使用新的运算时, 会运行失败(抛出异常).
有更好的方法: 在枚举类型中声明一个抽象的apply方法, 并在特定于常量的类主体(constant-specific class body)中, 用具体的方法覆盖每个常量的抽象apply方法:称为特定于常量的方法实现
/
* 四大运算: 特定于常量的方法实现
*
* @author lzlg
* 2021/5/1 18:30
*/
public enum 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;
}
};
public abstract double apply(double x, double y);
}
该版本的Operation增加新的常量, 编译器会提醒你, 实现抽象的apply方法
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/
* 四大运算: 静态常量
*
* @author lzlg
* 2021/5/1 18:30
*/
public enum 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 static final Map<String, Operation> stringToEnum = Stream.of(values())
.collect(Collectors.toMap(Object::toString, e -> e));
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
@Override
public String toString() {
return this.symbol;
}
public abstract double apply(double x, double y);
}
试图使每个常量从自己的构造器将自身放入stringToEnum是不起作用的, 会导致编译时错误, 如果这是合法的, 可能会引发NPE异常.除了编译时常量域之外, 枚举构造器不允许访问枚举的静态域, 因为构造器运行时, 这些静态域还没有被初始化.枚举常量无法通过其构造器访问另一个构造器.
特定于常量的方法实现的缺点: 使得在枚举常量中共享代码变得困难.
/
* 工资计算,根据给定的某工人的基本工资(按分钟)以及当天的工作时间,来计算当天的报酬
*
* @author lzlg
* 2021/5/1 18:53
*/
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
private static final int MIN_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 <= MIN_PER_SHIFT ? 0 :
(minutesWorked - MIN_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
代码简洁, 但从维护的角度看, 非常危险.假设将一个元素添加到该枚举中, 或许是一个表示假期天数的特殊值, 但是忘记给switch语句添加相应的case, 程序可编译, 但pay方法会将节假日的工资计算成正常工作日的工资.
为了利用特定于常量的方法实现安全的执行工资计算, 你可能必须重复计算每个常量的加班工资, 或将计算移到两个辅助方法中(一个用来计算工作日, 一个用来计算节假日), 并从每个常量调用相应的辅助方法. 任何一种方法都会产生相当数量的样板代码, 会降低可读性, 并增加了出错的机率.
真正想要的是每当增加一个枚举常量时, 就强制选择一种加班报酬策略, 做法: 将加班工资计算移到一个私有的嵌套枚举中, 将这个策略枚举(strategy enum)的实例传到PayrollDay枚举的构造器中, 之后将加班工资计算委托给策略枚举, PayrollDay不需要switch语句或者特定于常量的方法实现:
/
* 工资计算,根据给定的某工人的基本工资(按分钟)以及当天的工作时间,来计算当天的报酬
*
* @author lzlg
* 2021/5/1 18:53
*/
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay() {
this(PayType.WEEKDAY);
}
PayrollDay(PayType payType) {
this.payType = payType;
}
int pay(int minutesWorked, int payRate) {
return this.payType.pay(minutesWorked, payRate);
}
private enum PayType {
WEEKDAY {
@Override
int overtimePay(int minutes, int payRate) {
return minutes <= MIN_PER_SHIFT ? 0 : (minutes - MIN_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
@Override
int overtimePay(int minutes, int payRate) {
return minutes * payRate / 2;
}
},
;
private static final int MIN_PER_SHIFT = 8 * 60;
abstract int overtimePay(int minutes, int payRate);
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
return basePay + overtimePay(minutesWorked, payRate);
}
}
}
枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为.
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);
}
}
什么时候使用枚举类型?
每当需要一组固定常量, 并且在编译时就知道其成员的时候, 就应该使用枚举, 枚举类型中的常量集并不一定要始终保持不变.
许多枚举天生和一个单独的int值相关 所有的枚举都有一个ordinal方法, 返回每个枚举常量在类型中的数字位置:
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal() + 1;
}
}
上诉代码, 如果常量重新排序, 则numberOfMusicians方法就会遭到破坏. 如果要再添加一个与已经用过的int值关联的枚举常量, 那是做不到的.要是没有给所有的这些int值添加常量, 也无法给某个int值添加常量.(对于11位演奏家组成的合奏曲没有标准的术语, 因此只好给没用过的int值11添加一个虚拟dummy常量)
有很简单的方法解决: 永远不要根据枚举的序数导出与它相关联的值, 而是要将它保存在一个实例域中:
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), NONET(9), DECTET(10),
DOUBLE_QUARTET(8), TRIPLE_QUARTET(12),
;
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return this.numberOfMusicians;
}
}
如果一个枚举类型的元素主要用在集合中, 一般使用int枚举模式.
public class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
public static final int STYLE_STRIKETHROUGH = 1 << 3;
public void applyStyles(int styles) {
}
}
这种表示法可用OR运算符将几个常量合并到一个集合中, 称作位域(bit field). 位域表示法允许使用位操作, 有效地执行union(联合) 和 intersection(交集) 这样的集合操作.
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
}
位域的缺点:
可使用EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合.优点:
public class Text {
public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}
public void applyStyles(Set<Style> styles) {}
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
有时会利用ordinal方法来索引数组或列表的代码.
public class Plant {
enum LifeCycle {
// 每年
ANNUAL,
// 多年的
PERENNIAL,
// 两年一次的
BIENNIAL
}
private final String name;
private final LifeCycle lifeCycle;
public Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
现在假设有一个香草的数组, 表示一座花园中的植物, 你想按照类型(LifeCycle)进行组织之后将这些植物列出来, 需要构建三个集合, 每种类型各一个, 并遍历整座花园, 将每种香草放到相应的集合中, 有些会将这些集合放到一个按照类型的序数进行索引的数组中来实现:
public static void main(String[] args) {
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
Plant[] garden = new Plant[3];
garden[0] = new Plant("玫瑰", LifeCycle.ANNUAL);
garden[1] = new Plant("康乃馨", LifeCycle.PERENNIAL);
garden[2] = new Plant("菊花", LifeCycle.BIENNIAL);
for (Plant p : garden) {
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
}
上诉代码的问题:
使用EnumMap改写后的程序:
public static void main(String[] args) {
Plant[] garden = new Plant[3];
garden[0] = new Plant("玫瑰", LifeCycle.ANNUAL);
garden[1] = new Plant("康乃馨", LifeCycle.PERENNIAL);
garden[2] = new Plant("菊花", LifeCycle.BIENNIAL);
// 这里EnumMap的构造器是采用键类型的Class对象, 提供了运行时的泛型信息.
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (LifeCycle lc : LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant plant : garden) {
plantsByLifeCycle.get(plant.lifeCycle).add(plant);
}
System.out.println(plantsByLifeCycle);
}
使用EnumMap的优点:
基于Stream的代码:
System.out.println(Arrays.stream(garden).collect(Collectors.groupingBy(p -> p.lifeCycle)));
上诉代码的问题在于映射的实现不是一个EnumMap, 需使用有三种参数形式的Collectors.groupingBy方法, 它允许利用mapFactory参数定义映射实现:
System.out.println(Arrays.stream(garden).collect(Collectors.groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
基于Stream版本的行为和EnumMap的版本稍有不同, EnumMap的版本总是给每一个植物生命周期都设计一个嵌套映射, 基于Stream版本则仅当花园中包含了一种或多种植物带有该生命周期时才会设计一个嵌套映射.比如花园中没有两年生植物, plantsByLifeCycle在EnumMap的版本中的数量是3中, 而在基于Stream版本中都是两种.
Plant[] garden = new Plant[2];
garden[0] = new Plant("玫瑰", LifeCycle.ANNUAL);
garden[1] = new Plant("康乃馨", LifeCycle.PERENNIAL);
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (LifeCycle lc : LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant plant : garden) {
plantsByLifeCycle.get(plant.lifeCycle).add(plant);
}
System.out.println(plantsByLifeCycle);
System.out.println(Arrays.stream(garden).collect(Collectors.groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
// 打印结果
{ANNUAL=[玫瑰], PERENNIAL=[康乃馨], BIENNIAL=[]}
{ANNUAL=[玫瑰], PERENNIAL=[康乃馨]}
按照序数进行索引两次的数组的数组例子:
public enum Phase {
// 固体 液体 气体
SOLID, LIQUID, GAS;
// 过渡
public enum Transition {
// 熔化 冷冻 沸腾 浓缩 升华 沉淀
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
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()];
}
}
}
上诉代码的问题:
使用EnumMap改造, 因为每个过渡阶段都是通过一对阶段枚举进行索引的, 最好将这种关系表示为一个映射, 这个映射的键是一个枚举(起始阶段), 值为另一个映射, 第二个映射的键为第二个枚举(目标阶段), 它的值为结果(阶段过渡), 即Map(起始阶段, Map(目标阶段, 阶段过渡))这种形式, 一个阶段过渡所关联的两个阶段, 最好通过数据与阶段过渡枚举之间的关系来获取, 之后用该阶段过渡枚举初始化EnumMap:
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;
}
private static final Map<Phase, Map<Phase, Transition>> M = Stream.of(values())
.collect(Collectors.groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
Collectors.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);
}
}
}
如果要添加一个新阶段: plasma(离子), 只有两个过渡和这个阶段相关,: 电离化(ionization)将气体变成离子, 消电离化(deionization)将离子变成气体. 必须给Phase添加一种新常量, 给Phase.Transition添加两种新常量. 如果是基于数组的版本, 给数组添加的元素过多或过少, 或者元素位置放置不妥当, 程序会运行失败; 如果是基于EnumMap的版本, 只需将PLASMA添加到Phase列表, 并将IONIZE(GAS, PLASMA)和DEIONIZE(PLASMA, GAS)添加到Phase.Transition中, 程序会自行处理其他的事情, 几乎没有出错的可能:
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),
IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
private static final Map<Phase, Map<Phase, Transition>> M = Stream.of(values())
.collect(Collectors.groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
Collectors.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);
}
}
}
可伸缩的枚举类型用例: 操作码(operation code), 也作opcode, 它的元素表示在某种机器上的那些操作.有时要尽可能地让API的用户提供它们自己的操作, 这样可以有效地扩展API所提供的操作集.
枚举类型可以通过给操作码类型和枚举定义接口来实现上诉效果:
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;
}
}
虽然枚举类型BasicOperation是不可扩展的, 但接口类型Operation却是可扩展的, 你可以定义另一个枚举类型, 实现这个接口, 并利用这个新类型的实例代替基本类型. 比如, 你要定义上述操作类型的扩展, 由求幂(exponentiation)和求余(remainder)操作组成:
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);
}
// T extends Enum<T> & Operation 确保了Class对象既表示枚举又表示Operation的子类型
private static <T extends Enum<T> & Operation> void test(Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
第二种方法:
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);
}
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, 使用方法或类的名称来进行某些特殊处理)的缺点:
注解很好地解决了这些问题. 定义一个注解类型来指定简单的测试, 它们自动运行, 并在抛出异常时失败:
// 只用于无参的静态方法
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
Test注解声明通过Retention和Target注解进行了注解. 注解类型声明中的这种注解成为元注解(meta-annotation).@Retention(RetentionPolicy.RUNTIME)
表明Test注解在运行时也应该存在, @Target(ElementType.METHOD)
表明Test注解只在方法声明中才是合法的, 不能应用到类声明, 域声明或其他程序元素上.
Test注解上的注释, 如果编译器能强制这一限制最好, 如果做不到, 除非编写一个注解处理器(annotation processor), 让它来完成. 如果没有注解处理器, 如果将Test注解放在实例方法的声明中, 或者放在带有一个或多个参数的方法中, 测试程序还是可以编译, 让测试工具在运行时来处理这个问题.
现实应用中的Test注解, 称作标记注解(marker annotation), 因为它没有参数, 只是标注被注解的元素.如果程序拼错了Test或将Test注解应用到其他程序元素而非方法声明, 程序就无法编译.
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() {
}
@Test
public void m5() {
}
public static void m6() {
}
@Test
public static void m7() {
throw new RuntimeException("Crash");
}
public static void m8() {
}
}
Test注解对Sample类的语义没有直接的影响, 注解只负责提供信息供相关程序使用, 注解永远不会改变被注解代码的语义, 但是使它可以通过工具提供进行特殊的处理:
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("io.ms.scloud.tools.study.Sample");
for (Method m : testClass.getDeclaredMethods()) {
// isAnnotationPresent告知该工具类运行那些方法
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
// 测试方法抛出异常, 会封装在InvocationTargetException中
} catch (InvocationTargetException e) {
Throwable ex = e.getCause();
System.out.println(m + " failed: " + ex);
// 第二个catch块用于捕捉Test注解用法错误
} catch (Exception e) {
e.printStackTrace();
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, (tests - passed));
}
}
现在要针对只在抛出特殊异常时才成功的测试添加支持, 添加新注解类型:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
实际应用中注解:
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("io.ms.scloud.tools.study.Sample2");
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 e) {
Throwable ex = e.getCause();
Class<? extends Throwable> exType = m.getAnnotation(ExceptionTest.class).value();
// 如果是注解中value指定的抛出的异常的实例,则表明测试通过
if (exType.isInstance(ex)) {
passed++;
} else {
System.out.printf("Test %s failed: excepted %s, got %s%n", m, exType.getName(), ex);
}
} catch (Exception e) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, (tests - passed));
}
}
上述代码和处理Test注解的代码不同: 提取了注解参数的值, 并用它检验该测试抛出的异常是否为正确的类型.
再深入一点, 测试可以在抛出任何一种指定异常时都能够通过, 将ExceptionTest注解的参数类型改为Class对象的一个数组:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
注解中数组参数的语法十分灵活, 是进行过优化的单元素数组, 使用了ExceptionTest新版的数组参数之后, 之前的所有ExceptionTest注解仍然有效, 并产生单元素数组. 为了指定多元素数组, 要用花括号将元素包围起来, 并用逗号隔开:
@ExceptionTest({IndexOutOfBoundsException.class,
NullPointerException.class})
public static void doublyBad() {
List<String> list = new ArrayList<>();
list.addAll(5, null);
}
修改测试运行工具类处理新的ExceptionTest:
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("io.ms.scloud.tools.study.Sample2");
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 e) {
Throwable ex = e.getCause();
Class<? extends Exception>[] exTypes = m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Exception> exType : exTypes) {
// 如果是注解中value指定的抛出的异常的实例,则表明测试通过
if (exType.isInstance(ex)) {
passed++;
break;
} else {
System.out.printf("Test %s failed: excepted %s, got %s%n", m, exType.getName(), ex);
}
}
} catch (Exception e) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, (tests - passed));
}
}
Java 8 另一种多值注解方式, 用@Repeatable元注解对注解的声明进行注解, 表示该注解可以被重复地应用给单个元素, 它唯一的参数是注解类型数组.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
// 使用新的多值注解方式后
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
List<String> list = new ArrayList<>();
list.addAll(5, null);
}
处理可重复的注解要非常小心, 重复的注解ExceptionTestContainer会产生一个包含注解类型ExceptionTest的合成注解.getAnnotationsByType方法覆盖了这个事实, 可以用于访问可重复注解类型ExceptionTest的重复和非重复的注解.但isAnnotationPresent使它变成了显示的, 重复的注解ExceptionTest不是注解类型(而是所包含的注解类型ExceptionTestContainer)的一部分.如果一个元素具有某种类型的重复注解, 并且用isAnnotationPresent方法检验该元素是否具有该类型的注解, 会发现它没有.用isAnnotationPresent方法检验是否存在包含的注解类型, 会导致程序默默地忽略掉非重复的注解.
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() {
}
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
List<String> list = new ArrayList<>();
list.addAll(5, null);
}
}
// 将ExceptionTest注解改为可重复注解时, 进行测试
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("io.ms.scloud.tools.study.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
// 这里是ExceptionTest注解
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
}
}
// 这时tests的值为3, 而不是4或5
System.out.printf("Passed: %d, Failed: %d%n", passed, (tests - passed));
}
}
// 将ExceptionTest注解改为可重复注解时, 进行测试
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("io.ms.scloud.tools.study.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
// 这里是ExceptionTestContainer注解
if (m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
}
}
// 这时tests的值为1, 而不是4或5
System.out.printf("Passed: %d, Failed: %d%n", passed, (tests - passed));
}
}
为了利用isAnnotationPresent检测重复和非重复的注解, 必须检测注解类型ExceptionTest及其包含的注解类型ExceptionTestContainer.
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("io.ms.scloud.tools.study.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
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 (InvocationTargetException e) {
Throwable ex = e.getCause();
ExceptionTest[] exTests = m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest exTest : exTests) {
// 如果是注解中value指定的抛出的异常的实例,则表明测试通过
if (exTest.value().isInstance(ex)) {
passed++;
break;
} else {
System.out.printf("Test %s failed: excepted %s, got %s%n", m, exTest.value().getName(), ex);
}
}
} catch (Exception e) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, (tests - passed));
}
}
加入可重复的注解, 提升了源代码的可读性, 逻辑上是将同一个注解类型的多个实例应用到了一个指定的程序元素.
如果是在编写一个需要程序员给源文件添加信息的工具, 就要定义一组适当的注解类型, 没有理由使用命名模式.
除了工具铁匠(toolsmiths, 即平台框架程序员)之外, 大多数程序员都不必定义注解类型, 但是所有的程序员都应该使用Java平台提供的预定义的注解类型.
@Override注解只能用在方法声明中, 表示注解的方法声明覆盖了超类型中的一个方法声明.坚持使用该注解, 可以防止一大类的非法错误.
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;
}
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));
}
}
// 预期打印出的结果是26, 但运行后打印出的是260
System.out.println(s.size());
}
}
出现和预期结果不一致的原因:
Bigram类的创建者原本想覆盖equals方法, 同时还记得覆盖了hashCode方法, 但是没能覆盖equals方法, 而是将它重载了.为了覆盖equals方法, 必须定义一个参数类型是Object的equals方法, 但Bigram类的equals方法的参数并不是Object类型, 因此Bigram继承了Object的equals方法.
使用@Override注解, 编译器(或IDE)会告知你覆盖的equals方法不对:
@Override
~~~~~~~~ // Method does not override method from its superclass
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
// 然后将equals方法修改为:
@Override
public boolean equals(Object o) {
if (!(o instanceof Bigram)) {
return false;
}
Bigram b = (Bigram) o;
return b.first == first && b.second == second;
}
// 这时打印出的结果是26
应该在你想要覆盖超类声明的每个方法声明中使用Override注解.
标记接口(marker interface)是不包含方法声明的接口, 它只是指明(或标明)一个类实现了具有某种属性的接口, 比如Serializable接口, 实现接口表明类的实例可以被序列化.
标记接口有两点胜过标记注解:
Set接口可以说就是这种有限制的标记接口(restricted marker interface), 它只适用于Collection子类型, 但是它不会添加除了Collection之外的方法.这种标记接口可以描述整个对象的某个约束条件, 或者表明实例能够利用其他某个类的方法进行处理.
标记注解胜过标记接口的最大优点, 标记注解是更大的注解机制的一部分.标记注解在那些支持注解作为编程元素之一的框架中同样具有一致性.
如果标记是应用于任何程序元素而不是类或接口, 就必须使用标记注解, 因为只有类和接口可以用来实现或者扩展接口.如果标记只应用于类和接口, 要编写一个或多个只接受这种标记的方法, 就应该优先使用标记接口, 这样你就可以用接口作为相关方法的参数类型, 可以真正为你提供编译时进行类型检查的好处.如果你确信自己永远不需要编写一个只接受带有标记的对象, 那么或许最好使用标记注解.此外, 如果标记是广泛使用注解的框架的一个组成部分, 则显然应该选择标记注解.