Skip to content

创建和销毁对象

1. 用静态工厂方法代替构造器

对于类而言,最传统的获取其实例的方法就是提供一个公有的构造器; 类也可以提供一个公有的静态工厂方法,它只是返回类的实例的静态方法

优点

  • 静态工厂方法可以自定义名称,有更好的可读性
java
//传统构造器
new BigInteger(10,new Random());

//静态工厂方法可以一眼明确该方法是创建一个素数
BigInteger.probablePrime(10,new Random());
  • 不必在每次调用它们的时候都创建一个新对象(单例、享元模式)
java
StackWalker stackWalker = StackWalker.getInstance();
  • 灵活性高,可以返回子类的对象
java
// 父类接口
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 机制动态注册驱动

java
// 而构造器调用的本质是静态绑定,编译时必须确定目标类
new MySQLConnection(url);

缺点

  • 类如果不含公有的或者受保护的构造器,就不能被子类化
java
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类似于 newInstanceFiles.newBufferedReader(path)
getXXX类似 getInstanceFiles.getFileStore(path)
  • 总结:在大多数时候静态工厂方法往往比构造器更有优势

2. 遇到多个构造器参数时要考虑使用构建器

静态工厂方法和构造器有一个共同的局限性,不能很好地扩展大量的可选参数。

重叠构造器

重叠构造器是可行的,但当有许多参数时,客户端代码会很难写,而且可读性不好。

java
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 模式使得把类做成不可变的可能性不复存在

java
// 先调用无参构造器来创建对象,再调用 setter 方法设置参数
Girl girl = new Girl();
girl.setUsername("小甜甜");
girl.setAge("18");
girl.setHeight(162);
girl.setWeight(50);

Builder 模式

java
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

java
//只传必填项
Girl girl = new Girl.Builder("小甜甜", "18").build();
//加上可选项,可读性非常好
Girl myGirl = new Girl.Builder("大甜甜", "28").height(162).weight(50).build();

Builder 模式也适合类层次

  • Pizza 基类定义公共的属性,比如调料
java
  // 浇头     奶酪 / 蘑菇  / 胡椒 / 番茄酱 / 奶油
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 属性
java
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 控制是否内部含有馅
java
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;
    }
}
  • 客户端创建代码
java
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 的最佳方式。这点已经在生产代码中有应用,不过多阐述。

java
public enum Elvis {
    INSTANCE;
    
    public void do() {...}
}

4. 通过私有构造器强化不可实例化的能力

有时候我们需要编写只包含静态方法和静态域的工具类,这些类不希望被实例化,实例化它们没有任何意义。

但在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的 缺省构造器 ,这样的类还是可以被实例化。

所以,我们可以编写一个私有的显示构造器,让这个类无法被实例化,比如 java.util.Math 类。

Math类源码截图.png

5. 优先考虑依赖注入来引用资源

传统资源绑定的问题:紧耦合

如果一个类需要依赖外部资源,传统方式需要在类内部实例化该资源,导致类和资源的强耦合。

java
// 紧耦合的拼写检查器(直接绑定 DefaultDictionary)
public class SpellChecker {
    private final Dictionary dictionary = new DefaultDictionary();
    
    public boolean isValid(String word) { return dictionary.contains(word); }
}

依赖注入 (通过构造器注入)

java
// 松耦合的拼写检查器(通过构造函数传入依赖)
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 对象

java
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);
}
  • 优化实现,重用对象
java
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 接口:

java
String[] array = {"A", "B", "C"};
List<String> list = Arrays.asList(array);

// list.add("D"); // ❌ 抛异常,因为此处是“视图适配”,非完全功能的 List

案例2: I/O 流的适配

InputStreamReader 将字节流适配为字符流:

java
InputStream in = new FileInputStream("data.txt");
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); 
//将 InputStream 适配为 Reader(字符流)

关键源码

java
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 实例。

java
public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

自动装箱

另一种创建多余对象的情形就是自动装箱。基本类型和装修类型在语义上差别很小,但在性能上有比较明显的差别。

java
//虽然这段代码计算结果是正确的,但会创建大量的 Long 对象
Long sum = 0L;
for (long i = 0; i < Long.MAX_VALUE; i++) {
    sum += i;
}

总结: 小对象的创建和回收是非常廉价的;但是大对象的创建和回收则非常昂贵,因此尽量避免创建大对象。数据库连接池就是一个很好的实践。

7. 消除过期的对象引用

JVM 会自动回收垃圾,但有时候一个对象引用被无意识地保留起来了,那么垃圾回收机制便不会处理它以及它引用的所有其他对象。

1. 只要类是自己管理内存,程序员就应该警惕内存泄漏问题

这段代码有一个内存泄露问题,栈中元素弹出后仅仅改变了栈顶指针,实际数组仍持有对象引用

java
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];
  }

}
  • 修复后代码
java
public Object pop() {
   if (size == 0)
    throw new EmptyStackException();
   Object result = elements[--size];
   elements[size] = null; //手动显示置为 null
   return result;
}

2. 内存泄露的另一个常见来源是缓存

  • 强引用缓存导致内存泄露
java
// 危险!基于 HashMap 实现的简单缓存
Map<String, BigObject> cache = new HashMap<>(); 

// 放入缓存
cache.put("user:1001", new BigObject()); 

// 即便应用不再需要此条目,若不手动remove BigObject将永远驻留内存
  • 键为弱引用的缓存
java
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 来关闭资源没那么糟
java
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
try {
    return br.readLine();
}finally {
    br.close();
}
  • 但如果有多个资源,就会一团糟了,还会导致异常日志打印异常,因为 finally 中也可能出现异常
java
   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 优雅又清晰
java
//读取文件,如果无法打开文件等异常则返回一个默认值
static String firstLineOfFile(String path,String defaultVal) {
        try (BufferedReader br = new BufferedReader(
                new FileReader("path"))){
            return br.readLine();
        }catch (IOException e) {
            return defaultVal;
        }
}