目录
[3.3、生成器模式 - Builder](#3.3、生成器模式 - Builder)
10、与tey-finally相比,首选try-with-resources
1、前言
《Effective Java》这本书可以说是程序员必读的书籍之一。这本书讲述了一些优雅的,高效的编程技巧。对一些方法或API的调用有独到的见解,还是值得一看的。刚好最近重拾这本书,看的是第三版,顺手整理了一下笔记,用于自己归纳总结使用。建议多读一下原文。今天整理第一章节:创建和销毁对象。
2、用静态工厂方法替代构造函数
构造函数是提供一个公有的,用于创建实例对象的一个传统方法。除此以外,创建实例对象还有另一种方式:让类提供一个公有的静态工厂方法,就是一个用来返回这个类的实例的静态方法。如Boolean.valueOf("true")等。
java
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
2.1、优点
- **静态工厂方法与构造函数不同,他们有名称。**如果构造器参数本身不能清晰地描述所返回对象地信息,那么可以使用静态工厂方法,给他精心挑选一个名称,使用起来更容易,可读性也更好。
java
package org.example.chapter_01;
public class StaticFactoryMethodClassDemo {
public static void main(String[] args) {
// 传统构造函数
new Person("zhangsan", 18).sayHello();
// 静态工厂方法
Person.createInstance("lisi", 20).sayHello();
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public static Person createInstance(String name, int age) {
return new Person(name, age);
}
public void sayHello(){
System.out.printf("hello, my name is %s, my age is %d%n", this.name, this.age);
}
}
- **静态工厂方法不用再每次被调用时都创建一个新对象。**可以将原有地实例对象缓存起来重复使用,而无需创建不必要的重复对象。这样可以设置一些不可变类预先构建好实例。如Boolean.java类的实现。
java
package org.example.chapter_01;
import java.util.Objects;
public class StaticFactoryMethodClassDemo {
public static void main(String[] args) {
// 缓存使用旧的对象
// - 构造方法
ImmutableSex male1 = new ImmutableSex("male");
ImmutableSex male2 = new ImmutableSex("male");
System.out.println(male1 == male2); // 打印为false,创建2个不同对象
// - 静态工厂
ImmutableSex male3 = ImmutableSex.of("male");
ImmutableSex male4 = ImmutableSex.of("male");
System.out.println(male3 == male4); // 打印为true,使用预先创建好的对象
}
}
final class ImmutableSex {
public static final ImmutableSex MALE = new ImmutableSex("male");
public static final ImmutableSex FEMALE = new ImmutableSex("female");
private final String sex;
public ImmutableSex(String sex) {
this.sex = sex;
}
public static ImmutableSex of(String sex){
return Objects.equals(sex, "male") ? MALE : FEMALE;
}
}
- **静态工厂方法可以返回所声明的返回类型的任何子类型的对象。**通常适用于基于接口的框架,如各种工具类,典型的有Collections.java。
java
package org.example.chapter_01;
import java.util.Objects;
public class StaticFactoryMethodClassDemo {
public static void main(String[] args) {
// 返回类型的任何子类型对象
Shape circle = Shape.createInstance(true);
circle.say();
Shape triangle = Shape.createInstance(false);
triangle.say();
}
}
abstract sealed class Shape permits Circle, Triangle{
public static Shape createInstance(boolean isCircle){
return isCircle ? Circle.of() : Triangle.of();
}
void say(){
// do nothing
}
}
final class Circle extends Shape {
public static Circle of(){
return new Circle();
}
public void say(){
System.out.println("我是圆形");
}
}
final class Triangle extends Shape {
public static Triangle of(){
return new Triangle();
}
public void say(){
System.out.println("我是三角形");
}
}
- **静态工厂方法在每次被调用时,所返回对象的类可以随输入参数的不同而改变。**这点个人感觉与第3点有相似之处,只是第3点强调的是类似泛型的标准约束,而这里强调的是根据不同参数返回的子类型。两者之间所提升的都是程序代码的灵活性。
- **在编写包含该方法的类时,所返回对象的类并不一定要存在。**举个简单的例子,比如Collections.java中我们通常会创建一些可见的list集合,比如ArrayList等,但是像不可改变的UnmodifiableRandomAccessList或者UnmodifiableList集合类,其实是内部私有的类,对外部来说是不存在的。
java
// 常见的arrayList
List<Object> list = new ArrayList<>();
// 外部不存在的不可变list,UnmodifiableList
// List.of或Collections.unmodifiableList()
List<Object> unmodifiableList = List.of(123);
2.2、缺点
- 只有静态工厂方法,没有公有或受保护的构造器,也无法创建子类。
- 使用静态工厂方法,程序员们很难发现他们。 一般我们看到一个new xxx()我们不用跟进去代码的具体实现,就能知道说这个是创建了子类,但是如果改为xxx.of(),这个可能就差点意思了。但是这里我认为不一定是缺点,因为如果有多个构造器,我们很难识别出来这个创建的子类用途是什么,相应的如果我们通过工厂方法命名,如ofFile()或ofDb()可以跟容易发现他们的具体使用场景。书中也给出了一些通用的命名惯例:
- from。类型转换方法,接收一个参数并返回该类型的一个对应实例。如Date date = Date.from(instant);
- of。一个聚合方法,接收多个参数并返回该类型的一个包含这些参数的实例。
- valueOf。根据value值生成相应的实例。如Boolean.valueOf(true);
- instance或getInstance。获取返回的实例。
- create或createInstance。创建一个实例,但是该方法会确保每次调用都创建一个新的实例返回。
3、当构造参数较多时考虑使用生成器
3.1、重叠构造器
静态工厂方法和构造器有一个共同的缺点:当可选参数非常多时,不能很好的扩展。当遇到一个类属性有十几个二十几个的时候,如果利用构造器创建实例,我们需要写一个很大的构造函数,包含十几个参数,然后每次创建的时候,大多参数可能都要设置一些空值以为了正常调用该方法。或者聪明的你会使用多个重叠构造器模式:第一个构造器只有必须的参数,第二个构造器有一个可选参数,第三个构造器有2个可选参数,以此类推。如:
java
@Data
class Dialog {
private final String title;
private final Integer width;
private final Integer height;
private final boolean modal;
private final boolean shadow;
private final boolean close;
// 普通窗体大小
public Dialog(Integer width, Integer height) {
this("默认名称", width, height);
}
public Dialog(String title, Integer width, Integer height) {
this(title, width, height, false);
}
public Dialog(String title, Integer width, Integer height, boolean modal) {
this(title, width, height, modal, false, false);
}
public Dialog(String title, Integer width, Integer height, boolean modal, boolean shadow) {
this(title, width, height, modal, shadow, false);
}
public Dialog(String title, Integer width, Integer height, boolean modal, boolean shadow, boolean close) {
this.title = title;
this.width = width;
this.height = height;
this.modal = modal;
this.shadow = shadow;
this.close = close;
}
}
3.2、JavaBeans模式
使用重叠构造器可以工作,但是当参数数量非常多时,代码写起来会很困难,读起来也会很困难。这时候另一个聪明的你可能会采用另一种方式:构造器包含了必须参数,可选参数使用setter方法赋值:
java
@Data
class Dialog0 {
private final String title;
private final Integer width;
private final Integer height;
private boolean modal;
private boolean shadow;
private boolean close;
public Dialog0(String title, Integer width, Integer height) {
this.title = title;
this.width = width;
this.height = height;
}
}
Dialog0 dialog0 = new Dialog0("窗口0", 400, 800);
dialog0.setClose(false);
dialog0.setModal(false);
dialog0.setShadow(false);
这样写创建实例很容易,虽然代码有些冗长,但生成的代码可读性很强。但是这样的话,这个类就不可能成为不可变类了(final class)。当然你可以手动采取一些措施来杜绝这类问题。
3.3、生成器模式 - Builder
生成器模式结合了重叠构造器模式的安全性和JavaBeans模式的可读性。基本模式是类中带一个生成器,该生成器带有所有必须参数的构造器(或静态工厂),得到该生成器对象,然后调用无参的build()方法构建这个对象。如:
java
@Data
class DialogBuilder {
private final String title;
private final Integer width;
private final Integer height;
private final boolean modal;
private final boolean shadow;
private final boolean close;
@Getter
@Setter
public static class Builder {
private final String title;
private final Integer width;
private final Integer height;
private boolean modal;
private boolean shadow;
private boolean close;
public Builder(String title, Integer width, Integer height) {
this.title = title;
this.width = width;
this.height = height;
}
public Builder modal(boolean val) {
modal = val; return this;
}
public Builder shadow(boolean val) {
shadow = val; return this;
}
public Builder close(boolean val) {
close = val; return this;
}
public DialogBuilder build() {
return new DialogBuilder(this);
}
}
private DialogBuilder(Builder builder) {
this.title = builder.title;
this.width = builder.width;
this.height = builder.height;
this.modal = builder.modal;
this.shadow = builder.shadow;
this.close = builder.close;
}
}
// 生成器模式
DialogBuilder dialog2 = new DialogBuilder.Builder("窗口1", 500, 500)
.modal(true)
.shadow(false)
.close(true).build();
生成器模式中,Builder中的setter方法返回的是该对象本身,这样就可以使用类似AccessChain的方式将调用链接起来,形成流程API。
3.4、缺点
生成器模式也有缺点:
- 使用生成器创建对象,首先必须要创建其生成器。可以看到上面Dialog类的方法是比较繁琐的。书中推荐只有当参数值多到值得这么做时(4个或更多参数),才应该被使用
- 当我们的对象随着代码演进而需要添加越来越多参数时,如果原先使用了构造器或JavaBeans模式,转而使用生成器,这时候废弃的构造器或静态工厂会显得很不协调,因此生成器模式最好是一开始就用。
4、使用私有构造器或枚举类型强化Sigleton属性
这里说白了,就是程序中常用的单例模式。我之前整理过创建单例模式的8种方式,详见:《单例模式的8种写法》。
5、利用私有构造器防止类被实例化
有时候我们需要编写仅包含静态方法和静态字段的类,最常见的就是工具类。工具类被创造出来并不是为了被实例化的,而是通过类直接调用静态方法或静态属性,因为实例化对他来说没太大意义。这时候可以给他一个私有的构造器,让他禁止被实例化。
如果有使用SonarQube检测,这个是会被检测出来的。
如:
java
class DateUtil {
private DateUtil(){
// nop
}
}
6、优先考虑通过依赖注入来连接资源
很多类依赖于一个或多个底层资源。例如,拼写检查程序依赖字典。一种实现方法是使用单例模式:不恰当的使用了静态工具类或Singleton,变得不够灵活且难以测试
java
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // 不可实例化
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
推荐的方式是使用依赖注入,将字典作为一个依赖项,在创建工具实力时注入:
java
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
7、避免创建不必要的对象
尽可能复用对象,而不是每次需要时都创建一个新的功能相同的对象。
7.1、典型案例一:String
java
// 错误的做法
String s = new String("abc")
// 正确的做法
String s == "abc"
正如上面的例子,new String()没被执行以此都会创建一个新的实例(好在JVM虚拟机优化的时候会进行优化),如果被频繁调用,那么就会不必要的创建出数百万个String实例。而String s = abc"只用了一个String实例,而不会每次执行的鸥创建一个新的实例。
对于既提供了静态工厂方法,又提供了构造器的不可变类,我们通常首选静态工厂方法。典型的如Boolean.valueOf()比直接new Boolean()更好。
7.2、典型案例二:正则表达式
上述使用String构造出来的对象开销还比较小,有一些创建实例的时候开销巨大,这就更加需要将其缓存下来以供复用。典型的例子就是正则表达式Pattern对象。
如:
java
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实例的开销很大,以为他需要将这个正则表达式编译成一个有限状态机。
正确的做法是,将其在初始化类的时候显式的编译成一个Pattern实例,并缓存下来复用:
java
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();
}
}
7.3、典型案例三:自动装箱
自动装箱也会创建不必要的对象。自动装箱模糊了基本类型与其封装类的区别,但是并没有消除这种区别。举个例子:
java
public static void main(String[] args) {
count1();
count2();
}
private static long count1(){
Long sum = 0L;
long start = System.currentTimeMillis();
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("count1()计算结果:" + sum + ",耗时:" + (end - start));
return sum;
}
private static long count2(){
long sum = 0L;
long start = System.currentTimeMillis();
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("count2()计算结果:" + sum + ",耗时:" + (end - start));
return sum;
}
可以看出这两个方法的执行结果:
count1()方法使用了Long类型,在整个执行过程中大约会构造了2^31个不必要的Long实例。当我们将Long改为long后,从6811ms提升到了519ms。这意味着:应该优先使用基本类型而不是封装类型,并提防无意中的自动装箱。
8、清除过期的对象引用
清除过期的对象引用,就是为了避免内存泄漏,不管是栈泄露还是堆泄露。最简单的清除过期应用的方式为:instance = null; 常见的内存泄露来源有以下几个:
1、每当出现类自己管理自己的内存的情况时,程序员都应该警惕内存泄露
2、另一个常见的内存泄露来源是缓存
3、来源监听器和其他回调
9、避免使用终结方法和清理方法
终极方法(finializer)是不可预测的,往往存在风险,而一般来说并不重要。Java9引入了清理方法(cleaner)来替代终结方法。清理方法的危险性比终结方法小,但仍然是不可预测的,而且运行很慢,一般来说也是不必要的。
C++程序员注意:不要把终结方法和清理方法看作是Java版的析构函数。
- 终结和清理方法都无法保证会被及时执行。从一个对象变得不再可达,到其终极方法或清理方法运行,中间花掉多长时间都有可能。这意味着不应该在终结方法或清理方法中做任何堆世家有严格要求的事情。
- Java语言规范并没有保证哪个线程会执行终结方法,所以除了避免使用终结方法,没有任何可移植的方法能防范此类问题。
- Java语言规范不仅不保证终结方法和清理方法会及时执行,甚至根本不会保证他们会运行。因此永远不要依赖终结方法或清理方法来更新持久化状态。就算是System.gc()和Systemm.runFinalization()也无法保证。
- 使用终结方法和清理方法还会有严重的性能问题。清理对象,建议使用try-with-resources来清理。
- 终结方法使我们的类容易收到终结方法攻击。如果在类的构造器或其功能类似的序列化方法中会抛出异常,攻击者就可以利用这一点,为其创建一个子类,该恶意子类的终结方法就可以在这个本应"夭折"、尚未构造完全的对象上运行。而这个终结方法可以将对当前对象的引用记录到一个静态字段中,从而阻止它被垃圾收集处理。一旦这个异常的对象被记录下来,就可以轻而易举地在它上面调用本不应该存在的任何方法了。从构造器中抛出异常,本来是足以阻止对象产生的;但是当存在终结方法时,情况就不同了。final类不会受到此类攻击,因为攻击者无法为其编写一个恶意子类。为了保护非 final类免受此类攻击,可以在其中编写一个空的 final的 finalize 方法。
- 如果类的对象中给封装了需要终止的资源,只需让这样的类实现AutoCloseable接口,让要求客户端在每个实例不再需要时调用其close方法。
10、与tey-finally相比,首选try-with-resources
传统关闭资源的方式,采用try-finally方式进行关闭。try-finally:
java
static String oprFileTryFinally() throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(""));
try {
return reader.readLine();
} finally {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
但是当我们加入更多的资源时,情况就变糟了:
java
static String oprFileTryFinally() throws IOException {
InputStream in = new FileInputStream("");
try {
OutputStream out = new FileOutputStream("");
try{
out.write(...);
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
即使正确地使用了try-finally关闭资源,也存在微妙缺陷。try代码块和finally代码块代码都有可能抛出异常。这时候第二个异常通常会掩盖第一个异常,导致第一个异常不会被记录在异常堆栈中。
在Java7引入了try-with-resources语句,要配合该语句使用,资源必须实现AutoCloseable接口,该接口仅包含一个含返回类型为void的close方法。
上述的两个代码使用try-with-resources实例分别为:
java
static String oprFileTryFinally() throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(""))){
return reader.readLine();
}
}
java
static String oprFileTryFinally() throws IOException {
try (InputStream in = new FileInputStream("");
OutputStream out = new FileOutputStream("")) {
out.write(...);
}
}
这些流可以使用try-with-resources来操作资源的关闭,就是因为他们都实现了AuthCloseable接口:
与try-finally相比,try-with-resources版本可读性更好,还提供了更好的诊断支持。如果try块调用和close调用都抛出了异常,后者的异常会被抑制,以便让前者正常表现出来。如果被抑制了多个异常,这些异常信息并没有被丢弃,而是会被打印到栈轨迹信息中,并表明他们被抑制了,Java7在Throwable中加入getSuppressed方法以编程方式访问他们。
所以结论就是:在处理必须关闭的资源时,应该总是选择try-with-resources,而不是try-finally。