Raspberry Pi でラムダ式 (2015/01/07)

Raspberry Pi を使って、メニューやボタン、スクロールバーを備えたアプリケーションを作ってみましょう。 このようなグラフィカルユーザインターフェイス(GUI)を持つアプリケーションを、Raspberry Pi の標準のOSである Raspbian に含まれている Java8 を使って作成してみます。今回は次のような単純な Swing を使ったGUIアプリケーションを Java の最新機能の1つである ラムダ式 (パート2) を使って作ってみることにします。


SwingButton.png


外部クラスが ActionListner

上のスクリーンショットのようなボタンを押したときに何かを表示するアプリケーションでは、ボタンが押されたイベントを受け取って処理するクラスを用意します。 このクラスは ActionListener インターフェイスを実装 (implements) して、ボタンが押された場合の処理を actionPerformed メソッド に書いた (@Override) ものです。 ボタンの addActionListener メソッドに作成したクラスのインスタンスを引数として設定することで、ボタンが押されたときに発生するイベントに対してボタンの処理が実行されるようになります。 デスクトップ用のSwingを使ったプログラムでも、Andridのアプリでも、このイベント処理の書き方がちょっと分かり難い部分です。

Button1.java

まず、最初のバージョンを示します。 このバージョンはわざと面倒な書き方をしています。 つまり、ActionListener を実装 (implements) する ButtonListener クラスを定義して、そのインスタンスをボタンに設定しています。 コード中で赤字で示している部分です。 これに続く4つのバージョンでは順次、ラムダ式に近づくように変化していきます。

ちょっと長いですが、細かい設定を除くと行っていることは次の 6つです。

  1. ウィンドウを作成する
  2. 部品配置用のパネルを作成する
  3. テキストエリアを中央に置く
  4. ボタンを下に置く
  5. ボタンが押されたら、テキストエリアに「 Button」を追加する
  6. タイトルバーの [X] ボタンで終了する
// Java のオブジェクトを使うときに javax.swing.JFrame のように
// 長い表記を避けるために import を使う。
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;

// Java の実行開始用のメインクラスを定義する

class Button1 {
  // Button1クラスのプロパティとしてボタンとテキストエリア用の変数を用意
  JButton button;
  JTextArea textArea;

  // コンストラクタ。main メソッドから初期化のために呼ばれる。
  Button1(String title) {

    // ウインドウのインスタンスを生成する
    JFrame frame = new JFrame(title);

    // タイトルバーの[X]ボタンをクリックした場合の処理を指定
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ウインドウのサイズを指定する
    frame.setSize(200, 100);

    // ウインドウのサイズをピクセル単位で指定する
    frame.setTitle(title);

    // ウインドウの内容領域を取得する
    Container contentPane = frame.getContentPane();

    // パネルを生成する
    JPanel panel = new JPanel(new BorderLayout());

    // パネルをウインドウに設置
    contentPane.add(panel);

    // 複数行テキストを表示する JTextArea を中央に配置する
    // テキストエリアのインスタンスを生成する
    textArea = new JTextArea();

    // 最初に表示される文字列
    textArea.setText("Button");

    // 行の折り返しを有効にする
    textArea.setLineWrap(true);

    // 表示するフォントを設定
    Font f = new Font("SansSerif", Font.BOLD, 14);
    textArea.setFont(f);

    // パネルの中央に配置
    panel.add(textArea, BorderLayout.CENTER);

    // ボタンのインスタンスを生成する
    button = new JButton();

    // ボタンの名前を設定
    button.setText("Button");

    // ボタンをパネルの下部に配置する
    panel.add(button, BorderLayout.SOUTH);

    // ウインドウを表示
    frame.setVisible(true);

    // ActionListener を実装したインスタンスを作成
    ButtonListener listener = new ButtonListener(textArea);

    // ボタンに処理を定義したインスタンスを設定
    button.addActionListener(listener);
  }

  // ★ 一番最初に実行されるメソッド
  public static void main(String args[]) {
    Button1 app = new Button1("SwingButton");
  }
}


// ボタンの処理を記述するために ActionListener の
// actionPerformed メソッドの処理を定義するためのクラス
class ButtonListener implements ActionListener {

  JTextArea textArea;

