第 8 回 多重継承、GUI

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

8-1. 多重継承

多重継承の問題点

二つのクラスを継承することを多重継承と言いますが、 Java では基本的にはできません。

さて、 アンドレイ・アレキサンドレスクの「Modern C++ Design」(ピアソン・ エデュケーション)と言う本では多重継承の例として TemporarySecretary (一 時雇いの秘書)というクラスが Temporary と Secretary の二つのクラスから の多重継承で、それぞれの性質を引き継いでいるというような説明がされてい ます(これはもともとは C++ の作者 Stroustrup が多重継承の必要性を説明す るためにした説明からきてます)。 多重継承はなぜ Java ではできないのでしょうか?

それは多重継承を許すとさまざまな問題が生じるからです。 その問題の本質は、複数のクラスで同一の名前を使用しているメソッドの実装 があるとき、どの実装を使用するかということです。 多重継承を許す C++ でも、下記のプログラムはエラーが生じます。


#include <iostream>
class A {
public:
  virtual char get(){return 'A';}
};
class B {
public:
  virtual char get(){return 'B';}
};
class C : public A, public B { // 多重継承
};
int main(){
  C c;
  std::cout << c.get() << std::endl;
  return 0;
}

これは get メソッドを呼び出すとき、A の get か B の get かが区別がつか ないからです。 この場合、 C++ では c.A::get() とするか、 C のクラス宣言内で using A::get; など、どちらのクラスかを指定することで 動作させることができます。

また、菱形継承(ダイアモンド継承)という問題があります。 下記の例でもエラーが生じます。


#include <iostream>
class A {
public:
  virtual char get(){return 'A';}
};
class B : public A {
};
class C : public A {
};
class D : public B, public C {
};
int main(){
  D d;
  std::cout << d.get() << std::endl;
  return 0;
}

この場合、A の get を実行することは字面的に明らかですが、コンパイラ的 には B の経路からの A なのか C の経路からの A なのかを区別してしまうた め、エラーになってしまいます。 ここでは class D : virtual public B, virtual public C と 「仮想継承」なる概念を導入して解決するらしいですが、これは C++ 固有の 解決法です。

Java ではこの多重継承問題について、ひとつは基本的に多重継承を禁止する という方策で対応しています。 そしてもうひとつは、実装を持たないクラスとして interface を導入し、interface に対しては多重継承を認めるというものです。 実装が無ければ上記のような問題は生じません。 但し、上記の TempolarySecretary のような実装を含んだ独立した小さなクラ スを寄せ集めてひとつのクラスを作るようなことはできません。

Java の interface の利用法

既に触れている interface を中心に、 Java の interface がどのように使わ れているかを説明します。

ストラテジデザインパターンにより特定の機能を使う

Java のクラスライブラリでよく使用されている使いかたは、 interface を implements で指定して特定のメソッドを実装し、 そして、作成したオブジェクトを渡してメソッドを呼び出してもらい、目的の 機能を実現するものです。 機能には、整列、 foreach 構文など基本的かつ重要なものが含まれます。 そして、これらの機能は排他的でないので、必要に応じて多重継承を使うこと ができます。

java.lang.Comparable

Comparable を実装したクラスは compareTo メソッドが実装されているので、 比較、さらに整列が可能になります。 java.util.Arrays.sort や java.util.Collections.sort などの整列するメソッ ドでは compareTo メソッドを呼び出します。

java.lang.Iterable と foreach

Iterable インターフェイスを実装したクラスに対してforeach 構文が使えま す。

java.util.Collection関連

interface java.util.Collection は java.lang.Iterable のサブクラスです (implements ではなく extends で拡張)。 さらに、この Collection のサブクラスには java.util.List と java.util.Set があります。 List や Set などは抽象化されたデータ構造として非常に有用なものです。 これらを変数型として活用することで、実装に依存しないプログラムを作るこ とができます。 そして、利用状況に応じて、 ArrayList や LinkedList など 実装されているクラスを総合的に判断して選ぶことができるようになります。

