GrabDuck

Вложенные и внутренние классы в Java. Локальные и анонимные классы в Java | Easy-Code.ru

:

Продолжаем рассматривать вложенные и внутренние классы в Java. На этот раз — локальные и анонимные классы.

Локальные классы (local classes)

Локальные классы — это классы, объявленные в блоке операторов между фигурными скобками. Например, локальные классы, объявленные в теле метода.

Объявление локальных классов

Вы можете объявить локальный класс внутри любого блока операторов. Например внутри методов, циклов или операторов if.

В следующем примере, проверяющем правильность телефонных номеров, объявляется локальный класс PhoneNumber в методе validatePhoneNumber:

public class LocalClassExample {

    static String regularExpression = "[^0-9]";

    public static void validatePhoneNumber(
        String phoneNumber1, String phoneNumber2) {

        final int numberLength = 10;

        // Valid in JDK 8 and later:
        // int numberLength = 10;

        class PhoneNumber {

            String formattedPhoneNumber = null;

            PhoneNumber(String phoneNumber){
                // numberLength = 7;
                String currentNumber = phoneNumber.replaceAll(regularExpression, "");
                if (currentNumber.length() == numberLength)
                    formattedPhoneNumber = currentNumber;
                else
                    formattedPhoneNumber = null;
            }

            public String getNumber() {
                return formattedPhoneNumber;
            }

            // Valid in JDK 8 and later:
            //public void printOriginalNumbers() {
            //    System.out.println("Original nubmers are " + phoneNumber1 + " and " + phoneNumber2);
            //}
        }

        PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
        PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);

        // Valid in JDK 8 and later:
        // myNumber1.printOriginalNumbers();

        if (myNumber1.getNumber() == null)
            System.out.println("First number is invalid");
        else
            System.out.println("First number is " + myNumber1.getNumber());
        if (myNumber2.getNumber() == null)
            System.out.println("Second number is invalid");
        else
            System.out.println("Second number is " + myNumber2.getNumber());

    }

    public static void main(String... args) {
        validatePhoneNumber("123-456-7890", "456-7890");
    }
}

Данная программа проверяет телефонные номера на соответствие некоторому стандарту. Сперва из номера удаляются все символы, отличные от цифр (0-9). После, происходит проверка длины номера и, если номер состоит из 10 цифр, то номер корректный. Данный пример выведет:

First number is 1234567890
Second number is invalid

Доступ к переменным в локальных классах

Локальный класс имеет доступ к членам класса, в котором он объявлен. В предыдущем примере конструктор PhoneNumber обращается к полю LocalClassExample.regularExpression.

Также, локальный класс имеет доступ к локальным переменным. Локальные классы имеют доступ только к переменным, объявленным как final. Когда локальный класс обращается к локальной переменной, происходит «захват»  этой переменой. Например, конструктор PhoneNumber имеет доступ к локальной переменной numberLength  потому что она объявлена как final; numberLength — захваченная переменная.

Начиная с  Java SE 8, локальные классы имеют доступ к финальным (final) локальным переменным и параметрам, а также к неизменяемым (effectively final) переменным, т.е. к переменным, которые не изменились с момента инициализации. Например, предположим, что переменная numberLength объявлена не финальной, и вы добавили строчку в конструктор PhoneNumber:

PhoneNumber(String phoneNumber) {
    numberLength = 7;
    String currentNumber = phoneNumber.replaceAll(
        regularExpression, "");
    if (currentNumber.length() == numberLength)
        formattedPhoneNumber = currentNumber;
    else
        formattedPhoneNumber = null;
}

Из-за этой строчки переменная больше не является неизменной (effectively final). В результате компилятор Java выдаст подобную ошибку: «local variables referenced from an inner class must be final or effectively final», когда внутренний класс PhoneNumber попытается обратиться к переменной numberLength:

if (currentNumber.length() == numberLength)

