Java高效编程(6):避免创建不必要的对象

解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界

在开发中,尽量复用已经存在的对象,而不是每次都创建一个功能等价的新对象。对象复用不仅速度更快,也显得更加优雅。尤其是对于不可变对象,可以始终安全地复用(详见【条目17】)。举一个极端的反例:

String s = new String("bikini"); // 不要这样做!

这种写法每次执行时都会创建一个新的 String 实例,而其实完全没有必要。字符串字面量 "bikini" 本身已经是一个 String 实例,它与构造函数生成的对象功能上完全相同。如果这种代码出现在循环或频繁调用的方法中,可能会无端创建成千上万的 String 对象,浪费资源。改进后的代码如下:

String s = "bikini";

此版本只使用了一个 String 实例,且在同一虚拟机中任何包含相同字面量的代码都会重用该对象,从而避免不必要的对象创建。

静态工厂方法的优势

在使用不可变类时,优先选择静态工厂方法(详见【条目1】),可以进一步避免创建不必要的对象。例如,Boolean.valueOf(String)Boolean(String) 构造函数更优,因为后者每次都会创建一个新对象,而前者可以选择复用已有的实例。实际上,Java 9 中已将 Boolean(String) 构造函数废弃。

除了不可变对象,某些可变对象在确保不会被修改的前提下也可以复用。对于某些开销较大的对象,反复创建显然是不划算的。这时我们可以通过缓存这些对象来实现复用,减少性能消耗。

避免重复创建昂贵对象

并不是所有对象的创建成本都是相同的。假设我们要编写一个方法,用于判断某个字符串是否是有效的罗马数字。最简单的实现方式是使用正则表达式:

// 性能可以显著提升!
static boolean isRomanNumeral(String s) {
    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

虽然这种写法简洁,但每次调用 matches 方法时,都会新建一个 Pattern 对象,并在使用后将其丢弃,导致性能下降。为了提升效率,可以将正则表达式编译为一个静态的 Pattern 对象并缓存起来:

// 通过复用昂贵对象提升性能
public class RomanNumerals {
    private static final Pattern ROMAN = Pattern.compile(
            "^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

改进后的 isRomanNumeral 方法在高频调用时性能提升显著。在我的机器上,原版本处理8字符输入需要1.1微秒,而改进版本只需0.17微秒,速度提升了6.5倍。此外,代码的可读性也有所提高,因为我们将正则表达式封装成了具有语义的静态字段 ROMAN,使代码更加清晰。

复用其他类型的对象

不仅仅是不可变对象可以复用,有时一些"适配器"(如视图)对象也可以复用。适配器对象是一种将操作委托给其他对象的模式,通常只提供不同的接口,而不包含额外的状态。例如,Map 接口的 keySet 方法返回一个视图对象,表示 Map 中的所有键。尽管 keySet 返回的实例是可变的,但其背后依赖的是同一个 Map 实例,因此无论 keySet 被调用多少次,返回的对象功能上都是相同的。

// 返回相同的 Set 视图对象
Set<String> keys1 = myMap.keySet();
Set<String> keys2 = myMap.keySet();
// keys1 和 keys2 实际上是同一个视图对象

在这种情况下,重复创建视图对象不仅没有任何好处,还会徒增对象数量,增加内存开销。

自动装箱的代价

自动装箱(autoboxing)是另一种常见的导致不必要对象创建的机制。自动装箱允许我们在基本类型和包装类型之间自由切换,编译器会自动进行类型转换。然而,自动装箱带来了性能上的隐性成本。例如,下面的代码在计算所有正整数的和时,由于一个细微的错误而导致大量不必要的对象创建:

// 极其缓慢!能发现对象创建的原因吗?
private static long sum() {
    Long sum = 0L; // 使用了包装类型 Long,而不是基本类型 long
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

上述代码每次 long 加法操作都会创建一个新的 Long 实例,导致生成约 2^31 个不必要的对象,极大地影响了性能。将 Long 换为 long 后,运行时间从 6.3 秒减少到 0.59 秒,性能提升了十倍。

对象创建并不总是昂贵

需要注意的是,本文讨论的并不是说对象创建本身是昂贵的。实际上,现代 JVM 对象创建和垃圾回收的效率非常高,创建一些轻量级对象并不会带来显著的性能开销。因此,为了提升代码的简洁性或可读性,适当地创建对象是值得的。反过来,手动管理对象池通常是一个糟糕的主意,除非对象非常重量级(例如数据库连接)。维护对象池会导致代码复杂度增加,内存占用上升,性能反而下降。

总结

不要为了性能避免创建对象,而是要避免不必要的对象创建。在可以复用对象时,应优先复用,尤其是不可变对象、昂贵的对象和适配器对象等。使用自动装箱时也应小心,避免无意中引入不必要的对象创建。同时,不要误认为对象创建一定是昂贵的——现代 JVM 的垃圾回收机制已经非常高效。只有在对象真正影响性能时,才需要关注对象的创建和复用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值