なお、java.util.List のサブクラスには、 java.util.AbstractList や java.util.AbstractSet などの抽象クラスがあります。 これらは、実装の際に必要なメソッドのうちのいくつかが実装されている スケルトンと呼ばれるクラスです。 java.util.Collection などを実装するより、目的のクラスに近いスケルトン を選んだ方が実装が楽になります。

マーカーインターフェイス

java.util.RandomAccess

java.util.RandomAccess にはメソッドが宣言されていません。 このようなメソッド無しの interface を マーカーインターフェースと呼びます。 get(int index) メソッドなどが高速で利用できるクラスにおいて implements します。 なお、 このような interface を implements しているかどうかはプログラムで判断 できます。


interface Mark {}
abstract class A {}
class B extends A {}
class C extends A  implements Mark {}
class Rei {
    private static <E> void trait(E e){
	System.out.println("method for unMarked");
    }
    private static <E extends Mark> void trait(E e){ //implements ではない
	System.out.println("method for Marked");
    }
    public static void main(String[] arg){
	A[] x = new A[]{new B(), new C()};
	for(A a : x){
	    System.out.println(	a instanceof Mark? "Marked" : "unMarked");
	    trait(a); //ポリモーフィズムではなく変数型に反応する
	}
	trait(new C());
    }
}
java.lang.Cloneable

java.lang.Cloneable もマーカーインターフェースです。 但し、このマーカーインターフェースが implements されてないクラスにおい て、 java.lang.Object.clone メソッドを super.clone() で呼び出そうとす ると java.lang.CloneNotSupportedException 例外が発生します。

java.io.Serializable

インスタンスをファイルに保存可能にするのが interface java.io.Serializable というマーカーインターフェースです。 これを implements することで、 java.io.ObjectOutputStream に対して writeObject でインスタンスを書き込むことができるようになります。 なお、この interface 自体はメソッドを宣言していませんが、 static final long serialVersionUID という定数が必要になる ようです。 これらの詳しい説明は必要になったら行います。

8-2. GUI 関連のデザインパターン

MVC

古くは Smalltalk の時代から、オブジェクト指向における GUI の定石として 知られていたのが MVC(Model, View, Control) です。

通常の逐次型を含み、プログラムは大きく分けて次の三つの部分に分かれるこ とを利用しています。

逐次プログラムでは、入力により変数にデータを格納した後、変数の計算を行 い、結果を表示するという、直線的な三段階に分かれます。

一方、 GUI では、アイコンやボタンなどさまざまなメタファを利 用し、ユーザにボタンのクリックなどのイベントを発生させてユー ザ入力を実現します。 これをオブジェクト指向で実現するにはボタンなどをオブジェクトとして取扱 い、イベントが発生すると特定のメソッドが実行されるように動作します。 ここで、イベントに対応するようなプログラミング手法をイベントドリ ブンと呼びます。

ひとつのプログラミング手法として、このボタンを継承し、イベントに対応す る特定のメソッドをオーバーライドしてデータの処理を考えることができます。 しかし、これではうまくいきません。 この、ボタンに直接データ処理などの複雑な処理をさせてしまう プログラミングスタイルはマジックボタンアンチパターンと呼ば れ、悪いプログラミング手法の代表例になっています。 今までの知識を使うと、ボタンとデータはそれぞれ異なる意味の名詞ですので、 別のオブジェクトになります。 そして、 is-a 関係がないので、継承をしてはまずいです。

このように GUI の入力とデータは分離しなければなりませんが、同じ理由で、 GUI におけるデータそのものとデータの表示も分離しなければなりません。

このような、データ、データ表示、ボタンなどのイベント処理をそれぞれ別オ ブジェクトにして分離し、相互に関連付ける手法を Model(データ)、 View (表示)、Control(入力イベント)の頭文字を取って MVC と呼びます。 さて、 MVC を実現するにはどのようなデザインパターンがあるのでしょうか?

