Друзья, как вы уже заметили, в конце июня у нас стартует немало новых групп, среди них очередной поток полюбившегося всем курса «Разработчик Java». Прямо сейчас делимся с вами новым переводом подготовленным для студентов данного курса.



При добавлении в язык программирования новых фич, разработчики языка обычно обращают внимание на конфликты с текущими возможностями языка, на изменения, приводящие к несовместимости с предыдущими версиями, на ошибки и любые ситуации, которые могут привести к неопределенному или непредвиденному поведению.

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

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


В Java внутренние (inner) классы — это классы, определенные как члены класса. Есть четыре вида внутренних классов:

  • статические вложенные (static nested)
  • внутренние (inner)
  • локальные (method local)
  • анонимные (anonymous)

Анонимные внутренние классы — это классы без имени, которые предоставляют реализацию существующего класса. Такие классы широко применяются для обработки событий в событийно-ориентированном программировании. Обычно анонимный внутренний класс предоставляет “на лету” реализацию абстрактного класса. Однако, это не обязательно. Анонимный внутренний класс можно создать на основе не абстрактного класса.

Я полагаю, что есть момент, который недостаточно полно понимается в отношении анонимных внутренних классов. Дело в том, что программист фактически создает подкласс исходного класса. Этому подклассу присваивается имя Class$X, где Class представляет собой внешний класс, а X — число, представляющее собой порядок создания экземпляров внутренних классов во внешнем классе. Например, AnonDemo$3 — третий внутренний класс, созданный в AnonDemo. Вы не можете вызывать эти классы обычным способом. И, в отличие от других видов внутренних классов, анонимный внутренний класс всегда неявно является дочерним классом типа, на основе которого он создан (за исключением использования var, что мы скоро рассмотрим).
Давайте посмотрим на пример.

/* AnonDemo.java */
class Anon { };

public class AnonDemo {
   public static void main (String[] args) {
       Anon anonInner = new Anon () {
           public String toString() {
               return "Overriden";
           };
           public void doSomething() {
               System.out.println("Blah");
           };
       };
       System.out.println(anonInner.toString());
       anonInner.doSomething(); // Не скомпилируется!
   };
};

В этом примере мы создали экземпляр анонимного внутреннего класса на основе класса Anon. По сути, мы создали безымянный подкласс конкретного класса. До Java 10 анонимные внутренние классы были почти всегда неявно полиморфными. Я говорю “почти”, потому что такой, не полиморфный, код как этот, конечно, выполнится.

new Anon() { public void foo() { System.out.println("Woah"); } }.foo();

Однако, если мы захотим присвоить результат создания экземпляра анонимного внутреннего класса исходному типу, то такая операция будет по своей природе полиморфной. Причины этого кроются в том, что мы неявно создаем подкласс класса, который мы указали в качестве исходного для анонимного внутреннего класса и нам не будет доступен конкретный тип объекта (Class$X) для того, чтобы указать его в исходном коде.

Полиморфизм и анонимные внутренние классы, практические следствия


Вы обратили внимание на код выше? Поскольку мы используем ссылку базового класса на объект подкласса, то по законам полиморфизма мы можем ссылаться только на 1) методы, определенные базовым классом или 2) переопределенные виртуальные методы в подклассе.

Поэтому в предыдущем фрагменте кода вызов toString() для объекта анонимного внутреннего класса дал бы нам переопределенное значение “Overridden”, однако вызов doSomething() приведет к ошибке компиляции. В чем причина?

Объект подкласса с ссылкой на тип базового класса не имеет доступа к членам подкласса через эту ссылку на базовый класс. Единственное исключение из этого правила — если подкласс переопределяет метод базового класса. В этом случае Java, верный своей полиморфной природе, с помощью Dynamic Method Dispatch выбирает версию виртуального метода подкласса во время выполнения.