  // コンストラクタで操作対象のテキストエリアを指定する
  ButtonListener(JTextArea t) {
    textArea = t;
  }

  // ボタンが押された場合の処理
  public void actionPerformed(ActionEvent event) {

    // テキストエリアに「 Button」という文字列を追加
    textArea.setText(textArea.getText() + " Button");
  }
}

ボタンが押されたイベントの処理は、「ActionListenerインターフェースを実装するクラス」の actionPerformed メソッドで行いますが、Button1クラスの textArea プロパティは、ButtonListener クラス内からは見えない(アクセス出来ない)ため、インスタンス生成時にButton1クラスの textArea プロパティへの参照を渡して、actionPerformed メソッドからButton1クラスの textArea プロパティを操作しています。

以上のソースコードを Button1.java というファイル名で保存します。

ターミナルの起動

Raspberry Pi のデスクトップが表示されていない場合は「startx」コマンドを実行してデスクトップ環境にしておいて下さい。次の図のように上部のターミナルのアイコンか、メニューから LXTerminal を選択して、ターミナルを起動します。 コマンド入力できるようにします。


LXTerminal.png


コンパイル

Button1.java を保存したディレクトリで、「javac Button1.java」とすると Button1.java はコンパイルされて、Java仮想マシン(JVM)で実行可能な、拡張子が class のファイル(クラスファイル)が生成されます。

$ javac Button1.java
$ ls -l
-rw-rw-rw- 1 jun jun 1250 Jan  6 01:13 Button1.java
-rw-r--r-- 1 jun jun 1508 Jan  6 01:14 Button1.class
-rw-r--r-- 1 jun jun  705 Jan  6 01:14 ButtonListener.class

javaファイルが1つであるにもかかわらず Button1.class と ButtonListener.class という2つのクラスファイルができている理由は、java ではクラスごとにクラスファイルが生成されるためです。クラスの数が多いと大量のクラスファイルが生成されます。 これらのクラスファイルはすべて実行時に必要です。 配布するような場合は jar コマンドで1つのファイルにまとめる事ができるので問題にはなりませんが。

クラスファイルを実行するには「java Button1」とファイル名ではなく、メインクラス名を指定します。javaコマンドが Button1.class と ButtonListener.class を読み込んで実行を始めます。

$ java Button1

Java アプリは起動時に関連する多くのAPIのクラスファイルをロードするため、起動するまでにちょっと時間がかかりますが、一旦起動すると十分速く動作します。

メインクラスと main メソッド

この例では Button1 と ButtonListener の2つのクラスがあります。 そのうち Button1 クラスは main メソッドを含んでいてメインクラスと呼びます。次の MainClass.java で示すように「public static void main(String args[])」という決まった形式のメソッドを持つクラスを java コマンドで指定します。メインクラスの名前とjavaファイル名は同じにします。

次の MainClass.java はコンソールに「Hello Pi!」と表示するだけの Java プログラムです。

class MainClass {
  public static void main(String args[]) {
    System.out.println("Hello Pi!");
  }
}

コンパイルして、実行します。

$ javac MainClass.java
$ java MainClass
Hello Pi!

MainClass クラスの main メソッドが実行されているのが分かります。


内部クラスが ActionListner

Button1.java では ButtonListener クラス内から 直接 Button1 クラスの textArea プロパティをアクセスできませんでしたが、このバージョン (Button2.java) では ButtonListener クラスを Button2 クラスの内部で定義 (内部クラス) することで textArea プロパティを直接操作しています。

Button2.java

import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;

class Button2 {

  JButton button;
  JTextArea textArea;

  Button2(String title) {
    JFrame frame = new JFrame(title);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(200, 100);
    frame.setTitle(title);
    Container contentPane = frame.getContentPane();
    JPanel panel = new JPanel(new BorderLayout());
    contentPane.add(panel);

    textArea = new JTextArea();
    textArea.setText("Button");
    textArea.setLineWrap(true);
    Font f = new Font("SansSerif", Font.BOLD, 14);
    textArea.setFont(f);
    panel.add(textArea, BorderLayout.CENTER);

    button = new JButton();
    button.setText("Button");
    panel.add(button, BorderLayout.SOUTH);
    frame.setVisible(true);

    ButtonListener listener = new ButtonListener();
    button.addActionListener(listener);
  }

