Javaのイベント処理とラムダ式

2018年9月20日



JavaでGUIを作成する際に、Java8のラムダ式が出現する以前に最も面倒だと思ったのは、イベントハンドラの作成である。ラムダ式の出現によって、これが大幅に改善されたのだが、どこの誰もこれを言わないのは、誰もJavaでGUIを作っていないのだろうか?

そして、例えばSwingのサンプルコードを探してみると、おおよそ「昔ながらのイベントハンドラ」が記述されており、Java8対応にはなっていない。ラムダ式を使えば非常に簡潔になるにも関わらず、誰もこれを指摘しないのだろうか?

ここでは特に、イベントハンドリングに限って、ラムダ式の威力を見ていこうと思う。

古き良き時代の昔ながらのSwingのイベントハンドラ

いきなりすべてのコードを提示する。以下のコードでは、mainの中で、execute1、execute2、execute3のいずれかを実行する。

昔ながらのSwingのイベントハンドラは、execute1の形になっている。

import java.awt.event.*;

import javax.swing.*;

public class Swing1 {

  public static void main(String[]args) {
    new Swing1().execute1();
  }

  void execute1() {
    JFrame frame = createFrame();
    JButton button = new JButton("button");
    frame.add(button);
    button.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        System.out.println("clicked");
      }      
    });
    frame.setVisible(true);  
  }

  void execute2() {
    JFrame frame = createFrame();
    JButton button = new JButton("button");
    frame.add(button);
    button.addActionListener(e->System.out.println("clicked"));
    frame.setVisible(true);  
  }

  void execute3() {
    JFrame frame = createFrame();
    JButton button = new JButton("button");
    frame.add(button);
    button.addActionListener(this::clicked);
    frame.setVisible(true);  
  }

  void clicked(ActionEvent e) {
    System.out.println("clicked");
  }

  JFrame createFrame() {
    JFrame frame = new JFrame("sample");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(100,100);    
    return frame;
  }
}

Swingに限らず、SWTだろうが他でも同じなのだが、Javaでイベントハンドラを書くには、次のようにしないといけない。これが非常に面倒であった。何とかコードを減らせないものかと思うのだが、これ以上短くはならない。いくつものボタンや何やらを持つ画面を作成するのがとても面倒であった(ちなみにGUIビルダのようなものは使わないことを前提とする。その類一切を私は否定している)。

    button.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        System.out.println("clicked");
      }      
    });

この面倒なところは、インターフェースActionListenerを継承した匿名クラスを作成しなければならない点である。これをもう少しわかりやすく書くと、こういうことになる。

    class MyListener implements ActionListener {
      @Override
      public void actionPerformed(ActionEvent e) {
        System.out.println("clicked");
      }   
    }
    button.addActionListener(new MyListener());

クラス名称があるか無いかの違いだけで、本質的に行っていることはこれと同じである。単にボタンからの通知を受け取るだけなのに、わざわざクラスを定義し、そのインスタンスを作成しなければならないのである。これはいかにも面倒だ。

ラムダ式を使った例

しかし、ラムダ式を使うと、これが以下のように簡単になってしまう(execute2)。

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

もちろん、これでも相変わらず内部的には匿名クラスが作られ、そのインスタンスが作られているようだ。単にこう書けるというだけの話しであるが、しかしこれでもありがたい。余計なタイピングをする必要がなくなり、本質に専念することができる。

どういう条件であれば、このように書けるかは、他ページを検索してもらいたいのが、基本的には、

  • インターフェースにメソッドが一つしかない場合

である。ActionListenerは次のような定義なので、

public interface ActionListener extends EventListener {
    public void actionPerformed(ActionEvent e);
}

引数と処理だけを書けば、あとは阿吽の呼吸でわかってくれるらしい。引数が無い場合とか、引数が複数の場合の書き方は他ウェブを参照して欲しい。

メソッド参照を使った例

さらに、メソッド参照というものが使えるようになった(execute3)。これは、インターフェースのメソッドと同じ引数と返り値を持つメソッドであれば「メソッド名を書くだけ」というものだ。

    button.addActionListener(this::clicked);

    .....

    void clicked(ActionEvent e) {
      System.out.println("clicked");
    }

このclickedというメソッドは、元のactionPerformedメソッドと同一の引数と同一の返り値になっているのだが、その場合はオブジェクト(この場合はthis)にコロン二つを続けた後、メソッド名を書けばよい。

今までの苦労は何だったのか?

あまりに呆気なく簡単にかけてしまうので、これまで何年も続いてきた苦しみは一体何だったのかと思う。

そして、このことを広く世に知らしめるために、このページを書くものである。

くれぐれも、他のヘボく更新もされてないSwingの解説ページを参考にして面倒なコードを書かないようお願いするものである。