W3docs

Анонимные внутренние классы Java

Создавайте одноразовые реализации интерфейсов и абстрактных классов в Java с помощью анонимных внутренних классов.

Анонимный класс — это одноразовый подкласс или реализация интерфейса, которые вы определяете и создаёте в одном выражении — без имени, без отдельного файла, без заголовка класса. Именно так Java обрабатывала обратные вызовы, слушатели и небольшие адаптеры до появления лямбда-выражений в Java 8. Лямбды заменили большинство их вариантов использования, но анонимные классы по-прежнему допустимы, по-прежнему полезны в ряде конкретных ситуаций и до сих пор встречаются в старых кодовых базах.

Синтаксис

Форма записи: new SomeType() { ... body ... }. Тело является определением класса; окружающее выражение также создаёт экземпляр:

Runnable r = new Runnable() {
  @Override
  public void run() {
    System.out.println("hi");
  }
};
r.run();

Этот единственный оператор (1) определяет новый класс, реализующий Runnable, (2) создаёт один его экземпляр, (3) присваивает экземпляр переменной r. Класс не имеет имени в исходном коде; компилятор присваивает ему сгенерированное имя вида Outer$1 в файле .class.

То же самое можно сделать с абстрактным классом:

Shape s = new Shape() {
  @Override double area() { return 42; }
};

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

ArrayList<String> chatty = new ArrayList<>() {
  @Override
  public boolean add(String s) {
    System.out.println("added " + s);
    return super.add(s);
  }
};

Когда их используют

Классический случай: обратный вызов, реализация которого невелика и используется ровно в одном месте:

button.addClickListener(new ClickListener() {
  @Override
  public void onClick() {
    System.out.println("clicked");
  }
});

В современном Java это обычно записывается как лямбда:

button.addClickListener(() -> System.out.println("clicked"));

…и лямбда короче, легче читается и не удерживает ссылку на внешний класс. Когда же анонимная форма всё ещё оправдана?

  • Несколько методов. Лямбда реализует один абстрактный метод. Если нужно переопределить два метода абстрактного класса или реализовать два метода интерфейса, только анонимный класс справится с задачей.
  • Наследование от конкретного класса. Лямбды нацелены только на функциональные интерфейсы. Чтобы на лету переопределить метод у ArrayList, HashMap или вашего собственного конкретного класса, нужен анонимный класс.
  • Вызов super.method(...) в переопределении. У лямбд нет super. У анонимных классов есть.
  • Блоки инициализации. Анонимные классы могут содержать блоки инициализации экземпляра ({ ... }); лямбды — нет.

На практике это небольшой набор случаев. Большинство современных обратных вызовов используют лямбды.

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

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

void schedule() {
  String msg = "hello";
  Runnable r = new Runnable() {
    @Override public void run() {
      System.out.println(msg);     // captures msg
    }
  };
  r.run();
  // msg = "bye";          // would make msg no longer effectively final → ERROR above
}

То же правило применяется к лямбдам — это свойство окружающего кода, а не синтаксической формы. Причина в том, что захваченное значение копируется в поля синтетического класса; если бы msg могло измениться позже, захваченная копия незаметно устарела бы.

Внешний this

Как и внутренние классы, анонимные классы, объявленные в нестатическом контексте, хранят ссылку на экземпляр внешнего класса. Это означает, что this внутри анонимного класса ссылается на анонимный экземпляр, а не на внешний. Чтобы обратиться к внешнему экземпляру, используйте квалифицированную форму Outer.this:

public class Server {
  String name = "outer";

  Runnable handler() {
    return new Runnable() {
      String name = "inner";
      public void run() {
        System.out.println(name);            // "inner"
        System.out.println(Server.this.name);// "outer"
      }
    };
  }
}

И то же предостережение насчёт внешней ссылки, что и у внутренних классов: возврат анонимного экземпляра в долгоживущий код удерживает внешний экземпляр в памяти.

Ограничения

  • Анонимный класс может расширять один суперкласс или реализовывать один интерфейс — но не то и другое одновременно и не более одного из них.
  • У него не может быть собственного конструктора — нет имени, которое можно дать конструктору. Для инициализации можно использовать блок инициализации экземпляра ({ ... }).
  • Он не может быть static.

Сравнение с лямбдами — пример в коде

Одна задача, два способа:

// Anonymous class — older style
Comparator<String> byLength = new Comparator<String>() {
  @Override
  public int compare(String a, String b) {
    return Integer.compare(a.length(), b.length());
  }
};

// Lambda — modern equivalent
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());

Оба варианта создают Comparator<String>. Лямбда короче и имеет несколько иную семантику this и областей видимости (нет ссылки на внешний класс в контексте экземпляра; this внутри лямбды — это экземпляр-замыкание, а не новый объект). Если оба варианта подходят для вашего случая, предпочтите лямбду.

Рабочий пример

java— editable, runs on the server

Что дальше

Оставшаяся разновидность вложенных классов — локальный класс: класс, объявленный внутри тела метода, с настоящим именем, но очень узкой областью видимости. Они пересекаются с анонимными классами (и с лямбдами), но иногда являются наиболее чистым выбором. Продолжайте изучение с локальных классов.

Практика

Практика
В современном Java какая задача по-прежнему требует анонимного класса, а не лямбды?
В современном Java какая задача по-прежнему требует анонимного класса, а не лямбды?
Was this page helpful?