Смекни!
smekni.com

Общие представления о языке Java 5 (стр. 37 из 68)

private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {

System.out.println("locat1.x="+locat1.x);

System.out.println("locat1.y="+locat1.y);

}

private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {

m1(locat1);

System.out.println("Прошёл вызов m1(locat1)";

}

Легко проверить, что вызов m1(locat1) приводит к увеличению значений полей locat1.x и locat1.y .

При передаче в подпрограмму ссылочной переменной имеется особенность, которая часто приводит к ошибкам – потеря связи с первоначальным объектом при изменении ссылки. Модифицируем наш метод m1:

public static void m1(Location obj){

obj.x++;

obj.y++;

obj=new Location(4,4);

obj.x++;

obj.y++;

}

После первых двух строк, которые приводили к инкременту полей передаваемого объекта, появилось создание нового объекта и перещёлкивание на него локальной переменной obj, а затем две точно такие же строчки, как в начале метода. Какие значения полей x и y объекта, связанного с переменной locat1 покажет нажатие на кнопку 1 после вызова модифицированного варианта метода? Первоначальный и модифицированный вариант метода дадут одинаковые результаты!

Дело в том, что присваивание obj=new Location(4,4); приводит к тому, что переменная obj становится связанной с новым, только что созданным объектом. И изменение полей данных в операторах obj.x++ и obj.y++ происходит уже для этого объекта. А вовсе не для того объекта, ссылку на который передали через список параметров.

Следует обратить внимание на то, какая терминология используется для описания программы. Говорится “ссылочная переменная” и “объект, связанный со ссылочной переменной”. Эти понятия не отождествляются, как часто делают программисты при описании программы. И именно строгая терминология позволяет разобраться в происходящем. Иначе трудно понять, почему оператор obj.x++ в одном месте метода даёт совсем не тот эффект, что в другом месте. Поскольку если бы мы сказали “изменение поля x объекта obj”, было бы невозможно понять, что объекты-то разные! А правильная фраза “изменение поля x объекта, связанного со ссылочной переменной obj” подталкивает к мысли, что эти объекты в разных местах программы могут быть разными.

Способ передачи данных (ячейки памяти) в подпрограмму, позволяющий изменять содержимое внешней ячейки памяти благодаря использованию ссылки на эту ячейку, называется передачей по ссылке. И хотя в Java объект передаётся по ссылке, объектная переменная, в которой хранится адрес объекта, передаётся по значению. Ведь этот адрес копируется в другую ячейку, локальную переменную. А именно переменная является параметром, а не связанный с ней объект. То есть параметры в Java всегда передаются по значению. Передачи параметров по ссылке в языке Java нет.

Рассмотрим теперь нетривиальные ситуации, которые часто возникают при передаче ссылочных переменных в качестве параметров.

Мы уже упоминали о проблемах, возникающие при работе со строками. Рассмотрим подпрограмму, которая, по идее, должна бы возвращать с помощью переменной s3 сумму строк, хранящихся в переменных s1 и s2:

void strAdd1(String s1,s2,s3){

s3=s1+s2;

}

Строки в Java являются объектами, и строковые переменные являются ссылочными. Поэтому можно было бы предполагать возврат изменённого состояния строкового объекта, с которым связана переменная s3. Но всё обстоит совсем не так: при вызове

obj1.strAdd1(t1,t2,t3);

значение строковой переменной t3 не изменится. Дело в том, что в Java строки типа String являются неизменяемыми объектами, и вместо изменения состояния прежнего объекта в результате вычисления выражения s1+s2 создаётся новый объект. Поэтому присваивание s3=s1+s2 приводит к перещёлкиванию ссылки s3 на этот новый объект. А мы уже знаем, что это ведёт к тому, что новый объект оказывается недоступен вне подпрограммы – “внешняя” переменная t3 будет ссылаться на прежний объект-строку. В данном случае, конечно, лучше сделать функцию strAdd1 строковой, и возвращать получившийся строковый объект как результат вычисления этой функции.

Ещё пример: пусть нам необходимо внутри подпрограммы обработать некоторую строку и вернуть изменённое значение. Допустим, в качестве входного параметра передаётся имя, и мы хотим добавить в конец этого имени порядковый номер – примерно так, как это делает среда разработки при создании нового компонента. Следует отметить, что для этих целей имеет смысл создавать подпрограмму, хотя на первый взгляд достаточно выражения name+count. Ведь на следующем этапе мы можем захотеть проверить, является ли входное значение идентификатором (начинающимся с буквы и содержащее только буквы и цифры). Либо проверить, нет ли уже в списке имён такого имени.

Напишем в классе нашего приложения такой код:

String componentName="myComponent";

int count=0;

public void calcName1(String name) {

count++;

name+=count;

System.out.println("Новое значение="+name);

}

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

private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {

calcName1(componentName);

System.out.println("componentName="+componentName);

}

Многие начинающие программисты считают, что раз строки являются объектами, то при первом нажатии на кнопку значение componentName станет ”myComponent1”, при втором – ”myComponent2”, и так далее. Но значение myComponent остаётся неизменным, хотя в методе calcName1 новое значение выводится именно таким, как надо. В чём причина такого поведения программы, и каким образом добиться правильного результата?

Если мы меняем в подпрограмме значение полей у объекта, а ссылка на объект не меняется, то изменение значения полей оказывается наблюдаемым с помощью доступа к тому же объекту через внешнюю переменную. А вот присваивание строковой переменной внутри подпрограммы нового значения приводит к созданию нового объекта-строки и перещёлкивания на него ссылки, хранящейся в локальной переменной name. Причём глобальная переменная componentName остаётся связанной с первоначальным объектом-строкой "myComponent".

Как бороться с данной проблемой? Существует несколько вариантов решения.

Во-первых, в данном случае наиболее разумно вместо подпрограммы-процедуры, не возвращающей никакого значения, написать подпрограмму-функцию, возвращающую значение типа String:

public String calcName2(String name) {

count++;

name+=count;

return name;

}

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

private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {

componentName=calcName2(componentName);

System.out.println("componentName="+componentName);

}

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

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

В-третьих, возможно создание оболочечного объекта (wrapper), у которого имеется поле строкового типа. Такой объект передаётся по ссылке в подпрограмму, и у него внутри подпрограммы меняется значение строкового поля. При этом, конечно, это поле будет ссылаться на новый объект-строку. Но так как ссылка на оболочечный объект внутри подпрограммы не меняется, связь с новой строкой через оболочечный объект сохранится и снаружи. Такой подход, в отличие от использования подпрограммы-функции строкового типа, позволяет возвращать произвольное количество значений одновременно, причём произвольного типа, а не только строкового. Но у него имеется недостаток – требуется создавать специальные классы для формирования возвращаемых объектов.

В-четвёртых, имеется возможность использовать классы StringBuffer или StringBuilder. Это наиболее адекватный способ при необходимости возврата более чем одного значения, поскольку в этой ситуации является и самым простым, и весьма эффективным по быстродействию и используемым ресурсам. Рассмотрим соответствующий код.

public void calcName3(StringBuffer name) {

count++;

name.append(count);

System.out.println("Новое значение="+name);

}

StringBuffer sbComponentName=new StringBuffer();

{sbComponentName.append("myComponent");}

private void jButton8ActionPerformed(java.awt.event.ActionEvent evt){

calcName3(sbComponentName);

System.out.println("sbComponentName="+sbComponentName);

}

Вместо строкового поля componentName мы теперь используем поле sbComponentName типа StringBuffer. Почему-то разработчики этого класса не догадались сделать в нём конструктор с параметром строкового типа, поэтому приходится использовать блок инициализации, в котором переменной sbComponentName присваивается нетривиальное начальное значение. В остальном код очевиден. Принципиальное отличие от использования переменной типа String – то, что изменение значения строки, хранящейся в переменной StringBuffer, не приводит к созданию нового объекта, связанного с этой переменной.

Вообще говоря, с этой точки зрения для работы со строками переменные типа StringBuffer и StringBuilder подходят гораздо лучше, чем переменные типа String. Но метода toStringBuffer() в классах не предусмотрено. Поэтому при использовании переменных типа StringBuffer обычно приходится пользоваться конструкциями вида sb.append(выражение). В методы append и insert можно передавать выражения произвольных примитивных или объектных типов. Правда, массивы преобразуются в строку весьма своеобразно, так что для их преобразования следует писать собственные подпрограммы. Например, при выполнении фрагмента

int[] a=new int[]{10,11,12};

System.out.println("a="+a);

был получен следующий результат:

a=[I@15fea60

И выводимое значение не зависело ни от значений элементов массива, ни от их числа.

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

void m1(Double d){

d++;

}

Несмотря на то, что переменная d объектная, изменение значения d внутри подпрограммы не приведёт к изменению снаружи подпрограммы по той же причине, что и для переменных типа String. При инкременте сначала производится распаковка в тип double, для которого выполняется оператор “++”. После чего выполняется упаковка в новый объект типа Double, с которым становится связана переменная d.