  class ButtonListener implements ActionListener {
    public void actionPerformed(ActionEvent event) {
      textArea.setText(textArea.getText() + " Button");
    }
  }

  public static void main(String args[]) {
    Button2 app = new Button2("SwingButton");
  }
}

コンパイル結果

-rw-rw-rw- 1 jun jun 1180 Jan  6 01:00 Button2.java
-rw-r--r-- 1 jun jun 1547 Jan  6 01:11 Button2.class
-rw-r--r-- 1 jun jun  789 Jan  6 01:11 Button2$ButtonListener.class

Button2 クラスの内部で定義された ActionListner 用のクラスの名前が ButtonListener であるため、クラスファイルはButton2$ButtonListener.class といった名称で作成されます。

無名内部クラスが ActionListner

Button2.java の方法では、ボタンが多い場合に Button01Listener、Button02Listener、Button03Listener、... といったように多くのクラスを作ることになります。 こういったクラスは特定のボタンにだけ設定したらその後は使いません。 わざわざ名前を付けるのは無駄です。 このような場合には名前が不要な内部クラスとして、無名内部クラス (Anonymous Inner Class) を使うことができます。

無名内部クラスは継承や実装に extends や implements を使わず、スーパークラスのコンストラクタを 呼び出しているかのように指定しつつ、オーバーライドするメソッドを実装する書き方をします。 以下の親クラス名はクラスでもインターファエイスでもOKです。

 new 親クラス名(コンストラクタへのパラメータ) { 内部クラスのメソッド、プロパティ }

Button3.java

ActionListener を実装する無名内部クラスを使ったバージョン。

import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;

class Button3 {

  JButton button;
  JTextArea textArea;

  Button3(String title) {
    JFrame frame = new JFrame(title);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(200, 100);
    frame.setTitle(title);
    Container contentPane = frame.getContentPane();
    JPanel panel = new JPanel(new BorderLayout());
    contentPane.add(panel);

    textArea = new JTextArea();
    textArea.setText("Button");
    textArea.setLineWrap(true);
    Font f = new Font("SansSerif", Font.BOLD, 14);
    textArea.setFont(f);
    panel.add(textArea, BorderLayout.CENTER);

    button = new JButton();
    button.setText("Button");
    panel.add(button, BorderLayout.SOUTH);
    frame.setVisible(true);

    button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent event) {
        textArea.setText(textArea.getText() + " Button");
      }
    });
  }

  public static void main(String args[]) {
    Button3 app = new Button3("SwingButton");
  }
}

ずいぶんスッキリしました。

コンパイル結果

-rw-rw-rw- 1 jun jun 1098 Jan  6 01:01 Button3.java
-rw-r--r-- 1 jun jun 1509 Jan  6 01:11 Button3.class
-rw-r--r-- 1 jun jun  792 Jan  6 01:11 Button3$1.class

ActionListner 用の内部クラスに名前がついていないので、無名内部クラス用のクラスファイルは、Button3$1.class といった名称で作成されています。


ラムダ式が ActionListner

やっとラムダ式 を使う段階まで来ました。 無名内部クラスよりもさらに簡潔な表記法を使うことができます。

ラムダ式

ラムダ式は、1つだけメソッドを持つインターフェイス(Single-Abstruct Method interface) のメソッドを実装するときに、次のように書くことができる機能です。

  ( 実装するメソッドの引数 ) -> { 処理 }

これだけを見るとわけが分からないものですが、次のメソッドの定義と比較すると、メソッド名がなく、「->」が付いているだけで、無名のメソッドを定義している感じです。

  メソッド名( メソッドの引数 ) { 処理 }

また、ラムダ式は次のように変数に代入することができます。

  SAMインターフェイス名 変数 = ( 実装するメソッドの引数 ) -> { 処理 }

省略

ラムダ式は上の書き方に加えて、色々な省略した書き方ができます。

  @FunctionalInterface
  public interface MyFunc {
    public abstract int func(int a);
  }

上のような MyFunc インターフェイスがある場合に、Java7 までは次のように書きました。

  MyFunc f = new MyFunc() {
    @Override
    public int func(int a) {
      return a * a;
    }
  }