オブザーバデザインパターン、リスナ

オブザーバデザインパターンは、観測者が観測物に変化が生じた ときに観測者が特定の動作を行うというものです。

例えば、「ボタンを押したら値が増える」というプログラムを考えます。 まず、オブジェクトとして「ボタン」と「値」が考えられます。 すると、「値」が観測者になり、「ボタン」が観測物です。 そして機能として、ボタンが「押される」と観測者の値が「増え」なければな りません。

ここで、オブザーバデザインパターンはストラテジデザインパターンのよ うなオブジェクトの扱いをします。 つまり、観測者は特定のメソッドを持ったオブジェクトを観測物に渡し、イベント が起きたときに観測物にそのメソッドを実行してもらうものです。 但し、ストラテジと違い、観測者に渡すオブジェクトはいくつでもよく、イベ ントの際には登録した全てのオブジェクトに対してメソッドを呼び出します。

Java では java.awt.event.ActionListener という interface があります。 これには public void actionPerformed(ActionEvent e) という抽象メソッド が宣言されています。 Java のクラスライブラリ中のボタンなどは既にできあがっていて修正は容易 ではありませんが、 この ActionListener を実装したクラスを addActionListner メソッドで登録 しておくと、ボタンが押されたときに呼び出されるというオブザーバデザイン パターンがもともと組み込まれています。

例8-1


import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class SimpleFrame extends JFrame {
    Container c;
    public SimpleFrame(){
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(300,200);
	c = getContentPane();
	c.add(new ButtonPanel());
    }
}
class ButtonPanel extends JPanel {
    public ButtonPanel(){
	JButton x = new JButton("X");
	JButton y = new JButton("Y");
	add(x);
	add(y);
	x.addActionListener(new X());
	y.addActionListener(new Y());
    }
	
}
class X implements ActionListener {
    public X(){}
    public void actionPerformed(ActionEvent event){
	System.out.println("x");
    }
}
class Y implements ActionListener {
    public Y(){}
    public void actionPerformed(ActionEvent event){
	System.out.println("y");
    }
}
class Rei {
    public static void main(String[] arg){
	SimpleFrame frame = new SimpleFrame();
	frame.setVisible(true);
    }
}

無名クラス

この ActionListner を使用したオブザーバデザインパターンでは、ひとつの アクションに対してひとつのクラスが必要です。 しかも、そのクラスはひとつのメソッドしか持たず、また ActionListener の 登録以外には全くアクセスされず、一回しか使われません。 このようなクラスを簡便に宣言するために Java では無名クラス という機能があります。 それは「 new interface名(){メソッド宣言}」という構文です。 これを用いると、指定した interface を implements したクラスのインスタン スをひとつ作ることができます。 上記の例をこの無名クラスで書き換えると次のようになります。


import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class SimpleFrame extends JFrame {
    Container c;
    public SimpleFrame(){
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(300,200);
	c = getContentPane();
	c.add(new ButtonPanel());
    }
}
class ButtonPanel extends JPanel {
    public ButtonPanel(){
	JButton x = new JButton("X");
	JButton y = new JButton("Y");
	add(x);
	add(y);
	x.addActionListener(new ActionListener (){
		public void actionPerformed(ActionEvent event){
		    System.out.println("x");
		}
	    });
	y.addActionListener(new ActionListener(){
		public void actionPerformed(ActionEvent event){
		    System.out.println("y");
		}
	    });
    }
	
}
class Rei {
    public static void main(String[] arg){
	SimpleFrame frame = new SimpleFrame();
	frame.setVisible(true);
    }
}

無名クラスの長所は、記述がシンプルになることと、ActionListner を実装し たクラスを他から全く呼び出しできないようにカプセル化できることです。 しかし、本当にこれは改良なのでしょうか?

