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.