Предположим, у меня есть простой интерфейс, представляющий комплексное число, экземпляры которого будут неизменными. Для краткости я опустил очевидные методы plus
, minus
, times
и divide
, которые просто создавали бы и возвращали новый неизменный экземпляр.
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;
}
}
Итак, мой вопрос: какой из этих трех подходов является лучшим? Есть ли какой-то другой, еще лучший подход? Стоит ли вообще заботиться о производительности, придерживаться первого подхода и думать об оптимизации только тогда, когда производительность становится реальной проблемой?