Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 интерфейсы».
Предыдущая статья — «Java 8 аннотации».
Содержание
Для чего использовать вложенные классы
— Внутренний класс, являющийся нестатическим членом класса
Когда использовать вложенные классы, локальные классы, анонимные классы и лямбда-выражения
Введение
Язык программирования Java позволяет объявлять классы внутри другого класса. Такой класс называется вложенным классом:
1 2 3 4 5 6 |
class OuterClass { ... class NestedClass { ... } } |
Вложенные классы бывают статическими и нестатическими. Вложенные классы, объявленные с ключевым словом static, называются статическими вложенными классами (static nested classes). Вложенные классы, объявленные БЕЗ ключевого слова static, называются внутренними классами (inner classes).
1 2 3 4 5 6 7 8 9 10 11 12 |
class OuterClass { ... // Статический вложенный класс static class StaticNestedClass { ... } // Внутренний класс class InnerClass { ... } } |
Вложенный класс является членом класса, в который он вложен. Внутренние классы имеют доступ к членам класса, в который они вложены, даже если эти члены объявлены с модификатором private, для этого компилятор создаёт специальные методы доступа к этим полям, так что сама виртуальная машина принципов ООП не нарушает.
Как члены класса вложенные классы могут быть объявлены с ключевым словом private , protected , public или без модификатора доступа (package-private).
Внешний класс (OuterClass) может быть только public или package-private!
Для чего использовать вложенные классы
Причины для использования вложенных классов в Java:
- Логическая группировка классов, которые используются только в одном месте. Если класс используется только одним другим классом, то есть смысл вложить его в этот класс, чтобы обозначить их связь.
- Увеличение инкапсуляции. Если класс B должен обращаться к членам класса A , которые в противном случае были бы объявлены private , то имеет смысл вложить класс B в класс A , тогда эти члены можно будет объявить private , но B сможет к ним обращаться. В дополнение B можно будет скрыть от внешнего мира.
- Облегчение чтения и сопровождения кода. Маленькие классы можно вложить во внешние классы, ближе к месту использования.
Статические вложенные классы
Статические вложенные классы связаны со своим внешним классом так же, как методы и переменные.
И так же как и статические методы они не могут обращаться к переменным экземпляров и методам экземпляров внешнего класса, в который они вложены, напрямую, они могут обращаться к ним только через ссылку на объект. Через ссылку на объект они могут обращаться к членам экземпляров внешнего класса независимо от их модификатора доступа.
Статические вложенные классы могут обращаться к static членам класса, в который они вложены, с любым модификатором доступа.
Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
class OuterClass { private static int a1; protected static int a2; static int a3; public static int a4; private int x1; protected int x2; int x3; public int x4; private static void privateStaticOuterMethod1() {} static void packagePrivateStaticOuterMethod1() {} protected static void protectedStaticOuterMethod1() {} public static void publicStaticOuterMethod1() {} private void privateInstanceOuterMethod1() {} void packagePrivateInstanceOuterMethod1() {} protected void protectedInstanceOuterMethod1() {} public void publicInstanceOuterMethod1() {} static class StaticNestedClass { public void method1() { // можно обращаться к приватным статическим членам. int y1 = a1; int y2 = a2; int y3 = a3; int y4 = a4; privateStaticOuterMethod1(); packagePrivateStaticOuterMethod1(); protectedStaticOuterMethod1(); publicStaticOuterMethod1(); //int x4 = x2; НЕЛЬЗЯ! Только через ссылку на объект } public void method2(OuterClass oc) { // К членам экземпляров только через ссылку. int z1 = oc.x1; int z2 = oc.x2; int z3 = oc.x3; int z4 = oc.x4; oc.privateInstanceOuterMethod1(); oc.packagePrivateInstanceOuterMethod1(); oc.protectedInstanceOuterMethod1(); oc.publicInstanceOuterMethod1(); } } } |
К статическим вложенным классам обращаются через имя их внешнего класса:
1 2 |
OuterClass.StaticNestedClass obj1 = new OuterClass.StaticNestedClass(); obj1.method1(); |
Либо можно импортировать статический вложенный класс и обращаться к нему по имени:
1 2 3 4 5 6 7 8 9 10 |
package ru.urvanov.javaexamples.nestedclassexamples; class OuterClass { public static final int X1 = 4; static class NestedClass { } } |
1 2 3 4 5 6 7 8 9 10 11 |
package ru.urvanov.javaexamples.nestedclassexamples; import static ru.urvanov.javaexamples.nestedclassexamples.OuterClass.X1; import static ru.urvanov.javaexamples.nestedclassexamples.OuterClass.NestedClass; class Main { public static void main(String[] args) { System.out.println("X1=" + X1); NestedClass obj1 = new NestedClass(); } } |
Внутренние классы
Внутренние классы связаны с экземпляром класса, в который они вложены, и они имеют доступ ко всем методам и полям внешнего класса независимо от их модификатора доступа.
Внутренние классы не могут объявлять static членов внутри себя (кроме констант), так как они связаны с экземпляром внешнего класса.
Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class OuterClass { private int x1; private void method1() {} private static void method2() {} class InnerClass { // Внутренние классы имеют доступ // ко всем членам внешнего класса // независимо от модификатора доступа. int y1 = x1; { method1(); method2(); } // ... и т. д. // Можно обявлять константы public static final int MY_CONSTANT = 34; // Никаких других статических членов объявлять // во внутренних классах нельзя! // Будет ошибка компиляции. // НЕЛЬЗЯ! //static { // //} // // НЕЛЬЗЯ //static void method1() {}; } } |
Внутренние классы бывают:
- Нестатическими членами класса.
- Локальными классами.
- Анонимными классами.
Хотя сериализация конструкций с внутренними классами возможна, но на практике строго НЕ рекомендуется так делать. Для работы с внутренними классами компилятор создаёт синтетические конструкции, которые могут сильно отличаться в различных реализациях компиляторов.
Внутренний класс, являющийся нестатическим членом класса
Внутренний класс будет нестатическим членом класса, если он объявлен прямо внутри тела внешнего класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class OuterClass { int x1; double x2; class InnerClass { long field1; String field2; void method1() { } } public void someMethod1() { InnerClass obj1 = new InnerClass(); obj1.method1(); // ... } } |
Такие внутренние классы обычно обычно логически связаны со своим внешним классом. Они имеют доступ ко всем полям этого внешнего класса. Экземпляры этих классов могут создаваться внутри внешнего класса и, при достаточном уровне доступа, другими классами из других пакетов.
Нестатические вложенные классы, являющиеся членами класса, могут быть объявлены с любым из модификаторов private , protected , public или без модификатора (package-private).
Локальные классы
Локальным классом называется класс, который не является членом какого-либо другого класса и имеет имя.
Локальные классы не могу иметь никаких модификаторов доступа: ни private , ни protected , ни public.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
class OuterClass { class Cyclic {} private int someField1; void foo1() { // Нельзя. Циклическое наследование. // Область видимости Cyclic для локального класса // включает само объявление класса. // class Cyclic extends Cyclic {} // Нельзя. Здесь ещё LocalClass не объявлено. // LocalClass lc = new LocalClass(); final int x1 = 100; int x2 = 200; int x3 = 300; // А вот так можно class LocalClass { private void method1() { // Переменная x1 final. // Можно обращаться из локального класса System.out.println(x1); // Переменная x2 не меняется фактически, хотя и // не объявлена как final. Можно обращаться. System.out.println(x2); //System.out.println(x3); НЕЛЬЗЯ!. // Внутренние классы имеют доступ ко всем членам // своего внешнего класса. System.out.println(someField1++); } } class LocalClassB { void method2(LocalClass lc) { lc.method1(); // Можно. Так как они внутренние классы // одного и того же внешнего класса } } x3++; LocalClass lc = new LocalClass(); lc.method1(); } public static void main(String[] args) { OuterClass oc = new OuterClass(); oc.foo1(); } } |
Анонимные классы
Анонимные классы объявляются внутри выражения с ключевым словом new . Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class Main { interface MyInterface { void someMethod1(); } public static void main(String[] args) { final int x1 = 100; int x2 = 200; int x3 = 300; MyInterface obj1 = new MyInterface() { private int x1; // ... ещё поля. public void someMethod1() { // ... выполение действий. System.out.println(x1); System.out.println(x2); // System.out.println(x3); НЕЛЬЗЯ, так как x3 меняет значение } //... ещё методы. }; obj1.someMethod1(); x3++; // x3 НЕ final } } |
Выражение анонимного класса состоит из:
- Операции new .
- Имени интерфейса для реализации или родительского класса. В данном примере используется интерфейс MyInterface.
- Скобки с аргументами для конструктора родительского класса. Анонимный класс не может объявить в своём теле новых конструкторов, так как у него нет имени.
- Тело класса.
Анонимный класс никогда не может быть abstract (абстрактные классы будут рассмотрены позже).
Анонимный класс всегда неявно final.
Анонимные классы могут обращаться к переменным метода, в котором они объявлены, если эти переменные объявлены как final , или они final по действию, то есть фактически не меняются.
Затенение переменных
Если имя переменной в какой-либо области имеет такое же имя, что и переменная во внешней области, то такая переменная затеняет (shadow) переменную из внешней области. Вы не можете обратиться к переменной из внешней области просто по имени. Пример ниже показывает, как нужно обращаться к затенённой переменной:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class ShadowClass { int x = -1; class FirstInnerClass { int x = 1; class SecondInnerClass { int x = 2; void method1(int x) { // Параметр метода method1 System.out.println("x=" + x); // Член класса SecondInnerClass System.out.println("this.x=" + this.x); // Член класса FirstInnerClass System.out.println("FirstInnerClass.this.x=" + FirstInnerClass.this.x); // Член класса ShadowClass System.out.println("ShadowClass.this.x=" + ShadowClass.this.x); } } } public static void main(String[] args) { ShadowClass sc = new ShadowClass(); ShadowClass.FirstInnerClass fic = sc.new FirstInnerClass(); ShadowClass.FirstInnerClass.SecondInnerClass sic = fic.new SecondInnerClass(); sic.method1(3); } } |
В этом примере параметр x метода method1 затеняет член класса SecondInnerClass , а x из SecondInnerClass затеняет x из FirstInnerClass , а x из FirstInnerClass закрывает x из ShadowClass соответственно.
Обратите внимание на обращение к x из различных уровней вложенности классов ( x , this.x , SecondInnerClass.this.x ).
Лямбда-выражения
Зачастую анонимный класс реализует интерфейс, который содержит только один абстрактный метод. В этом случае код можно написать ещё более коротко и понятно, если использовать лямбда-выражения.
Интерфейс, который содержит только один абстрактный метод, называется функциональным интерфейсом. Функциональный интерфейс может также содержать любое количество статических методов и default методов. Более подробно интерфейсы будут разобраны в соответствующей статье.
Лямбда-выражение состоит из:
- Списка формальных параметров, разделённых запятой и заключённых в скобки. Если формальный параметр только один, то скобки можно опустить. Если формальных параметров нет, то используются просто пустые скобки. Тип формальных операторов указывать можно, но не обязательно.
- Токен стрелки ->.
- Тело, состоящие из одного оператора/инструкции или из блока операторов/инструкций. В случае блока операторов и результата метода отличного от void для возврата значения используется ключевое слово return . В случае одного выражения результатом лямбда-выражения является результат этого выражения. Блок операторов может быть пустым.
Примеры лямбда-выражений:
1 |
() -> {} |
1 |
x -> x * 2 - 3 |
1 2 3 4 5 6 |
x->{ System.out.println(x); int z = x * 2 - 3; System.out.println(x); return z; } |
1 |
(x,y) -> x + y |
1 |
x -> System.out.println(x) |
Простой пример использования лямбда-выражения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class Main { interface Operation { int operation(int x, int y); } interface SimpleOperation { void simpleOperation(int x); } static int[] arrayOperation(int[] x, int[] y, Operation operation) { int[] result = new int[x.length]; for (int n = 0; n < x.length; n++) { result[n] = operation.operation(x[n], y[n]); } return result; } static void arraySimpleOperation(int[] x, SimpleOperation simpleOperation) { for (int n = 0; n < x.length; n++) { simpleOperation.simpleOperation(x[n]); } } public static void main(String[] args) { // Пример сложения элементов массива: int[] resultSum = arrayOperation(new int[] {1, 0, 3}, new int[] {2, 1, 0}, (int x, int y) -> x + y); // Пример вычитания элементов массива: int[] resultMinus = arrayOperation(new int[] {1, 2, 3, 4}, new int[] {2, 2, 3, 1}, (x, y) -> x - y); // Вывод в консоль SimpleOperation writelnOperation = x -> System.out.println(x); System.out.println("Sum result:"); arraySimpleOperation(resultSum, writelnOperation); System.out.println("Minus result:"); arraySimpleOperation(resultMinus, writelnOperation); } } |
Так же как и локальные и анонимные классы лямбда-выражения могут обращаться к локальным переменным своей области, если эти переменные объявлены final или являются final по действию. Лямбда-выражения в Java не порождают новую область видимости переменных, они используют ту же область, что и метод.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class LambdaScopeTest { int x = 23; interface A { void method2(int y); }; class InnerClass { int x = 10; public void method1(int x) { A a = z -> { System.out.println("z=" + z); // можем обратиться к параметру method1, // так как он final по действию, // то есть его значение не меняется. System.out.println("x=" + x); System.out.println("this.x=" + this.x); System.out.println("LambdaScopeTest.this.x=" + LambdaScopeTest.this.x); }; a.method2(x); } } public static void main(String[] args) { LambdaScopeTest lsc = new LambdaScopeTest(); LambdaScopeTest.InnerClass ic = lsc.new InnerClass(); ic.method1(44); } } |
Результатом работы этого класса будет:
1 2 3 4 |
z=44 x=44 this.x=10 LambdaScopeTest.this.x=23 |
Так как лямбда-выражения используют ту же область видимости переменных, что и метод, в котором они объявлены, то они не могут вызывать затенения (shadow) переменных. Если в коде выше мы объявим лямбда-выражение так:
1 2 |
A a = x -> { ... |
То будет ошибка компиляции, так как переменная x в этой области уже объявлена.
Тип результата у лямбда-выражения будет такой, какой ожидается в этом месте, поэтому лямбда-выражения можно использовать только там, где компилятор Java может определить его тип:
- Объявления переменных.
- Операции присвоения.
- Операторы return .
- Инициализации массивов.
- Аргументы конструкторов или методов.
- Тела лямбда-выражений.
- условные операторы, ?: .
- Выражения приведения типа.
Лямбда-выражения можно сериализовать, если его аргументы и результат сериализуемые, однако так делать строго НЕ рекомендуется.
Пакет java.util.function содержит большое количество стандартных интерфейсов, которые специально предназначены для использования с лямбда-выражениями. Хотя в примерах выше мы всегда создавали свой интерфейс, но в реальных приложениях рекомендуется использовать подходящий стандартный интерфейс, который можно поискать в документации. Все интерфейсы там обобщённые (статья про обобщение в Java будет написана позже), и могут использоваться с любым объектом.
Некоторые из функциональных интерфейсов пакета java.util.function :
Consumer<T> — содержит один метод с одним объектом в качестве параметра без результата метода.
Function<T, R> — содержит один метод с одним объектом в качестве параметра, возвращающий другой объект в качестве результата.
Predicate<T> — содержит один метод с объектом в качестве параметра, возвращающий результат boolean.
Supplier<T> — содержит один метод без параметров, возвращающий объект.
Ссылки на методы
Если лямбда-выражение выполняет только вызов определённого метода или конструктора, то вместо него можно использовать ссылку на метод.
Посмотрите код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class Main { interface Operation { double method1(double x, double y); } static class OperationProvider { static double staticSum(double x, double y) { return x + y; } double instanceMinus(double x, double y) { return x - y; } } static double[] massOperation(double[] a, double[] b, Operation operation) { double[] result = new double[a.length]; for (int n = 0; n < a.length; n++ ) { result[n] = operation.method1(a[n], b[n]); } return result; } public static void main (String[] args) { double[] a = {1.0, 2.2, 3.1}; double[] b = {3.2, 4.1, 9.3}; final OperationProvider myOperationProvider = new OperationProvider(); massOperation(a, b, (x, y) -> OperationProvider.staticSum(x, y)); massOperation(a, b, (x, y) -> myOperationProvider.instanceMinus(x, y)); } } |
Здесь лямбда-выражения просто вызывают один метод. В таком случае можно использовать ссылки на методы:
1 2 3 4 |
// Ссылка на статический метод massOperation(a, b, OperationProvider::staticSum); // Ссылка на метод экземпляра massOperation(a, b, myOperationProvider::instanceMinus); |
Всего существует четыре вида ссылок на методы:
Вид ссылки | Пример |
---|---|
Ссылка на статический метод | ContainingClass::staticMethodName |
Ссылка на метод экземпляра определённого объекта | containingObject::instanceMethodName |
Ссылка на метод экземпляра произвольного объекта | ContainingType::methodName |
Ссылка на конструктор | ClassName::new |
Пример ссылка на статический метод:
1 |
massOperation(a, b, OperationProvider::staticSum); |
Пример ссылки на метод определённого экземпляра:
1 2 |
OperationProvider myOperationProvider = new OperationProvider(); massOperation(a, b, myOperationProvider::instanceMinus); |
Пример ссылки на метод экземпляра произвольного объекта:
1 2 |
String[] stringArray = { "Джо", "Александр", "Марфа", "Святослав" }; Arrays.sort(stringArray, String::compareToIgnoreCase); |
Эквивалентное лямбда-выражения для String::compareToIgnoreCase будет (String a, String b) , где a и b — произвольные имена. Эта ссылка на метод будет вызывать a.compareToIgnoreCase(b) .
Пример ссылки на конструктор:
Интерфейс java.util.function.Supplier описывает только один метод, который возвращает объект. <Main> в угловых скобках только показывает, что интерфейс будет использоваться для класса Main (более подробно это будет описано в статье про обобщение).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import java.util.function.Supplier; class Main { static Main[] createArray(int count, Supplier<Main> supplier) { Main[] result = new Main[count]; for (int n = 0; n < count; n++) { result[n] = supplier.get(); } return result; } public static void main(String[] args) { // Лямбда-выражение. Main[] a1 = createArray(10, () -> new Main()); // То же самое, но с ссылкой на конструктор. Main[] a2 = createArray(10, Main::new); } } |
Когда использовать вложенные классы, локальные классы, анонимные классы и лямбда-выражения
Используйте локальные классы, если вы собираетесь создавать более одного экземпляра класса, использовать конструктор или собираетесь вводить новый именованный тип.
Используйте анонимные классы, если вам нужно добавлять поля или дополнительные методы.
Используйте лямбда-выражения, если вам нужен один экземпляр функционального интерфейса, или если вы собираетесь передавать одно действие в другой метод, например обработчик события.
Используйте вложенный класс, если у вас такие же требования, как и для локального класса, но вы хотите сделать его более широко доступным. Если вам нужен доступ к переменным внешнего класса, то используйте нестатический вложенный класс (внутренний класс), в противном случае используйте статический вложенный класс.
Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 интерфейсы».
Предыдущая статья — «Java 8 аннотации».
Много, сложно, но посильно. Только стоит ли на этом зацикливаться начинающему или лучше вернутся сюда потом? (хорошо знаю AsctionScript 3)
=
Ещё один момент: я читал, что внутренние классы тесно связаны с событиями в Java. А в этой главе об этом ни слова. Да и вообще в списке статей про Java нет такой статьи «Java 8 события».
Хотя может я что-то не правильно понял.