第2章 创建和销毁对象
第2条 遇到多个构造器参数时要考虑使用构建器
-
概念:
基于创建型模式的建造者设计模式,来创建复杂类对象的方法。 -
适用场景:
目标类内含大量属性(即字段)且采用不断重载构造方法的方式来创建对象。 -
适用范围:
- 具有平行层次结构的普通单类。
- 具有继承层次结构的子父类。
-
构造方法 VS 构建器:
构造方法 构建器 代码量 精简 冗长 可维护性 低 高 可阅读性 低 高 性能 高 一般 使用场景 类属性较少的场合 类属性较多的场合 -
注意事项:
为避免业务增长增加代码重构的成本,建议开发初期就立即考虑使用构建器。 -
实现步骤:
-
在目标外部类中,定义业务所需的
若干
无初值的私有终态属性
:/* * 外部业务类 */ public class A { private final int necessaryProperty1; // 必要的私有终态属性 private final int necessaryProperty2; // 必要的私有终态属性 private final int availableProperty1; // 非必要的私有终态属性 private final int availableProperty2; // 非必要的私有终态属性 }
-
在目标外部类内,定义
一个公共静态
的构建器成员类
:public class A { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; /* * 公共静态的构建器成员类 */ private static class B { } }
-
在其中继续定义
若干
与目标外部类的属性类型和数量一致的私有终态属性
:public class A { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; private static class B { private final int necessaryProperty1; // 与目标外部类一致的、必要的私有终态属性 private final int necessaryProperty2; // 与目标外部类一致的、必要的私有终态属性 private final int availableProperty1; // 与目标外部类一致的、非必要的私有终态属性 private final int availableProperty2; // 与目标外部类一致的、非必要的私有终态属性 } }
-
在其中继续定义
一个
以所有必要私有终态属性作为入参的公共构造方法
,以创建公共静态成员类的对象:public class A { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; private static class B { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; /* * 以所有必要私有终态属性作为入参的公共构造方法 * 无论是创建目标外部类A的对象,还是创建公共静态建造者成员类的对象,这里的入参都必传 * * @param necessaryProperty1 必要的私有终态属性值 * @param necessaryProperty2 必要的私有终态属性值 */ public B(int necessaryProperty1, int necessaryProperty2) { this.necessaryProperty1 = necessaryProperty1; this.necessaryProperty2 = necessaryProperty2; } } }
-
在其中继续为每个非必要属性分别定义
若干
返回其本身的公共构造方法
,以使用链式函数调用法,不断地在一个建造者对象上填充、叠加不同的可选值:public class A { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; private static class B { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; public B(int necessaryProperty1, int necessaryProperty2) { this.necessaryProperty1 = necessaryProperty1; this.necessaryProperty2 = necessaryProperty2; } /* * 以非必要私有终态属性作为入参的公共构造方法 * 无论是创建目标外部类A的对象,还是创建公共静态构建器成员类的对象,这里的入参都非必传,这便可利用建造者模式的链式函数调用方式,实现了有选择地设值构建 * * @param availableProperty2 非必要的私有终态属性值 * @return 公共静态构建器成员类对象 */ public availableProperty1(int availableProperty1) { this.availableProperty1 = availableProperty1; // 返回公共静态构建器成员类本身,以使用链式函数调用法,不断地在一个构建器类对象上,叠加不同属性的值 return this; } /* * 以非必要私有终态属性作为入参的公共构造方法 * 无论是创建目标外部类A的对象,还是创建公共静态构建器成员类的对象,这里的入参都非必传,这便可利用建造者模式的链式函数调用方式,实现了有选择地设值构建 * * @param availableProperty2 非必要的私有终态属性值 * @return 公共静态构建器成员类对象 */ public availableProperty1(int availableProperty2) { this.availableProperty2 = availableProperty2; // 返回公共静态构建器成员类本身,以使用链式函数调用法,不断地在一个构建器类对象上,叠加不同属性的值 return this; } } }
-
在其中继续定义
一个
名为 build 的、无参的、返回目标外部类对象的普通公共方法
,以实现用静态成员类创建外部类的对象:public class A { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; private static class B { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; public B(int necessaryProperty1, int necessaryProperty2) { this.necessaryProperty1 = necessaryProperty1; this.necessaryProperty2 = necessaryProperty2; } public availableProperty1(int availableProperty1) { this.availableProperty1 = availableProperty1; return this; } public availableProperty1(int availableProperty2) { this.availableProperty2 = availableProperty2; return this; } /* * 名为 build 的、无参的、返回目标外部类对象的普通公共方法 * 外部调用者若要创建目标外部类A的对象,不再直接实例化它,而通过调用此 build() 方法,委托建造者创建它的对象 * * @return 目标外部类对象 */ public A build() { return new A(this); } } }
此时需要想想,语句 return new A(this) 中,外部类A的构造方法,是共有的还是私有的?
当然是私有的!如果是共有的,外部调用者就又能直接通过实例化目标外部类对象的方式创建对象了,建造者模式不就失效了吗!!!
所以,接下来,还需要在外部类中定义一个入参为公共静态建造者类成员类型的私有构造方法。 -
在目标类中,定义
一个
入参为公共静态的建造者成员类类型的私有构造方法
:public class A { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; private static class B { private final int necessaryProperty1; private final int necessaryProperty2; private final int availableProperty1; private final int availableProperty2; public B(int necessaryProperty1, int necessaryProperty2) { this.necessaryProperty1 = necessaryProperty1; this.necessaryProperty2 = necessaryProperty2; } public availableProperty1(int availableProperty1) { this.availableProperty1 = availableProperty1; return this; } public availableProperty1(int availableProperty2) { this.availableProperty2 = availableProperty2; return this; } public A build() { return new A(this); } } /* * 目标外部类的、入参为公共静态构建器类成员类型的私有构造方法 * 此方法可使公共静态构建器成员类通过其 build() 将它内部的所有属性值映射到目标外部类的所有属性上 * * @param b 公共静态的构建器成员类类型的构建器对象 */ public A(B b) { this.necessaryProperty1 = b.necessaryProperty1; this.necessaryProperty2 = b.necessaryProperty2; this.availableProperty1 = b.availableProperty1; this.availableProperty2 = availableProperty2; } }
-
-
示例:
-
平行层次结构的普通单类中应用构建器的示例:
/** * 目标外部类 NutritionFacts */ 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; /** * 公共静态的构建器成员类 Builder */ 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; /** * 以所有必要私有终态属性 servingSize、servings 作为入参的公共构造方法 * * @param servingSize * @param servings */ public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } /** * 以非必要私有终态属性 calories 作为入参的公共构造方法 * * @param val * @return */ public Builder calories(int val) { this.calories = val; return this; // 返回公共静态构建器成员类本身,以实现在构建器对象上的链式函数调用 } /** * 以非必要私有终态属性 fat 作为入参的公共构造方法 * * @param val * @return */ public Builder fat(int val) { this.fat = val; return this; // 返回公共静态构建器成员类本身,以实现在构建器对象上的链式函数调用 } /** * 以非必要私有终态属性 sodium 作为入参的公共构造方法 * * @param val * @return */ public Builder sodium(int val) { this.sodium = val; return this; // 返回公共静态构建器成员类本身,以实现在构建器对象上的链式函数调用 } /** * 以非必要私有终态属性 carbohydrate 作为入参的公共构造方法 * * @param val * @return */ public Builder carbohydrate(int val) { this.carbohydrate = val; return this; // 返回公共静态构建器成员类本身,以实现在构建器对象上的链式函数调用 } /** * 名为 build 的、无参的、返回目标外部类对象的普通公共方法 * * @return 目标外部类 NutritionFacts 的对象 */ public NutritionFacts build() { return new NutritionFacts(this); } } /** * 目标外部类的、入参为公共静态构建器类成员类型的私有构造方法 * * @param builder */ private NutritionFacts(Builder builder) { this.servingSize = builder.servingSize; this.servings = builder.servings; this.calories = builder.calories; this.fat = builder.fat; this.sodium = builder.sodium; this.carbohydrate = builder.carbohydrate; } }
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100) .sodium(35) .carbohydrate(27) .build();
-
具有继承层次结构的子父类类中应用构建器的示例:
/** * 抽象类 Pizza * 此类是所有披萨的父类 */ public abstract class Pizza { // 披萨中可加的配料 public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } // 披萨配料的集合 final Set<Topping> toppings; /** * 构建某种具体的披萨的静态构建器成员类 * * @param <T> */ abstract static class Builder<T extends Builder<T>> { // 为了保存每个创建的具体披萨对象中能保存它的配料,那么需要在实例化前就创建一个临时存放它们的集合。又由于配料是枚举类型,所以使用EnumSet集合来存放枚举 EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); /** * 为某一具体的披萨添加配料 * 某个具体披萨在添加配料时,会直接调用此方法来添加,然后返回在子类中各自实现的构建器 * * @param topping * @return */ public T addTopping(Topping topping) { toppings.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); /** * 返回子类自己的构建器对象 * * 此方法必须让子类重写,以返回子类自己的构建器对象。 * 这样做的原因是因为披萨抽象类是抽象的东西,肯定不能被实例化的,所以要让它的子类重写此方法来返回子类的构建器,用于创建子类的对象。 * * @return */ protected abstract T self(); } /** * 披萨抽象类的有参构造方法 * * 在披萨抽象类中定义一个以构建器为入参的构造方法,当调用子类的build()方法时,将会调用子类中形如此方法一样的构造方法, * 在第一条语句中使用super关键字调用此父类的构造方法,来为披萨抽象类中的配料集合赋值。 * 因为子类是继承于父类的,那么披萨抽象类中的配料集合属性,子类也同样具有,就可以在子类中获取并操作配料集合了。 * * @param builder */ Pizza(Builder<?> builder) { toppings = builder.toppings.clone(); } }
/** * 纽约风味的披萨 * * 是披萨抽象类的子类 * 此披萨强制要求的参数是披萨的尺寸 */ public class NyPizza extends Pizza { // 纽约风味披萨的尺寸大小枚举 public enum Size { SMALL, MEDIUM, LARGE } private final Size size; public static class Builder extends Pizza.Builder<Builder> { private final Size size; /** * 纽约风味披萨中,有参构建器的构造方法 * * 和平行层次结构中一样,必填参数要作为有参构造方法的入参,传入它们进行构建器的初始化 * * @param size 披萨尺寸。此参数是必须的 */ public Builder(Size size) { this.size = Objects.requireNonNull(size); } /** * 创建纽约风味披萨的方法 * * 此方法对父类抽象类披萨的build()方法进行了重写。 * 要注意此方法返回的是子类类型,所以调用build()后创建的就是子类对象了。 * * @return */ @Override public NyPizza build() { return new NyPizza(this); } /** * 返回纽约风味披萨类自己的构建器对象 * * @return */ @Override protected Builder self() { return this; } } /** * 纽约风味披萨类中私有的以构建器为入参的构造方法 * <p> * 此方法是为当前类的构建器类中,build()方法服务的。 * 第一句使用super关键字并传入了构建器对象,来为父类抽象类披萨中的配料集合赋值,这样纽约风味披萨类中就有了它对应的配料集合了 * * @param builder */ private NyPizza(Builder builder) { super(builder); this.size = builder.size; } @Override public String toString() { return String.format("size:%s\n", this.size); } }
NyPizza nyPizza = new NyPizza.Builder(NyPizza.Size.SMALL) .addTopping(Pizza.Topping.SAUSAGE) .addTopping(Pizza.Topping.ONION) .build();
代码执行顺序:
- 当执行 new NyPizza.Builder(NyPizza.Size.SMALL) 时,根据子父类的执行机制,会先执行到父类抽象类 Pizza 的静态成员类 Builder 中。
根据子父类中元素类型的初始化机制,会先初始化 Pizza 类中成员变量 toppings 为一个空集合,再执行到子类 NyPizza 中的有参构造方法 public Builder(Size size) 创建构建器对象。 - 继续执行 .addTopping(Pizza.Topping.SAUSAGE) 逻辑代码。因为子类中没有此方法,所以会执行父类中的此方法。
最后调用的 self() 在子类实现中返回了 this 是为了链式调用后每次都返回同一个构建器对象。这样一来,设值(即调用 addTopping() )就是在操纵同一对象,和平行模式中链式调用的用途是一致的。 - 最后调用 build() 创建子类对象。和平行模式一样地,此方法调用当前类的构造方法创建当前类的对象并返回。在子类需要有一个以构建器对象为入参的私有构造方法,来为 build() 服务。
- 当执行 new NyPizza.Builder(NyPizza.Size.SMALL) 时,根据子父类的执行机制,会先执行到父类抽象类 Pizza 的静态成员类 Builder 中。
-
第4条 通过私有构造器来强化不可实例化的能力
-
概述:
对于像工具类这样无需也没必要实例化的类,应显式创建一个无参的私有构造方法,以覆盖编译器自动生成的无参公共构造方法,强制外部调用者无法通过 new 关键字实例化它。 -
误区:
抽象类也不能实例化,里面又可以存在普通变量和非抽象方法,那我用抽象类阻止它实例化也行?
解答:
抽象类本身不可以实例化是正确的,但别忘了抽象类可以被继承,里面的方法也可以被实现,那么它的子类就可以被实例化。
另外不能用这种投机取巧的方式解决这种问题。抽象类产生的本意也不会允许你这样去使用它。
第5条 优先考虑依赖注入来引用资源
-
概述:
当一个方法需要依赖其它资源才能正常工作时,可采用类似Spring框架的依赖注入方式,将依赖的资源使用传参的方式(构造器传参或方法传参)传入到需要正常工作的类或方法中来。 -
优点:代码复用。
第6条 避免创建不必要的对象
-
概述:
能不创建额外对象实现功能的,就不要创建额外对象,以提高性能。 -
示例1:
Java正则匹配时用到的 Pattern 类,若处理不当,将会创建不必要的、额外的对象。
解析:/* * 正则匹配 * * @param regex 正则表达式字符串 * @param input 待匹配字符串 * @return true - 匹配成功;false - 匹配失败 */ public static boolean matches(String regex, CharSequence input) { Pattern p = Pattern.compile(regex); // 生成正则表达式对象时,每次都会实例化一个正则表达式对象。而对于表达式字符串无变化的情况,根本无需多次创建,只创建1次足矣 Matcher m = p.matcher(input); return m.matches(); }
/* * 生成表达式对象 * * @param regex 正则表达式字符串 * @return 表达式对象 */ public static Pattern compile(String regex) { return new Pattern(regex, 0); }
据上述源码可知:
语句 Pattern p = Pattern.compile(regex) 在生成正则表达式对象时,每次都会实例化一个正则表达式对象。而对于表达式字符串无变化的情况,根本无需多次创建,只创建1次足矣。
对于这种对象的创建,不仅没必要,而且创建成本也很高。建议:
应在类加载时就把 Pattern 对象缓存起来。如定义成静态的,就会在堆内存中创建 Pattern 类的唯一实例。
-
示例2:
数值计算时同时使用基本数据类型和包装类型,稍有不慎,就会产生无必要的拆、装箱操作。Integer a = null; /* 在执行if条件判断的代码时,将会抛出NPE异常,并有以下提示: Cannot invoke "java.lang.Integer.intValue()" because "a" is null 这足以说明,在执行 a == 42 比较计算时,JVM 将 Integer 类型的局部变量 a,进行了自动拆箱。 调用了Integer类的intValue()方法来取得它的基本数据类型的值,并与基本数据类型的值 42 进行比较的。 所以,当基本数据类型与包装基本类型混用时,将会导致包装基本类型侧的自动拆箱。 */ if (a == 42) { LOGGER.info("A value is 42."); } else { LOGGER.info("A value doesn't 42."); }
解析:
运算中的操作数若既有基本数据类型,又有包装数据类型,则包装数据类型将会被自动的、隐式的拆箱,再与基本数据类型运算。若结果接收变量又为包装数据类型,则基本数据类型的计算结果又要被自动的、隐式的装箱。
这种非必要的拆、装箱操作,成本很高。
建议:除非要特别使用包装类型运算,一般情况要使用基本数据类型。
第7条 消除过期的对象引用
-
概述:
当开发者如以自行维护队列等的方式,使Java自动对象回收机制失效时,就需主动关注对象回收问题。 -
示例:
对于开发者自行维护了的队列,若其内的元素处理不当,很容易因为在它们用毕后未置为 null 而导致永久不会被JVM自动回收,造成内存泄漏。
解析:
对于开发者自行维护了的队列,其内的对象若未被任何外部对象所引用,那么垃圾回收器就不会感知到其的存在,就一直不会一直不会回收它们),造成内存泄漏。
建议:当自行维护队列中的某个元素,在业务上不再使用时,要立即置为 null 以手动释放内存资源。
第8条 避免使用终结方法和清除方法
- 概述:
使用终结和清除方法,可实现提醒、敦促JVM进行垃圾回收。但往往这样做以后反而会适得其反。 - 为什么不要使用?
- 即便调用后,JVM也不会立即执行,甚至不会执行。
- 调用后会阻止垃圾回收,使得垃圾回收更慢。
- 执行终结方法的过程中若发生异常但未捕获,则此过程也会跟着结束。
- 终结方法有终结方法攻击问题。
- 若自定义了某些资源,但确实想在用毕后自动释放资源,以实现自动垃圾回收,该怎么办?
让需自动释放资源的类,实现 Autocloseable 接口。
第9条 try-with-resources 优先于 try-finally
- 概述:
当某些资源需在用毕后自动释放,使用 try-with-resources 结构来包裹使用资源的代码。当业务逻辑执行完毕后,会自动调用目标资源类的 close() 释放资源。
前提是自动释放资源的类必须实现 AutoCloseable 接口。
第3章 对于所有对象都通用的方法
第10条 覆盖 equals() 时请遵守通用约定
-
概述:
当覆盖equals() 实现自定义判断对象是否相等时,要满足一些约定,以确保不会在程序运行中发生错误或Bug。 -
为什么要覆盖equals()?
因为此方法默认比较的是两对象的内存地址,而这种比较,往往不适合实际业务场景。
实际业务场景中的“相等”,往往指的是通过判断两对象间的一个或多个属性的相等性,来整体判别两对象最终是否相等的。
故一般而言,若要判定自定义对象的相等性,就要重写此方法,以覆盖默认逻辑。 -
覆盖 equals() 需要遵守的约定:
-
自反性
equals() 对自身比较,必须为 true。x.equals(x) 必须为 true。
-
对称性
equals() 对2对象比较,2对象位置无论如何颠倒,结果均一致。若 x.equals(y) 为 true,则 y.equals(x) 也为 true,不能再有其他任何结果出现。
-
传递性
equals() 对3对象两两比较,若对象1与对象2、对象2与对象3均为 true,则对象1与对象3也必须为 true。若 x.equals(y) 为 true 且 y.equals(z) 为 true,则 x.equals(z) 也为 true,不能再有其他任何结果出现。
-
一致性
equals() 对2对象比较,除非有一方被再次修改,否则相等或不等是永久成立的。x.equals(y) 为 true,除非 x 或 y 被再次修改了,否则此表达式永久成立。
-
非空性
任意对象与 null 比较,必须为 false。x.equals(null)、y.equals(null) 均为 false。
-
-
编写高质量 equals() 的步骤:
- 使用 == 操作符检查 equals() 中传递的入参,是否为当前对象的引用。
- 使用 instanceof 关键字检查 equals() 中传递的入参,类型是否正确。
若不正确,可用显式类型转换(向下转型),将其转为正确类型。 - 在 equals() 中视业务需要,有选择地检查属性值的相等性。
-
注意事项:
- equals() 能不覆盖就别覆盖。非要覆盖,请遵守约定。
- 优先比较业务权重重的、变化几率大的、比较开销小的的属性,以降低判断逻辑对性能的影响。
- equals() 必须标注 @Override 注解,以确保不是重载了 equals()。否则将导致判断结果出错。
- 比较某些数据类型或对象的值时,要注意:
-
若属性为整型基本数据类型,用 == 比较。
-
若属性为单精度浮点型基本数据类型,用 Float 类的静态方法 compare(float1, float2) 比较。
不要使用会导致拆、装箱的 Float.equals(f) 。
-
若属性为双精度浮点型基本数据类型,用 Double 类的静态方法 compare(float1, float2) 比较。
不要使用会导致拆、装箱的 Double.equals(d) 。
-
若属性为对象,覆盖 equals() 并在其内比较关键属性。
-
-
示例:
/** * 学生类 */ public class Student { // 初始化学生时,必须的属性 private final String name; private final double sitNum; // 初始化学生时,可选的属性 private final int age; private final float height; private final float weight; /** * 构建器类 */ static class