Всем привет. В данной теме я хочу подробно рассказать про классы Java и их виды, чтобы помочь новичкам разобраться с данной темой, и, возможно, не новичкам узнать что-то новое. По возможности, все будет показано на примерах из реальной жизни с прилегающими примерами кода. Давайте приступать.
Если коротко, в Java есть четыре вида классов, объявленных внутри другого класса: вложенный внутренний (нестатический), вложенный статический, локальный (объявлен внутри метода или другого блока кода) и анонимный (создается прямо в выражении
И хотелось бы отметить, что главное осознать первые два вида классов, а локальные и анонимные - это просто подвиды внутреннего класса.
Для начала, нам нужно создать класс
new). Первые два объявляются в теле внешнего класса и отличаются наличием слова static, а два последних это разновидности внутреннего класса, привязанные к конкретному месту в коде.Кратко
- В Java четыре вида классов внутри другого класса: вложенный внутренний, вложенный статический, локальный и анонимный.
- Вложенный внутренний объявляется без
staticи живет только вместе с объектом внешнего класса: крыло существует только у самолета. - Вложенный статический объявляется со
static, объект внешнего класса ему не нужен, и создается он какnew Building.Shop(...). - Локальный объявляется внутри блока кода, обычно внутри метода, и виден только там же.
- Анонимный пишется прямо в выражении
new: это класс без имени, нужный ровно один раз. - Вкладывают классы, чтобы держать связанную логику рядом и не плодить отдельные файлы там, где деталь имеет смысл только внутри целого.
И хотелось бы отметить, что главное осознать первые два вида классов, а локальные и анонимные - это просто подвиды внутреннего класса.
Что такое класс?
Класс это логическое описание чего-либо, шаблон, с помощью которого можно создавать реальные экземпляры этого самого чего-либо. Другими словами, это просто описание того, какими должны быть созданные сущности: какими свойствами и методами должны обладать. Свойства это характеристики сущности, методы это действия, которые она может выполнять. Хорошим примером класса из реальной жизни, дающим понимание, что же такое класс, можно считать чертежи: чертежи используются для описания конструкций (катапульта, отвертка), но чертеж это не конструкция. Инженеры используют чертежи, чтобы создавать конструкции, так и в программировании классы используются для того, чтобы создавать объекты, обладающие описанными свойствами и методами.
public class Student {
private String name, group, specialty;
public Student(String name, String group, String specialty) {
this.name = name;
this.group = group;
this.specialty = specialty;
}
// getters/setters
}
В данном примере мы создали Java класс, описывающий сущность «студент»: у каждого студента есть имя, группа и специальность. Теперь в других местах программы мы можем создавать реальные образцы данного класса. Другими словами: если класс Student это портрет того, каким должен быть студент, то созданный экземпляр - это непосредственно реальный студент.
Пример создания нового студента: new Student("Ivan", "KI-17-2", "Computer Engineering"); Оператор new ищет класс Student, после чего вызывает специальный метод (конструктор) данного класса. Конструктор возвращает готовый объект класса Student - нашего родного, голодного студента без стипендии :))
Виды классов в Java
В Java есть 4 вида классов внутри другого класса:Вложенные внутренние классы: нестатические классы внутри внешнего класса.
Вложенные статические классы: статические классы внутри внешнего класса.
Локальные классы Java: классы внутри методов.
Анонимные Java классы: классы, которые создаются на ходу.
Нестатические классы внутри внешнего класса
Сначала, я хочу, чтобы вы осознали, что это такое, на реальном примере, потому что это облегчает понимание в разы. Так что сейчас мы будем разбивать реальную большую вещь на более мелкие составные части, а разбирать мы будем самолет! Однако, для примера будет достаточно показать немного, полностью разбивать мы не будем. Для визуализации данного процесса, будем использовать схему самолета.
Для начала, нам нужно создать класс Airplane, куда мы можем занести немного описания: название самолета, идентификационный код, рейс.
public class Airplane {
private String name, id, flight;
public Airplane(String name, String id, String flight) {
this.name = name;
this.id = id;
this.flight = flight;
}
// getters/setters
}
Теперь мы хотим добавить крылья. Создавать отдельный класс? Возможно, в этом и есть логика, если у нас сложная программа для конструирования самолетов, и где нам нужно создавать огромное количество производных классов (классы, которые обладают такой же логикой, как и родительский класс, то есть класс, от которого они наследуются, но так же расширяют родительский класс, добавляя логику или более подробные характеристики), но что, если у нас просто игра, где у нас есть один самолет? Тогда нам будет рациональней укомплектовать всю структуру в одном месте (в одном классе).
Тут идут в бой нестатические вложенные классы. По сути, это более подробное описание каких-то деталей нашего внешнего класса. В данном примере, нам нужно создать крылья для самолета: левое и правое. Давайте создавать!
public class Airplane {
private String name, id, flight;
private Wing leftWing = new Wing("Red", "X3"), rightWing = new Wing("Blue", "X3");
public Airplane(String name, String id, String flight) {
this.name = name;
this.id = id;
this.flight = flight;
}
private class Wing {
private String color, model;
private Wing(String color, String model) {
this.color = color;
this.model = model;
}
// getters/setters
}
// getters/setters
}
Так мы создали нестатический вложенный класс Wing (крыло) внутри класса Airplane (самолет), и добавили две переменные: левое крыло и правое крыло. И у каждого крыла есть свои свойства (цвет, модель), которые мы можем изменять. Так можно укомплектовывать структуры столько, сколько нужно.
И заметьте: ранее на схеме было довольно много деталей у самолета, и, по сути, мы можем все детали разбить на внутренние классы, однако не всегда такой процесс целесообразен. Такие моменты нужно прослеживать в зависимости от задачи. Возможно, вам вообще не нужны крылья для решения задачи. Тогда и незачем их делать. Это как распилить человека на ноги, руки, торс и голову. Можно, но зачем, если данный класс используется только для хранения данных о людях?
Особенности нестатических вложенных классов Java:
- Они существуют только у объектов, потому для их создания нужен объект. Другими словами: мы укомплектовали наше крыло так, чтобы оно было частью самолета, потому, чтобы создать крыло, нам нужен самолет, иначе оно нам не нужно.
- До Java 16 внутренний класс мог объявлять только константы, то есть поля вида
static final int MAX = 10;, а любые другие статические члены приходилось выносить во внешний класс. Начиная с Java 16 это ограничение сняли: внутренний класс может объявлять и обычные статические поля с методами, и статические блоки инициализации, хотя сам статическим по-прежнему не является. - У класса полный доступ ко всем приватным полям внешнего класса. Данная особенность работает в две стороны.
- Можно получить ссылку на экземпляр внешнего класса. Пример: Airplane.this это ссылка на самолет, this это ссылка на крыло.
Статические классы внутри внешнего класса
Данный вид классов не отличается ничем от обычного внешнего класса, кроме одного: для создания экземпляра такого класса, нужно через точку перечислить весь путь от внешнего класса до нужного. Например:Building.Platform platform = new Building.Platform("Южная", "Kalyaeva 8/53");
Статические классы используются для того, чтобы укомплектовать связанные классы рядышком, чтобы с логической структурой было работать проще. Например: мы можем создать внешний класс Building, где будет конкретный список классов, которые будут представлять из себя уже конкретную постройку.
public abstract class Building {
private String name, address, type;
Building(String name, String address) {
this.name = name;
this.address = address;
}
public static class Platform extends Building {
public Platform(String name, String address) {
super(name, address);
setType("Platform");
}
// some additional logic
}
public static class House extends Building {
public House(String name, String address) {
super(name, address);
setType("House");
}
// some additional logic
}
public static class Shop extends Building {
public Shop(String name, String address) {
super(name, address);
setType("Shop");
}
// some additional logic
}
// getters/setters
}
Данный пример демонстрирует, как статические классы позволяют укомплектовывать логическую структуру в более удобный вид. Если бы их не было, нам бы понадобилось создавать 4 совершенно разных класса.
Плюсы такого подхода:
- Количество классов уменьшилось.
- Все классы внутри их класса-родителя. Мы способны прослеживать всю иерархию без открытия каждого класса отдельно.
- Мы можем обратиться к классу Building, а IDE уже будет подсказывать весь список всех подклассов данного класса. Это будет упрощать поиск нужных классов и показывать всю картину более целостно.
Building.Shop myShop = new Building.Shop("Food & Fun!", "Kalyaeva 8/53");
Хотелось бы еще отметить, что данная стратегия задействована в 2D классах AWT для описания фигур, таких, как Line2D, Arc2D, Ellipse2D и другие.
Локальные классы
Данные классы объявляются внутри других методов. По сути, они обладают всеми свойствами нестатического вложенного класса, только создавать их экземпляры можно исключительно внутри того же блока. Объявить локальный класс можно и в статическом методе, но тогда у него не будет ссылки на объект внешнего класса: в обычный метод такая ссылка передается неявно, а в статическом ее просто нет, поэтому и обращаться такой класс сможет только к статическим полям и методам внешнего класса. Но, свои особенности у них есть:- Локальные классы способны работать только с final переменными метода. Все дело в том, что экземпляры локальных классов способны сохраняться в «куче» после завершения работы метода, а переменная может быть стерта. Если же переменная объявлена final, то компилятор может сохранить копию переменной для дальнейшего использования объектом. И еще: с 8+ версий Java можно использовать не final переменные в локальных классах, но только при условии, что они не будут изменяться.
- Локальные классы нельзя объявлять с модификаторами доступа.
- Локальные классы обладают доступом к переменным метода.
Person (будем считать, что это человек) со свойствами street (улица), house (дом). Нам бы хотелось возвращать какой-то объект для доступа только к местоположению человека. Для этого, мы создали интерфейс AddressContainer, который подразумевает собой хранилище данных о местоположении человека.
public class Person {
private String name, street, house;
public Person(String name, String street, String house) {
this.name = name;
this.street = street;
this.house = house;
}
private interface AddressContainer {
String getStreet();
String getHouse();
}
public AddressContainer getAddressContainer() {
class PersonAddressContainer implements AddressContainer {
final String street = Person.this.street, house = Person.this.house;
@Override
public String getStreet() {
return this.street;
}
@Override
public String getHouse() {
return this.house;
}
}
return new PersonAddressContainer();
}
public static void main(String[] args) {
Person person = new Person("Nikita", "Sholohova", "17");
AddressContainer address = person.getAddressContainer();
System.out.println("Address: street - " + address.getStreet() + ", house - " + address.getHouse());
}
// getters/setters
}
Как можно заметить, внутри метода мы создали класс, реализующий хранилище местоположения человека, создали там константные переменные (чтобы после выхода из метода переменные хранились в объекте) и реализовали метод для получения адреса и дома. Теперь мы можем использовать данный объект в других местах программы, чтобы получать местоположение человека.
Понимаю, что данный пример неидеальный и его было правильней сделать просто оставив геттеры в классе Person, однако создание данного класса и его возможное использование было показано, а далее решать вам.
Анонимные классы
Под капотом анонимные классы это просто обычные нестатические вложенные классы. Их особенность в удобстве их использования. Вы можете написать свой класс прямо при создании экземпляра другого класса.
public class Animal {
public void meow() {
System.out.println("Meow!");
}
public static void main(String[] args) {
Animal anonTiger = new Animal() {
@Override
public void meow() {
System.out.println("Raaar!");
}
};
Animal notAnonTiger = new Animal().new Tiger();
anonTiger.meow(); // будет выведено Raaar!
notAnonTiger.meow(); // будет выведено Raaar!
}
private class Tiger extends Animal {
@Override
public void meow() {
System.out.println("Raaar!");
}
}
}
По сути, мы просто совмещаем в одном месте две вещи: создание экземпляра одного класса (Animal) и создание экземпляра его внутреннего-класса наследника (Tiger). Иначе нам нужно создавать класс отдельно и использовать более длинные конструкции, чтобы добиться того же самого результата.
Использование анонимных классов оправдано во многих случаях, в частности когда:
- тело класса является очень коротким;
- нужен только один экземпляр класса;
- класс используется в месте его создания или сразу после него;
- имя класса не важно и не облегчает понимание кода.
JButton b2 = new JButton("Click");
b2.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Кнопка нажата!");
}
});
Однако после Java 8 начали использовать лямбда-выражения, но все равно много кода было написано до 8 версии и вы можете столкнуться (и столкнетесь в ходе обучения на JavaRush) с такими вот надписями.
Аналог с лямбдами:
JButton b2 = new JButton("Click");
b2.addActionListener(e -> System.out.println("Кнопка нажата!"));
Четыре вида вложенных классов: сводка
Чтобы не листать статью туда-сюда, соберем все четыре вида в одну таблицу:| Вид класса | Где объявляется | Как создать экземпляр | Доступ к внешнему классу | Типичное применение |
|---|---|---|---|---|
| Вложенный внутренний | В теле внешнего класса, без static |
outer.new Inner(), нужен объект внешнего класса |
Ко всем полям и методам внешнего класса, включая приватные | Деталь целого: крыло у самолета |
| Вложенный статический | В теле внешнего класса, со static |
new Outer.Nested(), объект внешнего класса не нужен |
Только к статическим членам внешнего класса | Группировка родственных классов рядом: Building.House |
| Локальный | Внутри блока кода, обычно внутри метода | new Inner(), но только внутри того же блока |
К полям внешнего класса (в статическом методе только к статическим) и к переменным метода, если те не меняются | Вспомогательный тип, нужный ровно в одном методе |
| Анонимный | Прямо в выражении new, тело класса пишется на месте |
Создается тем же выражением new |
Такой же, как у внутреннего класса | Разовая реализация интерфейса или переопределение метода |
Конец статьи
Спасибо всем за внимание и надеюсь, что вы узнали что-нибудь новое или разобрались в чем-то, чего раньше не понимали. Хочу так же уточнить, что данная статья относится к номинации «внимание к деталям». Это моя первая работа, так что буду рассчитывать, что кому-то она была полезна. В ближайшее время, когда придут новые идеи, буду пытаться написать еще что-то, как раз есть одна идея... Удачи всем и успехов в программировании :)Вопросы и ответы
Чем вложенный статический класс отличается от внутреннего?
Наличием словаstatic и всем, что из него следует. Внутреннему классу нужен объект внешнего: без самолета крыла не существует, и создается оно через airplane.new Wing(). Статическому вложенному объект внешнего класса не нужен, он создается как new Building.Shop() и видит только статические члены внешнего класса. По сути это обычный класс, которому просто отвели место внутри другого.
Как создать объект внутреннего класса?
Через объект внешнего класса и операторnew после точки: Airplane airplane = new Airplane(...); Airplane.Wing wing = airplane.new Wing(...);. Конструкция непривычная, но логика простая: сначала должен появиться самолет, и только потом у него может появиться крыло. Изнутри самого внешнего класса писать длинную форму не нужно, там достаточно обычного new Wing(...).
Когда использовать анонимный класс, а когда лямбду?
Лямбда подходит, если вы реализуете интерфейс ровно с одним абстрактным методом: тогда запись короче и читается лучше. Анонимный класс нужен во всех остальных случаях: когда методов больше одного, когда надо переопределить метод обычного класса, а не интерфейса, или когда классу нужны собственные поля. В старом коде анонимные классы встречаются повсюду, потому что до Java 8 лямбд просто не было.Можно ли в локальном классе использовать переменные метода?
Да, и это его главное преимущество. Единственное условие: переменная не должна менять значение после инициализации. До Java 8 такую переменную требовалось помечать словомfinal, с Java 8 достаточно, чтобы она фактически не менялась. Причина в том, что объект локального класса может пережить метод, поэтому компилятор копирует значение переменной внутрь объекта, а копия и оригинал не должны разойтись.
Зачем вообще вкладывать классы друг в друга?
Чтобы держать связанные вещи рядом. Если тип имеет смысл только внутри другого типа, отдельный файл для него это лишний шум: крыло без самолета никому не нужно. Вложенность заодно дает доступ к приватным полям внешнего класса без геттеров и позволяет IDE показывать всю иерархию из одного места. Если же класс нужен в нескольких местах программы, его лучше вынести наружу.Что означает запись Outer.this?
Это ссылка на объект внешнего класса из кода внутреннего. Внутри метода вложенного класса словоthis указывает на сам вложенный объект, то есть на крыло, а Airplane.this на объект внешнего класса, то есть на самолет. Запись нужна, когда у внутреннего и внешнего классов совпадают имена полей и надо явно сказать, чье поле имеется в виду.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