Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Предпочитайте постоянство



Неизменяемый класс - это такой класс, экземпляры которого нельзя поменять.

Вся информация, содержащаяся в любом его экземпляре, записывается в момент его создания и остается неизменной в течение всего времени существования этого объекта. В библиотеках для платформы ]ауа имеется целый ряд неизменяемых классов; в том числе String, простые классы-оболочки, Biglnteger и BigDecimal. На это есть много веских причин: по сравнению с изменяемыми классами, их проще проектировать, раз­рабатывать и и<;пользовать. Они менее подвержены ошибкам и более надежны.

Делая класс неизменяемым, выполняйте следующие пять правил:

1. Не создавайте каких-либо методов, которые модифицируют представленный объект (эти методы называются мутаторами  (mutator)).

2. Убедитесь в том, что ни один метод класса не может быть переопределен. Это предотвратит потерю свойства неизменяемости данного класса в небрежном или умышленно плохо написанном подклассе. Защита методов от переопределения обычно осуществляется путем объявления класса в качестве окончательного, однако есть

и другие способы (см. ниже).

3. Сделайте все поля окончательными (final). Это ясно выразит

ваши намерения, причем в некоторой степени их будет поддерживать сама система. Это может понадобиться и для обеспечения правильного поведения программы в том случае, когда ссылка на вновь созданный экземпляр передается из одного потока в другой без выполнения синхронизации [Pugh01a] (как результат ведущихся работ

по ис~равлению модели памяти в ]ауа).

4. Сделайте все поля закрытыми (private). Это не позволит клиентам непосредственно менять значение полей. Хотя формально неизменяемые классы и могут иметь открытые поля с модификатором final, которые содержат либо значения простого типа, либо ссылки на неизменяемые объекты, делать это не рекомендуется, поскольку они будут препятствовать изменению в последующих версиях внутреннего представления класса (статья 12).

 

5. Убедитесь в монопольном доступе ко всем изменяемым компонентам. Если в вашем классе есть какие-либо поля, содержащие ссылки на изменяемые объекты, удостоверьтесь в том, что клиенты этого класса не смогут получить ссылок на эти объекты. Никогда не инициализируйте такое поле ссылкой на объект, полученной от клиента, метод доступа не должен возвращать хранящейся в этом поле ссылки на объект. При использовании конструкторов, методов доступа к полям и методов readObject (статья 56) создавайте резервные копии (defensive copies) (статья 24).

 

 

61

 

В при мерах из предыдущих статей многие классы были неизменяемыми. Так, класс PhoneNumber (статья 8) имеет метод доступа для каждого атрибута, но не имеет соответствующего мутатора. ГIредставим более сложный пример:

 

public final class Complex {

private final float rе;

pгivate final float im;

public Complex(float ге, float im) {

this. ге = ге;

this.im = im;

}

// Методы доступа без соответствующих мутаторо

 public float realPart() { return ге; }

 public float imaginaryPart() { return im; }

public Complex add(Complex с) {

return new Complex(re + С.ге, im + c,im);

}

public Complex subtract(Complex с) {

return new Complex(re - С.ге, im - c.im);

}

public Complex multiply(Complex с) {

return new Complex(re*c.гe - im*c.im, re*c.im + im*c. ге);

}

public Complex divide(Complex с) {

float tmp = с. ге*с. ге + c.im*c.im;

return new Complex((re*c. ге + im*c.im)/tmp, (im*c.re - re*c.im)/tmp);

}

public boolean equals(Object о) {

 if (о == this)

return true;

if (!(о instanceof Complex))

return false;

 

с = (Complex)o;

return (Float.floatTolntBits(re) ==Float.floatTolntBits(c.re)) && (Float.floatTolntBits(im) == Float.floatTolntBits(c.im));

}

 

// Чтобы понять,

// почему используется

// метод floatTolntBits

// см. статью 7.

 

public int hashCode() {

int result = 17 + Float.floatTolntBits(re);

result = 37*result + Float.floatTolntBits(im);

return result;

}

public String toString() {

return "(" + ге + " + " + im + "i)";

}

}

 

