Внутренние и вложенные классы

Современный язык Java допускает объявление внутренних классов, то есть, когда один класс вложен в другой. Для примера давайте построим класс Dog (собака), у которого будет вложенный класс Foot (ноги). Как вы понимаете, у любой стандартной собаки должны быть ноги, поэтому мы сделаем класс Foot неотъемлемой частью класса Dog:

class Dog {
    private String name;
    private Foot foot;
 
    {
        name = "";
        foot = new Foot();
        System.out.println("Создан экземпляр класса foot");
    }
 
    Dog() {}
 
    Dog(String name) {
        this.name = name;
    }
 
    void run() {
        foot.run();
    }
 
    class Foot {
        void run() {
            System.out.println("Собака " + name + " бежит...");
        }
    }
}

Смотрите, мы внутри класса Dog объявили класс Foot, затем создали приватное поле foot, которое будет ссылкой на экземпляр класса Foot. Далее, при создании каждого нового объекта будет автоматически создаваться экземпляр класса Foot и задаваться пустое имя (name). После этого определены два конструктора и метод run, который, в свою очередь, вызывает такой же метод run, но из объекта foot. В результате, вся логика программы, отвечающая за движение собаки, описана отдельно в классе Foot. Такой подход бывает полезным для построения прозрачных, модульных текстов программ.

Теперь, можно создавать экземпляры этого класса, используя оператор new:

        Dog d = new Dog("Жучка");
        d.run();
        Dog d2 = new Dog("Жучка 2");
        d2.run();

Здесь у нас получаются две собаки и обе бегут.

В ООП вложенный класс, который динамически создается совместно с внешним, еще называется внутренним.

Также обратите внимание, что из класса Foot можно обращаться ко всем элементам внешнего класса Dog, даже частным (private). Например, при выводе в консоль, используется переменная name, объявленная в классе Dog. Но, что будет, если в классе Foot задать переменную с таким же именем?

    class Foot {
        String name = "Foot";
 
        void run() {
            System.out.println("Собака " + name + " бежит...");
        }
    }

При запуске программы видим, что имя собаки стало Foot, то есть, берется именно локальное поле name экземпляра класса Foot. Но в данном случае нам нужна не локальная переменная, а переменная из внешнего класса Dog. Как указать компилятору, чтобы он брал именно ее? Для этого используется доступ к внешним полям с явным указанием внешнего класса:

Имя_класса.this.имя_элемента

В нашем случае это будет выглядеть так:

    class Foot {
        String name = "Foot";
 
        void run() {
            System.out.println("Собака " + Dog.this.name + " бежит...");
        }
    }

Теперь name берется из класса Dog. А вот наоборот, такая конструкция не работает: из класса Dog нельзя напрямую обратиться к полю name класса Foot:

    void run() {
        System.out.println(Foot.this.name);
        foot.run();
    }

При выполнении этой программы возникнет ошибка. Мы можем получать только открытые члены класса Foot через ссылку foot на экземпляр класса:

    void run() {
        System.out.println(foot.name);
        foot.run();
    }

Внутренние классы можно определять даже в теле методов класса. Я приведу искусственный пример – просто перенесу объявление класса Foot в метод run:

class Dog {
    private String name = "";
 
    Dog() {}
 
    Dog(String name) {
        this.name = name;
    }
 
    void run() {
        class Foot {
            void run() {
                System.out.println("Собака " + Dog.this.name + " бежит...");
            }
        }
 
        Foot foot = new Foot();
        System.out.println("Создан экземпляр класса foot");
        foot.run();
    }
}

И, далее, вызывая метод run, мы всякий раз будем создавать новый объект класса Foot:

        Dog d = new Dog("Жучка");
        d.run();
        d.run();

Конечно, у нас получилась весьма странная собака, которая генерирует новые ноги всякий раз, как начинает бежать. Но для некоторых объектов такое поведение может быть оправданным.

Статические вложенные классы

В языке Java вложенные классы можно объявлять приватными (private), публичными (public), защищенными (protected) и статическими (static). Как работают первые два ключевых слова, я думаю понятно, про третье мы еще будем говорить, а вот последнее static накладывает определенные ограничения. Как мы с вами отмечали, когда рассматривали статические поля и методы, любые статические элементы принадлежат непосредственно классу, но не его экземплярам. Поэтому, определяя статический вложенный класс, мы как бы «отцепляем» его от объектов класса и он работает сам по себе, имея доступ только к статическим элементам внешнего класса:

Например, добавим в класс Dog вложенный статический класс Stat для подсчета числа созданных собак и генерации их id:

class Dog {
    private String name = "";
    private Foot foot = new Foot();
    private int id = -1;
 
    {
        Stat.counter++;
        id = ++Stat.id;
    }
 
    Dog() {}
 
    Dog(String name) {
        this.name = name;
    }
 
    void run() {
        foot.run();
    }
 
    class Foot {
        void run() {
            System.out.println("Собака " + Dog.this.name + " бежит...");
        }
    }
 
    static class Stat {
        public static int counter = 0;
        public static int id = 0;
    }
}

Смотрите, здесь в инициализаторе при создании каждого нового объекта класса Dog, мы увеличиваем счетчик собак на 1 и формируем текущий id объекта Dog. По идее здесь еще нужно контролировать уменьшение счетчика собак при уничтожении объекта. Как вариант, можно использовать метод finalize(), который вызывается при освобождении ресурсов объекта во время выполнения приложения. Но этот метод весьма не безопасный и устаревший (deprecated), поэтому лучше его не применять.

Путь кодера

Подвиг 1. Записать класс Properties для описания свойств графических примитивов с полями: толщина линии, тип линии, цвет. Последнее поле цвет должно представляться внутренним классом Color с полями: red, green, blue. Создать несколько объектов класса Properties с разным набором данных и вывести их в консоль.

Подвиг 2. Описать класс для представления музыкальных инструментов с полями: название, тип (целое число), габариты, цена, год производства. Поле «габариты» реализовать в виде внутреннего класса с полями: width, height, depth. Также добавить вложенный статический класс для накопления статистики по инструментам: количество объектов по типам.

Видео по теме