5.03. Основные элементы Java
Основные элементы Java
Java — объектно-ориентированный язык общего назначения, в котором структура программы строится вокруг понятий, позволяющих описывать как данные, так и поведение, связывая их в единые логические сущности. Несмотря на то, что Java эволюционировала с течением времени — включая появление лямбда-выражений, модулей, записей и прочих современных конструкций — её ядро остаётся стабильным и опирается на ограниченный набор фундаментальных элементов. Понимание этих элементов необходимо для написания корректного кода и для осознанного проектирования архитектурных решений, читаемости кодовой базы и эффективного участия в командной разработке.
В этой главе мы последовательно рассмотрим ключевые строительные блоки языка: пакеты, классы и объекты, интерфейсы, конструкторы, поля и методы, а также механизмы перегрузки и переопределения. Особое внимание будет уделено тому, почему существует каждая из этих концепций, как они реализованы на уровне синтаксиса и как взаимодействуют друг с другом в процессе компиляции и выполнения.
Пакеты
Пакет в Java — это логическая группировка классов и интерфейсов, обеспечивающая иерархическую систему пространств имён. Основная задача пакетов заключается в устранении коллизий имён: если два разработчика или библиотеки независимо создадут класс с именем Logger, то без механизма пространств имён их использование в одном проекте будет невозможно. Пакеты решают эту проблему, присваивая каждому классу уникальный полноквалифицированный путь, например java.util.logging.Logger и org.apache.log4j.Logger — два разных класса, объявленных в разных пакетах.
Объявление пакета — самая первая строка (за исключением комментариев и директив import) в любом Java-файле:
package com.myapp.model;
Это утверждение связывает все объявленные в файле public классы с пространством имён com.myapp.model. По соглашению, имена пакетов записываются строчными буквами и следуют обратному порядку доменного имени организации: org.example.project, ru.university.department. Такой подход минимизирует вероятность конфликтов даже при совместном использовании кода из независимых источников.
Компилятор требует, чтобы структура каталогов на диске точно соответствовала иерархии пакетов. Файл с объявлением package com.myapp.model; должен находиться в подкаталоге com/myapp/model/. Эта жёсткая связь между логической и физической организациями помогает инструментам сборки (Maven, Gradle), IDE и самому разработчику быстро локализовать любой класс по его имени.
Пакеты также играют важную роль в управлении доступом. Помимо стандартных модификаторов (public, private, protected), в Java существует пакетно-приватный уровень видимости — это умолчательный, неявный модификатор, применяемый, когда явное указание отсутствует. Класс, метод или поле без модификатора доступны только другим членам того же пакета, что позволяет инкапсулировать детали реализации на уровне целого модуля, а не только отдельного класса.
Классы
Класс — это основная конструктивная единица в Java. Он представляет собой шаблон, описывающий совокупность состояний (через поля — переменные внутри класса) и поведений (через методы — функции, привязанные к классу). В отличие от некоторых языков, где допускаются свободно лежащие функции, в Java любой исполняемый код должен находиться внутри класса. Даже самая простая программа «Hello, World!» требует объявления класса с методом main.
Простейший класс определяется следующим образом:
public class Car {
String brand;
int year;
void startEngine() {
System.out.println(brand + " engine started");
}
}
Здесь Car — имя типа, brand и year — поля (состояние), startEngine — метод (поведение). Модификатор public делает класс доступным из любого другого пакета; без него класс будет виден только в рамках собственного пакета.
Каждый класс в Java неявно наследуется от корневого класса java.lang.Object, если явное наследование не указано через ключевое слово extends. Это означает, что любой объект в Java поддерживает базовые операции: сравнение (equals), получение хеш-кода (hashCode), строковое представление (toString), клонирование (clone) и проверку типа (getClass). Благодаря этой унифицированной иерархии становится возможным, например, хранить объекты разных типов в одном списке типа List<Object> или передавать их в общие утилиты, не зная их конкретной реализации.
Важно подчеркнуть: класс сам по себе — это описание, статическая сущность времени компиляции. Чтобы задействовать его функциональность, необходимо создать экземпляр — то есть объект.
Объекты
Объект — это динамическая сущность, существующая во время выполнения программы. Он представляет собой конкретную реализацию класса: выделенный в куче (heap) блок памяти, содержащий значения всех полей, а также ссылку на метаданные типа (включая таблицу виртуальных методов для поддержки полиморфизма).
Создание объекта осуществляется с помощью оператора new, который выполняет три действия:
- Выделяет память под все нестатические поля класса (и его предков).
- Вызывает конструктор — специальный метод инициализации.
- Возвращает ссылку на вновь созданный объект.
Пример:
Person person = new Person();
Правая часть (new Person()) — выражение, создающее объект. Левая часть (Person person) — объявление переменной типа Person, которая будет хранить ссылку на этот объект (а не сам объект целиком). Это принципиально: Java работает с объектами по ссылке, то есть переменные-ссылки содержат адрес объекта в памяти, а не данные напрямую.
Такая модель позволяет нескольким переменным ссылаться на один и тот же объект:
Person p1 = new Person();
Person p2 = p1; // p2 теперь указывает туда же, куда и p1
p2.name = "Алексей";
System.out.println(p1.name); // напечатает "Алексей"
Изменение состояния через одну ссылку немедленно отражается на всех остальных, потому что объект один.
Интерфейсы
Если класс определяет что есть (состояние и поведение), то интерфейс определяет что умеет (только поведение, без состояния). Это абстрактный тип, представляющий собой набор сигнатур методов — своего рода «договор», которому обязана соответствовать любая реализующая его сущность.
Объявление интерфейса использует ключевое слово interface:
interface Flyable {
void fly();
}
До Java 8 интерфейсы могли содержать только абстрактные методы и public static final константы. Начиная с Java 8 появилась возможность объявлять дефолтные методы (default) с реализацией по умолчанию и статические методы (static), а в Java 9 — приватные методы для внутреннего переиспользования в дефолтных реализациях. Однако основная идея остаётся неизменной: интерфейс задаёт поведенческий контракт, не вдаваясь в детали реализации.
Класс реализует интерфейс с помощью ключевого слова implements:
class Bird implements Flyable {
public void fly() {
System.out.println("Раскрываем крылья...");
}
}
Чтобы компилятор принял такой класс, он обязан предоставить реализацию для всех абстрактных методов интерфейса — либо напрямую, либо унаследовав её от суперкласса. Если реализация отсутствует, класс должен быть объявлен как abstract.
Интерфейсы позволяют строить гибкие и расширяемые иерархии. Они не подвержены проблеме множественного наследования реализации (которая в Java запрещена для классов), но допускают множественную реализацию интерфейсов:
class Drone implements Flyable, Controllable {
// должен реализовать fly() и control()
}
Такой подход лежит в основе принципа программирования на интерфейсах, а не на реализациях — одного из краеугольных камней проектирования в Java.
Конструкторы
Конструктор — это специальный метод, предназначенный исключительно для инициализации нового объекта. В отличие от обычных методов, у конструктора нет возвращаемого типа (даже void), а имя всегда совпадает с именем класса. Его вызов происходит автоматически при выполнении оператора new, и он завершается до того, как ссылка на объект станет доступной вызывающему коду.
Простой конструктор
class Animal {
String name;
Animal(String name) {
this.name = name;
}
}
Здесь конструктор принимает один параметр и присваивает его полю name. Ключевое слово this используется для явного обращения к текущему экземпляру и разрешения конфликта имён между параметром name и полем name.
Неявный конструктор по умолчанию
Если разработчик не объявляет ни одного конструктора, компилятор автоматически генерирует публичный конструктор без параметров — так называемый конструктор по умолчанию. Однако стоит добавить хотя бы один собственный конструктор — и неявный генерироваться перестанет. Это важное поведение: если класс требует обязательной инициализации (например, должен знать своё имя), наличие только параметризованного конструктора гарантирует, что объект без данных создан не будет.
Цепочка вызовов конструкторов
При наследовании инициализация проходит через всю иерархию предков. При создании объекта класса Cat:
class Animal {
Animal(String name) {
System.out.println("Animal: " + name);
}
}
class Cat extends Animal {
Cat(String name) {
super(name); // вызов конструктора родителя
System.out.println("Cat: " + name);
}
}
— первым делом выполняется тело конструктора Animal, затем тело конструктора Cat. Это обеспечивает корректную последовательность: сначала инициализируется базовое состояние (родитель), затем — расширение (дочерний класс).
Если явный вызов super(...) отсутствует, компилятор неявно вставляет вызов super() — конструктора без параметров родительского класса. Следовательно, если у родителя нет конструктора без параметров, код не скомпилируется:
class Animal {
Animal(String name) { } // нет конструктора по умолчанию
}
class Cat extends Animal {
Cat() { } // ← ошибка компиляции: super() недоступен
}
Это вынуждает разработчика явно управлять инициализацией, что повышает предсказуемость и безопасность.
Кроме super(...), внутри конструктора допустим вызов другого конструктора того же класса через this(...). Такая техника позволяет избежать дублирования кода инициализации:
class Rectangle {
int width, height;
Rectangle(int size) {
this(size, size); // делегирование другому конструктору
}
Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
Важно: вызов this(...) или super(...) — должен быть первой исполняемой инструкцией в конструкторе. Других вариантов нет.
Поля
Поле (field), или член класса — это переменная, объявленная на уровне класса (вне методов, конструкторов и блоков). Поля определяют состояние сущности: у объекта — его внутренние данные, у класса — общие для всех экземпляров свойства.
Классификация полей
Поля подразделяются по двум ключевым признакам:
-
По принадлежности:
- Нестатические (instance fields) — создаются для каждого объекта. Каждый экземпляр имеет свою копию таких полей. Они доступны через
thisили напрямую в методах экземпляра. - Статические (static fields) — принадлежат самому классу, а не объектам. Существует только одна копия, независимо от количества созданных экземпляров. Доступ осуществляется через имя класса:
ClassName.fieldName.
- Нестатические (instance fields) — создаются для каждого объекта. Каждый экземпляр имеет свою копию таких полей. Они доступны через
-
По способу инициализации:
- Явная инициализация — при объявлении:
private int count = 0; - Инициализация в конструкторе — при создании объекта.
- Инициализация в блоке инициализации — анонимный блок кода на уровне класса:
{
// instance инициализационный блок — выполняется при создании объекта
System.out.println("Initializing instance...");
}
static {
// статический инициализационный блок — выполняется один раз при загрузке класса
System.out.println("Loading class...");
}
- Явная инициализация — при объявлении:
Порядок инициализации
При создании объекта Java следует строгой последовательности:
- Рекурсивная инициализация всех статических членов предков, сверху вниз.
- Инициализация статических полей и выполнение
static {}блоков текущего класса. - Рекурсивная инициализация нестатических членов предков:
— инициализация полей (по порядку объявления),
— выполнение instance-блоков (по порядку),
— вызов конструктора предка (super()). - Инициализация нестатических полей и
instance {}блоков текущего класса. - Выполнение тела конструктора текущего класса.
Этот порядок гарантирует, что на момент входа в конструктор подкласса уже полностью инициализированы все поля предков — что критично для корректной работы переопределённых методов, вызываемых изнутри конструктора (хотя такая практика считается небезопасной и не рекомендуется).
Поля и локальные переменные
Термин поле применяется только к переменным на уровне класса. Переменные внутри методов, конструкторов или блоков {} называются локальными. Отличия фундаментальны:
| Характеристика | Поле (член класса) | Локальная переменная |
|---|---|---|
| Время жизни | Существует, пока жив объект (или класс — для static) | Существует только во время выполнения метода/блока |
| Значение по умолчанию | Присваивается автоматически (0, false, null) | Не присваивается — должна быть инициализирована явно перед использованием |
| Доступность | Зависит от модификатора (private, public, package-private, protected) | Всегда недоступна за пределами своего scope |
| Хранение | В куче (heap) | В стеке вызовов (stack) или в регистрах процессора |
Пример, иллюстрирующий разницу:
public class Example {
String instanceField; // null по умолчанию
static int staticField; // 0 по умолчанию
void method() {
// String localVar; // ← ошибка: переменная не инициализирована
String localVar = "init"; // OK
System.out.println(instanceField); // null → допустимо
System.out.println(localVar); // "init"
}
}
Попытка использовать неинициализированную локальную переменную приведёт к ошибке компиляции — это защитный механизм, предотвращающий использование «мусорных» значений.
Методы
Метод — это именованный блок кода, инкапсулирующий алгоритм, который можно вызвать по требованию. В отличие от функций в процедурных языках, методы в Java всегда привязаны к типу (классу или интерфейсу) и могут оперировать как собственным состоянием (this), так и внешними данными (параметрами).
Структура метода
Минимальное объявление состоит из:
- модификатора доступа (
public,private, и т.д. — может быть опущен), - типа возвращаемого значения (
void, если ничего не возвращается), - имени (идентификатора),
- списка параметров в скобках (может быть пустым),
- тела в фигурных скобках
{}.
public int calculate(int a, int b) {
return a * b + 10;
}
Ключевые аспекты:
- Параметры передаются по значению. Для примитивов — копируется само значение; для ссылочных типов — копируется ссылка (адрес), но не сам объект. Это означает, что метод может изменить состояние объекта по ссылке, но не может заменить саму ссылку в вызывающем коде.
- Возвращаемое значение обязано совпадать по типу с указанным в сигнатуре (с учётом правил неявного приведения). Оператор
returnзавершает выполнение метода и передаёт управление обратно. - Модификатор
finalу метода запрещает его переопределение в подклассах — фиксирует реализацию. - Модификатор
staticделает метод привязанным к классу, а не к объекту. В нём недоступенthis, и нельзя использовать нестатические поля/методы напрямую.
Перегрузка методов (Overloading)
Перегрузка позволяет использовать одно имя для нескольких методов в пределах одного класса, если их сигнатуры различаются. Сигнатура включает имя метода и типы параметров (в порядке объявления); количество, типы и порядок параметров определяют уникальность.
public class Printer {
void print(String s) { ... }
void print(int i) { ... }
void print(String s, int times) { ... }
}
Важные нюансы:
- Возвращаемый тип не входит в сигнатуру. Два метода с одинаковыми параметрами, но разными типами возврата — не перегрузка, а ошибка компиляции.
- Автоматическое приведение типов (например,
int→long,char→int) учитывается при разрешении вызова. Компилятор выбирает наиболее специфичный метод, соответствующий аргументам. - Перегрузка — механизм времени компиляции. Какой именно метод будет вызван, определяется по типу переменной, а не по типу фактического объекта.
Переопределение методов (Overriding)
Переопределение — это предоставление новой реализации метода в подклассе, уже существующего в суперклассе. Оно лежит в основе полиморфизма и динамического связывания.
Условия переопределения:
- Имя метода и список параметров (сигнатура) должны полностью совпадать.
- Возвращаемый тип должен быть совместим (для ссылочных типов — ковариантен: можно вернуть подтип).
- Нельзя сузить доступ: если метод в суперклассе
public, в подклассе он не может бытьprotectedилиprivate. - Исключения: переопределённый метод может выбрасывать меньше проверяемых исключений, но не больше.
class Shape {
public double area() { return 0; }
}
class Circle extends Shape {
private double radius;
public Circle(double r) { radius = r; }
@Override
public double area() { // возвращаемый тип double — совместим
return Math.PI * radius * radius;
}
}
Аннотация @Override не обязательна, но настоятельно рекомендуется: она заставляет компилятор проверить, действительно ли метод переопределяет существующий в иерархии. Если сигнатура не совпадает — будет ошибка, а не неявное создание нового метода.
Главная особенность переопределения — динамическое связывание: при вызове shape.area(), где shape имеет тип Shape, но ссылается на объект Circle, реально вызовется реализация из Circle. Это происходит благодаря таблице виртуальных методов (vtable), формируемой для каждого класса во время загрузки.
Переменные и области видимости
Переменная в Java — это именованная ячейка памяти для хранения данных определённого типа. Поскольку Java — язык со статической типизацией, тип переменной фиксируется на этапе компиляции и не может быть изменён в runtime.
Классификация переменных по области видимости
-
Локальные переменные
Объявляются внутри метода, конструктора или блока{}.- Время жизни: с момента инициализации до выхода из scope.
- Должны быть инициализированы перед первым чтением.
- Не имеют модификаторов доступа (кроме
final). - Хранятся в стеке вызовов.
-
Параметры методов
Формально — разновидность локальных переменных. Живут в течение всего вызова метода. Изменение параметра-ссылки (например, присваивание новой ссылки) не влияет на аргумент в вызывающем коде. -
Поля экземпляра (instance fields)
Объявлены на уровне класса, безstatic.- Создаются при
new, уничтожаются при сборке мусора. - Имеют значения по умолчанию.
- Доступны во всех нестатических методах класса (через
thisили напрямую).
- Создаются при
-
Статические поля (class fields)
Объявлены с модификаторомstatic.- Инициализируются один раз при загрузке класса.
- Доступны даже без создания объектов.
- Используются для хранения общего состояния (счётчики, кэши, конфигурации).
Правила разрешения имён (Name Resolution)
Когда компилятор встречает идентификатор (например, name), он ищет его в следующем порядке:
- Локальные переменные и параметры текущего scope.
- Поля текущего класса (включая унаследованные).
- Имена из импортированных статических членов (
import static) — если применимо. - Имена классов из импортов и текущего пакета.
Это приводит к явлению shadowing (затенения): локальная переменная с тем же именем, что и поле, «скрывает» поле в своём scope:
class Example {
int value = 10;
void method(int value) {
System.out.println(value); // параметр (локальный)
System.out.println(this.value); // поле класса
}
}
Затенение не запрещено, но требует аккуратности: использование this делает намерение явным и снижает риск ошибок.
Типы, область видимости и правила инициализации
В Java переменная — это именованная ссылка на область памяти, предназначенную для хранения значения определённого типа. В отличие от динамически типизированных языков, где переменная может менять тип в процессе выполнения, в Java тип переменной фиксируется на этапе компиляции и не может быть изменён. Эта строгая типизация позволяет компилятору проводить глубокий статический анализ кода, выявляя ошибки до запуска программы.
Однако важно сразу разделить два связанных, но принципиально разных понятия: тип переменной и тип значения, на которое она ссылается. Например:
Object obj = new String("Привет");
Здесь тип переменной — Object, а фактический тип объекта в памяти — String. Такая разница возможна благодаря наследованию и полиморфизму, но ограничения на операции накладываются именно типом переменной: через obj нельзя напрямую вызвать метод substring(), не выполнив явное приведение типа.
Классификация переменных
В Java выделяют четыре категории переменных — по их роли, времени жизни и месту хранения:
| Категория | Где объявляется | Время жизни | Инициализация по умолчанию | Хранение | Доступность |
|---|---|---|---|---|---|
| Локальные переменные | Внутри метода, конструктора или блока {} | С момента инициализации до выхода из scope | Нет — обязательна явная инициализация перед использованием | Стек вызовов (или регистры) | Только внутри своего scope |
| Параметры методов и конструкторов | В заголовке метода/конструктора | В течение всего вызова метода | Получают значение при вызове | Стек вызовов | Всюду в теле метода |
| Поля экземпляра (instance fields) | На уровне класса, без static | От создания объекта (new) до его сборки мусором | Да: 0, 0.0, false, null, \u0000 | Куча (heap) — внутри объекта | Зависит от модификатора доступа (private, protected, package-private, public) |
| Статические поля (class fields) | На уровне класса, с модификатором static | От загрузки класса в JVM до его выгрузки (в обычных приложениях — за весь срок работы программы) | Да — по тем же правилам, что и для полей экземпляра | Сегмент кучи, выделенный под метаданные класса (Metaspace / Permanent Generation) | Через имя класса или объект (но предпочтительно — через класс) |
Эта классификация определяет синтаксические ограничения и поведение программы в целом.
Локальные переменные
Локальные переменные — единственные, которые не получают значения по умолчанию. Это осознанное решение: компилятор требует, чтобы каждая локальная переменная была гарантированно инициализирована по всем возможным путям выполнения перед её первым чтением.
Примеры:
void example() {
int x; // объявление
// System.out.println(x); // ← ошибка компиляции: variable x might not have been initialized
if (Math.random() > 0.5) {
x = 10;
} else {
x = 20;
}
System.out.println(x); // OK — во всех ветках x инициализирована
}
Даже в случае условий компилятор проводит анализ путей выполнения (definite assignment analysis). Если существует хотя бы один путь, по которому переменная может быть прочитана до записи — код отклоняется.
Особый случай — переменные в циклах for:
for (int i = 0; i < 5; i++) {
// i доступна только внутри цикла
}
// System.out.println(i); // ошибка: i вне видимости
Область видимости переменной, объявленной в заголовке for, ограничена телом цикла — включая секции инициализации, условия и инкремента. Это предотвращает загрязнение внешнего scope и упрощает рефакторинг.
Параметры
В Java все аргументы передаются по значению — это фундаментальный принцип, часто вызывающий путаницу.
- Для примитивных типов (
int,boolean,char,doubleи др.) — копируется само значение. - Для ссылочных типов (
String,List, пользовательские классы) — копируется значение ссылки, то есть адрес объекта в куче.
Пример:
void modify(int x, StringBuilder sb) {
x = 999; // изменяет локальную копию — не влияет на вызывающий код
sb.append(" world"); // изменяет объект по ссылке — изменения видны снаружи
sb = new StringBuilder(); // переназначает локальную ссылку — не влияет на внешнюю переменную
}
// Вызов:
int a = 5;
StringBuilder buffer = new StringBuilder("Hello");
modify(a, buffer);
System.out.println(a); // 5
System.out.println(buffer); // "Hello world"
Ключевой вывод: метод может изменить состояние объекта, на который указывает ссылка, но не может изменить саму ссылку в вызывающем коде. Это исключает эффекты вроде «возврата нескольких значений через параметры», характерные для языков с передачей по ссылке (например, C# с ref/out).
Поля
Поля — это то, что делает объекты различимыми. Два объекта одного класса считаются разными, если хотя бы одно из их полей имеет разное значение (если не переопределён equals).
Важно: объявление поля не создаёт объекта, если поле ссылочного типа. Оно лишь резервирует место для ссылки, которая изначально равна null:
class Container {
List<String> items; // null по умолчанию
}
Container c = new Container();
// c.items.size() → NullPointerException
Отсюда проистекает необходимость инициализации — либо при объявлении (List<String> items = new ArrayList<>();), либо в конструкторе, либо в отдельном методе. Это стандартная практика, и современные IDE подсвечивают потенциально null-поля как возможный источник ошибок.
Статические поля
Статические поля принадлежат классу как сущности, а не его экземплярам. Это делает их полезными для:
- хранения глобальных конфигурационных параметров,
- реализации синглтонов (хотя это спорный паттерн),
- ведения счётчиков (например,
private static int instanceCount).
Однако из-за глобальной доступности статические поля нарушают инкапсуляцию и затрудняют тестирование (тесты становятся зависимыми от порядка выполнения). В промышленной разработке их использование рекомендуется сводить к минимуму — особенно изменяемых (non-final) статических полей.
Константы объявляются как public static final и именуются ПРОПИСНЫМИ_БУКВАМИ_С_ПОДЧЕРКИВАНИЯМИ:
public static final double PI = 3.141592653589793;
public static final String DEFAULT_ENCODING = "UTF-8";
Такие поля компилируются в inline-значения в вызывающем коде (если тип — примитив или String), поэтому изменение их значения в библиотеке требует перекомпиляции зависимых модулей.
Инициализация
Java гарантирует детерминированный порядок инициализации:
- Сначала инициализируются статические члены в порядке объявления (включая
static {}блоки). - Затем, при каждом создании объекта:
- инициализируются поля экземпляра и
instance {}блоки (сверху вниз), - вызывается конструктор.
- инициализируются поля экземпляра и
Пример:
class InitOrder {
static { System.out.print("S1 "); }
{ System.out.print("I1 "); }
static { System.out.print("S2 "); }
{ System.out.print("I2 "); }
InitOrder() { System.out.print("C "); }
static { System.out.print("S3 "); }
{ System.out.print("I3 "); }
}
// При первом обращении к классу:
// S1 S2 S3
// При new InitOrder():
// I1 I2 I3 C
Этот порядок позволяет безопасно использовать одни поля при инициализации других — но только если зависимости направлены «вперёд» по тексту. Обращение к полю, объявленному ниже, даст значение по умолчанию, а не результат его инициализатора — что может быть источником едва уловимых ошибок.
Примитивные и ссылочные типы
| Характеристика | Примитивные типы (byte, short, int, long, float, double, char, boolean) | Ссылочные типы (String, Object, массивы, классы, интерфейсы) |
|---|---|---|
| Хранение в переменной | Само значение (например, число 42) | Адрес (ссылка) на объект в куче |
| Размер переменной | Фиксированный: от 1 (boolean, byte) до 8 байт (long, double) | Фиксированный (обычно 4 или 8 байт, в зависимости от архитектуры JVM) |
Сравнение через == | По значению | По ссылке (т.е. указывают ли на один и тот же объект) |
Возможность null | Нет — примитивы всегда имеют значение | Да |
| Автоматическая инициализация полей | Да — нулевыми значениями | Да — null |
Отсюда вытекает важное правило: == не сравнивает содержимое объектов, только идентичность ссылок. Для сравнения по значению используются методы equals(), compareTo() и т.п.
Пример:
String s1 = new String("text");
String s2 = new String("text");
System.out.println(s1 == s2); // false — разные объекты
System.out.println(s1.equals(s2)); // true — одинаковое содержимое
Исключение — пул строк (String literals и результат intern()), где JVM может повторно использовать один и тот же объект для одинаковых текстов, но на это нельзя полагаться в логике программы.