Начиная с  Java SE 8, если вы объявляете локальный класс в теле метода, он имеет доступ к параметрам этого метода. Например, вы можете объявить следующий метод в локальном классе PhoneNumber:

public void printOriginalNumbers() {
    System.out.println("Original numbers are " + phoneNumber1 +
        " and " + phoneNumber2);
}

Метод printOriginalNumbers обращается к параметрам phoneNumber1 и phoneNumber2 функции validatePhoneNumber.

Затенение (shadowing) и локальные классы

Объявление переменных в локальном классе затеняет ранее объявленные переменные с тем же именем в содержащей этот класс области. Затенение (shadowing) мы рассматривали в предыдущей статье.

Локальные классы похожи на внутренние

Локальные классы похожи на внутренние: они не могут содержать статические поля или методы. Локальные классы в статических методах, как класс PhoneNumber, который объявлен в статическом методе vaidatePhoneNumber, могут обращаться только к статическим полям внешнего класса. Например, если вы объявите поле regularExpression не статическим, то компилятор Java выдаст подобную ошибку: «non-static variable regularExpression cannot be referenced from a static context.»

Вы не сможете объявить интерфейс внутри метода — интерфейсы статичны. Например, следующий код не скомпилируется, т.к. интерфейс HelloThere объявлен внутри метода greetInEnglish:

public void greetInEnglish() {
    interface HelloThere {
       public void greet();
    }
    class EnglishHelloThere implements HelloThere {
        public void greet() {
            System.out.println("Hello " + name);
        }
    }
    HelloThere myGreeting = new EnglishHelloThere();
    myGreeting.greet();
}

При компиляции следующего кода будет сгенерирована ошибка: «modifier ‘static’ is only allowed in constant variable declaration», потому что метод EnglishGoodbye.sayGoodbye обявлен статическим (static).

public void sayGoodbyeInEnglish() {
    class EnglishGoodbye {
        public static void sayGoodbye() {
            System.out.println("Bye bye");
        }
    }
    EnglishGoodbye.sayGoodbye();
}

Локальный класс может содержать статические поля, только если они константы. (Константы — это финальные (final) переменные примитивных типов или типа String, которые инициализируются на стадии компиляции. Константы, инициализируемые на стадии компиляции — это, обычно, строки или арифметические выражения.) Следующий код скомпилируется, т.к. EnglishGoodbye.farewell — константа:

public void sayGoodbyeInEnglish() {
    class EnglishGoodbye {
        public static final String farewell = "Bye bye";
        public void sayGoodbye() {
            System.out.println(farewell);
        }
    }
    EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();
    myEnglishGoodbye.sayGoodbye();
}

Анонимные классы (Anonymous Classes)

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

Объявление анонимных классов

В отличие от объявления обычных классов, объявление анонимных классов происходит внутри выражения. В следующем примере используются анонимные классы при инициализации переменных frenchGreeting и spanishGreeting, и использует локальный класс для инициализации переменной englishGreeting:

public class HelloWorldAnonymousClasses {

    interface HelloWorld {
        public void greet();
        public void greetSomeone(String someone);
    }

    public void sayHello() {

        class EnglishGreeting implements HelloWorld {
            String name = "world";
            public void greet() {
                greetSomeone("world");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hello " + name);
            }
        }

        HelloWorld englishGreeting = new EnglishGreeting();

        HelloWorld frenchGreeting = new HelloWorld() {
            String name = "tout le monde";
            public void greet() {
                greetSomeone("tout le monde");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Salut " + name);
            }
        };

        HelloWorld spanishGreeting = new HelloWorld() {
            String name = "mundo";
            public void greet() {
                greetSomeone("mundo");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Hola, " + name);
            }
        };
        englishGreeting.greet();
        frenchGreeting.greetSomeone("Fred");
        spanishGreeting.greet();
    }

    public static void main(String... args) {
        HelloWorldAnonymousClasses myApp =
            new HelloWorldAnonymousClasses();
        myApp.sayHello();
    }
}

Синтаксис анонимных классов

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