Если вы еще не знали, виртуальный метод — это метод, который можно переопределить. В Java все не final, не private и не static методы являются виртуальными по умолчанию. Я говорю по умолчанию, а не неявно, потому что разные jvm могут выполнять оптимизации, которые могут изменить это.

При чем тут Java 10?


Небольшая фича, называемая выводом типа (type inference). Посмотрите на следующий пример:

/* AnonDemo.java */
class Anon { };
public class AnonDemo {
  public static void main(String[] args) {
    var anonInner = new Anon() {
      public void hello() {
       System.out.println(
       "New method here, and you can easily access me in Java 10!\n" +
       "The class is:  " + this.getClass()
       );
      };
    };

    anonInner.hello(); // Работает!!
  }
}

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

Что мы делали до Java 10, когда нам нужна была ссылка на подкласс?


Не секрет, что в далеком, но не забытом прошлом, были большие споры о внутренних классах. А если это секрет, то это один из самых плохих секретов в мире. Несомненно, многие не одобрят то, что вам нужна точная ссылка на анонимный внутренний класс, поскольку идея состоит в том, чтобы избежать добавления в класс мусора. Анонимные классы следует использовать для выполнения контракта на лету, для операции, логически связанной с другим классом, например, для обработки событий. Однако, вероятно, любопытной Варваре нос не оторвали, и я готов поспорить, что большинство разработчиков очень любознательны. Возможно, в ущерб здравому смыслу!

До Java 10 мы можем добиться аналогичного эффекта, используя reflection, следующим образом:

Anon anonInner2 = new Anon() {
   public void hello() { System.out.println("Woah! "); };
};
anonInner2.getClass().getMethod("hello").invoke(anonInner2);

Полный исходник


Можно взять здесь

public class VarAnonInner {
   public static void main (String[] args) throws Exception {
       var anonInner = new Anon() {
           public void hello() {
               System.out.println("New method here, and you can easily access me in Java 10!\n" +
                       "The class is:  " + this.getClass()
               );
           };
       };
       anonInner.hello();

       Anon anonInner2 = new Anon() {
           public void hello() { System.out.println("Woah! "); };
       };
       anonInner2.getClass().getMethod("hello").invoke(anonInner2);

       new Anon() { public void hello() { System.out.println("Woah!!! "); };  }.hello();

       // VarAnonInner$1 vw = anonInner;

     /*
    
     Anon anonInner4 = new Anon() {
        public void hello() {
           System.out.println("New method here!\n" +
              "The class is:  " + this.getClass()   
           );
        };
     };
     anonInner4.hello();
    
     */

   }
}

class Anon { };

Ждем ваши комментарии, а также напоминаем о том, что уже сегодня в 20.00 состоится бесплатный вебинар по курсу.

Комментарии (5)


  1. sheknitrtch
    13.06.2019 23:31

    А ещё с помощью выведения типов ключевым словом "var" можно делать пересечения типов:

    public static void main(String... args) {
       var duck = (Quacks & Waddles) Mixin::create;
       duck.quack();
       duck.waddle();
    }
     
    interface Quacks extends Mixin {
       default void quack() {
           System.out.println("Quack");
       }
    }
     
    interface Waddles extends Mixin {
       default void waddle() {
           System.out.println("Waddle");
       }
    }
     
    interface Mixin {
       void __noop__();
       static void create() {}
    }


    1. 3draven
      14.06.2019 21:33

      В fasterxml наконец то миксины сделают без грязных махинаций :)


    1. eliduvid
      16.06.2019 07:59

      Можно-то можно, но за пределы метода такой миксин толком не вынешь.


  1. Ketovdk
    14.06.2019 09:56

    пугает, что новые фичи и способы реализации больше похожи на какие-то хаки, чем на возможности языка


  1. Beholder
    14.06.2019 10:52

    Kotlin:


    fun main() {
        val anon = object {
            fun hello() {
                println("hello")
            }
        }
        anon.hello()
    }

    Но вы реально считаете это очень полезным трюком?