缓存方法导致不可变对象

Nat*_*tix 7 java caching immutability

假设我有一个表示复数的简单接口,其实例将是不可变的.为了简便起见,我省略了明显的plus,minus,timesdivide方法会简单地创建并返回一个新的不可变的实例.

public interface Complex {

    double real();

    double imaginary();

    double absolute();

    double angle();

}
Run Code Online (Sandbox Code Playgroud)

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

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我想出的第三种可能性是在第一次需要时懒洋洋地计算绝对值和角度.但正如您所看到的,这会使代码变得混乱且容易出错.另外,我不确定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;
    }

}
Run Code Online (Sandbox Code Playgroud)

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

mik*_*era 9

我每次都会去NonCachingComplex.

原因:

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

在其他方面,LazyCachingComplex特别糟糕,因为它使用绝对和角度的盒装值(这意味着访问的额外内存取消引用,加上两个额外的大量对象开销).我认为这不太可能曾经看到这样一个性能优势.

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