62

 

 

Данный класс представляет комплексное число (число с действительной и мнимой частями). Помимо обычных методов класса Object, он реализует методы доступа к действительной и мнимой частям числа, а также четыре основные арифметические операции: сложение, вычитание, умножение и деление. Обратите внимание на то, что представленные арифметические операции вместо того, чтобы менять данный экземпляр, генерируют и передают новый экземпляр класса Complex. Такой подход используется для большинства сложных неизменяемых классов. Называется это функциональным подходом (functiona! approach), поскольку рассматриваемые методы возвращают результат применения некоей функции к своему операнду, не изменяя при этом сам операнд. Альтернативой является более распространенный процедурный подход (procedura! approach), при котором метод выполняет для своего операнда некую процедуру, которая меняет его состояние.

При первом знакомстве функциональный подход может показаться искусствен­ным, однако он создает условия для неизменяемости объектов, а это имеет множество преимуществ. Неизменяемые объекты просты. Неизменяемый объект может на­ходиться только в одном состоянии - в том, с которым он был создан. Если вы удостоверитесь, что каждый конструктор класса устанавливает требуемые инвариан­ты, это будет гарантией того, что данные инварианты будут оставаться действитель­ными всегда, без каких-либо дополнительных усилий с вашей стороны и со стороны программиста, использующего этот класс. Что же касается изменяемого объекта, то он может иметь относительно сложное пространство состояний. Если в документации не представлено точного описания смены состояний, осуществляемой методами-­мутаторами, надежное использование изменяемого' класса может оказаться сложной или даже невыполнимой задачей.

 

 

63

 

Неизменяемые объекты по своей сути безопасны при работе с потоками (thread-safe): им не нужна синхронизация. Они не могут быть разрушены только из-за того, что одновременно к ним обращается несколько потоков. Несомненно, это самый простой способ добиться безопасности при работе с потоками. Действительно, ни один поток никогда не сможет обнаружить какого-либо воздействия со стороны другого потока через неизменяемый объект. По этой причине неизменяемые объекты можно свободно использовать для совместного доступа. Неизменяемые классы должны задействовать это преимущество, заставляя клиентов везде, где возможно, применять yжe существующие экземпляры. Один из простых приемов, позволяющих достичь этого: для часто используемых значений создавать константы типа publiC static final. Например, в классе Соmрех можно представить следующие константы:

publiC static final Complex ZERO = new Complex(0, 0);

 public static final Complex ONE = new Complex(1, 0);

 public static final Complex I = new Complex(0, 1);

Mожно сделать еще один шаг в этом направлении. В неизменяемом классе MO~HO предусмотреть статические методы генерации, которые кэшируют часто запрашивае­мые экземпляры вместо того, чтобы при каждом запросе создавать новые экземпляры, дублирующие уже имеющиеся. Подобные статические методы генерации есть в клас­сах 8iglnteger и 8oo1ean. Применение статических методов генерации заставляет клиентов совместно использовать уже имеющиеся экземпляры, а не создавать новые. Это снижает расход памяти и сокращает работу по ее освобождению.

Благодаря тому, что неизменяемые объекты можно свободно предоставлять для 'совместного доступа, не требуется создавать для них резервные копии (defensive copies) (статья 24). В действительности вам вообще не нужно делать никаких копий, поскольку они всегда будут идентичны оригиналу. Соответственно, для неизменяемого класса не надо, да и не следует создавать метод clone и конструктор копии (сору constructor) (статья 10). Когда платформа Java только появилась, еще не было четкого понимания этого обстоятельства, и потому класс String имеет конструктор копий. Лучше им не пользоваться (статья 4).

Можно совместно использовать не только неизменяемый объект, но и его содержимое. Например, класс 8iglnteger применяет внутреннее представление знак/модуль (sign/magnitude). Знак числа задается полем типа int, его модуль ­массивом int. Метод инвертирования negate создает новый экземпляр 8iglnteger с тем же модулем и с противоположным знаком. При этом нет необходимости копиро­вать массив, поскольку вновь созданный экземпляр 8iglnteger имеет внутри ссылку на тот же самый массив, что и исходный экземпляр.

