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