书籍介绍:https://book.douban.com/subject/36818907/
创建销毁对象
用静态工厂方法替代构造器
静态工厂方法具有名称,更具有可读性,尤其是构造器比较多的类。
通过静态工厂方法创建对象时,可以不必每次创建对象,使用预先构建好的实例,例如
Boolean
类。
- 工厂方法可以返回原类型的任何子类,这相比构造器更加灵活。
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
遇到多个构造器参数时考虑使用构建器
静态工厂方法和构造器都不能很好的扩展到大量参数。
对于大量参数的场景,一种方法是使用重叠构造器的方式:
第一个构造器只有少量必要参数,第二个构造器除了必要参数,还添加一些可选参数,以此类推。
- 重叠构造器在参数量可控的时候还好,随着参数增多,构造器方法也会爆炸式增多,变得难以维护。
另一种办法是
Java Beans
模式,通过无参构造器构造对象,调用setter
方法传递参数。
- 但是这种方法会导致对象在构建过程中处于不一致状态,而且把类做成不可变的可能不复存在。
构建器模式通过让调用方传递必要参数调用
Builder
类的构造器得到builder
对象。
- 然后调用可选参数的
setter
方法设置可选参数,最后调用build
方法生成不可变的最终对象。
public final class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
// 私有构造函数,仅通过Builder类实例化
private NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
}
// Getter方法
public int getServingSize() {
return servingSize;
}
public int getServings() {
return servings;
}
public int getCalories() {
return calories;
}
// 构建器类
public static class Builder {
// 必填属性
private final int servingSize;
private final int servings;
// 可选属性
private int calories = 0;
// 构造函数,设置必填属性
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
// 设置可选属性的方法
public Builder calories(int val) {
this.calories = val;
return this;
}
// 构建最终的NutritionFacts对象
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
所有的字段都是
final
的,并且没有提供任何setter
方法,对象一旦创建,其状态就不能再改变。
Builder
类用于构建NutritionFacts
对象,Builder
类的构造函数接受必填属性。
Builder
类提供了链式调用的方法来设置可选属性。最后,通过
build
方法创建NutritionFacts
对象。
用私有构造器或者枚举类型强化Singleton属性
Singleton
是指:仅仅需要被实例化一次的对象,通常用来代表没有状态的对象。通过定义私有构造器,提供公有静态域或者工厂方法来获取单例对象。
私有构造方法可以保证调用方无法通过构造器获取对象,只能获取创建好的单例对象。
- 为防止通过反射机制调用私有构造器,可以在第二次创建对象时抛出异常。
另一种实现单例的方法是:声明一个包含单个元素的枚举类型。
- 这种方法提供了序列化机制,不用考虑单例对象在序列化和反序列化时需要做的额外工作。
public enum Singleton {
INSTANCE;
}
通过私有构造器强化不可实例的能力
一些工具类只是用来提供一些静态变量或者静态的工具方法,不希望被实例化。
- 因为实例化没有意义。
但是在缺少显式声明的构造器时,编译器会自动提供一个无参的构造器,还是能被实例化和继承。
- 正确的做法是提供一个私有的构造器,让这种类不能被继承,也不能被实例化。
public final class Arrays {
private Arrays() {}
}
public class Collections {
private Collections() {}
}
避免创建不必要的对象
下面这段代码,将变量
sum
声明为Long
类型以后,每次在做加法操作时:
- 会先将
int
类型的i转变为Long
类型的实例,这将导致构建大量的Long
实例。要优先使用基本类型而不是对应的装箱类,防止无意识的自动装箱。
private static long sun() {
Long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
try-with-resources优先于try-finally
使用
try-finally
来关闭资源存在一些问题。比如在
try
块和finally
块中都抛出异常,try
块中抛出的异常会被finally
块中抛出的异常完全抹除。
- 在异常堆栈轨迹中完全没有
try
块中的异常记录。使用
try-with-resources
可以解决这个问题。资源必须实现
java.lang.AutoCloseable
接口,这样才能在try-with-resources
语句中使用。
- 在
try
块结束时,所有声明的资源都会自动关闭。
public class TryWithResourcesExample {
public static void main(String[] args) {
// 使用 try-with-resources 语句
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在
try
语句的括号内声明资源,如果是多个资源,使用分号分割。在
try
块中使用声明的资源,如果在try
块中或资源关闭时发生IOException
,会捕获并处理异常。如果处理资源或者关闭资源都发生了异常,后一个异常会被禁止,保留第一个异常,禁止的异常会被打印到堆栈轨迹中。
- 也可以通过
java.lang.Throwable#getSuppressed
获取。
对象通用方法
覆盖equals方法时请遵守通用约定
如果类具有自己逻辑相等的概念,应该覆盖
equals
方法。覆盖时,遵守以下约定:
- 自反性,对于非空性x,
x.equals(x)
必须返回true。- 对称性,对于非空引用x, y,
x.equals(y)
为true时,y.equals(x)
也必须为true。- 传递性,对于非空引用x, y, z,
x.equals(y)
为true,y.equals(z)
为true,x.equals(z)
也必须为true。- 一致性,对于非空引用x, y,
x.equals(y)
的多次调用结果应该相同。- 非空性,非空引用x,
x.equals(null)
必须为false。
实现高质量equals
方法的诀窍:
使用
==
操作符检查参数是否为当前对象的引用,如果是,则返回true。使用
instanceof
操作符检查参数类型是否为当前类型,不是则返回false。把参数转换为当前类型。
根据逻辑相等的定义,对关键域进行相等判断,如果全部判断相等,则返回true,否则返回false。
对于既不是
float
也不是double
的基本类型,使用==操作符判断。对于float类型或者double类型,使用
java.lang.Float#compare(float f1, float f2)
- 或者
java.lang.Double#compare(double d1, double d2)
进行判断。对于引用类型,可以递归调用引用类型的
equals
方法。
equals
方法不需要对入参进行null
检查,因为类型检查会返回false
。
覆盖equals方法时总要覆盖hashCode方法
这是因为如果两个对象调用
equals
方法比较是相等的,则hashCode
方法返回值也必须一致。
- 否则该类无法结合所有基于散列的集合一起工作。
比如两个通过
equal
方法判定为相等的对象,在添加到HashSet
中时。
- 由于没有覆盖
hashCode
方法,会重复添加,但是根据Set
的定义,Set
中的元素应该是唯一的,不能有两个 相等 的对象。
始终覆盖toString
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
这是默认的
toString
实现,通常情况下,需要和对象相关的更独特的信息,而不是类名和散列值。
使类和成员的可访问性最小
设计良好的组件会隐藏所有的实现细节。
- 这可以有效的解除组件之间的耦合关系,使得这些组件可以独立的开发、测试和修改。
对于顶层的类和接口,只有两种访问级别,包级私有和公有的。
- 类或者接口使用
pulic
修饰就是公有的,否则是包级私有的。- 如果一个包级私有的类只有使用它的类用到,就应该考虑将这个类设计为私有内部类。
对于成员,有四种访问级别,私有的、包级私有、受保护的、公开的。
公有类的实例域决不能被公开,如果实例域是公开的并且是非
final
的,公开之后,就等于放弃了存储在这个域值值的控制能力。对于公开的
final
的数组域,或者提供了返回域的方法也是错误的,会导致数组内容被修改。
- 如果必有,应该将数组域设置为私有,只提供一个数组拷贝。
使可变性最小化
不可变类是指其实例不能被修改的类。
每个实例包含的信息都应该在创建该实例时提供,并且在对象的整个生命周期内不可变。
不可变类更加易于设计、实现和使用,而且不易出错。
设计不可变类遵循以下原则:
不要为类提供
setter
方法。保证类不会被继承,通过
final
修饰类或者构造方法私有的方式。所有域都是
private final
修饰的,在构造时就需要赋值,并且不允许修改。如果类具有指向可变对象的域,需要确保使用该类对象的客户端无法获得指向可变对象的引用。
接口优先于抽象类
对于在设计抽象类时,应该首先考虑一下,这个抽象类能不能设计成为接口。
相比于抽象类,现有的实现了接口的类更容易被更新。
- 因为
Java
语言允许实现多个接口,但是只允许继承一个抽象类。接口虽然可以提供缺省方法,为某些方法提供实现,但是缺省方法仍然有一些缺点:
- 接口无法给
equals、hashCode
等方法提供缺省实现。- 接口中不能包含非公有的静态域或者实例域。
为了结合接口和抽象类的优势,通过对接口提供一个抽象的骨架实现,接口负责定义类型,或者提供一些缺省方法。
- 而骨架实现类则负责提供除基本方法之外的方法。
常量接口是对接口的不良使用
类在内部使用某些常量,属于实现细节,实现常量接口会导致把这样的实现细节泄露到该类导出的API中。
以
java.io.ObjectStreamConstants
为例:
public interface ObjectStreamConstants {
static final short STREAM_MAGIC = (short)0xaced;
static final short STREAM_VERSION = 5;
static final byte TC_BASE = 0x70;
// 审略部分代码
}
如果要导出常量,首先考虑这些常量是不是与某个类或者接口紧密相关。
如果是,应该把常量添加在这些接口或者类中。
其次,使用枚举类型导出这些常量。
最后,考虑使用不可实例化的工具类进行导出。
静态成员类优先于非静态成员类
如果成员类没有访问外围类实例的需求,就应该把成员类设计为
static
的。因为非
static
的成员类需要外围实例才能创建,在创建成员类实例以后,成员类实例会持有外围类实例的引用。
- 保留这份引用不但需要消耗空间,而且会导致外围类实例不能被GC,造成内存泄漏。
方法
检查参数有效性
应该在执行方法之前对参数进行检查,如果传递了无效参数,应该尽快出现合适的异常进行提示。
对于公有或者受保护的方法,要用
Javadoc@throws
标签在文档中说明违反参数限制时会跑出的异常。经常需要对空指针进行检查,可以使用
java.util.Objects#requireNonNull(T, java.lang.String)
public static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}
慎用重载
对象的运行时类型并不影响哪个重载版本将被执行,选择工作在编译时就已经确定。
- 这不同于重写,当调用被覆盖的方法时,最具体的子类的方法将被调用,而不是其父类的方法。
应该避免使用具有相同参数数目的重载方法,可以通过修改方法名称的方式实现这一点。
慎用可变参数
每次调用可变参数方法,都涉及到一次数组的分配和初始化。
因为可变参数的机制首先会创建一个数组,数组的大小为调用时传递的参数的数量。
- 然后将参数传值传入数组,再将数组传递给方法。
public class VarargsExample {
public static void main(String[] args) {
int sum = sumIntegers(1, 2, 3, 4, 5);
System.out.println("Sum: " + sum);
}
public static int sumIntegers(int... numbers) {
int sum = 0;
for (int number : numbers) {
sum += number;
}
return sum;
}
}
在重视性能的情况下,如果无法承受创建数组的成本,可以通过重载方法的方式避免使用可变参数。
- 比如90%的情况参数不会超过5个,那就定义五个重载方法,超过5个参数的再使用可变参数。
public void foo() {
}
public void foo(int a1) {
}
// 省略
public void foo(int a1, int a2, int a3, int a4, int a5) {
}
public void foo(int a1, int a2, int a3, int a4, int a5, int... restArgs) {
}
返回0长度的数组或者集合,而不是null
private final List<Integer> codes =...;
public List<Integer> getCodes() {
return codes.isEmpty() ? null : new ArrayList<>(codes);
}
调用方需要专门处理返回
null
的情况,这容易导致出错。对于返回没有包含元素的
list
的情况,可以使用java.util.Collections#emptyList
。对于返回
set
的情况,可以使用java.util.Collections#emptySet
。对于返回
map
的情况,可以使用java.util.Collections#emptyMap
。
异常
不要忽略异常
空的catch块会使异常达不到应有的目的,抛出了异常。
- 但是异常没有被处理或者传播出去,导致问题被隐藏,不能及时暴露,会给排查造成阻碍。
如果确定不需要处理,最好加一条注释。
public class IgnoreExceptionExample {
public static void readFile() {
try {
FileReader reader = new FileReader("file.txt");
// 其他读取文件的操作
reader.close();
} catch (IOException e) {
// 忽略异常
}
}
}