Файл: Курс лекция по Java. Лекция 9.pdf

Добавлен: 05.02.2019

Просмотров: 1413

Скачиваний: 7

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
background image

// результат работы метода hashCode()

System.out.println(new float[2].hashCode()); 

Результатом выполнения программы будет:

 

[I@26b249

[[I@82f0db

[Ljava.lang.String;@92d342

7051261 

3. Преобразование типов для массивов

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

Ранее  подробно  рассматривались  переходы  между  примитивными  и  обычными  (не
являющимися  массивами)  ссылочными  типами.  Хотя  массивы  являются  объектными
типами, их также будет полезно разделить по базовому типу на две группы - основанные
на примитивном или ссылочном типе.

Сразу  скажем,  что  переходы  между  массивами  и  примитивными  типами  являются
запрещенными. Преобразования между массивами и другими объектными типами возможны
только  для  класса  Object  и  интерфейсов  Cloneable  и  Serializable.  Массив  всегда  можно
привести к этим 3 типам, обратный же переход является сужением, и должен производится
явным образом по усмотрению разработчика. Таким образом, интерес представляют только
переходы  между  разными  типами  массивов.  Очевидно,  что  массив,  основанный  на
примитивном типе, принципиально нельзя преобразовать к типу массива, основанному на
ссылочном типе, и наоборот.

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

Для ссылочных же типов такого строгого правила нет. Например, если создать экземпляр
массива, основанного на типе Child, то ссылку на него можно привести к типу массива,
основанного на типе Parent.

 

Child c[] = new Child[3];

Parent p[] = c; 

Вообще,  существует  универсальное  правило:  массив,  основанный  на  типе  A,  можно
привести к массиву, основанному на типе B, если сам тип A приводится к типу B.

 

// Если допустимо такое приведение:

B b = (B) new A();

// то допустимо и приведение массивов:

B b[]=(B[]) new A[3]; 

Класс массива

Стр. 9 из 21

Программирование на Java

Rendered by 

www.RenderX.com


background image

Применяя  это  правило  рекурсивно  можно  преобразовывать  многомерные  массивы.
Например, массив Child[][] можно привести к Parent[][], так как их базовые типы приводимы
(Child[] к Parent[]) также на основе этого правила (поскольку базовые типы Child и Parent
приводимы в силу правил наследования).

Как обычно, расширения можно проводить неявно (как записано в предыдущем примере),
а сужения - только явным приведением.

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

 

// пример вызовет ошибку компиляции

byte b[]={1, 2, 3};

int i[]=b; 

В  таком  случае,  элементы  b[0]  и  i[0]  хранили  бы  значения  разных  типов.  Стало  быть
преобразование потребовало бы копирования с одновременным преобразованием типа
всех элементов исходного массива. В результате был бы создан новый массив, элементы
которого равнялись бы по значению элементам исходного массива.

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

Если  же  копирование  элементов  действительно  требуется,  то  нужно  сначала  создать
новый массив, а затем воспользоваться стандартной функцией System.arrayCopy(), которая
эффективно производит копирование элементов одного массива в другой.

3.1. Ошибка ArrayStoreException

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

Рассмотрим пример:

 

Child c[] = new Child[5];

Parent p[]=c;

p[0]=new Parent(); 

С точки зрения компилятора код совершенно корректен. Преобразование во второй строке
допустимо. В третьей строке элементу массива типа Parent присваивается значение того
же типа.

Однако  при  выполнении  такой  программы  возникнет  ошибка.  Нельзя  забывать,  что
преобразование не меняет объект, изменяется лишь способ доступа к нему. В свою очередь
объект  всегда  "помнит",  от  какого  типа  он  был  порожден.  С  учетом  этих  замечаний
становится ясно, что в третьей строке делается попытка добавить в массив Child значение
типа Parent, что некорректно.

Программирование на Java

Стр. 10 из 21

Преобразование типов для массивов

Rendered by 

www.RenderX.com


background image

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

 

c[0].onlyChildMethod(); 

где метод onlyChildMethod() определен только в классе Child. Такое обращение совершенно
корректно,  а  значит  недопустима  ситуация,  когда  элемент  c[0]  ссылается  на  объект,
несовместимый с Child.

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

Может сложиться впечатление, что разобранная ситуация является надуманной - зачем
преобразовывать массив и тут же класть в него неверное значение? Однако преобразование
при присвоении значений является лишь примером. Рассмотрим объявление метода:

 

public void process(Parent[] p) {

   if (p!=null && p.length>0) {

      p[0]=new Parent();

   }

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

 

process(new Child[3])); 

И это будет как раз ошибка ArrayStoreException.

3.2. Переменные типа массив, и их значения

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

Как обычно, массивы, основанные на простых и ссылочных типах, описываем раздельно.

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

Переменная типа массив ссылочных величин может хранить следующие значения:

null;

значения точно того же типа, что и тип переменной;

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

Переменные типа массив, и их значения

Стр. 11 из 21

Программирование на Java

Rendered by 

www.RenderX.com


background image

Все  эти  утверждения  непосредственно  следуют  из  рассмотренных  выше  особенностей
приведения типов массивов.

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

Сведем все эти утверждения в таблицу:

Тип переменной

Допустимые типы ее значения

Массив простых значений

null

в точности совпадающий с типом переменной

Массив ссылочных значений

null

совпадающий с типом переменной

массивы ссылочных значений, удовлетворяющих
следующему условию: Если тип переменной - массив на
основе типа A, то значение типа массив на основе типа
B допустимо тогда и только тогда, когда B приводимо к
A.

Object

null

любой ссылочный, включая все массивы

4. Клонирование

Механизм клонирования, как следует из названия, позволяет порождать новые объекты
на  основе  существующего,  которые  бы  обладали  точно  таким  же  состоянием,  что  и
исходный. То есть, ожидается, что для исходного объекта, представленного ссылкой x, и
результата клонирования, возвращаемого методом x.clone(), выражение

 

x != x.clone() 

должно быть истинным, также как и выражение

 

x.clone().getClass() == x.getClass() 

и, наконец, выражение

 

x.equals(x.clone()) 

также верно. Реализация такого метода clone() осложняется целым рядом потенциальных
проблем, например:

класс, от которого порожден объект, может иметь разнообразные конструкторы, которые
к тому же могут быть недоступны (например, модификатор доступа private);

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

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

Программирование на Java

Стр. 12 из 21

Клонирование

Rendered by 

www.RenderX.com


background image

возможна ситуация, когда объект нельзя клонировать, дабы не нарушить целостность
системы.

Поэтому было реализовано следующее решение.

Класс Object содержит метод clone(). Рассмотрим его объявление:

 

protected native Object clone() throws CloneNotSupportedException; 

Именно он используется для клонирования. Далее возможно два варианта.

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

Второй вариант предполагает использование реализации метода clone() в самом классе
Object. То, что он объявлен как native, говорит о том, что его реализация предоставляется
виртуальной машиной. Понятно, что перечисленные трудности легко могут быть преодолены
самой JVM, ведь она хранит в своей памяти все свойства объектов.

При  выполнении  метода  clone()  сначала  делается  проверка,  можно  ли  клонировать
исходный объект. Если разработчик хочет сделать объекты своего класса доступными для
клонирования через Object.clone(), то он должен реализовать в своем классе интерфейс
Cloneable.  В  этом  интерфейсе  нет  ни  одного  элемента,  он  служит  лишь  признаком  для
виртуальной  машины,  что  объекты  допустимы  для  клонирования.  Если  проверка  не
выполняется успешно, метод порождает ошибку
CloneNotSupportedException

.

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

Обратите  внимание,  что  сам  класс  Object  не  реализует  интерфейс  Cloneable,  а  потому
попытка вызова new Object().clone() будет приводить к ошибке времени исполнения. Метод
clone() предназначен скорее для использования в наследниках, которые могут обращаться
к  нему  с  помощью  выражения  super.clone().  При  этом  могут  быть  сделаны  следующие
изменения:

модификатор доступа расширен до public;

убрано предупреждение об ошибке CloneNotSupportedException;

результирующий объект может быть модифицирован любым образом на усмотрение
разработчика.

Напомним, что все массивы реализуют интерфейс Cloneable и, таким образом, доступы
для клонирования.

Важно помнить, что все поля клонированного объекта приравниваются, их значения никогда
не клонируются. Рассмотрим пример:

Стр. 13 из 21

Программирование на Java

Rendered by 

www.RenderX.com