本系列文章为《Effective Java 3rd Edition》的阅读手记,该著作中提供了90条java的编程建议,都是从实战中总结出的高阶经验。结合实际开发中的使用频率,对有的条目详细解读和实践,对于一些不常用的条目,就当走个意识,真正实践中遇到再回头细读。
1、静态工厂替代构造方法
优点:
- 有自己的名字,可读性强
- 无需每次调用都创建新对象
- 可以返回方法返回类型的任何子类型的对象
- 返回对象类可以根据输入参数的不同而不同
- 编写包含该方法的类时,返回的对象的类不需要存在
缺点:
- 如果只提供了静态工厂方法,没有公共或受保护构造方法的类不能被子类化
- 接口文档中不好找
常用的静态方法命名:
常用静态方法名称
常用名称 | 含义 | 优雅示例 |
---|---|---|
from | 类型转换方法,接受单个参数并返回此类型的相应实例 | Date d = Date.from(instant) |
of | 聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起 | Set faceCards = EnumSet.of(JACK, QUEEN, KING) |
valueOf | from 和 to 更为详细的替代 方式 | BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE) |
instancee 或 getinstance | 返回一个由其参数 (如果有的话) 描述的实例 | StackWalker luke = StackWalker.getInstance(options) |
create 或 newInstance | 同上 | Object newArray = Array.newInstance(classObject, arrayLen) |
getType | 与 getInstance 类似 | FileStore fs = Files.getFileStore(path) |
newType | 与 newInstance 类似 | BufferedReader br = Files.newBufferedReader(path) |
type | getType 和 newType 简洁的替代方式 | List litany = Collections.list(legacyLitany); |
2、当构造方法参数过多时使用 builder 模式
我们在创建类的时候,通常的定义方式如下:
public class A {
private int a;
private int b;
private String c;
// constructor with parameters
public A(int a, int b, int c) {
this.a = a;
this.b = b;
this.c = c;
}
// defalut constructor
public A() {
}
// setters and getters are omitted
}
在使用这个类的时候,通常会使用带参数的构造函数,对不需要设置的参数采用默认值,创建方式为:
A obj = new A(1, 2, 0);
// 其中c设置了默认值0
或使用默认构造函数(JavaBeans 模式)创建:
A obj = new A();
obj.setA(1);
obj.setB(2);
obj.setC(0);
以上创建类的方式分别为可伸缩方式和JavaBeans模式,但存在以下缺点:
- 可伸缩构造方法模式需要为不关心的参数设置默认值,当参数非常多的时候,这种方式容易参数错位,比如类A创建时被写成
A obj = new A(1, 0, 2)
,实际上要求c=0
,这就会产生灾难性的bug。另一方面对于阅读者也很不友好,即不够优雅 - JavaBeans模式实际上会存在线程安全问题
基于以上问题,业界给出优雅的解决方式,最典型的要数scala语言,比如在spark streaming中创建一个上下文,代码如下:
object SparkStreaming {
def printWebsites(): Unit= {
val conf = new SparkConf()
.setMaster("local[2]")
.setAppName("PrintWebsites")
// ...
}
}
我们可以看到上面SparkConf
类的创建,采用了链式赋值的方式,对于需要使用默认值的参数可以省略赋值,这样的结构看起来就清晰简单,也不会出现伸缩式构造函数可能出现的参数错位。
下面我们来看一个创建java线程池配置类的例子,从中感受两种方式的差异。
// 可伸缩构造函数方式
public class ConfigContext {
// Required
private final int corePoolSize;
private final int maximumPoolSize;
// Optional
private final long keepAliveTime;
private final TimeUnit unit;
private final BlockingQueue<Runnable> workQueue;
private final boolean enableAbortPolicy;
private final boolean enableDiscardPolicy;
private final boolean enableDiscardOldestPolicy;
private final boolean enableCallerRunsPolicy;
public ConfigContext(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
boolean enableAbortPolicy,
boolean enableDiscardPolicy,
boolean enableDiscardOldestPolicy,
boolean enableCallerRunsPolicy) {
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.keepAliveTime = keepAliveTime;
this.unit = unit;
this.workQueue = workQueue;
this.enableAbortPolicy = enableAbortPolicy;
this.enableDiscardPolicy = enableDiscardPolicy;
this.enableDiscardOldestPolicy = enableDiscardOldestPolicy;
this.enableCallerRunsPolicy = enableCallerRunsPolicy;
}
// setters and getters are omitted
因此我们创建一个ConfigContext
时就会这样写:
public void notGood() {
// 每个参数都要传值
ConfigContext configContext = new ConfigContext(
10,
1000,
3600,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(),
true,
false,
false,
false);
}
或:
public void notGood2() {
ConfigContext configContext = new ConfigContext();
configContext.setCorePoolSize(10);
configContext.setMaximumPoolSize(1000);
configContext.setKeepAliveTime(3600);
configContext.setUnit(TimeUnit.SECONDS);
configContext.setWorkQueue(new LinkedBlockingDeque<Runnable>());
configContext.enableAbortPolicy(true);
}
如果采用builder模式,会采用如下方式设置参数:
// 可伸缩构造函数方式
public class ConfigContext {
// 属性同上
// 创建一个静态方法Builder
public static class Builder {
// Required
private int corePoolSize;
private int maximumPoolSize;
// Optional
private long keepAliveTime;
private TimeUnit unit;
private BlockingQueue<Runnable> workQueue;
private boolean enableAbortPolicy;
private boolean enableDiscardPolicy;
private boolean enableDiscardOldestPolicy;
private boolean enableCallerRunsPolicy;
// setters and getters are omitted
// 为必须参数赋值
public Builder(int corePoolSize, int maximumPoolSize) {
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
}
// 创建设置函数,并返回Builder本身,这是实现链式赋值的关键步骤
public Builder keepAliveTime(int val) {
keepAliveTime = val;
// 返回Builder
return this;
}
// 其他参数设置方式同上,省略
}
// 创建一个私有构造函数
private ConfigContext(Builder builder) {
corePoolSize = builder.corePoolSize;
maximumPoolSize = builder.maximumPoolSize;
keepAliveTime = builder.keepAliveTime;
unit = builder.unit;
workQueue = builder.workQueue;
enableAbortPolicy = builder.enableAbortPolicy;
enableDiscardPolicy = builder.enableDiscardPolicy;
enableDiscardOldestPolicy = builder.enableDiscardOldestPolicy;
enableCallerRunsPolicy = builder.enableCallerRunsPolicy;
}
}
其结构如图所示:
创建类的方式就变为了如下方式:
public void prettyGood() {
ConfigContext configContext = new Builder(10, 1000)
.keepAliveTime(3600)
.unit(TimeUnit.SECONDS)
.workQueue(new LinkedBlockingDeque<Runnable>())
.enableAbortPolicy(true)
.build();
}
可见上述创建方式
- 由于没有提供可伸缩的构造函数,因此不会出现参数赋值错位的问题
- 强制通过参数名称赋值,阅读性非常好
- builder比JavaBeans更安全
当然,使用builder静态工厂方式的缺点是:
- builde创建比JavaBeans方式更复杂,在追求极致性能体验的场景要慎用
- 如果一开始没有使用builder模式,在参数不断增加多到一定程度才切换到builder模式,转化过程具有一定风险,因此最好一开始就是使用builder模式
4、使用私有构造方法执行非实例化
5、依赖注入优于硬连接资源
不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类 的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法(或静态工厂或 builder 模式)。这种 称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。
在Spring工程项目中,通常我们可以看到这样的资源方法注入写法:
@Service
public class DemoServiceImpl {
@Resource
private CustomerInfoDao customerInfoDao;
}
这个customerInfoDao
就是我们的链接资源类,通过依赖注入方式使用,而不是直接在DemoServiceImpl
中硬连接对应的资源。
6、避免创建不必要的对象
创建不必要的对象,有以下问题:
- 可能会出现OOM(OutOfMemerry)错误,因为类对象创建在JVM的堆内存中,堆内存是有限制的
- 增加堆内存中垃圾回收的负担。创建不必要的对象后,可能会频繁触发GC操作,从而拖慢程序的处理速度。如果创建的对象出现内存泄露(Memerry Leak),同样也会出现OOM以及实际可使用堆内存更小,进一步增加GC频率
如下方式是不建议的:
// DON'T DO THIS
String s = new String("gendlee");
正确的做法:
// this is recommand
String s = "gendlee";
其原因为:
- String应该优先使用
常量池
中已有的字符串,而不是用new 的方式在堆上创建,堆上创建的String字符串,是无法复用的 - new 对象会消耗更多性能
除此之外,优先使用基本类型而不是装箱的基本类型。如下方式因为使用装箱的基本类型Long导致创建了2^31个Long对象:
private static long sum() {
Long sum = 0L; // 不应该使用装箱的基本类型
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
应修改为:
long sum = 0L; // 使用基本类型
当然,不创建不必要的对象并不是绝对的,需要根据实际情况来判断。即当必须要创建一个对象的时候,就要创建,大不了就是开销点性能,但不能因为复用对象而引入防御性赋值(defensive copying)安全问题。
7、消除过期的对象引用
看这样一个类中的过期对象未消除的问题:
public class Stack {
private Object[] elements;
private int size = 0;
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size]; // 这里栈中的第size个元素未消除引用
}
}
正确的做法为:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object obj = elements[--size];
elements[size] = null; // 消除过期的引用
retrun obj;
}
清空对象引用应该是例外而不是规范,通常当一个类自己管理内存时,程序员应该警惕内存泄漏问题。另一个常见的内存泄漏来源是缓存,如果担心忘记清理,可以通过一个后台线程来定时清理。
8、避免使用 Finalizer 和 Cleaner 机制
9、使用 try-with-resources 语句替代 try-finally 语句
在try中使用资源时,通常都需要非常留意资源使用完后的善后工作,一旦忘记处理可能会造成资源的浪费。譬如:
static String firstLineOfFile(String path) throws IOException {
// 不建议的做法
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
如果打开的不止一个资源,就会有多个try嵌套,代码冗长且阅读性不佳,正确的做法是使用try-with-resources
:
// 推荐的做法
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在try语句中会自动关闭资源,安全而优雅。
10、重写 equals 方法时遵守通用约定
- 自反性: 对于任何非空引用
x
,x.equals(x)
必须返回true - 对称性: 对于任何非空引用
x
和y
,如果且仅当y.equals(x)
返回 true 时x.equals(y)
必须返回 true - 传递性: 对于任何非空引用
x
、y
、z
,如果x.equals(y)
返回 true,y.equals(z)
返回 true,则x.equals(z)
必须返回 true - 一致性: 对于任何非空引用
x
和y
,如果在 equals 比较中使用的信息没有修改,则x.equals(y)
的多次调用 必须始终返回 true 或始终返回 false - 对于任何非空引用
x
,x.equals(null)
必须返回 false
以下是一些重要的提醒:
当重写 equals 方法时,同时也要重写 hashCode 方法
不要让 equals 方法试图太聪明。如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。 例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做
在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时 苦苦思索为什么它不能正常工作
比如这里将Object
换成了MyClass
,这样是不会生效的:
public boolean equals(MyClass o) {
// ...
}
当然,你可以加 @Override
来在编译时候报错提示,但是不借助IDE赋能特性也能做对事情,是我们本该追求的工匠精神。
12、始终重写 toString 方法
对一个类,除非父类已经重写了toString
方法,否则在每个实例化的类中重写 Object 的 toString 实现。 其特点和要求是:
- 它使得类更加舒适地使用和协助调试
- toString 方法应该以一种美观的格式返回对象的简明有用的描述,通常IDE工具都有自动生成方法
13、谨慎地重写 clone 方法
14、考虑实现 Comparable 接口
15、使类和成员的可访问性最小化
使用尽可能低的访问级别,与你正在编写的 软件的对应功能保持一致。按照可访问性从小到大列出:
- private —— 该成员只能在声明它的顶级类内访问
- package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符 (接口成 员除外,它默认是公共的),这是默认访问级别
- protected —— 成员可以从被声明的类的子类中访问(受一些限制,JLS,6.6.2),以及它声明的包中的任何类
- public —— 该成员可以从任何地方被访问
16、在公共类中使用访问方法而不是公共属性
17、最小化可变性
要使一个类不可变,请遵循以下五条规则:
- 不要提供修改对象状态的方法(也称为 mutators)
- 确保这个类不能被继承。 这可以防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变 行为。 防止子类化通常是通过 final 修饰类,但是我们稍后将讨论另一种方法
- 把所有属性设置为 final。 通过系统强制执行,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用 从一个线程传递到另一个线程而没有同步,就必须保证正确的行为
- 把所有的属性设置为 private。 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对 象。 虽然技术上允许不可变类具有包含基本类型数值的公共 final 属性或对不可变对象的引用,但不建议这 样做,因为它不允许在以后的版本中更改内部表示(条目 15 和 16)
- 确保对任何可变组件的互斥访问。 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对 这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法, 访问方法和 readObject 方法(条目 88)中进行防御性拷贝(条目 50)
18、 组合优于继承
继承是实现代码重用的有效方式,但并不总是最好的工具。与方法调用不同,继承打破了封装。