Предположим, у меня есть простой интерфейс, представляющий комплексное число, экземпляры которого были бы неизменяемыми. Для краткости я исключены очевидные методы плюс
, минус
, умножить на
и разделить
методы, которые просто создают и возвращают новый неизменяемый экземпляр.
public interface Complex {
double real();
double imaginary();
double absolute();
double angle();
}
Теперь вопрос в том, как лучше всего реализовать его как неизменяемый класс? Самый простой и понятный подход «Я забочусь о производительности только тогда, когда это проблема» - это сохранить реальный и воображаемый класс. inary части как конечные поля и вычисляют абсолютное значение и угол при каждом вызове этих методов. Благодаря этому класс остается небольшим и простым, но очевидно, что последние два метода каждый раз возвращают один и тот же результат.
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;
}
}
Итак, мой вопрос: какой из этих трех подходов лучший? Есть ли еще лучший подход? Должен ли я вообще заботиться о производительности и придерживаться первого подхода и думать об оптимизации только тогда, когда производительность становится реальной проблемой?