Последнее изменение: 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: '==', '>=' и '<=' для объектных оболочек..