《Effective Java》这本书可以说是程序员必读的书籍之一。这本书讲述了一些优雅的,高效的编程技巧。对一些方法或API的调用有独到的见解,还是值得一看的。刚好最近重拾这本书,看的是第三版,顺手整理了一下笔记,用于自己归纳总结使用。建议多读一下原文。今天整理第一章节:创建和销毁对象。
构造函数是提供一个公有的,用于创建实例对象的一个传统方法。除此以外,创建实例对象还有另一种方式:让类提供一个公有的静态工厂方法,就是一个用来返回这个类的实例的静态方法。如Boolean.valueOf("true")等。
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
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);
}
}
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;
}
}
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("我是三角形");
}
}
// 常见的arrayList
List<Object> list = new ArrayList<>();
// 外部不存在的不可变list,UnmodifiableList
// List.of或Collections.unmodifiableList()
List<Object> unmodifiableList = List.of(123);
静态工厂方法和构造器有一个共同的缺点:当可选参数非常多时,不能很好的扩展。当遇到一个类属性有十几个二十几个的时候,如果利用构造器创建实例,我们需要写一个很大的构造函数,包含十几个参数,然后每次创建的时候,大多参数可能都要设置一些空值以为了正常调用该方法。或者聪明的你会使用多个重叠构造器模式:第一个构造器只有必须的参数,第二个构造器有一个可选参数,第三个构造器有2个可选参数,以此类推。如:
@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;
}
}
使用重叠构造器可以工作,但是当参数数量非常多时,代码写起来会很困难,读起来也会很困难。这时候另一个聪明的你可能会采用另一种方式:构造器包含了必须参数,可选参数使用setter方法赋值:
@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)。当然你可以手动采取一些措施来杜绝这类问题。
生成器模式结合了重叠构造器模式的安全性和JavaBeans模式的可读性。基本模式是类中带一个生成器,该生成器带有所有必须参数的构造器(或静态工厂),得到该生成器对象,然后调用无参的build()方法构建这个对象。如:
@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。
生成器模式也有缺点:
这里说白了,就是程序中常用的单例模式。我之前整理过创建单例模式的8种方式,详见:《单例模式的8种写法》。
有时候我们需要编写仅包含静态方法和静态字段的类,最常见的就是工具类。工具类被创造出来并不是为了被实例化的,而是通过类直接调用静态方法或静态属性,因为实例化对他来说没太大意义。这时候可以给他一个私有的构造器,让他禁止被实例化。
如果有使用SonarQube检测,这个是会被检测出来的。
如:
class DateUtil {
private DateUtil(){
// nop
}
}
很多类依赖于一个或多个底层资源。例如,拼写检查程序依赖字典。一种实现方法是使用单例模式:不恰当的使用了静态工具类或Singleton,变得不够灵活且难以测试
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // 不可实例化
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
推荐的方式是使用依赖注入,将字典作为一个依赖项,在创建工具实力时注入:
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) { ... }
}
尽可能复用对象,而不是每次需要时都创建一个新的功能相同的对象。
// 错误的做法
String s = new String("abc")
// 正确的做法
String s == "abc"
正如上面的例子,new String()没被执行以此都会创建一个新的实例(好在JVM虚拟机优化的时候会进行优化),如果被频繁调用,那么就会不必要的创建出数百万个String实例。而String s = abc"只用了一个String实例,而不会每次执行的鸥创建一个新的实例。
对于既提供了静态工厂方法,又提供了构造器的不可变类,我们通常首选静态工厂方法。典型的如Boolean.valueOf()比直接new Boolean()更好。
上述使用String构造出来的对象开销还比较小,有一些创建实例的时候开销巨大,这就更加需要将其缓存下来以供复用。典型的例子就是正则表达式Pattern对象。
如:
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实例,并缓存下来复用:
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();
}
}
自动装箱也会创建不必要的对象。自动装箱模糊了基本类型与其封装类的区别,但是并没有消除这种区别。举个例子:
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。这意味着:应该优先使用基本类型而不是封装类型,并提防无意中的自动装箱。
清除过期的对象引用,就是为了避免内存泄漏,不管是栈泄露还是堆泄露。最简单的清除过期应用的方式为:instance = null; 常见的内存泄露来源有以下几个:
1、每当出现类自己管理自己的内存的情况时,程序员都应该警惕内存泄露
2、另一个常见的内存泄露来源是缓存
3、来源监听器和其他回调
终极方法(finializer)是不可预测的,往往存在风险,而一般来说并不重要。Java9引入了清理方法(cleaner)来替代终结方法。清理方法的危险性比终结方法小,但仍然是不可预测的,而且运行很慢,一般来说也是不必要的。
C++程序员注意:不要把终结方法和清理方法看作是Java版的析构函数。
传统关闭资源的方式,采用try-finally方式进行关闭。try-finally:
static String oprFileTryFinally() throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(""));
try {
return reader.readLine();
} finally {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
但是当我们加入更多的资源时,情况就变糟了:
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实例分别为:
static String oprFileTryFinally() throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(""))){
return reader.readLine();
}
}
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。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。