GrabDuck

Java 8 вложенные классы и лямбда-выражения — urvanov.ru

:

Цикл статей «Учебник Java 8».

Следующая статья — «Java 8 интерфейсы».
Предыдущая статья — «Java 8 аннотации».

Язык программирования Java позволяет объявлять классы внутри другого класса. Такой класс называется вложенным классом:

Вложенные классы бывают статическими и нестатическими. Вложенные классы, объявленные с ключевым словом static, называются статическими вложенными классами (static nested classes). Вложенные классы, объявленные БЕЗ ключевого слова static, называются внутренними классами (inner classes).

Вложенный класс является членом класса, в который он вложен. Внутренние классы имеют доступ к членам класса, в который они вложены, даже если эти члены объявлены с модификатором private, для этого компилятор создаёт специальные методы доступа к этим полям, так что сама виртуальная машина принципов ООП не нарушает.

Как члены класса вложенные классы могут быть объявлены с ключевым словом private , protected , public  или без модификатора доступа (package-private).

Внешний класс (OuterClass) может быть только public  или package-private!

Для чего использовать вложенные классы

Причины для использования вложенных классов в Java:

  • Логическая группировка классов, которые используются только в одном месте. Если класс используется только одним другим классом, то есть смысл вложить его в этот класс, чтобы обозначить их связь.
  • Увеличение инкапсуляции. Если класс B  должен обращаться к членам класса A , которые в противном случае были бы объявлены private , то имеет смысл вложить класс B  в класс A , тогда эти члены можно будет объявить private , но B  сможет к ним обращаться. В дополнение B  можно будет скрыть от внешнего мира.
  • Облегчение чтения и сопровождения кода. Маленькие классы можно вложить во внешние классы, ближе к месту использования.

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

Статические вложенные классы связаны со своим внешним классом так же, как методы и переменные.

И так же как и статические методы они не могут обращаться к переменным экземпляров и методам экземпляров внешнего класса, в который они вложены, напрямую, они могут обращаться к ним только через ссылку на объект. Через ссылку на объект они могут обращаться к членам экземпляров внешнего класса независимо от их модификатора доступа.

Статические вложенные классы могут обращаться к static  членам класса, в который они вложены, с любым модификатором доступа.

Пример:

К статическим вложенным классам обращаются через имя их внешнего класса:

Либо можно импортировать статический вложенный класс и обращаться к нему по имени:

 

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

Внутренние классы связаны с экземпляром класса, в который они вложены, и они имеют доступ ко всем методам и полям внешнего класса независимо от их модификатора доступа.

Внутренние классы не могут объявлять static  членов внутри себя (кроме констант), так как они связаны с экземпляром внешнего класса.

Пример:

Внутренние классы бывают:

  • Нестатическими членами класса.
  • Локальными классами.
  • Анонимными классами.

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

Внутренний класс, являющийся нестатическим членом класса

Внутренний класс будет нестатическим членом класса, если он объявлен прямо внутри тела внешнего класса:

Такие внутренние классы обычно обычно логически связаны со своим внешним классом. Они имеют доступ ко всем полям этого внешнего класса. Экземпляры этих классов могут создаваться внутри внешнего класса и, при достаточном уровне доступа, другими классами из других пакетов.

Нестатические вложенные классы, являющиеся членами класса, могут быть объявлены с любым из модификаторов private , protected , public или без модификатора (package-private).

Локальные классы

Локальным классом называется класс, который не является членом какого-либо другого класса и имеет имя.

Локальные классы не могу иметь никаких модификаторов доступа: ни private , ни protected , ни public.

Анонимные классы

Анонимные классы объявляются внутри выражения с ключевым словом new . Пример:

Выражение анонимного класса состоит из:

  • Операции new .
  • Имени интерфейса для реализации или родительского класса. В данном примере используется интерфейс MyInterface.
  • Скобки с аргументами для конструктора родительского класса. Анонимный класс не может объявить в своём теле новых конструкторов, так как у него нет имени.
  • Тело класса.

Анонимный класс никогда не может быть abstract (абстрактные классы будут рассмотрены позже).

Анонимный класс всегда неявно final.

Анонимные классы могут обращаться к переменным метода, в котором они объявлены, если эти переменные объявлены как final , или они final  по действию, то есть фактически не меняются.

Затенение переменных

Если имя переменной в какой-либо области имеет такое же имя, что и переменная во внешней области, то такая переменная затеняет (shadow) переменную из внешней области. Вы не можете обратиться к переменной из внешней области просто по имени. Пример ниже показывает, как нужно обращаться к затенённой переменной:

В этом примере параметр x  метода method1  затеняет член класса SecondInnerClass , а x  из SecondInnerClass  затеняет x  из FirstInnerClass , а x  из FirstInnerClass  закрывает x  из ShadowClass  соответственно.

Обратите внимание на обращение к x  из различных уровней вложенности классов ( x , this.x , SecondInnerClass.this.x ).

Лямбда-выражения

