Без понимания синтаксиса Java невозможно стать серьезным разработчиком, поэтому продолжаем разбираться с основами. В прошлых статьях мы говорили о
примитивных переменных, а сегодня погружаемся во второй, не менее важный мир:
ссылочные типы данных.
Если совсем коротко: ссылочная переменная не хранит объект, она хранит адрес участка памяти, где этот объект лежит. Сам объект находится в куче, а переменная в стеке держит только ссылку на него. Из этого следует все остальное: ссылок на один объект может быть сколько угодно, оператор
== сравнивает адреса, а не содержимое, а объект, на который не осталось ни одной ссылки, забирает сборщик мусора.
Кратко
- Ссылочная переменная хранит не сам объект, а его адрес: объект лежит в куче, а переменная в стеке держит на него ссылку.
- Примитивных типов в Java всего восемь, все остальное это ссылочные типы: любой класс, включая String и ваши собственные, массив, интерфейс.
- Присваивание копирует адрес, а не объект. После a = b обе переменные управляют одним и тем же объектом, и правка через одну видна через другую.
- == для объектов сравнивает адреса, equals() сравнивает содержимое. Для строк почти всегда нужен equals().
- String неизменяем: любая правка создает новый объект. Литералы при этом попадают в общий String Pool, поэтому два одинаковых литерала это один объект.
- final держит только ссылку: переприсвоить переменную нельзя, а менять поля объекта, на который она смотрит, можно.
- Поле ссылочного типа по умолчанию равно null, а локальной переменной значение по умолчанию не полагается: ее нужно присвоить до первого использования (JLS 4.12.5). Объект, на который не осталось ни одной ссылки, забирает сборщик мусора.
Почему примитивов недостаточно?
![Мужчина с бумажной картой и штурмовик из «Звездных войн» стоят у старого голубого автомобиля посреди песчаных дюн и высматривают дорогу]()
Давайте представим: у нас есть объект телевизор с набором характеристик: номер канала, громкость звука, состояние (включен/выключен):
public class TV {
int numberOfChannel;
int soundVolume;
boolean isOn;
}
Теперь вопрос: как примитивный тип, например
int, может хранить эти данные? Напомню: одна переменная
int это всего 4 байта. А в нашем телевизоре две переменных
int (4 + 4 байта) плюс
boolean в довесок, то есть уже больше восьми байт. Точный размер
boolean в памяти, кстати,
спецификация JVM не фиксирует: отдельных инструкций под него нет, в выражениях он превращается в
int. Но сути это не меняет: в четыре байта такой объект не уложится при всем желании. И это самый простой объект! В реальных проектах объекты содержат десятки полей, другие объекты внутри себя, коллекции данных...
Что делать? Нельзя же впихнуть целый объект в переменную размером 4 байта. Вот тут-то и появляются
ссылочные переменные.
Что такое ссылочные типы?
Ссылочная переменная не хранит сам объект, она хранит
адрес ячейки памяти, где этот объект расположен. Представьте это как визитку с адресом: имея ее, мы можем найти нужный объект в памяти и работать с ним.
Любая ссылка на объект в Java это ссылочная переменная. Звучит запутанно? Сейчас разберем на примере.
Как это выглядит в коде
TV telly = new TV();
Что здесь происходит?
- new TV(): JVM выделяет память в куче (heap) и создает там объект типа TV;
- JVM возвращает адрес этого объекта в памяти;
- адрес сохраняется в переменной telly, которая хранится в стеке (stack).
Получается такая схема:
telly (в стеке) → адрес → объект
TV (в куче).
Важный момент: переменная типа
TV и объект типа
TV, заметили совпадение? Это не случайность. Объектам определенного типа должны соответствовать переменные того же типа (исключения это наследование и интерфейсы, но это тема для другого разговора).
Аналогия: объект это телевизор, а ссылочная переменная это пульт управления. С помощью этого пульта мы взаимодействуем с объектом:
telly.isOn = true;
telly.numberOfChannel = 53;
telly.soundVolume = 20;
Оператор точки
. дает нам доступ к внутренностям объекта. В первой строке мы говорим: "Дай мне поле
isOn объекта, на который ссылается
telly, и установи значение
true" (проще говоря, включи телевизор).
Примитив и ссылка: в чем разница
Чтобы окончательно развести эти два мира, соберем различия в одну таблицу.
| Что сравниваем |
Примитивный тип |
Ссылочный тип |
| Что лежит в переменной |
само значение |
адрес объекта |
| Где хранится значение |
прямо в переменной |
объект в куче, а в переменной только ссылка на него |
| Значение поля по умолчанию |
0, 0.0, false, '\u0000' |
null |
| Что копируется при присваивании |
значение, копии живут независимо |
адрес, обе переменные ведут к одному объекту |
| Что сравнивает == |
сами значения |
адреса, а не содержимое |
| Есть ли методы |
нет |
да, вызываются через точку |
| Может ли быть null |
нет |
да |
Переопределение ссылок: что происходит в памяти
Допустим, у нас две ссылочные переменные и два объекта:
TV firstTV = new TV();
TV secondTV = new TV();
Теперь пишем:
firstTV = secondTV;
Что происходит? Мы
копируем адрес из
secondTV в
firstTV. Теперь обе переменные указывают на второй объект. Получается два пульта от одного телевизора: если изменим что-то через
firstTV, это отразится и при обращении через
secondTV.
А что случилось с первым объектом? Он остался в памяти, но к нему больше нет ссылок. Такой объект превращается в мусор и будет удален
сборщиком мусора (Garbage Collector) при следующей очистке памяти.
Обнуление ссылки
Оборвать связь с объектом можно явно:
secondTV = null;
Теперь
secondTV ни на что не указывает, а на второй объект ссылается только
firstTV. Значение
null это отсутствие ссылки, "пустой пульт". Позже мы можем присвоить
secondTV ссылку на любой другой объект типа
TV.
Класс String: особый случай
Отдельного разговора заслуживает класс
String, один из самых используемых в Java. Это базовый класс для работы со строками, но у него есть несколько особенностей, которые сбивают с толку новичков.
Создание строк
Строку можно создать через конструктор, как любой другой объект:
String text = new String("This TV is very loud");
Но так никто не делает, и правильно: в
документации к конструктору
String(String original) прямо сказано, что без явной необходимости в отдельной копии он не нужен, ведь строки и так неизменяемы. Пишут проще:
String text = "This TV is very loud";
Гораздо удобнее, верно? Но здесь кроется важный нюанс.
String Pool: где живут строки
Когда вы создаете строку через двойные кавычки, происходит магия:
String lang1 = "Java";
String lang2 = "Java";
JVM не создает два объекта! Она видит, что строка "Java" уже есть в специальной области памяти под названием
String Pool (пул строк). Поэтому обе переменные получают ссылку на один и тот же объект.
Проверим:
String lang1 = "Java";
String lang2 = "Java";
System.out.println(lang1 == lang2);
А теперь создадим строки через
new:
String lang1 = new String("Java");
String lang2 = new String("Java");
System.out.println(lang1 == lang2);
Почему
false? Потому что
new всегда создает новый объект в куче, минуя String Pool. Это два разных объекта с одинаковым содержимым.
Сравнение строк: == против equals()
Это одна из самых частых ошибок новичков! Запомните:
Оператор == сравнивает адреса объектов (ссылки), а не содержимое.
Метод equals() сравнивает содержимое объектов.
Смотрите:
String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
Для строк
почти всегда нужно использовать
equals(). Оператор
== подходит только когда вы точно знаете, что работаете со строками из String Pool. Как этот метод устроен внутри и почему его переопределяют вместе с
hashCode(), разобрано в
отдельной статье.
Неизменяемость String
String это
immutable (неизменяемый) класс. Что это значит на практике?
String text = "This TV";
text = text + " is very loud";
Вторая строка выглядит как изменение, но это обман зрения! На самом деле:
- создается новый объект String со значением "This TV is very loud";
- переменная text получает ссылку на этот новый объект;
- старый объект "This TV" становится мусором.
Почему так сделано? Для безопасности и оптимизации. Неизменяемые строки можно безопасно переиспользовать в разных частях программы.
Конкатенация строк
У
String есть удобная возможность склеивания строк:
String text = "This TV" + " is very loud";
Но помните: каждая конкатенация через
+ создает новый объект! В циклах это может стать проблемой:
String result = "";
for (int i = 0; i < 1000; i++) {
result = result + i;
}
Для таких случаев используйте
StringBuilder:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
Модификатор final для ссылок
Что произойдет, если объявить ссылочную переменную с
final?
final TV telly = new TV();
Многие думают, что это делает объект неизменяемым.
Но нет! Модификатор
final действует только на саму ссылку, а не на объект.
Это значит:
telly = new TV();
telly = null;
telly.soundVolume = 30;
telly.isOn = false;
Представьте:
final это суперклей между пультом и конкретным телевизором. Пульт намертво приклеен к этому устройству, но сам телевизор вы можете настраивать как угодно.
final в параметрах метода
Иногда
final используют даже в аргументах:
public void enableTV(final TV telly) {
telly.isOn = true;
}
Это полезно для читаемости кода: сразу видно, что внутри метода мы не будем переопределять параметр. Меньше путаницы при чтении.
Настоящая константа
![Серо-белый кот сидит на снегу под падающими хлопьями и удивленно смотрит вверх с приоткрытым ртом]()
А что если применить
final к неизменяемому объекту, например
String?
final String PASSWORD = "password123";
Вот теперь получилась настоящая константа! Мы не можем:
- переопределить ссылку (из-за final);
- изменить содержимое объекта (потому что String immutable).
Практические примеры
Давайте посмотрим на реальные сценарии использования ссылочных типов.
Пример 1: Работа с пользователем
public class User {
private String name;
private String email;
private int age;
public User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
User user1 = new User("Иван", "ivan@example.com", 25);
User user2 = user1;
user2.setAge(26);
System.out.println(user1.getAge());
Пример 2: Null-проверки
public void processUser(User user) {
if (user == null) {
System.out.println("Пользователь не найден");
return;
}
System.out.println("Привет, " + user.getName());
}
Обойтись без ручных проверок помогает
Optional, он есть в Java начиная с восьмой версии. А сам
NullPointerException с Java 14 научился называть в сообщении конкретную переменную, которая оказалась пустой (
JEP 358), и с Java 15 такие подробные сообщения включены по умолчанию:
public Optional<User> findUser(String email) {
User user = findInDatabase(email);
return Optional.ofNullable(user);
}
findUser("test@example.com")
.ifPresent(user -> System.out.println("Найден: " + user.getName()));
Пример 3: Коллекции
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("Python");
languages.add("JavaScript");
List<String> sameList = languages;
sameList.add("Kotlin");
System.out.println(languages.size());
Часто задаваемые вопросы
В чем разница между == и equals() для объектов?
== сравнивает адреса в памяти (это одна и та же коробка?), а
equals() сравнивает содержимое (в коробках лежит одно и то же?). Для объектов почти всегда нужен
equals().
Почему String особенный, если это ссылочный тип?
String это обычный класс, но с тремя особенностями:
- его можно создавать через литералы ("текст");
- он неизменяемый (immutable);
- он хранится в специальном String Pool для экономии памяти.
Что такое NullPointerException?
Это ошибка, которая возникает, когда вы пытаетесь использовать ссылку, которая указывает на
null:
String text = null;
System.out.println(text.length());
У
null нет методов, поэтому JVM не знает, что делать, и выбрасывает исключение.
Можно ли изменить объект, на который указывает final переменная?
Да!
final запрещает только переопределение ссылки, но не изменение самого объекта (если он изменяемый).
Когда срабатывает сборщик мусора?
![Рабочий в оранжевом жилете забрасывает черный мусорный мешок в кузов мусоровоза на улице частного сектора]()
Сборщик мусора (Garbage Collector) автоматически удаляет объекты, на которые больше нет ссылок. Мы не контролируем точный момент запуска, но можем намекнуть JVM через
System.gc() (хотя это не гарантирует немедленной очистки).
Зачем нужен String Pool?
Строки используются повсеместно, и многие из них повторяются ("OK", "Error", "true"). String Pool позволяет переиспользовать одни и те же объекты вместо создания тысяч копий, это экономит память и ускоряет работу.
Подведем итоги
Главное вы уже знаете: переменная держит адрес, а не сам объект. Все остальное вырастает именно отсюда, и странности с
==,
null и неизменяемым
String перестают быть странностями. Если что-то забудется, короткая выжимка есть в начале статьи.
Дальше полезнее не перечитывать теорию, а потрогать руками. Возьмите любой свой класс, заведите две переменные на один объект и поменяйте поле через одну из них, а прочитайте через другую. Потом сравните две одинаковые строки через
== и через
equals(): сначала созданные литералом, потом через
new. Пара минут такой практики укладывает тему надежнее любого конспекта.
Понимание ссылочных типов это фундамент для работы с объектами, коллекциями, исключениями и практически всем остальным в Java. Освойте эти концепции, и дальнейшее обучение пойдет значительно проще!
Читайте также