Java における MVC モデル

ActionListner を実装した無名クラスのメソッドには何が記述されるのでしょ うか? 前にオブジェクト分析したように、 ActionListener を登録される側は Control であり、実際に呼出によりメソッドを実行するのは Model の方です。 したがって、オブジェクト分析からすると、この二つのオブジェクトは分離し ていなければなりません。 また、メソッドの中では Model に含まれるデータを処理する必要があります。

そこで、次の例を考えてみましょう。 「データとして整数値を考え、 GUI で 1 増やしたり、 1 減らしたりをボタン で操作できるようにしたい」というプログラムを考えます。 このとき、どのような手法が使えるでしょうか? ひとつの操作に対して、ひとつのクラスのインスタンスが対応しますので、 「増やすボタン」と「減らすボタン」には異なるオブジェクトが対応しなけれ ばなりません。 ひとつの整数値を持つのはひとつのクラスになります。 そのため、その整数値をいじるボタンのクラスがそれぞれ整数値をいじる権限 が必要です。 安易な解決策としては、整数値をいじる public メソッドを用意して ActionListener のクラスにそれぞれ使わせるということが考えられます。 しかし、値をいじれる public メソッドを作ってしまうとカプセル化ができな くなります。 また、増やす Listener も減らす Listener もクラスですが、なんとなく、整 数値を持っているクラスとだけ深い関連があるように思えます。 そこで Java 言語の機能であるインナークラス という クラス内にクラスを宣言する方法を使います。

インナークラスを使用すると、インナークラスの内部から外部のクラスに対し てはメンバ変数と final 宣言をしたローカル変数にだけ(private であろうと) アクセスできます。 インナークラスのインスタンスを作るには、外部クラスのインスタンスに対し て new を与えます。 詳しくは例8-2を御覧ください。

なお、フレームにボタンを配置するとき、パネルが必要になり、さらにパネル にボタンを貼り付けることになります。 GUI の部品的には階層構造になっており、Java の Swing クラスライブラリに おいても別々のクラスになっています。 一方、外部から見たときは、このような階層構造を意識せずに、各々の部品を 名前で呼び出せると便利です。 そのため、 GUI の一番外部の部品であるフレームのメンバ変数に、全ての GUI の部品を集めるようにします。 すると、フレームのオブジェクトに対して getter などで各部品にアクセスで きます。 そして、フレームのメンバ変数を使うために各 GUI の部品はインナークラス として定義します。

例8-2


import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class SimpleFrame extends JFrame {
    private Container c;
    private ButtonPanel panel;
    public SimpleFrame(){
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(300,200);
	c = getContentPane();
	panel = new ButtonPanel();
	c.add(panel);
    }
    private JButton plus;
    private JButton minus;
    class ButtonPanel extends JPanel {
	public ButtonPanel(){
	    plus = new JButton("+");
	    minus = new JButton("-");
	    add(plus);
	    add(minus);
	}
    }
    public JButton getPlusButton(){
	return plus;
    }
    public JButton getMinusButton(){
	return minus;
    }
}
class Data {
    private int data;
    public Data(){
	data=0;
    }
    class Up implements ActionListener {
	public Up(){}
	public void actionPerformed(ActionEvent event){
	    data++;
            System.out.println(data);
	}
    }
    class Down implements ActionListener {
	public Down(){}
	public void actionPerformed(ActionEvent event){
	    data--;
            System.out.println(data);
	}
    }
}
class Rei {
    public static void main(String[] arg){
	SimpleFrame frame = new SimpleFrame();
	Data data = new Data();
	frame.getPlusButton().addActionListener(data.new Up());
	frame.getMinusButton().addActionListener(data.new Down());
	frame.setVisible(true);
    }
}

