27.02.2019

Язык программирования Java. Внутри обычной кнопки – интерфейсы, лямбда-выражения, стратегия

Изучить основы высокоуровнего объектно-ориентированного языка программирования, такого как Java, можно разными способами: самостоятельно или пройти специальные курсы. И всегда хочется сразу же попробовать силы в самостоятельном создании простенького GUI (графического интерейса пользователя) - формочки, кнопочки, окошки ввода. Идём смотреть, что же нам предлагает Интернет: ага, есть AWT, Swing и, посвежее, JavaFX. Его и рекомендуют, хотя по сути не слишком важно, какую библиотеку компонетов брать.

Разбираемся с hello world, учимся выстраивать статическую сцену, и вот, наконец, надо написать алгоритм обработки нажатия на кнопку... А куда писать? В Delphi или C# было просто - вот список событий, два раза кликаем - и написался метод-обработчик, осталось лишь творить алгоритм. С Java наоборот! Если в контроллере уже написан соответствующий метод, то его легко назначить той же кнопке, но в большинстве примеров мы видим строки вида

     button1.setOnAction(e -> {label1.setText("qwerty"); } );

идущие в методе инициализации контроллера или где-то еще при запуске.

Что это за стрелка, и почему алгоритм (в данном случае меняющий текст некоей надписи) очутился в списке фактических параметров метода? Что надо передавать методу setOnAction и как он работает?

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

Интерфейс - важнейший инструмент объектно-ориентированных языков. По сути это развитие концепции абстрактного класса, вот только другие классы от интерфейса не наследуются, а реализуют его абстрактные методы. Напомню, в Java у класса может быть не более одного родительского класса (в отличие от C++), зато он может реализовывать сколько угодно интерфейсов. У интерфейса нет конструкторов, его нельзя инстанциировать (создать экземпляр), не имея конкретного класса-реализации.

Рассмотрим пример простенького интерфейса с одним методом (кстати, при наличии строго одного виртуального метода интерфейс называется функциональным, и именно такие нас интересуют с точки зрения лямбда-выражений и оживления GUI):

     interface Movable{
          public void move();
     }

и разных классов, его реализующих:

     class Elephant implements Movable
     {
          @Override
          public void move() {
               System.out.println("top-top");
          }
     }
     class Kangaroo implements Movable
     {
          @Override
          public void move() {
               System.out.println("pryg-pryg");
          }
     }
     class Dolphin implements Movable{
          @Override
          public void move(){
               System.out.println( "bul-bull");
          }
     }

Разумеется, теперь мы можем наплодить объектов каждого класса (скажем, в public staic void main())

     Elephant elephant1=new Elephant(),
              elephant2=new Elephant();
     Kangaroo kenga = new Kangaroo();

Но также есть возможность объявить экземпляр типа Movable - а инстанциировать его конструктором любого подходящего класса.

     Movable movingCreature = new Dolphin();

Ведь, как известно, на стеке все эти объекты так и так представлены ссылками...

Без явного преобразования, для нашего movingCreature доступен только метод move(), заявленный в интерфейсе, независимо от того, насколько могучий интеллект и тонкая душевная организация у объекта внутри. Интерфейс - это свого рода маска.

Подвигаем наши объекты «вручную»:

     elephant1.move();
     elephant2.move();
     kenga.move();
     movingCreature.move();

В консоли, как положено, увидим:

top-top
top-top
pryg-pryg
bul-bull

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

     class Pusher{
          private Movable obj;
          public Movable getObj() {return obj;}
          public void setObj(Movable o){obj = o;}
          public void push() {
               if(obj != null){
                    System.out.println( "Ready.. Steady.. Go!");
                    obj.move();
               }
          }
     }

Заметим, что при работе над этим классом мы абстрагировались от того, как конкретно работает move(). Зато при использовании экземпляра класса мы можем выбрать из имеющегося зоопарка

     Pusher pusher = new Pusher();
     pusher.setObj(elephant2);
     pusher.push();
     pusher.setObj(movingCreature);
     pusher.push();

А можем и новый объект сгенерировать:

     pusher.setObj(new Kengoroo());

Результаты теперь такие:

Ready.. Steady.. Go!
top-top
Ready.. Steady.. Go!
bul-bull
Ready.. Steady.. Go!
pryg-pryg

Ссылка на это существо хранится внутри объекта pusher, доступна через метод getObj(), а сам объект, по сути, анонимен. Цель его существования - выполнять метод move() своим особым алгоритмом, меняя поведение объекта-«хозяина». Если вы ранее не пользовались шаблонами программирования - поздравляю, перед вами паттерн Strategy в простенькой форме.

Что же делать, если по ходу использования разработчику потребуется подсунуть в pusher новый алгоритм move()? Можно пойти и сделать новый класс, имплементирующий интерфейс Movable, но ему ж еще название придумать надо, написать кучу кода - и все ради единственного применения при вызове pusher.setObj. К счастью, разумная лень в Java приветствуется, а потому мы пишем анонимный вложенный класс! Все что надо о нем знать, это то что он реализует интерфейс, переопределяя соответствующий метод

     pusher.setObj(new Movable(){
          @Override
          public void move() {
               System.out.println( "vrum-vrum");}
     });
     pusher.push();

И это уже прекрасно, pusher знает, как ему работать:

Ready.. Steady.. Go!
vrum-vrum

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

Как упомянуто выше, мы определили функциональный интерфейс, то есть для его реализации анонимным экземпляром анонимного класса требуется переопределить всего одну функцию (точнее метод, но слово функция здесь уместнее) с заранее известным заголовком. Даже имя этой функции лишний раз писать не надо, главное список формальных параметров - и собственно алгоритм выполнения.

Описанный таким образом безымянный класс надо инстанциировать, но даже слово new получается "лишним", само собой разумеющимся:

     pusher.setObj( () -> {System.out.println( "bah-bah");} );
     pusher.push();

И снова pusher «научился» новому алгоритму:

Ready.. Steady.. Go!
bah-bah

Не запутаться бы в скобочках (кстати, когда алгоритм move() состоит из 1 выражения, то и {} можно опустить), но явно такой код становится компактнее и яснее (для тех, кто привык). Ах да, мы ведь практически вернулись к примеру из области GUI. Просто кнопке нужен объекта класса, реализующего интерфейс EventHandler с методом void handle (ActionEvent event). Метод setOnAction позволяет задать такой обработчик, описанный любым удобным способом: именованным объектом, анонимным экземпляром класса, анонимным экземпляром анонимного вложенного класса или лямбда-выражением. А еще есть статические методы классов, но об этом в другой раз.