Рассмотрим создание объекта frenchGreeting:

        HelloWorld frenchGreeting = new HelloWorld() {
            String name = "tout le monde";
            public void greet() {
                greetSomeone("tout le monde");
            }
            public void greetSomeone(String someone) {
                name = someone;
                System.out.println("Salut " + name);
            }
        };

Определение анонимного класса содержит:

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

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

Доступ к локальным переменным. Определение и доступ к полям анонимных классов

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

  • Анонимный класс имеет доступ к полям внешнего класса.
  • Анонимный класс не имеет доступ локальным переменным области, в которой он определен, если они не финальные (final) или неизменяемые (effectively final).
  • Как и у других внутренних классов, объявление переменной с именем, которое уже занято, затеняет предыдущее объявление.

Анонимные классы имеют те же ограничения, что и локальные:

  • Вы не можете определять статические члены анонимного класса.
  • Анонимный класс может содержать статические поля, если это константы.

Анонимные классы также могут содержать в себе локальные классы. Конструктора в анонимном классе быть не может.

Примеры анонимных классов

Анонимные классы часто используются в приложениях с графическим интерфейсом (GUI).

Рассмотрим JavaFX пример HelloWorld.java (основы JavaFX). Этот пример создает окно, содержащее кнопку Say ‘Hello World’.

import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class HelloWorld extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Hello World!");
        Button btn = new Button();
        btn.setText("Say 'Hello World'");
        btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

        StackPane root = new StackPane();
        root.getChildren().add(btn);
        primaryStage.setScene(new Scene(root, 300, 250));
        primaryStage.show();
    }
}

 

В этом примере вызов метода btn.setOnAction определяет событие при нажатии на кнопку Say ‘Hello World’. Параметр этой функции — объект типа EventHandler<ActionEvent>. Интерфейс EventHandler<ActionEvent> содержит единственный метод — handle. Вместо определения нового класса, который реализует этот интерфейс, мы используем анонимный класс.

Так как интерфейс EventHandler<ActionEvent> содержит только один метод можно использовать лямбда-выражение вместо анонимного класса.

Анонимные классы идеальны для реализации интерфейсов, содержащих два и более методов. Следующее JavaFX приложение содержит форму для введения данных. В данном примере мы создаем текстовое поле, которое позволяет вводить только цифры. Анонимный класс переопределяет методы replaceText и replaceSelection класса TextField.

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class CustomTextFieldSample extends Application {

    final static Label label = new Label();

    @Override
    public void start(Stage stage) {
        Group root = new Group();
        Scene scene = new Scene(root, 300, 150);
        stage.setScene(scene);
        stage.setTitle("Text Field Sample");

        GridPane grid = new GridPane();
        grid.setPadding(new Insets(10, 10, 10, 10));
        grid.setVgap(5);
        grid.setHgap(5);

        scene.setRoot(grid);
        final Label dollar = new Label("$");
        GridPane.setConstraints(dollar, 0, 0);
        grid.getChildren().add(dollar);

        final TextField sum = new TextField() {
            @Override
            public void replaceText(int start, int end, String text) {
                if (!text.matches("[a-z, A-Z]")) {
                    super.replaceText(start, end, text);
                }
                label.setText("Enter a numeric value");
            }

            @Override
            public void replaceSelection(String text) {
                if (!text.matches("[a-z, A-Z]")) {
                    super.replaceSelection(text);
                }
            }
        };

        sum.setPromptText("Enter the total");
        sum.setPrefColumnCount(10);
        GridPane.setConstraints(sum, 1, 0);
        grid.getChildren().add(sum);

        Button submit = new Button("Submit");
        GridPane.setConstraints(submit, 2, 0);
        grid.getChildren().add(submit);

        submit.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                label.setText(null);
            }
        });

        GridPane.setConstraints(label, 0, 1);
        GridPane.setColumnSpan(label, 3);
        grid.getChildren().add(label);

        scene.setRoot(grid);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

На этом всё! 🙂