Неизменяемые объекты образуют крупные строительные блоки для осталь­ных объектов, как изменяемых, так и неизменяемых. Гораздо легче обеспечивать поддержку инвариантов сложного объекта, если известно, что составляющие его объекты не будут менять его "снизу". Частный случай данного принципа: неизменяемый объект формирует большую схему соответствия между ключами и набором элементов.

 

 

64

 

При этом вас не должно беспокоить то, что значения, однажды записанные в эту схему или набор, вдруг поменяются, и это приведет к разрушению инвариантов схемы или набора.

Единственный настоящий недостаток неизменяемых классов заключается в том, что для каждого уникального значения им нужен отдельный объект. Создание таких объектов может потребовать больших ресурсов, особенно если они имеют значительные размеры. Предположим, что у вас есть объект Biglnteger раз­мером в миллион битов и вы хотите логически дополнить его младший бит:

Biglnteger mоbу = ...;

 Mоbу = moby.flipBit(0);

Метод flip8it создает новый экземпляр класса 8iglnteger длиной также в мил­лион битов, который отличается от своего оригинала только одним битом. Этой опера­ции требуются время и место, пропорциональные размеру экземпляра 8iglnteger. Противоположный подход использует java.util.BitSet. Как и Biglnteger, BitSet представляет последовательность битов произвольной длины, однако, в отличие от BigInteger, BitSet является изменяемым классом. В классе BitSet предусмотрен метод, позволяющий в экземпляре, содержащем миллионы битов, менять значение отдельного бита в течение фиксированного времени.

Проблема производительности усугубляется, когда вы выполняете многошаговую операцию, генерируя на каждом этапе новый объект, а в конце отбрасываете все эти объекты, оставляя только окончательный результат. Справиться с этой проблемой можно двумя способами. Во-первых, можно догадаться, какие многошаговые операции будут требоваться чаще всего, и представить их в качестве элементарных. Если много­шаговая операция реализована как элементарная (primitive), неизменяемый класс уже не обязан на каждом шаге создавать отдельный объект. Изнутри неизменяемый класс может быть сколь угодно хитроумным. Например, у класса Biglnteger есть изменяемый "класс-компаньон", который доступен только в пределах пакета и применяется для ускорения многошаговых операций, таких как возведение в степень по модулю. По всем перечисленным выше причинам использовать изменяемый класс-компаньон гораздо сложнее. Однако делать этого вам, к счастью, не надо. Разработчики класса Biglnteger уже выполнили за вас всю тяжелую работу.

Описанный прием будет работать превосходно, если вам удастся точно предска­зать, какие именно сложные многошаговые операции с вашим неизменяемым классом будут нужны клиентам. Если сделать это невозможно, самый лучший вариант ­создание открытого изменяемого класса-компаньона. В библиотеках для платформы Java такой подход демонстрирует класс String, для которого изменяемым классом­ Компаньоном является StringBuffer. В силу ряда причин BitSet вряд ли играет роль Изменяемого компаньона для Biglnteger.

Теперь, когда вы знаете, как создавать неизменяемый класс и каковы доводы за и против неизменяемости, обсудим несколько альтернативных вариантов. Напомним, что для гарантии неизменяемости класс должен запретить любое переопределение своих методов.

 

65

 

 

 Помимо возможности сделать класс окончательным (final), есть еще два способа решения этой проблемы. Первый способ: вместо того, чтобы делать окончательным сам класс, сделать окончательными все его методы, пометив как final. Единственное преимущество данного подхода заключается в том, что он позволяет программистам расширять класс, добавляя к нему новые методы, выстроенные поверх старых. По эффективности это то же самое, что вводить ,новые методы как статические в отдельном вспомогательном классе, не создающем экземп­ляров (статья 3), а потому использовать такой подход не рекомендуется.

