Классы и объекты: введение
Объектно-ориентированное программирование — это способ мышления, а не просто набор инструментов. Сегодня мы делаем самый важный шаг: понимаем, что такое классы и объекты, и зачем они нужны в Java.
Программирование до и после ООП
Чтобы понять, зачем нужны классы и объекты, представьте мир без них. Программы писались в процедурном стиле: функции, глобальные переменные, данные хранились отдельно от логики. Это работало для 100–200 строк кода, но когда проекты вырастали до тысяч строк, начинался хаос.
Представьте, что вы готовите复杂ное блюдо. Процедурный рецепт: «возьмите 200 г муки, добавьте 2 яйца, перемешайте, запекайте при 180°C». Для нескольких рецептов — отлично. Но когда рецептов становится 100, и каждый раз приходится заново писать инструкции по замешиванию теста — вы сходит с ума.
Объектно-ориентированное программирование (ООП) предлагает другой подход: мы объединяем данные и методы в одну сущность — объект. Вместо «сначала данные, потом функции» мы думаем: «у меня есть объект Печенье, он знает свои ингредиенты и умеет сам себя запекать».
Java построена целиком на принципах ООП. Вы уже сталкивались с объектами: String, массивы, Scanner. Каждый String — это объект. Каждый массив — это объект. Теперь вы научитесь создавать собственные типы.
ООП — это парадигма программирования, в которой программа состоит из взаимодействующих объектов, каждый из которых имеет состояние (поля) и поведение (методы).
Класс — это чертёж
Класс — это фундаментальное понятие в Java. Насколько хорошо вы его освоите — так и будете программировать.
Аналогия: класс = рецепт торта, объект = торт. Рецепт описывает ингредиенты и шаги приготовления. Вы не можете съесть рецепт. Но вы можете испечь 10 тортов по одному рецепту. Каждый торт будет разным — ванильный, шоколадный, с ягодами. В Java: класс = описание, объект = конкретный экземпляр.
Ещё одна аналогия: класс — это форма для печенья. Форма одна, а печенье можно напечь сколько угодно. Каждое печенье будет иметь форму формы, но может отличаться по цвету, начинке, размеру. Форма описывает СТРУКТУРУ, а каждое печенье — это КОНКРЕТНЫЙ ВАРИАНТ.
Класс определяет СОСТОЯНИЕ (поля/данные) и ПОВЕДЕНИЕ (методы). Поля — это то, что объект ЗНАЕТ. Методы — это то, что объект УМЕЕТ ДЕЛАТЬ.
public class Dog {
String name;
int age;
String breed;
void bark() {
System.out.println("Гав! Меня зовут " + name);
}
void eat() {
System.out.println(name + " ест");
}
void displayInfo() {
System.out.println("Кличка: " + name);
System.out.println("Возраст: " + age);
System.out.println("Порода: " + breed);
}
}
Сам по себе класс — это просто описание. Как рецепт в книге. Чтобы получить живую собаку, которая гавкает и ест, нужно создать ОБЪЕКТ этого класса.
Обратите внимание на структуру класса. Сначала идут поля (переменные внутри класса), потом методы (функции внутри класса). Поля описывают данные: кличка, возраст, порода. Методы описывают действия: гавкать, есть, показывать информацию.
Объект — это экземпляр
Если класс — это чертёж, то объект — это конкретная вещь, построенная по этому чертежу. Если класс — это рецепт торта, то объект — сам торт, который можно съесть.
Ключевое слово new создаёт объекты в Java:
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.name = "Бобик";
myDog.age = 3;
myDog.breed = "Лабрадор";
myDog.bark();
myDog.eat();
myDog.displayInfo();
}
}
Разберём каждую строку:
Dog myDog— объявляем переменную типа Dog. Пока она равна null (никакого объекта нет).= new Dog()— создаём новый объект через оператор new и присваиваем ссылку на него переменной myDog.myDog.name = "Бобик"— обращаемся к полю объекта через точку и задаём значение.myDog.bark()— вызываем метод объекта через точку. Метод печатает «Гав! Меня зовут Бобик».
Из одного класса можно создать сколько угодно объектов. У каждого будут СВОИ значения полей:
Dog d1 = new Dog();
d1.name = "Бобик";
Dog d2 = new Dog();
d2.name = "Рекс";
Dog d3 = new Dog();
d3.name = "Мухтар";
d1.bark(); // Гав! Меня зовут Бобик
d2.bark(); // Гав! Меня зовут Рекс
d3.bark(); // Гав! Меня зовут Мухтар
Каждый объект — это отдельная сущность со своими данными. Бобик не знает, что Рекс существует, и наоборот. Они независимы.
Оператор new: что происходит «под капотом»
Когда вы пишете new Dog(), Java выполняет четыре шага:
- Выделяет память в куче (heap) — специальной области памяти для объектов.
- Инициализирует поля значениями по умолчанию — числа становятся 0, boolean — false, ссылочные типы — null.
- Вызывает конструктор — специальный метод, который настраивает объект (об этом подробно в уроке 3).
- Возвращает ссылку (адрес в памяти) — переменная хранит не сам объект, а указатель на него.
Переменная хранит ССЫЛКУ, а не сам объект. Это как пульт от телевизора: пульт — это не телевизор, но вы управляете телевизором через него. Пульт — это ссылка, телевизор — это объект в куче.
Если ссылочная переменная равна null, попытка вызвать через неё метод приведёт к NullPointerException — самой частой ошибке в Java. Всегда проверяйте ссылки на null перед вызовом методов!
Dog myDog = null;
// myDog.bark(); // NullPointerException — краш!
myDog = new Dog(); // Теперь myDog ссылается на объект
myDog.bark(); // Работает!
Ссылочные типы vs примитивы
В Java два мира: примитивы (int, double, boolean, char) и ссылочные типы (все объекты). Понимание их различий критически важно.
Примитивы хранят значение напрямую в переменной. Когда вы копируете примитив, создаётся полная копия значения. Изменение копии НЕ влияет на оригинал.
int a = 5;
int b = a;
b = 10;
System.out.println(a); // 5 — a не изменилось!
Представьте два стакана с водой. Вы налили воду в стакан A (a = 5), потом перелили в стакан B (b = a). Теперь в обоих по 5. Вы налили в B ещё воды (b = 10). В A всё ещё 5 — это отдельные стаканы.
Ссылочные типы хранят адрес объекта. Когда вы копируете ссылку, вы копируете АДРЕС, а не сам объект. Обе переменные указывают на ОДИН И ТОТ ЖЕ объект.
Dog d1 = new Dog();
d1.name = "Бобик";
Dog d2 = d1;
d2.name = "Рекс";
System.out.println(d1.name); // Рекс! Объект один!
Здесь d1 и d2 — два пульта от одного телевизора. Нажали кнопку через d2 — изображение изменилось. Нажали через d1 — увидели то же самое. Пультов два, телевизор один.
Это одна из самых частых ошибок новичков: думать, что Dog d2 = d1 создаёт нового собаку. Нет! Это создаёт вторую ссылку на ту же собаку.
Жизненный цикл объекта
У каждого объекта есть жизнь:
- Рождение:
new Dog()создаёт объект в куче. Выделяется память, вызывается конструктор. - Жизнь: На объект существует хотя бы одна ссылка. Через неё можно вызывать методы и менять поля.
- Смерть: На объект НЕТ ни одной ссылки. Сборщик мусора (Garbage Collector) удаляет его и освобождает память.
Dog dog = new Dog(); // Рождение — объект появился в куче
dog.bark(); // Жизнь — объект используется
dog = null; // Объект готов к сборке мусора
// dog.bark(); // Нельзя — ссылки нет!
Сборщик мусора работает автоматически. Вам не нужно вручную освобождать память (в отличие от C++). Java сама решает, когда удалить неиспользуемые объекты.
Можно иметь несколько ссылок на один объект. Объект удалится только когда ВСЕ ссылки на него станут равны null или выйдут из области видимости:
Dog d1 = new Dog();
Dog d2 = d1; // Две ссылки на один объект
d1 = null; // Объект всё ещё жив — d2 ссылается
d2 = null; // Теперь объект можно удалять
Практический пример: класс Книга
Давайте создадим класс Book — модель книги в библиотеке. Это классический пример, который показывает, как класс группирует данные и поведение:
public class Book {
String title;
String author;
int year;
int pages;
boolean isOpen;
void open() {
isOpen = true;
System.out.println("Книга \"" + title + "\" открыта");
}
void close() {
isOpen = false;
System.out.println("Книга \"" + title + "\" закрыта");
}
void read(int minutes) {
if (isOpen) {
System.out.println("Читаем \"" + title + "\" " + minutes + " минут");
} else {
System.out.println("Сначала откройте книгу!");
}
}
void displayInfo() {
System.out.println("Название: " + title);
System.out.println("Автор: " + author);
System.out.println("Год: " + year);
System.out.println("Страниц: " + pages);
System.out.println("Открыта: " + (isOpen ? "да" : "нет"));
}
String getDescription() {
return "\"" + title + "\" автора " + author + " (" + year + ")";
}
}
Теперь создадим объекты:
public class Library {
public static void main(String[] args) {
Book b1 = new Book();
b1.title = "Война и мир";
b1.author = "Толстой";
b1.year = 1869;
b1.pages = 1225;
Book b2 = new Book();
b2.title = "Преступление и наказание";
b2.author = "Достоевский";
b2.year = 1866;
b2.pages = 671;
b1.open();
b1.read(30);
b1.close();
System.out.println(b1.getDescription());
System.out.println("---");
b2.open();
b2.displayInfo();
b2.close();
}
}
Обратите внимание: каждая книга — отдельный объект со своими данными. «Война и мир» не знает о «Преступлении и наказании». Они независимы.
Стек и куча: где живут данные
Память Java разделена на две основные области:
Стек (Stack) — хранит примитивы и ссылки на объекты. Быстрая память, ограниченного размера. Когда метод завершается, его локальные переменные удаляются из стека.
Куча (Heap) — хранит сами объекты. Общая память, управляемая сборщиком мусора. Может быть размером в гигабайты.
int x = 5;
String name = "Привет";
Dog dog = new Dog();
// x (примитив) — в стеке, значение 5
// name (ссылка) — в стеке, указывает на строку в куче
// dog (ссылка) — в стеке, указывает на Dog в куче
// dog.name, dog.age — внутри объекта Dog в куче
Представьте стек как рюкзак, который носите с собой (быстро, удобно, мало места). А кучу — как склад (большой, но ходить до него дальше). Примитивы и ссылки — в рюкзаке. Самые объекты — на складе.
Почему классы — это здорово
Классы дают нам:
- Группировку данных: вместо 5 отдельных переменных — один объект Dog с полями name, age, breed.
- Группировку поведения: dog.bark() понятнее, чем bark(dog). Объект сам знает, как гавкать.
- Переиспользование: создаёте столько объектов, сколько нужно. Класс один, объектов — тысячи.
- Защиту данных: инкапсуляция (следующий урок) — контроль доступа к полям.
- Читаемость: код с объектами проще читать и поддерживать.
Без ООП код превращается в «кашу» из переменных и функций. С ООП — это структурированная, понятная система.
Ещё пример: класс Автомобиль
Давайте закрепим понимание на другом примере:
public class Car {
String brand;
String model;
int year;
double fuelLevel;
boolean isRunning;
void start() {
if (!isRunning && fuelLevel > 0) {
isRunning = true;
System.out.println(brand + " " + "заведён");
} else if (fuelLevel <= 0) {
System.out.println("Нет топлива!");
} else {
System.out.println("Автомобиль уже работает");
}
}
void stop() {
if (isRunning) {
isRunning = false;
System.out.println(brand + " заглушен");
}
}
void drive(double km) {
if (!isRunning) {
System.out.println("Сначала заведите двигатель!");
return;
}
double fuelUsed = km * 0.08;
if (fuelUsed > fuelLevel) {
System.out.println("Недостаточно топлива для поездки на " + km + " км");
} else {
fuelLevel -= fuelUsed;
System.out.println("Проехали " + km + " км. Осталось топлива: " + fuelLevel + " л");
}
}
void refuel(double liters) {
if (liters > 0) {
fuelLevel += liters;
if (fuelLevel > 60) fuelLevel = 60;
System.out.println("Заправлено. Уровень топлива: " + fuelLevel + " л");
}
}
void displayStatus() {
System.out.println(brand + " " + model + " (" + year + ")");
System.out.println(" Двигатель: " + (isRunning ? "работает" : "выключен"));
System.out.println(" Топливо: " + fuelLevel + " л");
}
}
public class Garage {
public static void main(String[] args) {
Car car1 = new Car();
car1.brand = "Toyota";
car1.model = "Camry";
car1.year = 2023;
car1.fuelLevel = 50;
car1.isRunning = false;
Car car2 = new Car();
car2.brand = "BMW";
car2.model = "X5";
car2.year = 2022;
car2.fuelLevel = 30;
car2.isRunning = false;
car1.start();
car1.drive(100);
car1.displayStatus();
System.out.println("---");
car2.start();
car2.drive(50);
car2.displayStatus();
}
}
Toyota и BMW — два разных объекта одного класса Car. У каждого свои данные и своё поведение. Изменение одного не влияет на другой.
Типичные ошибки новичков
Вот самые частые ошибки при работе с классами и объектами:
// Ошибка 1: Попытка использовать объект до создания
Dog myDog;
// myDog.bark(); // Ошибка! myDog равен null
// Ошибка 2: Забыли создать объект
// Dog dog; dog.name = "Рекс"; // Ошибка!
// Ошибка 3: Путаница примитивов и ссылок
String s1 = "Привет";
String s2 = s1;
s2 = "Пока";
System.out.println(s1); // "Привет" — строки неизменяемы!
// Ошибка 4: NullPointerException
Dog dog = null;
// dog.bark(); // Краш! Ссылка null
// Правильно:
Dog dog2 = new Dog();
dog2.name = "Рекс";
dog2.bark(); // Работает!
Краткое повторение
Давайте повторим ключевые概念ы этого урока:
- Класс — это чертёж/шаблон. Описывает поля (данные) и методы (действия).
- Объект — это экземпляр класса. Конкретная вещь, созданная по чертежу.
- new — оператор создания объекта. Выделяет память и возвращает ссылку.
- Ссылка — адрес объекта в памяти. Хранится в переменной.
- Примитивы — хранят значение напрямую. Копирование = копия значения.
- Ссылочные типы — хранят адрес. Копирование = копия адреса (объект один).
- null — означает «нет объекта». Вызов метода через null = NullPointerException.
- Сборщик мусора — автоматически удаляет объекты без ссылок.
- Стек — примитивы и ссылки. Куча — объекты.
Итоги урока
- Класс — это шаблон/чертёж; объект — это экземпляр, созданный из него.
- Оператор new выделяет память в куче и возвращает ссылку.
- Переменные хранят ссылки, а не сами объекты.
- Примитивы хранят значения; объекты доступны по ссылке.
- null означает «нет ссылки»; NullPointerException — самая частая ошибка.
- Сборщик мусора удаляет объекты без ссылок.
- Классы группируют данные (поля) и поведение (методы).
Следующий урок: Поля и методы класса — как их объявлять, использовать this, создавать геттеры и сеттеры.
Тест: Классы и объекты — введение
10 вопросов