缓存方法导致不可变对象

时间:2012-02-22 19:11:02

标签: java caching immutability

假设我有一个表示复数的简单接口,其实例将是不可变的。为简洁起见,我省略了明显的plusminustimesdivide方法,它们只是创建并返回一个新的不可变实例。

public interface Complex {

    double real();

    double imaginary();

    double absolute();

    double angle();

}

现在的问题是,将此实现为不可变类的最佳方法是什么?最简单直接的“我关心性能仅在问题出现时”的方法是将实部和虚部存储为最终字段,并计算每次调用这些方法时的绝对值和角度。这使得类变得小而简单,但显然最后两个方法每次都返回相同的结果。

public final class NonCachingComplex implements Complex {

    private final double real;
    private final double imaginary;

    public NonCachingComplex(double real, double imaginary) {
        this.real = real;
        this.imaginary = imaginary;
    }

    @Override public double real() {
        return real;
    }

    @Override public double imaginary() {
        return imaginary;
    }

    @Override public double absolute() {
        return Math.sqrt((real * real) + (imaginary * imaginary));
    }

    @Override public double angle() {
        return absolute() == 0 ? 0 : (Math.acos(real / absolute()) * Math.signum(imaginary));
    }
}

那么为什么不在创作时将绝对值和角度保存到一个字段中呢?好吧,很明显,类的内存占用现在有点大了,而且,如果这两种方法很少被调用,那么计算每个创建的实例的结果也可能会产生相反的效果。

public final class EagerCachingComplex implements Complex {

    private final double real;
    private final double imaginary;

    private final double absolute;
    private final double angle;

    public EagerCachingComplex(double real, double imaginary) {
        this.real = real;
        this.imaginary = imaginary;
        this.absolute = Math.sqrt((real * real) + (imaginary * imaginary));
        this.angle = absolute == 0 ? 0 : (Math.acos(real / absolute()) * Math.signum(imaginary));
    }

    // real() and imaginary() stay the same...

    @Override public double absolute() {
        return absolute;
    }

    @Override public double angle() {
        return angle;
    }
}

我想出的第三种可能性是在第一次需要时懒洋洋地计算绝对值和角度。但正如您所看到的,这会使代码变得混乱且容易出错。此外,我不确定volatile修饰符在这种情况下是否真正正确使用。

public final class LazyCachingComplex implements Complex {

    private final double real;
    private final double imaginary;

    private volatile Double absolute;
    private volatile Double angle;

    public LazyCachingComplex(double real, double imaginary) {
        this.real = real;
        this.imaginary = imaginary;
    }

    // real() and imaginary() stay the same...

    @Override public double absolute() {
        if (absolute == null) {
            absolute = Math.sqrt((real * real) + (imaginary * imaginary));
        }
        return absolute;
    }

    @Override public double angle() {
        if (angle == null) {
            angle = absolute() == 0 ? 0 : (Math.acos(real / absolute()) * Math.signum(imaginary));
        }
        return angle;
    }

}

所以我的问题是,这三种方法中哪一种最好?还有其他一些更好的方法吗?我是否应该关心性能并坚持第一种方法,只有在性能成为真正的问题时才考虑优化?

2 个答案:

答案 0 :(得分:9)

我每次都会去NonCachingComplex。

原因:

  • 这是最简单的 - 所以你应该先用这种方式编写它,如果你通过基准测试证明它是必要的,那么只会使事情变得更复杂。避免过早优化和所有这些!
  • 计算绝对()和角度()的公式可能不足以证明缓存。现代CPU上的浮点运算速度非常快,通常甚至比从内存中获取值更快。
  • 最低内存占用 - 这不仅有利于减少代码的整体内存消耗,而且还可以提高性能,因为更多数据将适合更高速的处理器缓存。对于某些工作集大小,这可能会产生很大的不同。

其他的,LazyCachingComplex特别糟糕,因为它使用绝对和角度的盒装值(这意味着访问的额外内存取消引用,加上两个额外的对象开销)。我认为永远不会看到这样做会带来性能上的好处。

请注意,如果你真的关心性能,那么你也不会使用复杂的接口 - 最好的表现来自直接制作最终的Complex类,并直接在你的代码中引用这个类。通过接口进行的方法调用(稍微)比最终类的方法调用更加昂贵。

答案 1 :(得分:2)

我确定这不是你要找的答案,但我认为这取决于;)

我个人认为懒惰的init根本就不好。但是我会根据您的应用程序的需求来衡量您的选择 - 性能真的是一个问题吗?您是否为同一对象多次使用绝对值和角度值?这些字段的计算真的很慢吗?

我的经验法则通常是尽可能保持代码干净(最小),只有在显示需要时才增加复杂性。在那之前我会坚持你的NonCaching版本。