Опубликован: 04.12.2009 | Доступ: свободный | Студентов: 8414 / 657 | Оценка: 4.30 / 3.87 | Длительность: 27:27:00
Лекция 6:

Начальные сведения об объектном программировании

6.4. Передача ссылочных типов в функции. Проблема изменения ссылки внутри подпрограммы

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

Для примера создадим в нашем пакете класс Location. Он будет служить для задания объекта соответствующего типа, который будет передаваться через список параметров в метод m1, вызываемый из нашего приложения.

public class Location {
     public int x=0,y=0;
     public Location (int x, int y) {
       this.x=x;
       this.y=y;
    }
}

А в классе приложения напишем следующий код:

Location locat1=new Location(10,20);
 
 public static void m1(Location obj){
     obj.x++;
     obj.y++;
 }

Мы задали переменную locat1 типа Location, инициализировав ее поля x и y значениями 10 и 20. А в методе m1 происходит увеличение на 1 значения полей x и y объекта, связанного с формальным параметром obj.

Создадим две кнопки с обработчиками событий. Нажатие на первую кнопку будет приводить к выводу информации о значениях полей x и y объекта, связанного с переменной locat1. А нажатие на вторую – к вызову метода m1.

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.

Приведем еще один аналогичный пример:

public void proc1(Double d1,Double d2,Double d3){
    d3=d1+sin(d2);
}

Надежда на то, что в объект, передаваемый через параметр d3, возвратится вычисленное значение d3=d1+sin(d2), является ошибочной, так как при упаковке вычисленного результата создается новый объект.

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

public class UsableDouble{
    Double value=0;
    UsableDouble(Double value){
       this.value=value;
    }
}

Объект UsableDouble d можно передавать в подпрограмму по ссылке и без проблем получать возвращенное измененное значение. Аналогичного рода оболочные классы легко написать для всех примитивных типов.

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

Заканчивая разговор о проблемах передачи параметров в подпрограмму, автор хочет выразить надежду, что разработчики Java либо добавят в стандартные оболочечные классы такого рода методы, либо добавят возможность передачи переменных в подпрограммы по ссылке, как, к примеру, это было сделано в Java-образном языке C#.

Полетаев Дмитрий
Полетаев Дмитрий
Не очень понятно про оболочечные Данные,ячейки памяти могут наверно размер менять,какое это значение те же операции только ячейки больше,по скорости тоже самое
Максим Старостин
Максим Старостин

Код с перемещением фигур не стирает старую фигуру, а просто рисует новую в новом месте. Точку, круг.