Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27.13 Mб
Скачать

Глава 4 Классы и интерфейсы

В открытых классах используйте методы доступа, а не открытые поля

Иногда вы можете поддаться искушению написать вырожден­ ные классы, служащие только для одной цели — группировать поля экземпляров.

// Вырожденные классы, подобные этому, не должны быть открытыми class Point {

public double x; public double y;

}

Поскольку доступ к таким классам осуществляется через поле дан­ ных, они лишены преимуществ инкапсуляции (статья 13). Вы не можете поменять структуру такого класса, не изменив его API. Вы не можете использовать каких-либо инвариантов. Вы также не можете предпринять каких-либо дополнительных действий, когда меняется значение поля. Для программистов, строго придерживающихся объектно ориентирован­ ного подхода, такой класс заслуживает осуждения и в любом случае его следует заменить классом с закрытыми полями и открытыми методами доступа и, для изменяемых классов, мутаторами (установщиками):

// Инкапсуляция данных методами доступа и мутаторами class Point {

private double x; private double y;

public Point(double x, double y) { this.x = x;

this.у = y;

}

public dounle getX() { return x; } public double getY() { return y; }

public void setX(double x) { this.x = x; } public void setY(double y) { this.у = у; }

}

100

С татья 14

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

Однако если класс доступен только в пределах пакета или является закрытым вложенным классом, то никакого настояще­ го ущерба от прямого доступа к его полям с данными не будет,

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

Несколько классов в библиотеках для платформы Java наруша­ ют данный совет не предоставлять непосредственного доступа к по­ лям открытого класса. В глаза бросаются такие примеры, как классы Point и Dimension из пакета java.awt. Вместо того чтобы соответство­ вать этим классам, их следует рассматривать как предупреждение. В статье 55 рассказывается, как решение раскрыть внутреннее со­ держание класса Dimension привело к серьезным проблемам с про­ изводительностью, которые нельзя было разрешить, не затрагивая клиентов.

Хотя и это и плохая идея, если открытый класс раскрывает поля напрямую, это все же менее опасно, чем если поля являются неизме­

101

Глава 4 Классы и интерфейсы

няемыми. Невозможно изменить представление такого класса, не по­ меняв его API, и вы не можете делать вспомогательных действий во время чтения поля, но вы можете ввести инвариант. В следующем примере класс должен гарантировать, что каждый экземпляр пред­ ставляет верное время:

// Открытый класс с раскрытым неизменяемым полем - спорно. public final class Time {

private static final int HOURS_PER_DAY = 24; private static final int MINUTES_PER_HOUR = 60; public final int hour;

public final int minute;

public Time(int hour, int minute) {

if (hour < 0”|[ hour >= H0URS_PER_DAY)

throw new IllegalArgumentException(“Hour: “ + hour); if (minute < 0 || minute >= MINUTES_PER_HOUR)

throw new IllegalArgumentException(“Min:” + minute); this.hour = hour;

this.minute = minute;

}

... // Остальное опущено.

}

Подведем итоги. Открытые классы никогда не должны раскры­ вать неизменяемые поля. Это не так опасно, но тем не менее спорно, чтобы открытые классы раскрывали неизменяемые поля. Иногда тре­ буется классам, открытым в рамках пакета, или закрытым вложен­ ным классам раскрывать поля вне зависимости от их изменяемости.

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

Неизменяемый класс — это просто такой класс, экземпляры ко­ торого нельзя поменять. Вся информация, которая содержится в лю­ бом его экземпляре, записывается в момент его создания и остается

102

С татья 15

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

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

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

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

3. Сделайте все поля окончательными (final). Это ясно вы­ разит ваши намерения, причем в некоторой степени их будет поддерживать сама система. Это может понадобиться для того, чтобы обеспечить правильное поведение программы в том случае, когда ссылка на вновь созданный экземпляр передается от одного потока в другой без выполнения син­ хронизации, известная как модель памяти (memory model) [Л Д 17.5; Goetz06, 16].

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

ЮЗ

Глава 4 Классы и интерфейсы

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

Впримерах из предыдущих статей многие классы были неиз­ меняемыми. Один из таких классов — PhoneNumber (статья 9) имеет метод доступа для каждого атрибута, но не имеет соответствующего мутатора. Представим чуть более сложный пример:

public final class Complex { private final double re; private final double im;

public Complex(double re, double im) { this.re = re;

this.im = im;

}

// Методы доступа без соответствующих мутаторов public double realPartO { return re; } public double imaginaryPart() { return im; } public Complex add(Complex c) {

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

}

public Complex subtract(Complex c) {

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

}

public Complex multiply(Complex c) {

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

}

104

С татья 15

public Complex divide(Complex c) { double tmp = c.re*c.re + c.im*c.im;

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

}

@0verride public boolean equals(Object o) {

 

if

(o == this)

 

 

 

return

true;

 

 

if

(!(o instanceof Complex))

 

 

 

return

false;

 

 

Complex c = (Complex) o;

 

 

 

return

Double.compare(re,

c.re)

== 0 &&

 

Double.compare(im, c.im)

== 0;

 

}

 

 

 

 

@0verride public int hashCode() {

 

 

int

result = 17 + hashDouble(re);

 

result = 31 * result + hashDouble(im);

 

return result;

 

 

}

 

 

 

 

private static int hashDouble(double val) {

 

long lingBits

= Double.doubleToLongBits(val);

return (int)

(longBits " (longBits » >

32));

}

 

 

 

 

@0verride public String toStringO {

 

 

return "(" +

re + “ + 1 + im + "i)”;

 

}

Данный класс представляет комплексное число (число с действи­ тельной и мнимой частями). Помимо обычных методов класса Obj ect он реализует методы доступа к действительной и мнимой частям чис­ ла, а также четыре основные арифметические операции: сложение, вычитание, умножение и деление. Обратите внимание, что представ­ ленные арифметические операции вместо того, чтобы менять данный экземпляр, генерируют и передают новый экземпляр класса Complex. Такой подход используется для большинства сложных неизменяе­ мых классов. Называется это функциональным подходом (functional

105

Глава 4 Классы и интерфейсы

approach), поскольку рассматриваемые методы возвращают резуль­ тат применения некой функции к своему операнду, не изменяя при этом сам операнд. Альтернативой является более распространенный процедурный, или императивный, подход (procedural or imperative approach), при котором метод выполняет для своего операнда некую процедуру, которая меняет его состояние.

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

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

106

С тать я 15

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);

Можно сделать еще один шаг в этом направлении. В неизме­ няемом классе можно предусмотреть статические методы генерации (статья 1), которые кэшируют часто запрашиваемые экземпляры вместо того, чтобы при каждом запросе создавать новые экземпля­ ры, дублирующие уже имеющиеся. Все упаковываемые примитив­ ные классы и классы BigIntegeг делают это. Использование таких статических методов генерации заставляет клиентов вместе исполь­ зовать уже имеющиеся экземпляры, вместо того чтобы создавать но­ вые. Это снижает расход памяти и сокращает работу по освобожде­ нию памяти. Выбор методов статической генерации вместо открытых конструкторов дает на этапе создания нового класса возможность добавлять кэширование позже, не изменяя при этом клиента.

Благодаря тому, что неизменяемые объекты можно свободно предоставлять для совместного доступа, не требуется создавать для них резервные копии (defensive copies, статья 39). В действительно­ сти вам вообще не надо делать никаких копий, поскольку они всег­ да будут идентичны оригиналу. Соответственно, для неизменяемого класса вам не нужно, да и не следует создавать метод clone и кон­ структор копий (copy constructor, статья 11). Когда платформа Java только появилась, еще не было четкого понимания этого обстоятель­ ства, и потому класс String сейчас имеет конструктор копий, но он редко используется, если используется вообще (статья 3).

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

107

Глава 4 Классы и интерфейсы

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

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

Biglnteger moby = ... ; moby = moby.flipBit(O);

Метод flipBit создает новый экземпляр класса Biglnteger дли­ ной также в миллион бит, который отличается от своего оригинала только одним битом. Эта операция требует времени и места, про­ порциональных размеру экземпляра Biglnteger. Противополож­

ный подход использует java. util. BitSet. Как и Biglnteger, BitSet

представляет последовательность битов произвольной длины, одна­ ко, в отличие от Biglnteger, BitSet является изменяемым классом. В классе BitSet предусмотрен метод, позволяющий в экземпляре, содержащем миллионы битов, менять значение отдельного бита в те­ чение фиксированного времени.

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

108

С тать я 15

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

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

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

статические методы генерации (статья 1).

Для пояснения представим, как бы выглядел класс Complex, если бы применялся такой подход:

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

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

109

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]