さて、この例では同じデータに対する出力部が別々の(インナー)クラスにあり、 抽象化されていません。 このようなプログラムでは、例えば出力を GUI の中で行いたいなどの仕様変 更が生じた場合、修正が難しくなります。 そのため、出力もひとつのオブジェクトとして分離しましょう。 ここで、出力オブジェクトの持つべき仕様を考えます。 「Data クラスのオブジェクトは値を持っていて、その値が変更されたら、出 力値を変える」というのが要求される仕様です。 そのため、同様にオブザーバデザインパターンを使用します。 Data クラスに addActionListener メソッドを作り、 OutputData クラスで ActionListener を implements します。

なお、 actionPerformed メソッドで渡す引数は java.awt.event.ActionEvent のオブジェクトです。 ActionEvent オブジェクトは、呼出側のインスタンスの参照と、番号と、文字 列をコンストラクタに与えて作成します。 受け側ではその情報を使用して表示を作成します。

例8-3

なお import 部分と SimpleFrame クラスは例8-2 と同じため省略してい ます。


class OutputData implements ActionListener {
    public OutputData(){}
    public void actionPerformed(ActionEvent event){
	Data data = (Data) event.getSource();
	System.out.println(data.getValue());
    }
}
class Data {
    private int data;
    private ActionListener al;
    public Data(){
	data=0;
    }
    public int getValue(){
	return data;
    }
    public void addActionListener(ActionListener a){
	al = a; // 手抜き
    }
    private void update(){
	al.actionPerformed(new ActionEvent(this,0,""));
    }
    class Up implements ActionListener {
	public Up(){}
	public void actionPerformed(ActionEvent event){
	    data++;
	    update();
	}
    }
    class Down implements ActionListener {
	public Down(){}
	public void actionPerformed(ActionEvent event){
	    data--;
	    update();
	}
    }
}
class Rei {
    public static void main(String[] arg){
	SimpleFrame frame = new SimpleFrame();
	Data data = new Data();
	OutputData out = new OutputData();
	frame.getPlusButton().addActionListener(data.new Up());
	frame.getMinusButton().addActionListener(data.new Down());
	data.addActionListener(out);
	frame.setVisible(true);
    }
}

なお、この出力を GUI の JLabel で行いたいという場合は、 OutputData を JLabel のサブクラスとし、 SimpleFrame のインナークラスとします。

例8-4

import 部分と Data クラスは変更無しなので、省略しました。


class SimpleFrame extends JFrame {
    private Container c;
    public SimpleFrame(){
	super();
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(300,200);
	c = getContentPane();
	c.add(new ButtonPanel(),BorderLayout.NORTH);
	c.add(new LowerPanel(),BorderLayout.CENTER);
    }
    private JButton plus;
    private JButton minus;
    class ButtonPanel extends JPanel {
	public ButtonPanel(){
	    super();
	    plus = new JButton("+");
	    minus = new JButton("-");
	    add(plus);
	    add(minus);
	}
    }
    public JButton getPlusButton(){
	return plus;
    }
    public JButton getMinusButton(){
	return minus;
    }
    private SimpleFrame.LowerPanel.OutputData out;
    class LowerPanel extends JPanel {
	public LowerPanel(){
	    super();
	    out = this.new OutputData();
	    add(out);
	    updateUI();
	}
	class OutputData extends JLabel implements ActionListener {
	    public OutputData(){
		super("Hello");
	    }
	    public void actionPerformed(ActionEvent event){
		Data data = (Data) event.getSource();
		setText(String.valueOf(data.getValue()));
	    }
	}
    }
    public ActionListener getOutputData(){
	return out;
    }
}
class Rei {
    public static void main(String[] arg){
	SimpleFrame frame = new SimpleFrame();
	Data data = new Data();
	frame.getPlusButton().addActionListener(data.new Up());
	frame.getMinusButton().addActionListener(data.new Down());
	data.addActionListener(frame.getOutputData());
	frame.setVisible(true);
    }
}

8-3. 演習問題

演習8-1

例8-4において、値を 0 にする Reset ボタンを付けなさい。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科