Второй прием заключается в том, чтобы сделать все конструкторы неизменяемого класса закрытыми либо доступными только в пакете и вместо открытых конструкторов использовать открытые статические методы генерации (статья 1). Для пояснения представим, как бы выглядел класс Complex, если бы применялся такой подход:

// Неизменяемый класс со статическими методами генерации

// вместо конструкторов

publiC class Complex {

private final float ге;

  private final float im;

private Complex(float ге, float im) {

 this. ге = ге;

 this.im = im;

public static Соmрlех valueOf(float ге, float im) {

 return new Сотрlех(ге, im);}                       

// Остальное не изменилось

}

Хотя данный подход не используется широко, из трех описанных альтернатив он часто оказывается наилучшим. Он самый гибкий, так как позволяет применять несколько классов реализации, доступных в пределах пакета. для клиентов за пределами пакета этот неизменяемый класс фактически является окончательным, поскольку они не могут расширить класс, взятый из другого пакета, у которого нет ни открытого, ни защи­щенного конструктора. Помимо того, что этот метод позволяет гибко использовать несколько классов реализации, он дает возможность повысить производительность класса в последующих версиях путем совершенствования механизма кэширования объектов в статических методах генерации.

Как показано в статье 1, статические методы генерации объектов имеют множест­во преимуществ по сравнению с конструкторами. Предположим, что вы хотите создать механизм генерации комплексного числа, отталкиваясь от его полярных координат. Использовать здесь конструкторы плохо, поскольку окажется, что собст­венный конструктор класса Complex будет иметь ту же самую сигнатуру, которую мы только что применяли: Complex(float, float).

 

66

 

Со статическими методами генерации все проще - достаточно добавить второй статический метод генерации с таким назва­нием, которое четко обозначит его функцию:

public static Complex valueOfPolar(float г, float theta) {

return new Complex((float) (r * Math.cos(theta)), (float) (r * Math,sin(theta»);

}

 

 

Когда писались классы BigInteger и BigDecimal, не было согласия в том, что не­изменяемые классы должны быть фактически окончательными. Поэтому любой метод этих классов можно переопределить. К сожалению, исправить что-либо впоследствии уже было нельзя, не потеряв при этом совместимость версий снизу вверх. Поэтому, если вы пишите класс, безопасность которого зависит от неизменяемости аргумента с типом BigInteger или BigDecimal, полученного от ненадежного клиента, вы должны выполнить проверку и убедиться в том, что этот аргумент действительно является "настоящим" классом BigInteger или BigDecimal, а не экземпляром какого-либо ненадежного подкласса. Если имеет место последнее, необходимо создать резервную копию этого экземпляра, поскольку придется исходить из того, что он может оказать­ся изменяемым (статья 24):

public void foo(BigInteger b) {

if (b.getClass() != BigInteger.class)

b = new BigInteger(b.toByteArray());

}

 

Список правил для неизменяемых классов, представленный в начале статьи, гла­сит, что ни один метод не может модифицировать объект и все поля должны иметь модификатор final. Эти правила несколько строже, чем необходимо, и их можно ослабить с целью повышения производительности программы. Действительно, ни один метод не может произвести такое изменение состояния объекта, которое можно было бы увидеть извне. Вместе с тем, многие неизменяемые классы имеют одно или не­сколько избыточных полей без модификатора final, в которых они сохраняют однаж­ды полученные результаты трудоемких вычислений. Если в дальнейшем потребуется произвести те же самые вычисления, будет возвращено ранее сохраненное значение, ненужные вычисления выполняться не будут. Такая уловка работает надежно именно благодаря неизменяемости объекта: неизменность его состояния является гарантией того, что если вычисления выполнять заново, то они приведут опять к тому же результату.

Например, метод hashCode из класса PhoneNumbeT (статья 8) вычисляет хэш-код.

Получив код в первый раз, метод сохраняет его на тот случай, если потребуется вычислять его снова. Такая методика, представляющая собой классический пример отложенной инициализации (статья 48), используется также и в классе String. Никакой синхронизации здесь не требуется, поскольку никаких проблем с повторным вычислением хэша не возникает.

 

 

67

 

 

 Приведем общую идиому для кэширующей функции с отложенной инициализацией для неизменяемого объекта:

 

// Кэширующая функция с отложенной инициализацией

// для неизменяемого объекта

.private volatile Foo cachedFooVal = UNLIKELY_FOO_VALUE;

publlic Foo foo() {

Foo result = cachedFooVal;

if (result == UNLIKELY_FOO_VALUE)

result = cachedFooVal = fooValue();

return result;

}

// Закрытая вспомогательная функция, вычисляющая

// значение нашего объекта foo

private Foo fooVal() { ... }

 

Следует добавить одно предостережение, касающееся сериализуемости объектов. Если вы решили, что ваш неизменяемый класс должен реализовывать интерфейс Sеrializable, но при этом у него есть одно или несколько полей, которые ссылаются на изменяемые объекты, то вы обязаны предоставить явный метод readObject или readResolve, даже если для этого класса можно использовать сериализуемую форму, предоставляемую по умолчанию. Метод readObject, применяемый по умолчанию, позволил бы пользователю создать изменяемый экземпляр вашего во всех остальных ситуациях неизменяемого класса. Эта тема детально раскрывается в статье 56.

Подведем итоги. Не стоит для каждого метода get писать метод set. Классы должны оставаться неизменяемыми, если нет веской причины делать их изменяе­мыми. Неизменяемые классы имеют массу преимуществ, единственный же их недо­статок - возможные проблемы с производительностью при определенных условиях. Небольшие объекты значений, такие как PhoneNumber и Complex, всегда следует делать неизменяемыми. (В библиотеках для платформы Java есть несколько классов например java.util.Date и java.awt.Point, которые должны быть неизменяемыми, но та­ковыми не являются.) Вместе с тем вам следует серьезно подумать, прежде чем делать неизменяемыми более крупные объекты значений, такие как 5tring и Biglnteger. Создавать для вашего неизменяемого класса открытый изменяемый класс-компаньон следует, только если вы уверены в том, что это необходимо для получения приемле­мой производительности (статья 37) ..

Есть классы, которым неизменяемость не нужна, например классы-процессы Thread и TimerTask. Если класс нельзя сделать неизменяемым, вы должны огра­ничить его изменяемость, насколько это возможно. Чем меньше число состояний, в которых может находиться объект, тем проще рассматривать этот объект, тем меньше вероятность ошибки. По этой причине конструктор такого класса должен создавать полностью инициализированный объект, у которого все инварианты уже установлены. Конструктор не должен передавать другим методам класса объект, сформированный частично.

 

68

 

 Не создавайте открытый метод инициализации отдельно от конструктора, если только для этого нет чрезвычайно веской причины. Точно так же не следует создавать метод "повторной инициализации", который позволил бы использовать объект повторно, как если бы он был создан с другим исходным состоя­нием. Метод повторной инициализации обычно дает (если вообще дает) лишь неболь­шой выигрыш в производительности за счет увеличения сложности приложения.

Перечисленные правила иллюстрирует класс ТimerTask. Он· является изменяе­мым, однако пространство его состояний намеренно оставлено небольшим. Вы создае­те экземпляр, задаете порядок его выполнения и, возможно, отменяете это решение. Как только задача, контролируемая таймером, запускается на исполнение или отменя­ется, повторно использовать ,его вы уже не можете.

Последнее замечание, которое нужно сделать в этой статье, касается класса Complex. Этот пример предназначался лишь для того, чтобы продемонстрировать свойство неизменяемости. Он не обладает достоинствами промышленной реализации класса комплексных чисел. для умножения и деления комплексных чисел он использует обычные формулы, для которых нет правильного округления и которые имеют скудную семантику для комплексных значений NaN и бесконечности [Kahan91, Smith62, Thomas94].

 


Поделиться:



Последнее изменение этой страницы: 2019-04-11; Просмотров: 243; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.05 с.)
Главная | Случайная страница | Обратная связь