Как вы уже знаете, "передавая" объект в качестве параметра на самом деле вы оперируете лишь ссылками на этот объект.
Практически все языки программирования предоставляют набор "стандартных" средств для операций с объектами и в большинстве случаев они прекрасно работают. Однако всегда существует граница, когда эти средства перестают работать и работа существенно усложняется (или, в случае с Си++, предельно усложняется). Java в этом плане также не является исключением, поэтому очень важно чтобы вы четко представляли себе возможные последствия своих манипуляций с объектами, и "Приложение А" поможет Вам в этом.
Если у Вас есть опыт работы с другими языками программирования, то тему этого Приложения можно сформулировать как: "Есть ли в языке Java указатели?". Многие разработчики считают использование указателей чересчур сложным и опасным. Поскольку Java - самый совершенный язык программирования, созданный дабы избавить вас от рутины, в нем не должно быть подобных сомнительных элементов. Тем не менее, правильнее все же будет сказать что указатели в Java есть. Действительно, все идентификаторы объектов в Java (кроме примитивов) по сути являются указателями, но использование таких указателей ограничено и защищено, причем не только на этапе трансляции, но и на этапе исполнения. Иными словами, в Java есть указатели но отсутствуют арифметические операции над ними. В дальнейшем я буду называть их "ссылками", а вы можете думать о них как о "безопасных указателях". Они очень напоминают безопасные ножницы, применяемых на уроках труда в начальной школе - у них затупленные концы, которыми практически невозможно пораниться, но из-за этого работа с ними продвигается медленно и чрезвычайно утомительна.
Передавая ссылку другому методу в качестве параметра, новая ссылка будет продолжать указывать на тот же самый объект. Следующий простейший пример наглядно это демонстрирует:
//: Приложение а:PassReferences.java // Передача ссылок. public class PassReferences { static void f(PassReferences h) { System.out.println("h внутри f(): " + h); } public static void main(String[] args) { PassReferences p = new PassReferences(); System.out.println("p внутри main(): " + p); f(p); } } ///:~
В этом примере при выводе результатов на экран автоматически вызывается метод toString(), а PassReferences наследуется непосредственно из класса Object, без переопределения метода toString(). Таким образом, при распечатке названия класса объекта и его адреса (не ссылки, а физического адреса по которому размещается объект) используется метод toString() класса Object. Результат работы примера:
p внутри main(): PassReferences@1653748 h внутри f(): PassReferences@1653748
Как вы видете, p и h ссылаются на один и тот же объект. Это более эффективно чем дублирование самого объекта PassReferences лишь для передачи параметра методу, но в то же время сопряжено с серьезными проблемами.
Термин "дублирующие ссылки" означает что с одним и тем же объектом связана более чем одна ссылка. Проблема с дублирующими ссылками возникают при попытке изменения данных в объекте. Если владельцы других ссылок не ожидают что объект изменился, такой поворот судьбы может преподнести им неприятный сюрприз. Приведем пример:
//: Приложение А:Alias1.java // Две дублирующие ссылки на один и тот же объект. public class Alias1 { int i; Alias1(int ii) { i = ii; } public static void main(String[] args) { Alias1 x = new Alias1(7); Alias1 y = x; // Дублирующая ссылка System.out.println("x: " + x.i); System.out.println("y: " + y.i); System.out.println("Увеличиваем x"); x.i++; System.out.println("x: " + x.i); System.out.println("y: " + y.i); } } ///:~
В строке:
Alias1 y = x; // Дублирующая ссылка
создается новая ссылка Alias1, но вместо того чтобы указывать на созданный с использованием команды new новый объект, ей присваивается значение уже существующей ссылки. Следовательно, содержимое ссылки x (то есть адрес расположения объекта, на который указывает эта ссылка) присваивается ссылке y. Таким образом обе ссылки x и y связаны с одним и тем же объектом и увеличение значения x.i в выражении:
x.i++;
также повлечет за собой изменение значения y.i, что и наблюдается в результате выполнения примера:
x: 7 y: 7 Увеличиваем x x: 8 y: 8
Единственный способ избежать подобных ситуаций - отказ от использования дублирующих ссылок. Постарайтесь в своих программах не допускать одновременного существования более одной ссылки на один и тот же объект. Это сделает код ваших программ более удобочитаемым и простым в отладке. Однако, при передаче сслыки другому методу в качестве параметра (Java позволяет такие операции) эта ссылка автоматически дублируется и операции совершаемые с ней в методе могут влиять на состояние "внешнего" объекта (т.е. на объект, созданный вне данного метода). Например:
//: Приложение А:Alias2.java // Вызванный метод изменяет внешний объект // используя передаваемую в качестве параметра ссылку. public class Alias2 { int i; Alias2(int ii) { i = ii; } static void f(Alias2 reference) { reference.i++; } public static void main(String[] args) { Alias2 x = new Alias2(7); System.out.println("x: " + x.i); System.out.println("Вызов метода f(x)"); f(x); System.out.println("x: " + x.i); } } ///:~
Результатом будет:
x: 7 Вызов метода f(x) x: 8
Метод используя ссылку, передаваемую ему в качестве параметра, изменяет внешний объект. В подобных случаях вам следует предусмотреть все возможные негативные последствия, чтобы эти изменения не оказались неожиданностью для пользователя и не привели к сбоям в работе программы.
То есть, создавая метод, изменяющий внешние объекты, вы должны четко проинформировать пользователя о результатах его выполнения. Но, во избежание "волчьих ям", лучше воздерживаться от изменения внешних объектов.
Если вам все же необходимо внести изменения в объект, переданный в качестве параметра, но при этом вы не хотите изменять внешний объект (т.е. изменения будут внесены лишь на время выполнения данного метода), тогда вам следует предварительно скопировать его в вашем методе. Тому как это лучше сделать и будет посвящена большая часть этого Приложения.
Итак, подведем итог вышеизложенному. В Java при передаче параметров методам используются ссылки, поэтому на самом деле "передавая объект" другому методу, на самом деле передается лишь ссылка на этот объект, а сам объект находится за пределами данного метода и все операции, совершаемые с этой ссылкой влекут за собой изменения во внешнем объекте. И еще:
Если вы используете объект только для чтения, можете смело передавать ссылку на объект. Однако, иногда возникает необходимость работы с объектом на "локальном уровне" таким образом, чтобы все вносимые в объект изменения распространялись только на его локальную копию и не изменяли внешний объект. Во многих языках программирования существуют механизмы автоматического создания локальных копий внешнего объекта при работе с методом [79]. В Java таких механизмов нет, но зато есть все необходимые для этого средства.
Тут необходимо внести ясность в понимание термина "передача параметров по значению" и то как он реализуется в программе. Суть метода заключается в использовании локальных копий параметров, передаваемых вашему методу. Камнем преткновения является различное отношение к передаваемым параметрам. Существуют два наиболее распространенных взгляда на параметры:
Итак, рассмотрев обе точки зрения, я скажу так: "Все это зависит лишь от вашего представления о ссылках." Теперь вернемся к нашей проблеме. В конце концов, это не так важно, гораздо важнее понимание того, что передача ссылок в качестве параметров может привести к неожиданным изменениям внешних объектов.
Наиболее часто клонирование применяется в тех случаях, когда в процессе работы метода необходимо внести изменения в объект, не изменяя при этом внешний объект. Для создания локальной копии объекта надо воспользоваться методом clone(). Это защищенный (protected) метод базового класса Object и все что от вас требуется, это переопределить его как public во всех классах, которые вы собираетесь клонировать. Например, переопределим метод clone() для класса стандартной библиотеки ArrayList, для дальнейшего использования clone() применительно к ArrayList:
//: Приложение А:Cloning.java // Операция clone() работает только для // нескольких элементов стандартной библиотеки Java. import java.util.*; class Int { private int i; public Int(int ii) { i = ii; } public void increment() { i++; } public String toString() { return Integer.toString(i); } } public class Cloning { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++ ) v.add(new Int(i)); System.out.println("v: " + v); ArrayList v2 = (ArrayList)v.clone(); // Увеличение всех элементов v2: for(Iterator e = v2.iterator(); e.hasNext(); ) ((Int)e.next()).increment(); // Проверка изменения элементов v: System.out.println("v: " + v); } } ///:~
Метод clone() создает объект типа Object, который затем должен быть преобразован в объект нужного типа. Из примера видно что метод clone() объекта ArrayList не выполняет автоматическое клонирование всех объектов, которые содержатся в ArrayList - старый ArrayList и клонированный ArrayList являются дублирующими ссылками одного и того же объекта. Это так называемое поверхностное копирование, когда копируется только "поверхность" объекта. Сам объект содержит так называемую "поверхность" плюс все те объекты, на которые указывают ссылки внутри него, плюс все те объекты, на которые в свою очередь ссылаются те объекты, и т.д. Такое явление называется "сетью объектов", а полное копирование всей этой сложной структуры называется глубоким копированием.
В приведенном выше примере вы можете наблюдать результат поверхностного копирования, при котором операции, совершаемые с v2 отражаются на состоянии v:
v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
v: [1, 2, 3, 4, 5, 6, 7, 8, 9,
10]
Не следует использовать clone() для клонирования объектов, содержащихся в ArrayList, поскольку нет никаких гарантий что эти объекты будут клонируемыми (cloneable) [80].
Несмотря на то что метод клонирования определен в классе Object, являющемся базовым для всех классов Java, это не означает что он автоматически может быть применен к любому классу [81]. Казалось бы, это идет в разрез с принципом наследования дочерними объектами методов родительских классов. Действительно, в Java клонирование идет вразрез с этим принципом. Поэтому, если вы хотите сделать эту функцию доступной для вашего класса, вы должны написать соответствующий код, обеспечивающий правильную работу метода клонирования.
Для блокирования возможности клонирования во всех классах Java, в базовом классе Object метод clone() был описан как защищенный (protected). Это не только исключает возможность использования метода клонирования программистом, просто использующим (не расширяющим) этот класс, но и означает что вы не можете использовать clone() используя ссылку на базовый класс. (Хотя это может показаться полезным. Например, при полиморфном клонировании связок классов Object). Такой метод применен для того, чтобы на этапе компиляции информировать о том что данный объект является неклонируемым. Как ни странно, большинство классов стандартных библиотек Java неклонируемые. Поэтому, написав:
Integer x = new Integer(1);
x =
x.clone();
на этапе компиляции это приведет к возникновению ошибки. Компилятор выдаст сообщение о том что метод clone() недоступен (поскольку Integer не переопределяет его и он по умолчанию является защищенным (protected)). Однако, если вы работаете с классом, производным от Object (а это все классы языка Java), то у вас есть возможность вызвать метод Object.clone(), поскольку этот метод является защищенным (protected), а ваш объект является объектом-наследником по отношению к классу Object. Метод clone() класса Object обладает полезными функциональными возможностями - он осуществляет поразрядное дублирование передаваемого класса объекта, что и является основной операцией при клонировании объекта. Тем не менее вам будет необходимо написать собственный метод клонирования и описать ее как public. Итак, два ключевых момента при реализации клонировании это:
В дальнейшем вам возможно понтребуется переопределить ваш метод clone() для классов-наследников, поскольку иначе при их клонировании будет использоваться ваш (теперь уже public) метод clone(), который может не выполнять своих функций для этих классов (хотя, поскольку создание копии самого объекта осуществляет метод Object.clone(), подобных проблем может и не быть). Такой прием с переопределением защищенного (protected) метода clone() может применяться только когда вы наследуете не клонируемый класс и хотите на его базе создать класс, поддерживающий клонирование. При этом для все классы, наследующие ваш класс, в свою очередь унаследуют и созданный вами метод clone(), поскольку в Java нельзя изменять статус наследуемых методов. Иными словами, если ваш класс является клонируемым, то и все наследующие его классы также будут клонируемыми, если только вы не примените приемы "отключения" клонируемости (они подробно рассмотрены далее).
Для создания клонируемых объектов вам понадобятся навыки реализации Cloneable интерфейса. Этот интерфейс примечателен уже тем, что он совершенно пустой!
interface Cloneable {}
Очевидно, что причины наследования пустого интерфейса никак не связаны с последующим использованием его методов. В данном случае интерфейс используется в нестандартных целях. Он служит своего рода "меткой" для типа класса. Существуют две причины существования интерфейса Cloneable. Первая заключается в том, что вы можете использовать ссылки на базовый тип и при этом не знать, является ли он клонируемым или нет. В таких случаях вы можете использовать ключевое слово instanceof (рассмотренное в главе 12) для выяснения, является ли объект с которым связана ссылка клнируемым:
if(myReference instanceof Cloneable) // ...
Вторая причина связана с вышеупомянутой блокировкой клонирования в классах. Перед началом работы метод Object.clone() осуществляет проверку класса на реализацию интерфейса Cloneable и, если класс не реализует этот интерфейс, возвращает значение CloneNotSupportedException. Поэтому для поддержки клонирования вы вынуждены реализовать интерфейс Cloneable.
Теперь, когда вы познакомились с нюансами реализации метода clone(), можно приступить к созданию классов, дублируемых с созданием локальных копий.
//: Приложение А:LocalCopy.java // Создание локальных копий используя метод clone(). import java.util.*; class MyObject implements Cloneable { int i; MyObject(int ii) { i = ii; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("MyObject не может быть клонирован"); } return o; } public String toString() { return Integer.toString(i); } } public class LocalCopy { static MyObject g(MyObject v) { // Передача ссылки, которая изменяет внешний объект: v.i++; return v; } static MyObject f(MyObject v) { v = (MyObject)v.clone(); // Локальная копия v.i++; return v; } public static void main(String[] args) { MyObject a = new MyObject(11); MyObject b = g(a); // Проверка ссылок (не объектов) на равенство if(a == b) System.out.println("a == b"); else System.out.println("a != b"); System.out.println("a = " + a); System.out.println("b = " + b); MyObject c = new MyObject(47); MyObject d = f(c); if(c == d) System.out.println("c == d"); else System.out.println("c != d"); System.out.println("c = " + c); System.out.println("d = " + d); } } ///:~
Прежде всего, метод clone() должен быть общедоступным, т.е. должен быть переопределен как public. Во-вторых, в первых строках вашего метода clone() должен находиться вызов базового метода clone(). Вызываемый таким образом метод clone() принадлежит классу Object, и вы имеете возможность его вызова, поскольку он определен как protected и потому доступен для дочерних классов.
Метод Object.clone() определяет размер объекта, выделяет необходимое количество свободной памяти для создания копии и осуществляет побитное копирование. Эта процедура называется поразрядным копированием и является сутью клонирования. Но перед выполнением этих операций Object.clone() выполняет проверку, является ли копируемый объект клонируемым - то есть, реализует ли он интерфейс Cloneable. Если нет - Object.clone() возвращает исключительную ситуацию CloneNotSupportedException, сигнализирующую о том, что данный объект не может быть клонирован. Таким образом вы должны поместить вызов метода super.clone( ) в блок операторов try-catch, чтобы перехватывать и обрабатывать подобные ситуации, которые не должны возникнуть (поскольку вы реализуете интерфейс Clonable).
В приведенном выше примере методы g() и f() класса LocalCopy демонстрируют различие между двумя способами передачи параметра. g() демонстрирует передачу по ссылке, которую он изменяет вне объекта, а затем возвращается ссылка на этот внешний объект. f() клонирует параметр, а затем отключает его, таким образом оставляя лишь первоначальный объект. После этого с объектом могут совершаться любые операции, вплоть до возвращения ссылки на него, и это никак не отразится на объекте-оригинале. Обратите свое внимание на любопытное выражение:
v = (MyObject)v.clone();
Именно таким образом осуществляется локальная копия. Чтобы предотвратить неразбериху, связанную с использованием такого выражения, хорошо запомните что такая довольно необычная идиома вполне типична для Java, поскольку все идентификаторы объектов являются ссылками. Поэтому ссылка v с помощью метода clone() используется для создания копии объекта, на который она ссылается, и в результате данной операции возвращается ссылка на базовый тип Object (поскольку он обозначен таким образом в Object.clone()) и должен затем быть приведен к соответствующему типу.
Выполнение main() позволяет наблюдать разницу между этими двумя методами передачи:
a == b a = 12 b = 12 c != d c = 47 d = 48
Важно отметить что при проверке на равенство ссылок в Java не происходит сравнения самих значений переменных, содержащихся в этих объектах. Операторы == и != просто сравнивают сами ссылки. Если адреса ссылок совпадают, значит обе ссылки указывают на один и тот же объект и следовательно они "равны". Таким образом, на самом деле операторы лишь проверяют, являются ли ссылки дублирующими ссылками на один и тот же объект.
Что же происходит при вызове Object.clone() и чем вызвана необходимость вызова метода super.clone() при переопределении метода clone() в вашем классе? Метод clone() базового класса отвечает за выделение необходимого количества памяти для хранения и поразрядного копирования битов из базового класса в новый объект. Но это не просто хранение и копирование объекта, а скорее полное воссоздание внешнего объекта.
Все эти операции описаны в коде метода clone базового класса (который был написан при отсутствии какой-либо информации о структуре классов, которые будут его наследовать), можно предположить что для определения клонируемого объекта использована технология RTTI. как бы там ни было, метод clone() может осуществлять операции по выделению памяти и осуществлять копирование классов этого типа.
Что бы вы ни делали, первой операцией вашего метода clone() должен быть вызов метода super.clone(). Эта операция является основой операции клонирования и обеспечивает создание точного дубликата. Далее могут следовать другие операции, необходимые для завершения клонирования.
Для того, чтобы определиться в этой операции вы должны четко представлять себе что выполняет Object.clone(). В частности, осуществляет ли он автоматическое копирование объектов, на которые указывают ссылки? Ответ на этот вопрос мы получим из следующего примера:
//: Приложение А:Snake.java // Тестирует клонирование для определения // было ли клонировано содержание ссылок на другие объекты public class Snake implements Cloneable { private Snake next; private char c; // Значение i == количеству сегментов Snake(int i, char x) { c = x; if(--i > 0) next = new Snake(i, (char)(x + 1)); } void increment() { c++; if(next != null) next.increment(); } public String toString() { String s = ":" + c; if(next != null) s += next.toString(); return s; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Змея не может быть клонирована"); } return o; } public static void main(String[] args) { Snake s = new Snake(5, 'a'); System.out.println("s = " + s); Snake s2 = (Snake)s.clone(); System.out.println("s2 = " + s2); s.increment(); System.out.println( "after s.increment, s2 = " + s2); } } ///:~
Объект Snake (змея) состоит из нескольких сегментов, каждый из которых также принадлежит типу Snake и по сути представляет собой связанный одиночными связями список. Сегменты создаются рекурсивно с уменьшением значения первого параметра конструктора до тех пор, пока тот не примет нулевое значение. Для того, чтобы присвоить каждому сегменту уникальную метку, при каждом очередном рекурсивном вызове конструктора значение второго параметра конструктора типа char увеличивается.
Метод increment() рекурсивно увеличивает каждую метку, а метод toString() рекурсивно печатает каждую метку:
s = :a:b:c:d:e
s2 = :a:b:c:d:e
после s.increment, s2 = :a:c:d:e:f
Это означает что метод Object.clone() создал дубликат только первого сегмента, то есть осуществил поверхностное копирование. Если вы хотите создать дубликат всей змеи (произвести глубокое копирование), вам понадобится включить дополнительный код в переопределенный метод clone().
Для этого вы, как всегда, должны вызвать метод super.clone(), чтобы быть уверенными что для любого унаследованного от клонируемого класса будут выполнены все необходимые операции (включая вызов метода Object.clone()). Затем требуется явный вызов метода clone() для всех ссылок, присутствующих в вашем объекте, иначе эти ссылки окажутся всего лишь дублирующими ссылками на исходные объекты. Это аналогично тому как осуществляется вызов конструктора: сначала конструктор базового класса, а затем конструктор его ближайшего класса-наследника. и так далее вплоть до вызова конструктора самого удаленного класса. Разница заключается в том, что метод clone() не является конструктором и поэтому ничто в нем не происходит автоматически. Вам придется реализовать эти функции самостоятельно.
Существует одна проблема, с которой вам придется столкнуться при реализации глубокого копирования составных объектов. Вы должны предусмотреть выполнение методом clone() глубокого копирования ссылок для составляющих его объектов, а затем, в свою очередь, для ссылок этих объектов и так далее. Это необходимое условие глубокого копирования. Таким образом, вы должны владеть, или по крайней мере, располагать достаточными знаниями о коде всех классов, участвующих в глубоком копировании и быть уверенными в том что их собственные механизмы глубокого копирования работают безотказно.
Следующий пример показывает последовательность операций для осуществления глубокого копирования составного объекта:
//: Приложение А:DeepCopy.java // Клонирование составных объектов class DepthReading implements Cloneable { private double depth; public DepthReading(double depth) { this.depth = depth; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } return o; } } class TemperatureReading implements Cloneable { private long time; private double temperature; public TemperatureReading(double temperature) { time = System.currentTimeMillis(); this.temperature = temperature; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } return o; } } class OceanReading implements Cloneable { private DepthReading depth; private TemperatureReading temperature; public OceanReading(double tdata, double ddata){ temperature = new TemperatureReading(tdata); depth = new DepthReading(ddata); } public Object clone() { OceanReading o = null; try { o = (OceanReading)super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } // Необходимо клонировать ссылку: o.depth = (DepthReading)o.depth.clone(); o.temperature = (TemperatureReading)o.temperature.clone(); return o; // Передаем его в Object } } public class DeepCopy { public static void main(String[] args) { OceanReading reading = new OceanReading(33.9, 100.5); // Теперь клонируем его: OceanReading r = (OceanReading)reading.clone(); } } ///:~
Классы DephReading (измерение глубины) и TemperatureReading (измерение температуры) очень похожи, они оба содержат только примитивы. Следовательно и метод clone() для этих классов также предельно прост: он вызывает super.clone() и возвращает результат. Заметьте что код для обоих методов clone() абсолютно идентичен.
OceanReading (исследование океана) состоит из объектов DephReading и TemperatureReading и поэтому, для выполнения глубокого копирования, его метод clone() должен клонировать все ссылки внутри класса OceanReading. Для выполнения этой задачи результат super.clone() должен возвращать ссылку на объект OceanReading (таким образом, вы получите доступ к ссылкам на объекты глубины и температуры).
Давайте повторно рассмотрим приведенный ранее в этом приложении пример с ArrayList. Теперь класс Int2 - клонируемый и можно произвести глубокое копирование ArrayList:
//: Приложение А: AddingClone.java // Для добавления клонирования в ваш класс // потребуется несколько циклов. import java.util.*; class Int2 implements Cloneable { private int i; public Int2(int ii) { i = ii; } public void increment() { i++; } public String toString() { return Integer.toString(i); } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Int2 не может быть клонирован"); } return o; } } // Поскольку он клонируемый, наследование // не сделает его не клонируемым: class Int3 extends Int2 { private int j; // Автоматически дублируется public Int3(int i) { super(i); } } public class AddingClone { public static void main(String[] args) { Int2 x = new Int2(10); Int2 x2 = (Int2)x.clone(); x2.increment(); System.out.println( "x = " + x + ", x2 = " + x2); // Все наследники также являются клонируемыми: Int3 x3 = new Int3(7); x3 = (Int3)x3.clone(); ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++ ) v.add(new Int2(i)); System.out.println("v: " + v); ArrayList v2 = (ArrayList)v.clone(); // Теперь клонируем каждый элемент: for(int i = 0; i < v.size(); i++) v2.set(i, ((Int2)v2.get(i)).clone()); // Увеличиваемзначения всех элементов v2: for(Iterator e = v2.iterator(); e.hasNext(); ) ((Int2)e.next()).increment(); // Смотрим, изменились ли значения элементов v: System.out.println("v: " + v); System.out.println("v2: " + v2); } } ///:~
Int3 наследует Int2 и добавляет новый примитив int j. Вам может показаться что снова потребуется переопределение метода clone() для обеспечения копирования j, но в данном случае это не так. Когда при вызове метода clone() класса Int3 вызывается метод clone() класса Int2, а он в свою очередь вызывает метод Object.clone(), который определяет что работает с классом Int3 и создает побитовый дубликат класса Int3. Таким образом, до тех пор, пока вы не используете в своем объекте ссылки, которые требуют клонирования, достаточно одного вызова метода Object.clone(), независимо от того насколько этот метод удален от вашего класса по иерархии объектов.
Как видите, для глубокого копирования ArrayList требуется последовательное выполнение операции клонирования для всех объектов, на которые ссылается ArrayList. Нечто подобное требуется и для глубокого клонирования HashMap.
Остальная часть примера нужна в качестве демонстрации успешного клонирования, показывая что изменения, вносимые в клонированные объекты, не отражаются на состоянии исходных объектов.
Изучая преобразование в последовательную форму серийности в Java (рассмотренную в Главе 11), вы могли обратить внимание на то, что при серийности и десерйности объектов фактически выполняется операция клонирования.
Так почему бы не использовать серийность для глубокого копирования? Следующий пример сравнивает эти два метода по затратам времени:
//: Приложение А:Compete.java import java.io.*; class Thing1 implements Serializable {} class Thing2 implements Serializable { Thing1 o1 = new Thing1(); } class Thing3 implements Cloneable { public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Thing3 не может быть клонирован"); } return o; } } class Thing4 implements Cloneable { Thing3 o3 = new Thing3(); public Object clone() { Thing4 o = null; try { o = (Thing4)super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Thing4 не может быть клонирован"); } // Клонировать поле: o.o3 = (Thing3)o3.clone(); return o; } } public class Compete { static final int SIZE = 5000; public static void main(String[] args) throws Exception { Thing2[] a = new Thing2[SIZE]; for(int i = 0; i < a.length; i++) a[i] = new Thing2(); Thing4[] b = new Thing4[SIZE]; for(int i = 0; i < b.length; i++) b[i] = new Thing4(); long t1 = System.currentTimeMillis(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); ObjectOutputStream o = new ObjectOutputStream(buf); for(int i = 0; i < a.length; i++) o.writeObject(a[i]); // Теперь получаем копии: ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream( buf.toByteArray())); Thing2[] c = new Thing2[SIZE]; for(int i = 0; i < c.length; i++) c[i] = (Thing2)in.readObject(); long t2 = System.currentTimeMillis(); System.out.println( "Дублирование с применением серийности: " + (t2 - t1) + " Миллисекунд"); // Теперь попробуем использовать клонирование: t1 = System.currentTimeMillis(); Thing4[] d = new Thing4[SIZE]; for(int i = 0; i < d.length; i++) d[i] = (Thing4)b[i].clone(); t2 = System.currentTimeMillis(); System.out.println( "Дублирование через клонирование: " + (t2 - t1) + " Миллисекунд"); } } ///:~
Thing2 и Thing4 содержат объекты, подлежащие глубокому копированию. Интересно отметить что хотя серийные классы легки при описании, но требуют хлопот при дублировании. Клонирование, наоборот, требует хлопот при описании класса, но операция дублирования относительно проста. Результаты работы примера говорят сами за себя. Вот результаты трех различных запусков примера:
Дублирование с применением серийности: 940 Milliseconds Дублирование через клонирование: 50 Milliseconds Дублирование с применением серийности: 710 Milliseconds Дублирование через клонирование: 60 Milliseconds Дублирование с применением серийности: 770 Milliseconds Дублирование через клонирование: 50 Milliseconds
Помимо значительной разницы в затратах времени, вы можете наблюдать что операция серийности менее стабильна чем операция клонирования.
Когда создается новый класс, ему по умолчанию передаются свойства базового класса Object, который по умолчанию является не клонируемым (об этом пойдет речь в следующем разделе), и остается таковым до тех пор, пока вы не захотите этого. Однако, после того как вы добавите возможность клонирования в какой-либо класс, она будет передана всем нижестоящим по иерархии классам:
//: Приложение А:HorrorFlick.java // Вы можете добавить клонируемость в // любой уровень иерархии наследования объектов. import java.util.*; class Person {} class Hero extends Person {} class Scientist extends Person implements Cloneable { public Object clone() { try { return super.clone(); } catch(CloneNotSupportedException e) { // этого не должно произойти: // он уже клонируемый! throw new InternalError(); } } } class MadScientist extends Scientist {} public class HorrorFlick { public static void main(String[] args) { Person p = new Person(); Hero h = new Hero(); Scientist s = new Scientist(); MadScientist m = new MadScientist(); // p = (Person)p.clone(); // Ошибка компиляции // h = (Hero)h.clone(); // Ошибка компиляции s = (Scientist)s.clone(); m = (MadScientist)m.clone(); } } ///:~
Перед тем как добавить клонируемость, компилятор остановит вас при попытке клонировать предметы (things). Когда клонируемость будет добавлена в Scientist, Scientist и все его наследники станут клонируемыми.
Возможно такая система показалась вам странной и вы задавались вопросом почему в ней возникла необходимость. Что же стоит за такой реализацией?
Первоначально Java разрабатывался как язык для управления устройствами и не был предназначен для использование в Internet. Клонирование объектов является неотъемлемой функцией таких языков. Поэтому в базовый класс Object был помещен метод clone(), но он был описан как public и таким образом обеспечивалась возможность клонирования любых объектов. На том этапе это казалось наиболее оптимальным вариантом.
Но позже, когда Java превратился в язык, активно применяемый в Internet, все изменилось. Тотальная клонируемость объектов привела к возникновению проблем с безопасностью. Кому хочется чтобы его объекты безопасности свободно клонировались? Поэтому в изначально простую схему были внесены изменения и метод clone() класса Object стал защищенным (protected) и теперь для реализации клонирования вам приходится переопределять его, реализовывать интерфейс Cloneable и иметь дело с обработкой исключительных событиями. Следует отметить, что интерфейс Cloneable реализуется только в том случае, если вы собираетесь вызывать метод clone() класса Object, работа которого начинается с проверки, является ли вызвавший его класс клонируемым. Но, во избежание противоречий, на всякий случай следует реализовать этот интерфейс (тем более, если учесть что он пустой).
У вас возможно сложилось впечатление что для отключения клонируемости достаточно определить метод clone() как private, но это не так, поскольку оперируя методом базового класса вы не можете изменять его статус. Так что это не такая простая задача. И, тем не менее, необходимо уметь управлять клонируемостью своих объектов. Существует ряд типовых реализаций, которыми вы можете руководствоваться при разработке своих классов:
Ниже приведен пример, демонстрирующий различные способы при которых клонирование может быть наследовано или "отключено" в объектах-наследниках:
//: Приложение А:CheckCloneable.java // Проверка, может ли ссылка клонироваться. // Не может клонироваться, поскольку не переопредлен // метод clone(): class Ordinary {} // Переопределяется clone, но не реализуется // интерфейс Cloneable: class WrongClone extends Ordinary { public Object clone() throws CloneNotSupportedException { return super.clone(); // Возвращает исключительную ситуацию } } // Соблюдены все необходимые для клонирования условия: class IsCloneable extends Ordinary implements Cloneable { public Object clone() throws CloneNotSupportedException { return super.clone(); } } // Клонирование отключено с генерацией исключительного события: class NoMore extends IsCloneable { public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } } class TryMore extends NoMore { public Object clone() throws CloneNotSupportedException { // Вызов NoMore.clone(), что приводит к появлению исключительного события: return super.clone(); } } class BackOn extends NoMore { private BackOn duplicate(BackOn b) { // Создается и возвращается копия b. // Это простейшее копирование, использованное лишь в качестве примера: return new BackOn(); } public Object clone() { // Метод NoMore.clone() не вызывается: return duplicate(this); } } // Не удается наследовать, а потому и переопределить // метод clone как это было сделано в BackOn: final class ReallyNoMore extends NoMore {} public class CheckCloneable { static Ordinary tryToClone(Ordinary ord) { String id = ord.getClass().getName(); Ordinary x = null; if(ord instanceof Cloneable) { try { System.out.println("Попытка клонирования " + id); x = (Ordinary)((IsCloneable)ord).clone(); System.out.println("Клонирован " + id); } catch(CloneNotSupportedException e) { System.err.println("Не удается клонировать "+id); } } return x; } public static void main(String[] args) { // Подмена типов: Ordinary[] ord = { new IsCloneable(), new WrongClone(), new NoMore(), new TryMore(), new BackOn(), new ReallyNoMore(), }; Ordinary x = new Ordinary(); // Это не удастся откомпилировать, пока clone() // описан как protected в классе Object: //! x = (Ordinary)x.clone(); // tryToClone() сначала осуществляет проверку чтобы // определить, реализует ли данный класс интерфейс Cloneable: for(int i = 0; i < ord.length; i++) tryToClone(ord[i]); } } ///:~
Первый класс, Ordinary, относится к группе классов, рассмотреных в этой книге: он не поддерживает клонирование, но при этом не имеет механизмов ее блокировки. Однако, если вы имеете дело с ссылкой на Ordinary объект, ставший таковым в результате подмены, вы не можете быть уверены в том, сможет ли он быть клонирован или нет.
Класс WrongClone иллюстрирует неправильную реализацию наследования клонирования. В нем метод Object.clone() переопределяется как public, но не реализован интерфейс Cloneable, поэтому вызов super.clone() (в результате которого вызывается Object.clone()) приводит к возникновению исключительной ситуации CloneNotSupportedException и клонирование не выполняется.
В классе IsCloneable клонирование реализовано правильно: метод clone() переопределяется и реализуется интерфейс Cloneable. Однако, метод clone(), а также некоторые другие методы в этом примере не перехватывают исключительную ситуацию CloneNotSupportException, а лишь возвращают ее вызвавшему методу, где должна быть предусмотрена обработка в блоке операторов try-catch. Вам скорее всего придется обрабатывать эту ситуацию внутри вашего метода clone() гораздо чаще чем просто передавать ее, но в качестве примера гораздо информативнее было ограничиться лишь передачей.
В классе NoMore предпринята попытка отключения клонирования способом, рекомендуемым разработчиками Java: в методе clone() класса-наследника генерируется исключительная ситуация CloneNotSupportedException. В методе clone() класса TryMore как и положено вызывается метод super.clone(), который таким образом приводит к вызову метода NoMore.clone(), который генерирует исключительную ситуацию и предотвращает клонирование.
Но что если программист не станет следовать "правильной" схеме вызова метода super.clone() в переопределенном методе clone()? На примере класса BackOn вы можете наблюдать пример таких действий. Этот класс использует специальный метод duplicate() для копирования текущего объекта и в clone() вызывает этот метод вместо вызова super.clone(). При этом исключительная ситуация не генерируется и класс может быть клонирован. Это пример того, что нельзя рассчитывать на генерацию исключительной ситуации как на защиту класса от клонирования. Единственный верный способ для этого, показан на примере класса ReallyNoMore где класс описан как завершенный (final), то есть как класс, который не может быть наследован. Это означает что если метод clone() генерирует исключительную ситуацию в final классе, то ее не удасться обойти при помощи наследования, что обеспечивает гарантированную защиту от клонирования (вы не можете явно вызвать Object.clone() из класса с произвольным уровнем наследования; можно вызвать лишь метод super.clone(), через который будет произведено обращение к методу базового класса). таким образом, разрабатывая объекты с высоким уровнем защиты, такие классы лучше описывать как final.
Первый метод класса CheckCloneability - tryToClone(), берет произвольный Ordinary объект и проверяет, является ли он клонируемым с помощью instanceof. Если да, он подменяет тип объекта на IsCloneable и вызывает для него метод clone(), после чего для результатов выполняет обратную подмену в Ordinary, перехватывая все возникающие в ходе операции исключительные ситуации. Обратите внимание на определение типа объекта в процессе выполнения метода (см. Главу 12) используемое для вывода на экран имени класса для идентификации событий.
В методе main(), создаются различные типы Ordinary объектов с подменой типа на Ordinary при определении массива. Следующие за этим две строки кода создают простой Ordinary объект и пытаются клонировать его. Однако этот код не удастся откомпилировать, поскольку в классе Object метод clone() определен как защищенный (protected). Остальной код пробегает по всему массиву и пытается клонировать каждый из его объектов, информируя при этом об успешности этих операций. Вы получите следующие результаты:
Попытка клонирования IsCloneable Клонирован IsCloneable Попытка клонирования NoMore Не удается клонировать NoMore Попытка клонирования TryMore Не удается клонировать TryMore Попытка клонирования BackOn Клонирован BackOn Попытка клонирования ReallyNoMore Не удается клонировать ReallyNoMore
В заключение, сформулируем требования, предъявляемые к клонируемым классам:
1. Реализация интерфейса Cloneable.
2. Переопределение метода clone()
3. Вызов метода super.clone() из переопределенного метода clone()
4. Обработка исключительных ситуаций в методе clone()
Возможно клонирование показалось вам сложным процессом и вам хочется найти ему более удобную альтернативу. Таким решением (особенно если вы владеете Си++) является создание специального конструктора, задачей которого будет создание дубликата объекта. В Си++ такие конструкторы называются конструкторами копирования. На первый взгляд они могут показаться очевидным выходом из положения, но применить их на практике вам не удастся. Рассмотрим пример:
//: Приложение А:CopyConstructor.java // Конструктор для копирования объектов одинаковых типов // как способ создания локальных копий. class FruitQualities { private int weight; private int color; private int firmness; private int ripeness; private int smell; // и т.д. FruitQualities() { // Конструктор по умолчанию // для совершения каких-либо необходимых действий... } // Прочие конструкторы: // ... // Конструктор копирования: FruitQualities(FruitQualities f) { weight = f.weight; color = f.color; firmness = f.firmness; ripeness = f.ripeness; smell = f.smell; // и т.д. } } class Seed { // Поля... Seed() { /* Конструктор по умолчанию */ } Seed(Seed s) { /* Конструктор копирования */ } } class Fruit { private FruitQualities fq; private int seeds; private Seed[] s; Fruit(FruitQualities q, int seedCount) { fq = q; seeds = seedCount; s = new Seed[seeds]; for(int i = 0; i < seeds; i++) s[i] = new Seed(); } // Прочие конструкторы: // ... // Конструктор копирования: Fruit(Fruit f) { fq = new FruitQualities(f.fq); seeds = f.seeds; // Быстрый вызов всех конструкторов копирования: for(int i = 0; i < seeds; i++) s[i] = new Seed(f.s[i]); // Действия других конструкторов копирования... } // Для обеспечения размещения полученных конструкторов (или других // методов) в различных качествах: protected void addQualities(FruitQualities q) { fq = q; } protected FruitQualities getQualities() { return fq; } } class Tomato extends Fruit { Tomato() { super(new FruitQualities(), 100); } Tomato(Tomato t) { // Конструктор копирования super(t); // Подмена для базового конструктора копирования // Прочие операции конструктора копирования... } } class ZebraQualities extends FruitQualities { private int stripedness; ZebraQualities() { // Конструктор по умолчанию // для совершения каких-либо необходимых действий... } ZebraQualities(ZebraQualities z) { super(z); stripedness = z.stripedness; } } class GreenZebra extends Tomato { GreenZebra() { addQualities(new ZebraQualities()); } GreenZebra(GreenZebra g) { super(g); // Вызов Tomato(Tomato) // Восстановление верных качеств: addQualities(new ZebraQualities()); } void evaluate() { ZebraQualities zq = (ZebraQualities)getQualities(); // Какие-нибудь операции с качествами // ... } } public class CopyConstructor { public static void ripen(Tomato t) { // Использование "конструктора копирования": t = new Tomato(t); System.out.println("В зрелых t это " + t.getClass().getName()); } public static void slice(Fruit f) { f = new Fruit(f); // Хмм... будет ли это работать? System.out.println("В нарезаных ломтиками f это " + f.getClass().getName()); } public static void main(String[] args) { Tomato tomato = new Tomato(); ripen(tomato); // OK slice(tomato); // Ой! GreenZebra g = new GreenZebra(); ripen(g); // Ой! slice(g); // Ой! g.evaluate(); } } ///:~
Сначала это кажется немного странным. Конечно, плоды обладают свойствами, но почему бы просто не поместить элементы данных, представляющие эти свойства непосредственно в классе Fruit? На то есть две причины. Первая заключается в том, что вам захочется иметь возможность с легкостью добавлять ли изменять эти качества. Обратите внимание что в классе Fruit есть защищенный (protected) метод addQualities(), позволяющий классам-наследникам производить подобные операции. (Возможно вам покажется что было бы логичнее создать для класса Fruit защищенный (protected) конструктор, которому передавался бы параметр FruitQualities, но конструкторы не наследуются, поэтому он не будет доступен для классов-наследников второго и выше уровней.) Поместив качества фруктов в различные классы вы обеспечиваете большую гибкость, включая возможность изменять качества по ходу существования каждого отдельного объекта Fruit.
Вторая причина размещения FruitQualities в отдельных объектах заключается в добавлении или изменении их при помощи механизмов наследования и полиморфизма. Заметьте что для объекта GreenZebra (зеленая зебра), который на самом деле происходит от типа Tomato. Конструктор вызывает метод addQualities() и передает их ZebraQualities объекту, который наследуется от FruitQualities и поэтому он может быть подключен к ссылке на FruitQualities в базовом классе. Разумеется, когда GreenZebra использует FruitQualities, он должен привести его к нужному типу (как показано в evalute()), но при этом всегда знает что работает с классом ZebraQualities.
Как вы видите, есть еще класс Seed (семя) и класс Fruit (который по определению содержит свои собственные семена)[82], содержит массив из объектов Seeds.
И, наконец, обратите внимание на то что для обеспечения глубокого копирования все классы имеют конструкторы копирования, и каждый конструктор копирования должен позаботиться о том, чтобы вызвать конструкторы копирования для базового класса и объектов-членов. Конструктор копирования тестируется внутри класса CopyConstructor. Метод ripen() получает в качестве параметра Tomato и осуществляет создание его копии.
t = new Tomato(t);
Тем временем slice() получает объект Fruit и также дублирует его:
f = new Fruit(f);
Таким образом в main() тестируются различные экземпляры Fruit. Вот результаты:
В зрелых t это Tomato В нарезаных ломтиками f это Fruit В зрелых t это Tomato В нарезаных ломтиками f это Fruit
Вот где появляются проблемы. После того как создается копия Tomato в slice(), в результате этой операции Tomato перестает существовать, остается только Fruit. Он теряет, так сказать, всю свою "помидорность". Затем, когда дойдет очередь до GreenZebra, ripen() и slice() также превратят его сначала в Tomato, а затем в Fruit. Поэтому, увы, методика конструкторов копирования не применима для Java, когда заходит речь о создании локальных копий.
Конструкторы копирования - фундаментальный элемент языка Си++, поскольку с их помощью автоматически создаются локальные копии объектов. Однако, как показывает приведенный выше пример, они не работают в Java. Почему? В Java мы можем манипулировать только с ссылками, тогда как в Си++ наряду с аналогами ссылок допускаются манипуляции непосредственно с самими объектами. Вот для чего нужны конструкторы копирования в Си++: они создают дубликат объекта в случаях когда требуется передать объект "по значению". Этот прием прекрасно работает в Си++, но вы должны помнить что такая конструкция не будет работать в Java и должны воздержаться от ее использования.
В то время как метод clone() реализует создание локальных копий объекта, на программиста (автора метода) ложится ответственность по предотвращению вредных воздействий, грозящих при использовании дублирующих ссылок. Предположим вы создаете библиотеку настолько универсальную и часто используемую, что вы не уверены в том, что она будет правильно клонирована? Или, скажем проще, что если вы хотите разрешить дублирующих ссылок для повышения эффективности (чтобы избежать излишнего дублирования объектов) но при этом хотите избежать негативных сторон применения дублирующих ссылок?
Одно из решений - создание неизменных объектов, относящихся к группе классов "только для чтения". Вы можете определить класс таким образом, что работа методов никак не будет отражаться на состоянии самого объекта. В таких классах использование дублирующих ссылок не приводит к возникновению каких-либо проблем, поскольку вам доступны лишь операции считывания данные из объекта, а параллельное считывание не вызывает никаких проблем. В качестве простого примера неизменных объектов может служить стандартная библиотека Java, содержащая "классы-ярлыки", созданные для примитивов всех типов. Возможно вы уже обнаружили что если вы хотите разместить int в классе- контейнере, таком как ArrayList (который содержит только ссылки на объекты), вы можете обернуть (wrap) ваш int внутри стандартной библиотеки класса Integer:
//: Приложение А:ImmutableInteger.java // Класс Integer не может быть изменен . import java.util.*; public class ImmutableInteger { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++) v.add(new Integer(i)); // Но как вы изменили int // внутри Integer? } } ///:~
Класс Integer ( как и другие "классы-обертки" примитивов) наследуют неизменность самым простым образом: просто у них нет методов, позволяющих изменять объект.
Если вам нужен объект, который содержит типы примитива, который может быть изменен, вы должны создать его самостоятельно. К счастью это очень просто:
//: Приложение А:MutableInteger.java // Изменяемый класс-ярлык. import java.util.*; class IntValue { int n; IntValue(int x) { n = x; } public String toString() { return Integer.toString(n); } } public class MutableInteger { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++) v.add(new IntValue(i)); System.out.println(v); for(int i = 0; i < v.size(); i++) ((IntValue)v.get(i)).n++; System.out.println(v); } } ///:~
Примечание: n использовано для упрощения кода.
Класс IntValue может быть даже упрощен, если при инициализации по умолчанию допускается устанавливать значение в ноль (тогда вам не нужен конструктор) и если вам не надо заботиться о выводе на печать (тогда вам не нужен метод toString()):
class IntValue { int n; }
Процедура выборки элементов и применения подмены типов выглядят несколько неуклюже, но это уже особенность ArrayList а не IntValue.
Вы можете создать свой собственный класс "только для чтения". Пример:
//: Приложение А:Immutable1.java // Не модифицируемые объекты // обладают иммунитетом от дублирующих ссылок. public class Immutable1 { private int data; public Immutable1(int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable1 quadruple() { return new Immutable1(data * 4); } static void f(Immutable1 i1) { Immutable1 quad = i1.quadruple(); System.out.println("i1 = " + i1.read()); System.out.println("quad = " + quad.read()); } public static void main(String[] args) { Immutable1 x = new Immutable1(47); System.out.println("x = " + x.read()); f(x); System.out.println("x = " + x.read()); } } ///:~
Все данные определены как private и, как видите, напрочь отсутствуют public методы, модифицирующие эти данные. Действительно, метод, который казалось бы вносит изменения в объект, quadruple(), на самом деле для своих операций создает новый объект Immutable1 не изменяя при этом объект-оригинал.
Метод f() совершает различные действия с объектом Immutable1, а выводимые на экран в процедуре main() результаты свидетельствуют о том, что они никак не отразились на состоянии x. Таким образом, ссылки на объект x могут быть многократно дублированы без какого-либо вреда, поскольку неизменные классы гарантируют что этот объект не будет изменен.
Создание неизменных классов на первый взгляд является элегантным решением. Однако, всякий раз, когда вам понадобится модифицировать новый объект этого типа, вы должны терпеть неудобства, связанные с необходимостью создания нового объекта, а также более частым "сбором мусора". Для каких-то объектов это не составит труда, но для некоторых (таких, как класс String) сопряжено с множеством проблем.
В таком случае хорошим выходом будет создание класса-компаньона, который может изменяться. Тогда, если вам требуется произвести множество изменений, вы можете переключаться на использование редактируемого класса-компаньона, а после завершения всех модификаций вновь работать с неизменным классом.
Пример:
//: Приложение А:Immutable2.java // Класс-компаньон для внесения изменений // в неизменный класс. class Mutable { private int data; public Mutable(int initVal) { data = initVal; } public Mutable add(int x) { data += x; return this; } public Mutable multiply(int x) { data *= x; return this; } public Immutable2 makeImmutable2() { return new Immutable2(data); } } public class Immutable2 { private int data; public Immutable2(int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable2 add(int x) { return new Immutable2(data + x); } public Immutable2 multiply(int x) { return new Immutable2(data * x); } public Mutable makeMutable() { return new Mutable(data); } public static Immutable2 modify1(Immutable2 y){ Immutable2 val = y.add(12); val = val.multiply(3); val = val.add(11); val = val.multiply(2); return val; } // Это приводит к тому же результату: public static Immutable2 modify2(Immutable2 y){ Mutable m = y.makeMutable(); m.add(12).multiply(3).add(11).multiply(2); return m.makeImmutable2(); } public static void main(String[] args) { Immutable2 i2 = new Immutable2(47); Immutable2 r1 = modify1(i2); Immutable2 r2 = modify2(i2); System.out.println("i2 = " + i2.read()); System.out.println("r1 = " + r1.read()); System.out.println("r2 = " + r2.read()); } } ///:~
Immutable2 содержит методы, которые, как и ранее, защищали неизменность объекта за счет создания новых объектов в тех случаях, когда требуется его модификация. Эти операции осуществляются методами add() и multiply(). Класс-компаньон Mutable также имеет методы add() и multiply(), но они уже служат не для создания нового объекта, а для его изменения. Кроме того, в классе Mutable есть метод для создания Immutable2 объекта с использованием данных и наоборот.
Два статических метода modify1() и modify2() демонстрируют два различных метода решения одной и той же задачи. В методе modify1() все действия выполняются внутри класса Immutable2 и в процессе работы создаются четыре новых Immutable2 объекта. (и каждый раз при переопределении val предыдущий объект становится мусором).
В методе modify2() первой операцией является Immutable2 y и создание Mutable объекта. (Это напоминает вызов метода clone(), рассмотренный нами ранее, но в то же время при этом создается объект нового типа). Затем объект Mutable используется для многочисленных операций не требующих создания новых объектов. В конце результаты передаются в Immutable2. Итак, вместо четырех новых объектов создаются только два (Mutable и результат Immutable2).
Такой прием имеет смысл использовать в случаях, когда:
Ознакомьтесь со следующим кодом:
//: Приложение А:Stringer.java public class Stringer { static String upcase(String s) { return s.toUpperCase(); } public static void main(String[] args) { String q = new String("howdy"); System.out.println(q); // howdy String qq = upcase(q); System.out.println(qq); // HOWDY System.out.println(q); // howdy } } ///:~
Когда q передается в качестве параметра методу upcase() на самом деле передается копия ссылки на q. Объект на который указывает эта ссылка физически не меняет своего положения. При передаче в качестве параметров копируются только сами ссылки.
Теперь посмотрим на содержание метода upcase(), как вы видите, ссылка полученная в качестве параметра носит имя s и существует только на время работы метода upcase(). Когда работа метода upcase() завершена, локальная ссылка уничтожается. upcase() возвращает результат - оригинальную строку с заменой всех прописных символов на заглавные. Разумеется, на самом деле возвращается лишь ссылка на этот результат. Но эта ссылка указывает на новый объект, а объект-оригинал q остается в одиночестве. Каким же образом это происходит?
Записав:
String s = "asdf";
String x =
Stringer.upcase(s);
действительно ли вы хотите чтобы uppercase() изменял параметр? Как правило нет, поскольку читающий код воспринимает параметр как информацию, передаваемую методу не предназначенную для модификации. Это важный момент для тех кто стремится сделать код своих программ более удобочитаемым.
В Си++ это сочли настолько важным, что ввели специальное ключевое слово const, дабы гарантировать программисту что ссылка (или, для Си++, указатель или ссылка) не могут быть использованы для модификации объекта-оригинала. Но это требует от программиста Си++ прилежности, чтобы он не забывал повсеместно вставлять const. Об этом легко забыть и это вносит лишнюю путаницу.
Объекты String созданы чтобы быть неизменными с применением технологии, рассмотренной выше. Если вы ознакомитесь с документацией по классу String (которая рассмотрена далее в этом приложении), вы увидите что все методы этого класса, которые изменяют объект String на самом деле лишь создают и возвращают абсолютно новый объект String содержащий изменения. При этом объект-оригинал String остается неизменным. В Java нет таких средств как const в Си++ для обеспечения неизменности объектов на уровне компиляции и если вы хотите то вам придется обеспечивать ее самостоятельно, как это реализовано в String.
Поскольку объект String неизменный, вы можете многократно дублировать ссылки на него. Поскольку он является объектом только для чтения, нет никакой опасности что действия с одной из ссылок приведут к изменению объекта, которое отразится на работе с другими ссылками. Так в объектах только для чтения решается проблема дублирующих ссылок.
Также представляется возможным обработка всех случаях, при которых вам необходимо вносить изменения в объект. С этой целью создается совершенно новый вариант объекта с уже внесенными изменениями, как это реализовано в String. Однако, в некоторых случаях это не эффективно. Примером является использование оператора '+', перегруженного для объектов String. Термин "перегруженный" означает что при использовании с классом определенного типа оператор выполняет специфические функции. (Операторы '+' и '+=' для String - единственные перегруженные операторы в Java и в Java программист не имеет возможности перегружать какие-либо иные операторы) [83]
Когда '+' используется с объектами String, он выполняет операцию объединения двух и более объектов String:
String s = "abc" + foo + "def" + Integer.toString(47);
Вы можете предположить то как это может работать: у объекта String "abc" есть метод append(), который создает объект String, содержащий "abc", объединенный с содержимым foo. Новый объект String в свою очередь создает новый объект String, в который добавляется "def" и так далее.
Так могло бы все и происходить, но это требует создания множества объектов String лишь для объединения этих новых объектов String и в результате у вас получилось бы огромное количество промежуточных объектов String, требующих сбора мусора. Могу предположить что разработчики Java сначала пробовали именно такой подход (это урок разработчикам программного обеспечения - вы ничего не знаете о системе до тех пор, пока сами не напишете что-либо и не заставите это работать) и полученные результаты не удовлетворили их своей эффективностью.
Решением является использование модифицируемого класса-компаньона, согласно рассмотренному ранее принципу. Для объекта String классом-компаньоном является StringBuffer, и компилятор автоматически создает StringBuffer для обработки некоторых выражений, в частности при использовании операторов '+' и '+=' применительно к объектам String. Вот пример того как это происходит:
//: Приложение А:ImmutableStrings.java // Демонстрация StringBuffer. public class ImmutableStrings { public static void main(String[] args) { String foo = "foo"; String s = "abc" + foo + "def" + Integer.toString(47); System.out.println(s); // "Равенство" с использованием StringBuffer: StringBuffer sb = new StringBuffer("abc"); // Создает String! sb.append(foo); sb.append("def"); // Создает String! sb.append(Integer.toString(47)); System.out.println(sb); } } ///:~
При создании строки String s компилятор создает грубую копию последующего кода, который использует sb: создается StringBuffer и используется append() для добавления новых символов непосредственно в объект StringBuffer (это лучше чем каждый раз создавать новые копии) При том что это более эффективно, следует отметить что каждый раз при создании строк заключенных в кавычки, таких как "abc" или "def", компилятор превращает их в объекты String. Поэтому на самом деле создается больше объектов чем вам могло показаться, несмотря на эффективность StringBuffer.
В этом разделе представлен обзор методов для классов String и StringBuffer и вы, таким образом, сможете увидеть их взаимодействие. Здесь рассмотрены не все методы, а только наиболее важные, имеющие отношение к обсуждаемой теме. Перегруженным методам отведена отдельная колонка.
Сначала класс String:
Метод |
Параметры, Перегрузка |
Применение |
---|---|---|
Constructor |
Перегруженные: значение по умолчанию, String,
StringBuffer, массивы char, массивы
byte. |
Создает объекты
String. |
length( ) |
Количество символов в
String. |
|
charAt() |
int индекс |
Возвращает символ с указанным индексом ячейки
String. |
getChars( ),
getBytes( ) |
Начальная и конечная ячейки, которые будут
скопированы и ячейка в внешнего массива, в которую будет произведено
копирование. |
Копирует char или byte в внешний
массив. |
toCharArray( ) |
Создает массив char[], хранящий символы из
String. |
|
equals( ),
equals-IgnoreCase( ) |
String с которой проводится
сравнение. |
Проверка на равенство содержимого двух
Strings. |
compareTo( ) |
String с которой проводится
сравнение. |
Результат отрицательный, ноль или положительный, на
основании лексиграфического
упорядочения String и параметра. Заглавные и прописные
символы не равны! |
regionMatches( ) |
Смещение в текущей String, другой
String и смещение и длина фрагмента для сравнения. Перегрузка
добавляет "игнорировать регистр символов." |
Результат boolean, свидетельствующий о
совпадении фрагментов. |
startsWith( ) |
String, который может начинать текущий
String. Перегрузка добавляет параметр для указания
смещения. |
Результат boolean свидетельствует о том,
начинается ли String с передаваемой в качестве параметра
строки. |
endsWith( ) |
String, который может завершать текущий
String. |
Результат boolean свидетельствует о том, завершается ли String передаваемой в качестве параметра строкой. |
indexOf( ),
lastIndexOf( ) |
Перегруженные: char, char и индекс
начала, String, String и индекс
начала. |
Возвращает -1 если аргумент не найден в данном
String, иначе возвращается индекс начала найденного фрагмента.
lastIndexOf( ) осуществляет поиск начиная с конца
строки. |
substring( ) |
Перегруженный: Индекс начала, индекс начала, и
индекс конца. |
Возвращает новый объект String, содержащий
указанный набор символов. |
concat( ) |
String для
объединения |
Возвращает новый объект String, содерщащий
символы оригинального объекта String и расположенные вслед за ними
символы переданные в качестве параметра. |
replace( ) |
Старый символ используемый для поиска, новый символ
используемый для замены. |
Возвращает новый объект String с результатами
проведенной замены. Если искомый символ не найден, используется старый
String. |
toLowerCase( )
toUpperCase( ) |
Возвращает новый объект String с измененными
на соответствующий регистр символами. Если изменения не требуется,
используется старый String. |
|
trim( ) |
Возвращает новый объект String с сокращением
с обоих концов пробелов до одинарных. Если изменения не требуются,
используется старый String. |
|
valueOf( ) |
Перегрузка: Object, char[],
char[] и смещение и указатель, boolean, char,
int, long, float,
double. |
Возвращает String, содержащий символьное представление
параметра. |
intern( ) |
Создает один
и только один String с уникальной последовательностью
символов. |
Как вы видите, все методы String возвращают новый объект String в тех случаях, когда необходимо его содержимое. Также обратите внимание на то что если модификация не требуется, возвращается ссылка на оригинал String. Это позволяет сэкономить память и избавляет от лишних трудностей.
Теперь рассмотрим класс StringBuffer:
Метод |
Параметры, перегрузка |
Применение |
---|---|---|
Constructor |
Перегруженный: значение по умолчанию, длина
создаваемого буфера, String используемый в качестве
источника. |
Создает новый объект
StringBuffer. |
toString( ) |
Создает String используя текущий
StringBuffer. |
|
length( ) |
Количество символов в
StringBuffer. |
|
capacity( ) |
Возвращает текущий объем занимаемой
памяти. |
|
ensure- |
Integer определяющий желаемый объем
памяти. |
StringBuffer резервирует как минимум
указанный объем памяти. |
setLength( ) |
Integer определяющий новую длину строки символов в
буфере. |
Расширяет ли укорачивает строку симоволов. Есл
строка расширяется, новые ячейки заполняются
нулями. |
charAt( ) |
Integer указывающий на позицию
элемента. |
Возвращает char для заданной позиции
буфера. |
setCharAt( ) |
Integer, указывающий на позицию элемента и новое
значение char для этого элемента. |
Изменяет значение в указанной
позиции. |
getChars( ) |
Начало и конец копируемого фрагмента, массив в
который производится копирование, индекс в целевом
массиве. |
Выполняет копирование символов char во
внешний массив. В отличие от String здесь нет метода
getBytes( ). |
append( ) |
Перегруженный: Object, String,
char[], char[] со смещением и длиной, boolean,
char, int, long, float,
double. |
Параметр преобразуется в строку и добавляется в
конец текущего буфера. При необходимости размер буфера
увеличивается. |
insert( ) |
Перегруженный, для всех первым параметром является
смещение с которым выполняется вставка: Object, String,
char[], boolean, char, int, long,
float, double. |
Второй параметр преобразуется в строку и вставляется
в текущий буфер начиная с указанного смещения. При необходимости размер
буфера увеличивается. |
reverse( ) |
Порядок следования символов в буфере меняется на
противоположный. |
Наиболее часто используется метод append(), применяемый компилятором при обработке выражений String, связанных операторами '+' и '+='. Метод insert() имеет ту же форму и оба метода выполняют операции с использованием буфера, не создавая при этом новых объектов.
Теперь вы видите что класс String - не совсем обычный класс Java. У String много особенностей, не последней из них является тот факт что String является одним из встроенных и фундаментальных классов Java. Кроме того, заключенные в кавычки символы автоматически преобразуются компилятором в String, и с ним допускается применение специальных перегруженных операторов + и +=. В этом приложении вы рассмотрели и другие специальные особенности: тщательной реализации неизменности используя компаньон StringBuffer, а также дополнительные особенности компиляции.
Поскольку в Java широко используются ссылки и поскольку каждый создаваемый объект создается в heap и становится мусором сразу же после того как перестает использоваться, поведение и манипуляция с объектом изменяется, особенно при передаче и возврате объектов. Например, в Си или Си++, если вы хотите инициализировать некоторые фрагменты памяти в методе, вы можете использовать для получения этого адреса получаемый методом параметр. Иначе вам пришлось бы беспокоиться на счет того, существует ли до сих пор необходимый вам объект или он был уничтожен. Поэтому интерфейс подобных методов несколько усложнен. Но в Java вы не должны волноваться о существовании объекта, за вас обо всем позаботятся. Вы можете создавать объекты тогда, когда вам захочется, не беспокоясь о самой механике создания объекта: вы просто передаете ссылку. Иногда такая простота практически незаметна, а иногда просто поражает.
За эти волшебные возможности от вас требуется учитывать следующие два момента:
Некоторые люди считают что клонирование в Java плохо реализовано и при наследовании используют собственные версии клонирования [84] не пользуясь вызовом метода Object.clone(), что избавляет от необходимости наследования интерфейса Cloneable и перехвата CloneNotSupportedException. Это весьма подходящий прием, поскольку clone() весьма редко поддерживается в пределах стандартных библиотек Java, к тому же он довольно безопасен.
Решения предложенных упражнений содержатся в электронном документе The Thinking in Java Annotated Solution Guide (Думая на Java. Ответы на вопросы с комментариями), доступном за умеренную плату на сайте http://www.bruceeckel.com/.
[79] В Си, где в основном работа происходит с данными малых объемов, по умолчанию осуществляется "передача по значению". Для Си такая форма весьма эффективна, но при работе с объектами эффективность "передачи по значению" значительно снижается. Кроме того, разработка классов с поддержкой "передачи по значению" на Си - весьма непростая задача.
[80] Это слово вы не встретите в словаре английского (и русского) языка, но оно применяется в библиотеках Java, и поэтому включено сюда. Надеюсь что это не вызовет недоразумений.
[81] В качестве контраргумента этому утверждению вы можете создать простой "пример". Например такой:
public class Cloneit
implements Cloneable {
public static void main (String[] args)
throws CloneNotSupportedException
{
Cloneit a = new
Cloneit();
Cloneit b =
(Cloneit)a.clone();
}
}
Однако, он будет работать лишь за счет того, что main() является методом класса Cloneit и, таким образом, имеет право вызывать защищенный метод базового класса clone(). Если вы попробуете осуществить этот вызов из другого класса, это неминуемо приведет к ошибке компиляции.
[82] За исключением несчастного авокадо.
[83] Си++ предоставляет программисту возможность по своему желанию перегружать операторы. Поскольку зачастую это весьма сложный процесс (см. раздел 10 книги Думая на Си++, 2-я редакция, Prentice-Hall, 2000), разработчики Java сочли что это "нежелательный элемент", который не должен применяться в Java. Однако они таки не реализовали это свое решение, и это не так уж плохо, поскольку в результате по иронии судьбы перегрузка операторов в Java использовать гораздо проще чем в Си++. Более подробную информацию вы найдете на сайте Python (www.Python.org), на котором хранится сборщик мусора и простые примеры перегрузки операторов.
[84] Дуг Леа, который помогал при решении этой проблемы предложил мне такой выход, сказав что он просто создает функцию duplicate() для каждого класса.