Наследование
1. Введение
Наследование - один из четырех основных принципов объектно-ориентированного программирования (ООП), наряду с инкапсуляцией, полиморфизмом и абстракцией. Наследование позволяет создавать новые классы на основе существующих, расширяя их функциональность.
2. Основные понятия
- Суперкласс (родительский класс) - класс, от которого производится наследование
- Подкласс (дочерний класс) - класс, который наследует характеристики суперкласса
- Ключевое слово
extends
используется для реализации наследования
3. Синтаксис наследования
class Родитель {
// поля и методы родительского класса
}
class Потомок extends Родитель {
// дополнительные поля и методы
}
4. Практический пример
Рассмотрим простой пример наследования:
// Родительский класс
class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " кушает");
}
public void sleep() {
System.out.println(name + " спит");
}
}
// Дочерний класс
class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // вызов конструктора родительского класса
this.breed = breed;
}
public void bark() {
System.out.println(name + " лает: Гав-гав!");
}
}
5. Важные аспекты наследования
5.1 Ключевое слово super
- Используется для вызова конструктора родительского класса
- Позволяет обращаться к методам родительского класса
class Dog extends Animal {
public void makeSound() {
super.eat(); // вызов метода родительского класса
bark(); // вызов собственного метода
}
}
5.2 Переопределение методов
class Cat extends Animal {
@Override // аннотация, указывающая на переопределение метода
public void eat() {
System.out.println(name + " аккуратно кушает рыбку");
}
}
5.3 Модификаторы доступа при наследовании
public
- доступен вездеprotected
- доступен внутри пакета и в подклассахprivate
- не наследуетсяdefault
(пакетный) - доступен только внутри пакета
6. Множественное наследование
Java не поддерживает множественное наследование классов, но позволяет реализовывать множество интерфейсов:
interface Swimmable {
void swim();
}
interface Flyable {
void fly();
}
class Duck extends Animal implements Swimmable, Flyable {
public Duck(String name, int age) {
super(name, age);
}
@Override
public void swim() {
System.out.println(name + " плавает в пруду");
}
@Override
public void fly() {
System.out.println(name + " летит по небу");
}
}
7. Практическое применение
Рассмотрим более сложный пример с иерархией классов:
// Базовый класс для транспортных средств
class Vehicle {
protected String brand;
protected String model;
protected int year;
public Vehicle(String brand, String model, int year) {
this.brand = brand;
this.model = model;
this.year = year;
}
public void start() {
System.out.println("Транспортное средство заводится");
}
public void stop() {
System.out.println("Транспортное средство останавливается");
}
}
// Класс для легковых автомобилей
class Car extends Vehicle {
private int numberOfDoors;
public Car(String brand, String model, int year, int numberOfDoors) {
super(brand, model, year);
this.numberOfDoors = numberOfDoors;
}
@Override
public void start() {
System.out.println("Автомобиль " + brand + " " + model + " заводится с помощью ключа");
}
public void honk() {
System.out.println("Би-бип!");
}
}
// Класс для электромобилей
class ElectricCar extends Car {
private int batteryCapacity;
public ElectricCar(String brand, String model, int year, int numberOfDoors, int batteryCapacity) {
super(brand, model, year, numberOfDoors);
this.batteryCapacity = batteryCapacity;
}
@Override
public void start() {
System.out.println("Электромобиль " + brand + " " + model + " включается кнопкой");
}
public void charge() {
System.out.println("Электромобиль заряжается");
}
}
8. Ограничения наследования
- Класс в Java может наследоваться только от одного класса
- Приватные члены класса не наследуются
- Конструкторы не наследуются, но могут вызываться с помощью
super()
- Финальные классы (с модификатором
final
) не могут быть унаследованы
9. Преимущества наследования
- Повторное использование кода
- Установление отношений между классами
- Полиморфное поведение
- Улучшение поддерживаемости кода
10. Практические задания для закрепления:
- Создайте базовый класс
Shape
с методами для вычисления площади и периметра - Создайте производные классы
Circle
,Rectangle
иTriangle
- Реализуйте в каждом классе соответствующие методы расчета
- Продемонстрируйте использование полиморфизма
abstract class Shape {
abstract double getArea();
abstract double getPerimeter();
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
double getArea() {
return Math.PI * radius * radius;
}
@Override
double getPerimeter() {
return 2 * Math.PI * radius;
}
}
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
double getArea() {
return width * height;
}
@Override
double getPerimeter() {
return 2 * (width + height);
}
}
11. Абстрактные классы и наследование
Абстрактные классы играют важную роль в наследовании, предоставляя базовый функционал для подклассов.
abstract class Employee {
protected String name;
protected double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
// Абстрактный метод - должен быть реализован в подклассах
abstract double calculateBonus();
// Обычный метод
public void displayInfo() {
System.out.println("Имя: " + name);
System.out.println("Зарплата: " + salary);
}
}
class Manager extends Employee {
private int teamSize;
public Manager(String name, double salary, int teamSize) {
super(name, salary);
this.teamSize = teamSize;
}
@Override
double calculateBonus() {
return salary * 0.15 + (teamSize * 1000); // бонус зависит от размера команды
}
}
class Developer extends Employee {
private int linesOfCode;
public Developer(String name, double salary, int linesOfCode) {
super(name, salary);
this.linesOfCode = linesOfCode;
}
@Override
double calculateBonus() {
return salary * 0.1 + (linesOfCode * 0.1); // бонус зависит от написанного кода
}
}
12. Композиция vs Наследование
Иногда вместо наследования лучше использовать композицию. Пример:
// Использование наследования (не всегда оптимально)
class ElectricCar extends Car {
private Battery battery;
}
// Использование композиции (часто лучше)
class ModernCar {
private Engine engine;
private Battery battery;
private Electronics electronics;
public ModernCar(Engine engine, Battery battery, Electronics electronics) {
this.engine = engine;
this.battery = battery;
this.electronics = electronics;
}
}
13. Наследование и исключения
При переопределении методов важно учитывать правила работы с исключениями:
class Parent {
void method() throws IOException {
// код
}
}
class Child extends Parent {
@Override
void method() throws FileNotFoundException { // ОК: FileNotFoundException является подклассом IOException
// код
}
}
14. Правило подстановки Лисков (LSP)
Важный принцип наследования - объекты базового класса должны быть заменяемы объектами его подтипов:
class Bird {
void fly() {
System.out.println("Птица летит");
}
}
class Sparrow extends Bird {
@Override
void fly() {
System.out.println("Воробей летит");
}
}
class Penguin extends Bird { // Нарушение LSP
@Override
void fly() {
throw new UnsupportedOperationException("Пингвины не летают!");
}
}
// Лучше переработать иерархию:
abstract class Bird {
abstract void move();
}
class FlyingBird extends Bird {
@Override
void move() {
System.out.println("Птица летит");
}
}
class SwimmingBird extends Bird {
@Override
void move() {
System.out.println("Птица плывет");
}
}
15. Наследование и внутренние классы
class Outer {
class Inner {
void method() {
System.out.println("Метод внутреннего класса");
}
}
}
class ChildOuter extends Outer {
class ChildInner extends Inner {
@Override
void method() {
System.out.println("Переопределенный метод");
}
}
}
16. Практический пример: Система управления банковскими счетами
abstract class Account {
protected double balance;
protected String accountNumber;
public Account(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public abstract void withdraw(double amount);
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Внесено: " + amount);
}
}
public double getBalance() {
return balance;
}
}
class SavingsAccount extends Account {
private double interestRate;
public SavingsAccount(String accountNumber, double initialBalance, double interestRate) {
super(accountNumber, initialBalance);
this.interestRate = interestRate;
}
@Override
public void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
System.out.println("Снято: " + amount);
} else {
System.out.println("Недостаточно средств");
}
}
public void addInterest() {
double interest = balance * interestRate;
deposit(interest);
}
}
class CheckingAccount extends Account {
private double overdraftLimit;
public CheckingAccount(String accountNumber, double initialBalance, double overdraftLimit) {
super(accountNumber, initialBalance);
this.overdraftLimit = overdraftLimit;
}
@Override
public void withdraw(double amount) {
if (amount <= balance + overdraftLimit) {
balance -= amount;
System.out.println("Снято: " + amount);
} else {
System.out.println("Превышен лимит овердрафта");
}
}
}
17. Тестирование кода с наследованием
public class BankTest {
public static void main(String[] args) {
// Создаем различные типы счетов
SavingsAccount savings = new SavingsAccount("SA001", 1000.0, 0.05);
CheckingAccount checking = new CheckingAccount("CA001", 500.0, 1000.0);
// Тестируем операции
savings.deposit(500.0);
savings.withdraw(200.0);
savings.addInterest();
checking.withdraw(800.0); // Уходим в овердрафт
// Демонстрация полиморфизма
Account[] accounts = {savings, checking};
for (Account account : accounts) {
System.out.println("Баланс счета: " + account.getBalance());
}
}
}
18. Наследование и инициализация объектов
Важно понимать порядок инициализации при наследовании:
class Parent {
// 1. Статические блоки родительского класса
static {
System.out.println("1. Статический блок Parent");
}
// 2. Нестатические блоки родительского класса
{
System.out.println("2. Блок инициализации Parent");
}
// 3. Конструктор родительского класса
public Parent() {
System.out.println("3. Конструктор Parent");
}
}
class Child extends Parent {
// 4. Статические блоки дочернего класса
static {
System.out.println("4. Статический блок Child");
}
// 5. Нестатические блоки дочернего класса
{
System.out.println("5. Блок инициализации Child");
}
// 6. Конструктор дочернего класса
public Child() {
System.out.println("6. Конструктор Child");
}
}
19. Наследование и коллекции
Рассмотрим, как работает наследование с коллекциями:
class AnimalShelter {
private List<Animal> animals = new ArrayList<>();
public void addAnimal(Animal animal) {
animals.add(animal);
}
// Демонстрация ковариантности
public <T extends Animal> List<T> getAnimalsByType(Class<T> type) {
return animals.stream()
.filter(type::isInstance)
.map(type::cast)
.collect(Collectors.toList());
}
}
// Использование:
AnimalShelter shelter = new AnimalShelter();
shelter.addAnimal(new Dog("Рекс", 3, "Овчарка"));
shelter.addAnimal(new Cat("Мурка", 2));
List<Dog> dogs = shelter.getAnimalsByType(Dog.class);
20. Защита от наследования
20.1 Финальные методы
class SecuritySystem {
// Этот метод нельзя переопределить в подклассах
final void authenticate() {
// Критически важная логика аутентификации
performSecurityCheck();
validateCredentials();
}
private void performSecurityCheck() {
// Внутренняя реализация
}
protected void validateCredentials() {
// Можно переопределить этот метод
}
}
20.2 Запечатанные классы (Sealed Classes) - Java 17+
// Определяем, какие классы могут наследоваться
public sealed class Shape permits Circle, Rectangle, Triangle {
// общий код для фигур
}
// Должен быть final, sealed или non-sealed
public final class Circle extends Shape {
// реализация круга
}
public final class Rectangle extends Shape {
// реализация прямоугольника
}
public final class Triangle extends Shape {
// реализация треугольника
}
21. Наследование и паттерны проектирования
21.1 Шаблонный метод (Template Method)
abstract class DataMiner {
// Шаблонный метод
public final void mine() {
openFile();
extractData();
parseData();
analyzeData();
sendReport();
closeFile();
}
abstract void openFile();
abstract void extractData();
// Методы с реализацией по умолчанию
void parseData() {
System.out.println("Парсинг данных по умолчанию");
}
void analyzeData() {
System.out.println("Анализ данных по умолчанию");
}
// Хук-методы
protected boolean shouldSendReport() {
return true;
}
private void sendReport() {
if (shouldSendReport()) {
System.out.println("Отправка отчета");
}
}
void closeFile() {
System.out.println("Закрытие файла");
}
}
class PDFDataMiner extends DataMiner {
@Override
void openFile() {
System.out.println("Открытие PDF файла");
}
@Override
void extractData() {
System.out.println("Извлечение данных из PDF");
}
@Override
protected boolean shouldSendReport() {
return false; // Переопределение хук-метода
}
}
22. Наследование и обобщенные типы (Generics)
class Container<T> {
private T value;
public Container(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// Наследование с сохранением типа
class SpecialContainer<T> extends Container<T> {
public SpecialContainer(T value) {
super(value);
}
public void printInfo() {
System.out.println("Значение: " + getValue());
}
}
// Наследование с ограничением типа
class NumberContainer<T extends Number> extends Container<T> {
public NumberContainer(T value) {
super(value);
}
public double getDoubleValue() {
return getValue().doubleValue();
}
}
23. Проблемы и антипаттерны наследования
23.1 Глубокая иерархия наследования
// Плохой пример - слишком глубокая иерархия
class Vehicle {}
class LandVehicle extends Vehicle {}
class Car extends LandVehicle {}
class SportsCar extends Car {}
class RaceCar extends SportsCar {}
class FormulaOneCar extends RaceCar {}
// Лучше использовать композицию
class Car {
private Engine engine;
private Transmission transmission;
private List<Feature> features;
}
23.2 Наследование ради кода, а не из-за отношений "является"
// Плохой пример
class ArrayList {
// методы работы со списком
}
class Stack extends ArrayList { // Неправильно! Стек не является списком
// методы стека
}
// Правильный пример
class Stack {
private ArrayList<Object> items; // Композиция
// методы стека, делегирующие работу списку
}
24. Практическое задание: Создание системы учета товаров
abstract class Product {
protected String name;
protected double price;
protected String sku; // Stock Keeping Unit
public Product(String name, double price, String sku) {
this.name = name;
this.price = price;
this.sku = sku;
}
abstract double calculateTax();
abstract int getWarrantyPeriod(); // в месяцах
public double getFinalPrice() {
return price + calculateTax();
}
}
class Electronics extends Product {
private String brand;
private int warrantyPeriod;
public Electronics(String name, double price, String sku, String brand, int warrantyPeriod) {
super(name, price, sku);
this.brand = brand;
this.warrantyPeriod = warrantyPeriod;
}
@Override
double calculateTax() {
return price * 0.2; // 20% налог на электронику
}
@Override
int getWarrantyPeriod() {
return warrantyPeriod;
}
}
class Book extends Product {
private String author;
private String isbn;
public Book(String name, double price, String sku, String author, String isbn) {
super(name, price, sku);
this.author = author;
this.isbn = isbn;
}
@Override
double calculateTax() {
return price * 0.05; // 5% налог на книги
}
@Override
int getWarrantyPeriod() {
return 0; // книги не имеют гарантии
}
}
// Система управления инвентарем
class InventorySystem {
private Map<String, Product> inventory = new HashMap<>();
public void addProduct(Product product) {
inventory.put(product.sku, product);
}
public double calculateTotalValue() {
return inventory.values().stream()
.mapToDouble(Product::getFinalPrice)
.sum();
}
public List<Product> getProductsWithWarranty() {
return inventory.values().stream()
.filter(p -> p.getWarrantyPeriod() > 0)
.collect(Collectors.toList());
}
}
Это завершает наш обзор наследования в Java. Мы рассмотрели как базовые концепции, так и продвинутые техники использования наследования, включая: - Основы синтаксиса и механики наследования - Абстрактные классы и методы - Порядок инициализации - Работу с коллекциями - Защиту от наследования - Паттерны проектирования - Проблемы и антипаттерны - Практические примеры
пример
[[Programming/java/1. osnovi/Тема 4. Наследование/Урок 1. Наследование/задание|задание]]