Effective Java 读书笔记(一)
构造方法参数过多使用Builder模式
Car
类是不可变的,所有的参数默认值都在一个地方。builder
的setter
方法返回builder
本身,这样就可以进行链式调用,从而生成一个流畅的API
。
public class Car {
/**
* id
*/
private String id;
/**
* 名称
*/
private String name;
/**
* 速度
*/
private float speed;
private Car(Builder builder) {
id = builder.id;
name = builder.name;
speed = builder.speed;
}
public static class Builder {
/**
* id
*/
private String id;
/**
* 名称
*/
private String name;
/**
* 速度
*/
private float speed;
public Builder() {
}
public Builder id(String id) {
this.id = id;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder speed(float speed) {
this.speed = speed;
return this;
}
public Car build() {
return new Car(this);
}
}
// 省略 getset()
}
优点
Builder
模式非常灵活。 单个builder
可以重复使用来构建多个对象。- 当设计类的构造方法或静态工厂的参数超过几个时,
Builder
模式是一个不错的选择,特别是许多参数是可选的或相同类型的。 builder
模式客户端代码比使用伸缩构造方法(telescopingconstructors
)更容易读写,并且builder
模式比JavaBeans
更安全。
缺点
- 为了创建对象,首先必须创建它的
builder
。虽然创建这个builder
的成本在实践中不太可能被注意到,但在看中性能的场合下这可能就是一个问题。而且,builder
模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。
工具类添加私有构造方法
-
工具类不是设计用来被实例化的,因为实例化对它没有任何意义。然而,在没有显式构造器的情况下,编译器提供了一个公共的、无参的默认构造器。对于用户来说,该构造器与其他构造器没有什么区别。
-
可以通过包含一个私有构造器来实现类的非实例化:
public class UtilityClass {
// 该类不能被实例化
private UtilityClass() {
throw new AssertionError();
}
}
避免创建不必要的对象
-
每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。
-
反例
// 语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的
String s = new String("hello");
// 使用单个 String 实例,而不是每次执行时创建一个新实例。
String s = "hello";
- 工厂方法
Boolean.valueOf(String)
比构造方法Boolean(String)
更可取,
Split编译告警
-
'split()' could be replaced with compiled 'java.util.regex.Pattern' construct
-
方法内部为正则表达式创建一个
Pattern
实例,并且只使用它一次,之后它就有资格进行垃圾收集。 创建Pattern
实例是昂贵的
// 反例
@Test
public void test2(){
String str = "Hello world";
String[] split = str.split("");
}
// 正例
private static final Pattern BLANK_PATTERN = Pattern.compile("");
@Test
public void test2(){
String str = "Hello world";
String[] split = BLANK_PATTERN.split(str);
}
自动装箱
- 优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱
private static long sum() {
// 可以使用 long 基本类型
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
使用 try-with-resources 语句替代 try-finally语句
- 常用写法
@Test
public void test1() throws IOException {
String path = "./test.txt";
BufferedReader br = new BufferedReader(new FileReader(path));
try {
String s = br.readLine();
logger.info(s);
} finally {
// 有可能会出错
br.close();
}
}
try-with-resources
@Test
public void test1() throws IOException {
String path = "./test.txt";
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String s = br.readLine();
logger.info(s);
}
}
-
要使用这个构造,资源必须实现
AutoCloseable
接口,该接口由一个返回为void
的close
组成。Java
类库和第三方类库中的许多类和接口现在都实现或继承了
AutoCloseable
接口。如果你编写的类表示必须关闭的资源,那么这个类也应该实现
AutoCloseable
接口。
public abstract class Reader implements Readable, Closeable{}
// BufferedReader 实现 close 接口
public void close() throws IOException {
synchronized (lock) {
if (in == null)
return;
try {
in.close();
} finally {
in = null;
cb = null;
}
}
}
-
可以在
try-with-resources
语句中添加catch
子句,就像在常规的try-finally
语句中一样。这允许你处理异常,而不会在另一层嵌套中污染代码。
-
使用
try-with-resources
语句替代try-finally
语句。 生成的代码更简洁,更清晰,并且生成的异常更有用。try-with-resources
语句在编写必须关闭资源的代码时会更容易,也不会出错,
重写 equals 方法时遵守通用约定
通用约定
- 重写
equals
方法时,必须遵守它的通用约定。Object
的规范如下:
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
。
违反约定
自反性
- 将对象添加到集合中,使用
contains
方法,如果违反了自反性会找不到这个对象
boolean contains(Object o);
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public boolean equals(Object obj) {
return (this == obj);
}
对称性
import java.util.Objects;
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
// 操作 String 字符串
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
}
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
System.out.println(cis.equals(s)); // true
System.out.println(s.equals(cis)); // false
CaseInsensitiveString
类中的equals
方法知道正常字符串,但String
类中的equals
方法却忽略了不区分大小写的字符串。 因此,s.equals(cis)
返回false
,明显违反对称性。
重写 equals 方法时同时也要重写hashcode 方法
// Object equals 比较 两个对象是否具有相同的引用
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
hashCode
和equals
两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度,如果在重写equals
时,不重写hashCode
,就会导致在某些场景下,例如将两个相等的自定义对象存储在Set
集合时,就会出现程序执行的异常,为了保证程序的正常执行,所以我们就需要在重写equals
时,也一并重写hashCode
方法才行。
始终重写 toString 方法
toString
通用约定「建议所有的子类重写这个方法」。
public String toString() {
// 类名后跟一个「at」符号(@)和哈希码的无符号十六进制表示组成
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
toString
方法应该以一种美观的格式返回对象的简明有用的描述。
使类和成员的可访问性最小化
- 让每个类或成员尽可能地不可访问。 换句话说,使用尽可能低的访问级别,与你正在编写的软件的对应功能保持一致。
- 应该尽可能地减少程序元素的可访问性(在合理范围内)
编译告警
- 这样的字段通常用于存储常量值的数组。尽管如此,它们仍然代表着安全隐患,因为它们的内容可能被修改,即使字段被声明为
final
。
public static final String[] STR_ARR = {"1", "2", "3", "4"};
解决方法
// 方法 1
private static final String[] STR_ARR = { ... };
public static final List<String> VALUES = Collections.unmodifiableList(Arrays.asList(STR_ARR));
// 方法 2
private static final String[] STR_ARR = {"1", "2", "3", "4"};
public static final String[] strArrayValues() {
return STR_ARR.clone();
}
接口优于抽象类
- 一个接口通常是定义允许多个实现的类型的最佳方式。 如果你导出一个重要的接口,应该强烈考虑提供一个骨架的实现类。 在可能的情况下,应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可以使用它。 也就是说,对接口的限制通常要求骨架实现类采用抽象类的形式。
接口仅用来定义类型
- 一种失败的接口就是所谓的常量接口,常量接口模式是对接口的糟糕使用
消除非检查警告
-
尽可能地消除每一个未经检查的警告
-
每当使用 @SuppressWarnings(“unchecked”) 注解时,请添加注释,说明为什么是安全的。
不要忽略异常
- 空的
catch
块会使异常达不到应有的目的 - 如果选择忽略异常,
catch
块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为ignored
:
@Test
public void test4() {
try {
String a = null;
method1();
logger.info(a);
} catch (RuntimeException ignored) {
// 捕获异常不做任何处理
}
logger.info("ok");
}
始终使用 Override 注解
- 在每个方法声明中使用
Override
注解,并且认为要重写父类声明,那么编译器可以保护免受很多错误的影响,但有一个例外。 在具体的类中,不需要注解标记你确信可以重写抽象方法声明的方法。