Java8 でラムダ式を使うと次のように書けます。

  MyFunc f = (int a) -> { return a * a; }

また、省略を使うと下のように表記することが可能です。

  MyFunc f = a -> a * a;

ActionListener は SAM インターフェイス

ActionListener の実体は次のようなものです。

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

EventListener は空なので、抽象メソッドを1つだけ含むインターフェイスとなり、 ActionListener をラムダ式で書くことができます。アノテーション (@FunctionalInterface) は無くても問題ありません。

Button4.java

さて、実際に ActionListener の部分にラムダ式を使ったコードです。

import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;

class Button4 {

  JButton button;
  JTextArea textArea;

  Button4(String title) {
    JFrame frame = new JFrame(title);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(200, 100);
    frame.setTitle(title);
    Container contentPane = frame.getContentPane();
    JPanel panel = new JPanel(new BorderLayout());
    contentPane.add(panel);

    textArea = new JTextArea();
    textArea.setText("Button");
    textArea.setLineWrap(true);
    Font f = new Font("SansSerif", Font.BOLD, 14);
    textArea.setFont(f);
    panel.add(textArea, BorderLayout.CENTER);

    button = new JButton();
    button.setText("Button");
    panel.add(button, BorderLayout.SOUTH);
    frame.setVisible(true);

    button.addActionListener(e -> textArea.setText(textArea.getText() + " Button"));
  }

  public static void main(String args[]) {
    Button4 app = new Button4("SwingButton");
  }
}

かなり簡潔になりました。コード中に ActionListener も ActionEvent も出てこないので、「import java.awt.event.*;」も不要になりました。

コンパイル結果

-rw-rw-rw- 1 jun jun  984 Jan  6 01:01 Button4.java
-rw-r--r-- 1 jun jun 2255 Jan  6 01:12 Button4.class

ラムダ式を使うと新たなクラスファイルも生成されないため、メニューのような多くの項目ごとに ActionListener を用意する場合でもクラスファイルが増えないというメリットもあります。


【おまけ】メインクラスが ActionListner

今回の話題から外れますが、メインクラスが ActionListnerインターフェースを継承することで、メインクラス自身がActionListner となることができます。 短いサンプルでは分かりやすいコードになります。 メインクラスがボタンに反応するメソッドを持つというのは、仕組み的には可能ですが、オブジェクト指向の意味的にはちょっと問題です。 メニューなど部品の数が多くなると actionPerformed メソッドが長くなる上、パフォーマンス的にも問題がありそうです。

Button0.java

import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;

class Button0 implements ActionListener {

  JButton button;
  JTextArea textArea;

  Button0(String title) {

    JFrame frame = new JFrame(title);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(200, 100);
    frame.setTitle(title);
    Container contentPane = frame.getContentPane();
    JPanel panel = new JPanel(new BorderLayout());
    contentPane.add(panel);

    textArea = new JTextArea();
    textArea.setText("Button");
    textArea.setLineWrap(true);
    Font f = new Font("SansSerif", Font.BOLD, 14);
    textArea.setFont(f);
    panel.add(textArea, BorderLayout.CENTER);

    button = new JButton();
    button.setText("Button");
    panel.add(button, BorderLayout.SOUTH);
    frame.setVisible(true);

    button.addActionListener(this);
  }

  public void actionPerformed(ActionEvent event){
    if(event.getSource() == button) {
      textArea.setText(textArea.getText() + " Button");
    }
  }

  public static void main(String args[]) {
    Button0 app = new Button0("SwingButton");
  }
}

コンパイル結果

-rw-r--r-- 1 jun jun 1890 Jan  6 01:13 Button0.class
-rw-rw-rw- 1 jun jun 1135 Jan  6 01:02 Button0.java

classファイルも1つでサイズも小さくなっています。

まとめ

Java8 はラムダ式と「Stream API」を使って関数型プログラミングができることが目玉機能かも知れませんが、GUIアプリケーションのイベント処理で便利に使うことができます。ラムダ式の省略した書き方は Java の文法から異質に見えますが、少しずつ慣れていく必要がありますね。 安価な Raspberry Piで簡単にラムダ式などの Java8 の新機能が試せるのは素晴らしいですね。



続く...

このページの目次