Зачастую анонимный класс реализует интерфейс, который содержит только один абстрактный метод. В этом случае код можно написать ещё более коротко и понятно, если использовать лямбда-выражения.

Интерфейс, который содержит только один абстрактный метод, называется функциональным интерфейсом. Функциональный интерфейс может также содержать любое количество статических методов и default  методов. Более подробно интерфейсы будут разобраны в соответствующей статье.

Лямбда-выражение состоит из:

  1. Списка формальных параметров, разделённых запятой и заключённых в скобки. Если формальный параметр только один, то скобки можно опустить. Если формальных параметров нет, то используются просто пустые скобки. Тип формальных операторов указывать можно, но не обязательно.
  2. Токен стрелки ->.
  3. Тело, состоящие из одного оператора/инструкции или из блока операторов/инструкций. В случае блока операторов и результата метода отличного от void  для возврата значения используется ключевое слово return . В случае одного выражения результатом лямбда-выражения является результат этого выражения. Блок операторов может быть пустым.

Примеры лямбда-выражений:

Простой пример использования лямбда-выражения:

Так же как и локальные и анонимные классы лямбда-выражения могут обращаться к локальным переменным своей области, если эти переменные объявлены  final  или являются final  по действию. Лямбда-выражения в Java не порождают новую область видимости переменных, они используют ту же область, что и метод.

Результатом работы этого класса будет:

Так как лямбда-выражения используют ту же область видимости переменных, что и метод, в котором они объявлены, то они не могут вызывать затенения (shadow) переменных. Если в коде выше мы объявим лямбда-выражение так:

То будет ошибка компиляции, так как переменная x  в этой области уже объявлена.

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

  • Объявления переменных.
  • Операции присвоения.
  • Операторы return .
  • Инициализации массивов.
  • Аргументы конструкторов или методов.
  • Тела лямбда-выражений.
  • условные операторы, ?: .
  • Выражения приведения типа.

Лямбда-выражения можно сериализовать, если его аргументы и результат сериализуемые, однако так делать строго НЕ рекомендуется.

Пакет java.util.function  содержит большое количество стандартных интерфейсов, которые специально предназначены для использования с лямбда-выражениями. Хотя в примерах выше мы всегда создавали свой интерфейс, но в реальных приложениях рекомендуется использовать подходящий стандартный интерфейс, который можно поискать в документации. Все интерфейсы там обобщённые (статья про обобщение в Java будет написана позже), и могут использоваться с любым объектом.

Некоторые из функциональных интерфейсов пакета java.util.function :

Consumer<T>  — содержит один метод с одним объектом в качестве параметра без результата метода.

Function<T, R>  — содержит один метод с одним объектом в качестве параметра, возвращающий другой объект в качестве результата.

Predicate<T>  — содержит один метод с объектом в качестве параметра, возвращающий результат boolean.

Supplier<T>  — содержит один метод без параметров, возвращающий объект.

Ссылки на методы

Если лямбда-выражение выполняет только вызов определённого метода или конструктора, то вместо него можно использовать ссылку на метод.

Посмотрите код:

Здесь лямбда-выражения просто вызывают один метод. В таком случае можно использовать ссылки на методы:

Всего существует четыре вида ссылок на методы:

Вид ссылки Пример
Ссылка на статический метод ContainingClass::staticMethodName
Ссылка на метод экземпляра определённого объекта containingObject::instanceMethodName
Ссылка на метод экземпляра произвольного объекта ContainingType::methodName
Ссылка на конструктор ClassName::new

Пример ссылка на статический метод:

Пример ссылки на метод определённого экземпляра:

Пример ссылки на метод экземпляра произвольного объекта:

Эквивалентное лямбда-выражения для String::compareToIgnoreCase  будет (String a, String b) , где a  и  b  — произвольные имена. Эта ссылка на метод будет вызывать a.compareToIgnoreCase(b) .

Пример ссылки на конструктор:

Интерфейс java.util.function.Supplier  описывает только один метод, который возвращает объект. <Main>  в угловых скобках только показывает, что интерфейс будет использоваться для класса Main (более подробно это будет описано в статье про обобщение).

Когда использовать вложенные классы, локальные классы, анонимные классы и лямбда-выражения

Используйте локальные классы, если вы собираетесь создавать более одного экземпляра класса, использовать конструктор или собираетесь вводить новый именованный тип.

Используйте анонимные классы, если вам нужно добавлять поля или дополнительные методы.

Используйте лямбда-выражения, если вам нужен один экземпляр функционального интерфейса, или если вы собираетесь передавать одно действие в другой метод, например обработчик события.

Используйте вложенный класс, если у вас такие же требования, как и для локального класса, но вы хотите сделать его более широко доступным. Если вам нужен доступ к переменным внешнего класса, то используйте нестатический вложенный класс (внутренний класс), в противном случае используйте статический вложенный класс.

 

 
Цикл статей «Учебник Java 8».

Следующая статья — «Java 8 интерфейсы».
Предыдущая статья — «Java 8 аннотации».


Поделиться: