创建和销毁对象
1. 用静态工厂方法代替构造器
对于类而言,最传统的获取其实例的方法就是提供一个公有的构造器; 类也可以提供一个公有的静态工厂方法,它只是返回类的实例的静态方法。
优点
- 静态工厂方法可以自定义名称,有更好的可读性
//传统构造器
new BigInteger(10,new Random());
//静态工厂方法可以一眼明确该方法是创建一个素数
BigInteger.probablePrime(10,new Random());
- 不必在每次调用它们的时候都创建一个新对象(单例、享元模式)
StackWalker stackWalker = StackWalker.getInstance();
- 灵活性高,可以返回子类的对象
// 父类接口
interface Animal {
static Animal create(String type) {
return switch (type) {
case "DOG" -> new Dog(); // 返回 Dog 子类
case "CAT" -> new Cat(); // 返回 Cat 子类
default -> throw new IllegalArgumentException();
};
}
}
- 动态扩展能力
比如 JDBC 中 DriverManager
在编写时,根本不知道 MySQL 驱动的存在,可通过反射或 SPI 机制动态注册驱动
// 而构造器调用的本质是静态绑定,编译时必须确定目标类
new MySQLConnection(url);
缺点
- 类如果不含公有的或者受保护的构造器,就不能被子类化
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
// 构造器是私有的,不能被继承 无法调用super()
private Singleton() {}
// 总是返回同一个实例
public static Singleton getInstance() {
return INSTANCE;
}
}
- API文档中不像构造器那样会明确标识出来,程序员很难发现
常见静态工厂方法命名规范
方法名 | 说明 | 示例 |
---|---|---|
from | 类似转换方法,单个参数 | Date.from(instant) |
of | 聚合方法,多个参数 | List.of(1,2,3) |
valueOf | 等价于 of /from (更冗长) | Integer.valueOf(1) |
getInstance | 获取单例或特定上下文实例 | Calendar.getInstance() |
create /newInstance | 明确表示每次获取新实例 | Array.newInstance(String.class, 10) |
newXXX | 类似于 newInstance | Files.newBufferedReader(path) |
getXXX | 类似 getInstance | Files.getFileStore(path) |
- 总结:在大多数时候静态工厂方法往往比构造器更有优势
2. 遇到多个构造器参数时要考虑使用构建器
静态工厂方法和构造器有一个共同的局限性,不能很好地扩展大量的可选参数。
重叠构造器
重叠构造器是可行的,但当有许多参数时,客户端代码会很难写,而且可读性不好。
class Boy {
private final String username;
private final String age;
private final float height;
private final float weight;
// username 和 age 是必填项
public Boy(String username,String age) {
this(username,age,0);
}
// height 是可选项
public Boy(String username,String age,float height) {
this(username,age,height,0);
}
// 全参构造器
public Boy(String username,String age,float height,float weight) {
this.username = username;
this.age = age;
this.height = height;
this.weight = weight;
}
}
JavaBeans 模式
JavaBeans 模式有很严重的缺点,构造过程被分到了几个调用中,在构造过程中 JavaBean 可能处于不一致的状态。
JavaBeans 模式使得把类做成不可变的可能性不复存在。
// 先调用无参构造器来创建对象,再调用 setter 方法设置参数
Girl girl = new Girl();
girl.setUsername("小甜甜");
girl.setAge("18");
girl.setHeight(162);
girl.setWeight(50);
Builder 模式
class Girl {
private final String username;
private final String age;
private final int height;
private final int weight;
public static class Builder {
//必选参数
private String username;
private String age;
//可选参数 赋默认值
private int height = 0;
private int weight = 0;
public Builder(String username,String age) {
this.username = username;
this.age = age;
}
public Builder height(int height) {
this.height = height;
return this;
}
public Builder weight(int weight) {
this.weight = weight;
return this;
}
public Girl build() {
return new Girl(this);
}
}
private Girl(Builder builder) {
this.username = builder.username;
this.age = builder.age;
this.height = builder.height;
this.weight = builder.weight;
}
}
Builder
的设值方法返回 builder
本身,以便把调用链接起来,得到一个流式的 API
//只传必填项
Girl girl = new Girl.Builder("小甜甜", "18").build();
//加上可选项,可读性非常好
Girl myGirl = new Girl.Builder("大甜甜", "28").height(162).weight(50).build();
Builder 模式也适合类层次
Pizza
基类定义公共的属性,比如调料
// 浇头 奶酪 / 蘑菇 / 胡椒 / 番茄酱 / 奶油
enum Topping {CHEESE , MUSHROOM, PEPPER ,TOMATO , CREAM}
enum Size {SMALL,MIDDLE,LARGE}
abstract class Pizza {
final Set<Topping> toppings;
// Pizza.Builder 的类型是泛型,带有一个递归类型参数
// 它和抽象的 self() 方法一样,允许在子类中适当地进行方法链接,不需要转换类型
abstract static class Builder<T extends Builder<T>> {
// 创建空的枚举调料集
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
//定义子类公共方法
public T addToppings(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
protected abstract T self();
}
Pizza(Builder<?> builder) {
//深克隆,保持 toppings 的不可变性
toppings = builder.toppings.clone();
}
}
NyPizza
包含一个独特的Size
属性
class NyPizza extends Pizza {
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
Pizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
Calzone
没有尺寸,但有独特的sauceInside
控制是否内部含有馅
class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // 默认值
public Builder sauceInside() {
sauceInside = true;
return this;
}
// 每个子类的 build 方法都返回子类类型
// 子类方法声明返回父类中声明的返回类型的子类型,称为 协变返回类型
// 客户端无需转换类型就能使用这些构建器
@Override
Pizza build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
- 客户端创建代码
Pizza nyPizza = new NyPizza.Builder(Size.SMALL)
.addToppings(Topping.TOMATO)
.addToppings(Topping.CREAM)
.build();
Pizza calzone = new Calzone.Builder()
.addToppings(Topping.CHEESE)
.sauceInside()
.build();
- 总结:如果类的构造器或静态工厂方法中有多个参数,特别是可变参数,设计这种类时,
Builder
模式是不错的选择。
3. 使用枚举类型强化 Singleton 属性
单元素的枚举类型已经是实现 Singleton 的最佳方式。这点已经在生产代码中有应用,不过多阐述。
public enum Elvis {
INSTANCE;
public void do() {...}
}
4. 通过私有构造器强化不可实例化的能力
有时候我们需要编写只包含静态方法和静态域的工具类,这些类不希望被实例化,实例化它们没有任何意义。
但在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的 缺省构造器
,这样的类还是可以被实例化。
所以,我们可以编写一个私有的显示构造器,让这个类无法被实例化,比如 java.util.Math
类。
5. 优先考虑依赖注入来引用资源
传统资源绑定的问题:紧耦合
如果一个类需要依赖外部资源,传统方式需要在类内部实例化该资源,导致类和资源的强耦合。
// 紧耦合的拼写检查器(直接绑定 DefaultDictionary)
public class SpellChecker {
private final Dictionary dictionary = new DefaultDictionary();
public boolean isValid(String word) { return dictionary.contains(word); }
}
依赖注入 (通过构造器注入)
// 松耦合的拼写检查器(通过构造函数传入依赖)
public class SpellChecker {
private final Dictionary dictionary;
// 依赖通过构造函数注入
public SpellChecker(Dictionary dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { return dictionary.contains(word); }
}
6. 避免创建不必要的对象
一般来说,最好能够重用单个对象,而不是每次调用都创建一个相同功能的新对象。
案例: 编写一个方法,判断一个字符是否为一个有效的罗马数字
- 传统写法
最合乎直觉的实现可能是使用正则表达式来匹配罗马数字。但这种实现方式在性能上存在问题,因为每次调用该方法都创建了一个 Pattern
对象
static boolean isRomanNumber(String s) {
return s.matches("^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$");
}
//源码
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
// Pattern.matches()
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
- 优化实现,重用对象
static final Pattern ROMAN =
Pattern.compile("^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$");
static boolean isRomanNumber2(String s) {
return ROMAN.matcher(s).matches();
}
适配器或视图情形
如果一个对象是不变的,那么它显然能够被安全地重用,但其他有些情形并不总是那么明显,比如适配器或视图的情形。
案例1: 集合框架的适配
java.util.Arrays.asList(T[])
将数组适配为 List
接口:
String[] array = {"A", "B", "C"};
List<String> list = Arrays.asList(array);
// list.add("D"); // ❌ 抛异常,因为此处是“视图适配”,非完全功能的 List
案例2: I/O 流的适配
InputStreamReader
将字节流适配为字符流:
InputStream in = new FileInputStream("data.txt");
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
//将 InputStream 适配为 Reader(字符流)
关键源码
public class InputStreamReader extends Reader {
private final StreamDecoder sd; // 负责将字节转换为字符的后备对象
public InputStreamReader(InputStream in, Charset cs) {
super(in);
sd = StreamDecoder.forInputStreamReader(in, this, cs);
}
@Override
public int read() throws IOException {
return sd.read(); // 委托给 StreamDecoder
}
}
案例3: Map接口的 KeySet
视图
乍看之下,好像每次调用 keySet()
都应该创建一个新的 Set
实例,但对于一个给定的 Map 对象,返回的是相同的 Set
实例。
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
自动装箱
另一种创建多余对象的情形就是自动装箱。基本类型和装修类型在语义上差别很小,但在性能上有比较明显的差别。
//虽然这段代码计算结果是正确的,但会创建大量的 Long 对象
Long sum = 0L;
for (long i = 0; i < Long.MAX_VALUE; i++) {
sum += i;
}
总结: 小对象的创建和回收是非常廉价的;但是大对象的创建和回收则非常昂贵,因此尽量避免创建大对象。数据库连接池就是一个很好的实践。
7. 消除过期的对象引用
JVM 会自动回收垃圾,但有时候一个对象引用被无意识地保留起来了,那么垃圾回收机制便不会处理它以及它引用的所有其他对象。
1. 只要类是自己管理内存,程序员就应该警惕内存泄漏问题
这段代码有一个内存泄露问题,栈中元素弹出后仅仅改变了栈顶指针,实际数组仍持有对象引用
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
//如果容量不够则扩容
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
}
- 修复后代码
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; //手动显示置为 null
return result;
}
2. 内存泄露的另一个常见来源是缓存
- 强引用缓存导致内存泄露
// 危险!基于 HashMap 实现的简单缓存
Map<String, BigObject> cache = new HashMap<>();
// 放入缓存
cache.put("user:1001", new BigObject());
// 即便应用不再需要此条目,若不手动remove BigObject将永远驻留内存
- 键为弱引用的缓存
Map<UserKey, UserSettings> cache = new WeakHashMap<>();
UserKey key = new UserKey("1001");
cache.put(key, loadUserSettings());
// 当 key 不再被外部强引时(例如 key = null),该缓存条目将被GC自动删除
WeakHashMap
有其局限性:
- 若值内部强引用键(如链表节点),即使外部无键引用,也无法释放缓存
- 若值本身内存泄露,如值持有其他对象的强引用(如数据库连接),缓存也无法释放
- 线程安全问题,
WeakHashMap
并非线程安全
最佳实践:优先采用工业级经过验证的成熟缓存库,如 Guava Cache 与 Caffeine
3. 内存泄露的第三个常见来源是监听器和其他回调
比如,你实现了一个 API ,客户端在这个 API 中注册回调,却没有显式地取消注册。
8. 避免使用终结方法和清除方法
- Java 语言规范不仅不保证终结方法或者清除方法会被及时地执行,而且根本就不保证 它们会被执行。
- 使用终结方法和清除方法有一个非常严重的性能损失。
- 终结方法有一个严重的安全问题: 它们为终结方法攻击(finalizer attack) 打开了类的大门
如果类的对象中封装的资源(例如文件或者线程)确实需要终止,只需让类实现 AutoCloseable
9. try-with-resources
优先于 try-finally
- 当只有单个资源时,使用
try-finally
来关闭资源没那么糟
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
try {
return br.readLine();
}finally {
br.close();
}
- 但如果有多个资源,就会一团糟了,还会导致异常日志打印异常,因为
finally
中也可能出现异常
static void copy(String src,String dst) throws IOException {
FileInputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
}finally {
out.close();
}
}finally {
in.close();
}
}
- 使用
try-with-resources
优雅又清晰
//读取文件,如果无法打开文件等异常则返回一个默认值
static String firstLineOfFile(String path,String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader("path"))){
return br.readLine();
}catch (IOException e) {
return defaultVal;
}
}