Последнее изменение: 1 сентября 2007г.
Объекты и ссылки
Речь у нас пойдет о ссылках на объекты. Это то, что вызывает определенные трудности у многих рабработчиков при переходе на Java с С. Я, к счастью, этого избежал, ибо С знал в то время не очень хорошо. Знал, что есть объекты, есть указатели на них, и есть ссылки, но вот в чем разница между двумя последними – увы. Да сейчас это и неважно, наверное, ибо на С я не писал уже как минимум 6 лет.
Итак, вернемся к нашим... гм... ссылкам. В Java, как вы знаете, легко можно создать переменную типа объект. Вопрос: а что содержит эта переменная? А содержит она ссылку на объект. В спецификации Java сказано следующее:
An object is a class instance or an array.
The reference values (often just references) are pointers to these objects, and a special null
reference, which refers to no object.
Почти дословный перевод (почти – потому как дословно это не переводится):
Объект – это экземпляр класса или массив
Значения ссылок (чаще просто ссылки) – это указатели на эти объекты, и специальная ссылка null,
которая означает отсутствиет объекта.
Итак – ссылки в Java это, фактически, указатели. Однако, в отличие от С, арифметика указателей в Java отсутствует. И хорошо – ничего хорошего, кроме плохого, я от этой возможности не видел. А что вообще можно делать с ссылками? Какие операции над ними можно производить?
- Присваивание. Ну это естественно, без этой операции ссылки бы не имели смысла.
- Доступ к полям объекта
- Вызов методов
- Операция приведения типа
- Конкатенация строк (оператор '+')
- Проверка принадлежности к определенному типу – оператор instanceof
- Операции сравнения ссылки – '==' и '!='
- Условный оператор ? :
Начнем с начала. Присваивание. Тут есть одна тонкость. У каждого объекта есть счетчик ссылок на него. Присваивание новой ссылке указателя на объект увеличивает этот счетчик. Замещение значения ссылки на другое или null – соответственно, уменьшает. Когда на объект не остается ссылок – он становится доступен для сборщика мусора.
Еще один момент. При присваивании контролируется тип объекта по ссылке. Делается это на этапе компиляции.
Доступ к полям объектов, вызов методов, приведение типа. Тут особо обсуждать нечего. Эти операции прозрачны. А вот конкатенация строк – намного более интересная операция.
Оператор конкатенации требует, чтобы один из операндов был строковым. В этом случае логика его работы со вторым операндом (ссылкой) такова: он конвертирует объект по ссылке в строку путем вызова его метода toString() (если ссылка равна null или же вызов toString() возвращает null – используется строковый литерал "null"), после чего создает новый объект типа String, который является конкатенацией двух полученных строк.
Отсюда вытекает очень интересный пример. Выполните следующий код:
/*
* Copyright (c) 2005 Eugene Matyushkin
*/
package test;
/**
* ConcatenationTest
*
* @author Eugene Matyushkin
*/
public class ConcatenationTest{
public static void main(String[] args){
String str1 = null;
String str2 = null;
print(str1 + str2);
}
private static void print(String msg){
System.out.println("Message="+msg);
}
}
Что выдает этот код на консоль?
Message=nullnull
Любопытно, не правда ли? В любом случае, знать это стоит.
Пойдем дальше. Операция instanceof. С ней в принципе тоже все понятно. Хочу упомянуть только два момента.
Первый – если ссылка variable имеет значение null, то результат операции (variable
instanceof SomeType) всегда false, что в общем-то логично – у пустой ссылки невозможно определить тип. И
второе – операция instanceof является реализацией принципа наследования is a, т.е. результатом операции
экземпляр_дочернего_класса instanceof родительский_класс будет true.
О сравнении ('==' и '!=') и условном операторе тоже много не расскажешь. Единственный момент – если в операторе
<expression> ? value1 : value2 типы value1 и value2 разные, то один тип должен
быть приводим у другому. Т.е. если тип value1 – T1, а value2 – T2, то либо T1
является одним из родителей T2, либо наоборот. И тогда тип результата условного оператора – тот из двух, который
является родительским. Если же T1 и T2 не состоят в отношениях наследования – дело окончится
ошибкой компиляции.
* * *
Еще один момент, вытекающий из самой сути ссылок – передача объектов в качестве параметров при вызове функций. А именно – как это делается. А делается это вот как. При вызове функции в ее фрейм копируется ссылка на объект. Рассмотрим следующий фрагмент кода:
private void method1(){
MyClass obj = new MyClass();
obj.x = 1;
method2(obj);
// 0.
System.out.println("obj.x="+obj.x);
}
private void method2(MyClass param){
// 1.
param.x = 2;
// 2.
param = new MyClass();
param.x = 3;
}
class MyClass{
int x;
}
Вопрос номер раз. Сколько ссылок на созданный объект будет в точке 1.? Правильный ответ – две! Одна ссылка – это
переменная obj метода method1. Вторая – ссылка во фрейме вызова метода method2,
которую мы знаем по имени – param.
Вопрос номер два. Что происходит при присваивании param.x = 2? А происходит вот что. По ссылке param
берется объект, переменной которого устанавливается значение. Поскольку param ссылается на тот же
самый объект, что и obj в методе method1 – в методе method1 БУДУТ видны изменения
состояния объекта obj ВНУТРИ другого метода. Это первые из граблей, на которые часто наступают.
Вопрос номер три. Что происходит после точки 2.? А происходит вот что. Создается новый объект. Ссылка на него
присваивается переменной param, т.е. записывается во фрейм вызова. И ТОЛЬКО во фрейм, ссылка obj
в method1 остается неизменной! Таким образом, с этого момента obj и param указывают
на РАЗНЫЕ объекты. Что подтверждается очень легко: несмотря на присваивание param.x = 3; в точке 0. на консоль
будет выведено obj.x=2. Это еще одни грабли для начинающих: по необъяснимой для меня причине зачастую считают,
что если параметр передается по ссылке, то эту ссылку можно изменить. ВНУТРИ метода – можно. Что я и сделал, создав новый
экземпляр MyClass и присвоив ссылку на него переменной param. Но ссылку вовне изменить нельзя
никак.
Еще одни грабли – модификатор final. Что будет, если в объявлении method2 заменить MyClass
param на final MyClass param? Я встречал мнение, что в этом случае нельзя будет менять состояние
объекта внутри метода method2. Т.е. присваивание param.x=2 закончится неудачей. Это не так.
Модификатор final в данном случае будет означать, что нельзя поменять значение переменной param
(то, что я делал после точки 2.) Иначе говоря, param ВСЕГДА будет ссылаться на экземпляр класса, созданного
до вызова метода method2. И попытка присвоить этой переменной другое значение приведет к ошибке компиляции. Я
не буду сейчас объяснять, зачем нужна такая конструкция, речь не об этом. Важно, что присваивание param.x = 2;
по прежнему будет давать результат.
Все вышесказанное верно как для объектов, так и для массивов. Массивы тоже являются объектами, потому любое изменение их содержимого внутри какого-либо метода будет отражаться и вовне него.
Особенно важно об этом помнить вот почему. Допустим, у вас в объекте есть ArrayList, содержащий другие
объекты. И для каких-то целей вам нужно возвращать этот ArrayList целиком – например, для того, чтобы пройтись
по всем объектам. Так вот, если вы вернете ссылку непосредственно на тот ArrayList, который есть внутри вашего
объекта – никто не сможет помешать изменить этот ArrayList как вздумается. Добавить что угодно. Удалить что
угодно. И если содержимое этого ArrayList-а входит в состояние объекта – у вас будут серьезные проблемы.
Решением в данной ситуации может служить либо возвращение копии (т.е. создали новый ArrayList, скопировали в
него содержимое исходного, вернули новый), либо возвращение неизменяегого List, например, используя
Collections.unmodifiableList(List). Однако, ни то ни другое не гарантирует неизменности состояния самих объектов,
содержащихся в возвращаемых списках.
* * *
Несколько слов о вопросе, который вызывает много споров. А именно – как передаются параметры-объекты, по значению или по ссылке. С одной стороны – содержимое объекта не копируется, потому вроде как передача происходит по ссылке. Но с другой стороны – сама ссылка (значение области памяти, занимаемой переменной типа ссылка) КОПИРУЕТСЯ во фрейм вызова, как я уже упоминал выше. Т.е. вроде как передается по значению. Это порождает некую путаницу.
На мой взгляд все упирается в один вопрос – на каком уровне абстракции мыслить. Можно думать о ссылке как о ячейке памяти, в которой находится адрес объекта, и тогда ссылка, естественно, передается по значению – содержимое копируется. А можно думать просто об объекте. И знать при этом, что при передаче объекта в качестве параметра реально передается ссылка на него. И что эта ссылка – копия исходной (если она была). В принципе, этого более чем достаточно. Нужно просто понимать то, что из такого представления вытекает. Именно это я и описывал в данной статье.
Мне ближе мышление именно на уровне объектов. Да, я знаю всю эту кухню, но думать о ней каждую секунду мне совсем необязательно. Я передаю ссылку на объект. И с таким представлением проблем у меня пока не было. А все эти споры по поводу действительного способа передачи – по большей части религиозные. Каждый может представлять себе процесс так, как наиболее близко ему. И если это не мешает работе – то и Бог с ним!
* * *
Пока, наверное, все. Основные грабли я вроде как описал, а больше и не припомню. Всем спасибо, все свободны. Пишите аккуратно и вдумчиво!
* * *
Еще одно добавление. Начиная с Java 5.0 для ссылок определены еще и операции сложения, вычитания, умножения, деления и сравнения. Эти операции имеют смысл для ссылок на объектные оболочки численных примитивных типов. В этом случае производится unboxing, в результате чего в операции участвуют не ссылки на объекты, а реальные значения, взятые из этих объектов. Я, однако, рекомендовал бы очень осторожно использовать эти возможности (в смысле, autoboxing/unboxing), ибо они иногда порождают ощутимую путаницу. Я об этом упоминал вот тут: Сравнение объектов: практика -> Java 5.0. Autoboxing/Unboxing: '==', '>=' и '<=' для объектных оболочек..



skipy.ru)