プログラム設計概論 (PDF, 343KB)
Transcript of プログラム設計概論 (PDF, 343KB)
平成 21 年 7 月 16 日
プログラム設計概論
渡辺宙志
東京大学情報基盤センター
概 要
Java言語を題材としてプログラムの設計手法を学ぶ。特にオブジェクト指向などの概念を通し、バグが無く、仕様変更に強いコーディング手法を学ぶ。
目 次
1 はじめに 3
1.1 目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.2 プログラムの保守性 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.3 ソフトウェアの開発手法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41.4 プログラミング言語の種類 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2 Java言語の基礎 6
2.1 Javaとは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62.2 コンパイルと実行 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62.3 プログラムの構成要素 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3 変数 10
3.1 変数とは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103.2 基本データ型 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103.3 定数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133.4 スコープ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
4 メソッド 19
4.1 メソッドとは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194.2 スコープ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194.3 値渡しと参照渡し . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194.4 返り値 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204.5 カプセル化 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5 クラス 23
5.1 オブジェクトとインスタンス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235.2 クラス変数とクラスメソッド . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235.3 コンストラクタ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1
6 継承と多態性 28
6.1 動的結合 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286.2 継承とは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286.3 オーバーライド . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296.4 カレントインスタンス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306.5 多態性 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316.6 多態の使い方 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
7 構造化例外処理 33
7.1 エラー処理について . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337.2 例外処理の仕組み . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347.3 チェック例外 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357.4 ランタイム例外 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367.5 独自例外の定義 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
8 インタフェースとイベント処理 38
8.1 GUIプログラミング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388.2 イベントドリブン型プログラミング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398.3 インタフェース . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408.4 アダプター . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
9 グラフィックスの基礎 44
9.1 描画の仕組み . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 449.2 グラフィックスコンテキスト . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 459.3 ダブルバッファリング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
10 ソフトウェアの開発手法 48
10.1 命名規約 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4810.2 設計モデル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5110.3 リファクタリング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
11 終わりに 58
2
1 はじめに
1.1 目的
本ゼミでは Java言語を題材に「バグの入りづらいプログラムを設計するにはどうすべきか」を実例を挙げながら解説する。その過程で、命名規約、構造化プログラミング、オブジェクト指向プログラミング
といったプログラムの技法を学ぶ。特に、オブジェクト指向プログラミングを理解することを目的とする
のではなく、なぜオブジェクト指向が必要であるかを、バグの入りづらいプログラムを書くという立場か
ら理解することを目的とする。一応基礎から解説するが、if文や while文といった構文は既知とするので、知らない人は別の参考書で勉強すること。また、適宜 Java以外の言語にも触れる。なお、参考書として以下の文献を挙げておく。
• 「Java プログラミング徹底マスター」 有賀妙子・竹岡尚三著 (SOFTBANK BOOKS)Javaについて基礎から解説してある良書。やや古い本だが、将来 Javaを使うつもりなら買って損は無い・・・と思ったら、現在絶版とのこと。
• 「オブジェクト指向における再利用のためのデザインパターン」Erich Gamma, Ralph Johnson,Richard Helm, John Vlissides 著、本位田 真一、吉田 和樹 訳 (ソフトバンク クリエイティブ)オブジェクト指向に頻繁に現れるパターンをまとめた古典的著書。この本を読んでおけば、JavaにあらわれるAdapterや Interfaceなどの用語の理解に役立つだろう。ただし、プログラムの入門者には向かない。
• 「Java言語で学ぶリファクタリング入門」 結城 浩著 (ソフトバンク クリエイティブ)「良いプログラム」の書き方が分かりやすく書いてある。Javaを題材としているが、得られた知識は言語を問わず応用できるだろう。ただし、業務経験がないとリファクタリングの必要性の理解は
難しいかも知れない。
• 「Cプログラミング診断室」 http://www.pro.or.jp/~fuji/mybooks/cdiag/
いわゆる「ダメなプログラム」が、なぜダメかを例を挙げながら説明してある。慣れてくるとダメ
なプログラムは見ただけで気持ちが悪くなり、ひどい場合には吐き気を催したりする。逆に、この
例に挙がっているようなソースを見て反射的に「気持ち悪い」と思わなければ、まだちゃんとした
コーディングが身についていないということでもある。このサイトにもたびたび触れられているが、
ちゃんとしたコーディングを身に着けるには、独学は無駄か下手をすると逆効果であることが多い。
他のちゃんとしたプログラマが書いたきれいなコードを見て勉強するのがもっとも効率が良い。
プログラムは実際に組まなければ身につかない。サンプルコードをつけるようにするので、是非各自でい
ろいろ試して欲しい。
1.2 プログラムの保守性
プログラムとは、コンピュータの動作を指示、記述するための言語のことであり、プログラミングとは、
プログラムを組むことである。言うまでも無いことだが、コンピュータは人間が指示した通りにしか動作
しない。この時、人間が意図しない動作をすることをバグ (bug)と言い、バグを取り除く作業をデバッグ
(debug)という。プログラミングにおいてもっとも時間がかかる部分はデバッグである。従って、最初か
ら手間がかかってもバグが無いように注意してプログラムを組むのが望ましい。また、デバッグにおいて
最も時間がかかるのは、バグの発生箇所の特定である。そこで、たとえバグが発生しても、その場所がす
ぐに特定できるようなプログラム設計をすべきである。
3
プログラムにはその場で使い捨てにする小さなものから長い間使われることが前提の大きなものまで
様々なタイプがあるが、大きなプログラムは移り変わる現状に合わせて保守されながら使われることが多
い。バグが最も入りやすいのは、このような仕様変更の時である。そこで最初にプログラムを組む際には
将来どのような仕様変更があるかを良く考え、仕様変更しやすいように、かつ仕様変更してもバグが入り
づらいように設計しなくてはならない。
プログラムは「とりあえず動けばよい」というものではない。いい加減に設計されたプログラムは、い
つか必ず破綻し、その保守に大きなコストがかかることになる。プログラムを実際に組み始める前にその
プログラムの良さが決まっているといっても過言ではない。
1.3 ソフトウェアの開発手法
プログラムという概念が生まれた当初は、プログラムとは単にコンピュータへの指示の羅列であり、リ
ストの上から順に実行されていくものであった。制御には主に goto文が使われていたが、goto文が乱用されたコードは読みづらく、保守、拡張が困難となる1。そんななか、コンピュータの普及に伴ってプロ
グラムへの品質、開発効率の向上の要求が高まってきた。そこでプログラムを機能ごとにより小さな単位
(モジュール)に分割することで保守性を高めようという考えが提唱された。これを構造化プログラミング(Structured Programming)という。現在存在する主なプログラム言語は、ほとんどこの構造化プログ
ラミングの思想に基づいて設計されている。構造化プログラミングにおいては、大きなプログラムを互い
に独立性の高いモジュールに分割することで拡張性、保守性を高める。このようなモジュールを関数とし
て実現したのが C言語であり、このような言語を手続き型言語と呼ぶ。C言語では、プログラムの単位であるモジュールへの入力は引数、モジュールからの出力がリターン値として定義される。モジュールの実
行は関数呼び出しによって実現される。この際、モジュールは「何をやるか (目的)」が分かりやすく、さらに「どうやってやっているか (方法)」は考えなくて良いように作成されなければならない。すなわち、モジュールの実装方法が隠蔽されるように設計することで保守性、再利用性が向上する2。
このような考え方をさらに推し進めたものがオブジェクト指向プログラミング (Object Oriented
Programming, OOP) である。オブジェクト指向の考え方を簡単に説明することは難しい。「手続き
(Procedure)」とは、一連の作業をまとめたものであるが、「オブジェクト (Object)」とは、プログラム上の役割を抽象化したものである。たとえば、画面上にウィンドウを出すというプログラムでは、ウィンド
ウの大きさや内容を保持し、内容に応じて画面に描画するといった処理が必要となる。これをすべてプロ
グラマが自分で責任を持つことも可能であるが、ウィンドウというオブジェクトを考え、ウィンドウに大
きさを変更するようにメッセージを送ると再描画も自動的にされるほうが便利である。前者はホワイト
ボックス的、後者はブラックボックス的とも表現できよう。構造化プログラミングでは、メインとなるプ
ログラムから手続きを分離したというイメージだが、オブジェクト指向では、データと手続きをひとまと
まり (オブジェクト)として、それぞれがメッセージを送りあうことで全体として機能を発現するイメージとなる。
なお、オブジェクト指向について「哺乳類クラスから犬クラスやネコクラスを派生させて・・・」といった
説明を良く見かけるが、このような例え話はオブジェクト指向の理解につながらないどころか有害でさえ
あるので注意して欲しい。また、「オブジェクト指向プログラミングとはクラスを継承することである3」
という誤った認識も散見されるが、実際にはクラスはなるべく継承しないほうが良い。いずれにせよ、「オ
1「goto 文は悪だ」と一概に決め付けることはできないが、goto 文を無制限に許すとスコープの概念が破壊されてしまい、これが保守性を著しく損なう原因となる。スコープについては後で説明する。
2プログラム設計においては「隠蔽」と言う言葉が頻出する。直接的にはスコープを絞るということを意味する。何を隠蔽し、何を公開すべきかを考えるのがオブジェクト指向 (正確に言えばクラス設計) の基礎となる。
3どうやらオブジェクト指向の再利用性を継承によって実現するという誤解のようであるが、継承により、必要な機能のみを付け加えることを「差分プログラミング」と呼ぶが、悪手であることが多いので使う場合は気をつけること。一般にプログラムの機能追加や再利用は継承ではなく合成によって行う方が良い。
4
call sub1
call sub2
main sub1
sub2
Structured ProgrammingSpaghetti Programming Object Oriented Programming
goto C:
goto A:
goto D:
goto B:
Obj A
Obj B
Obj C
message
message
message
図 1: プログラミングパラダイムの変遷。goto文の乱用されたコードは機能が分離されておらず、保守性に欠ける (スパゲティプログラム)。そこで主要な流れをメインルーチンに記述し、細かい手続きをサブルーチンに分けることで保守性を高めることができる (構造化プログラム)。さらにプログラムの機能をオブジェクトという概念で抽象化することでモジュールの独立性が高まる (オブジェクト指向プログラム)。
ブジェクト指向は保守性の高いプログラムを作るための方法論である」という観点から学ぶようにして欲
しい。
構造化プログラミングやオブジェクト指向プログラミングはプログラムの設計に関するパラダイムであ
るが、コードを分かりやすく記述するために命名規約という方法論もある。これはコードに現れる変数名
や関数名などに一貫性を持たせることでプログラムの可読性を高める手法である。変数の名前だけではな
く、空白の使い方や改行の位置、コメントの入れ方なども定めたコーディング規約も存在する。これらは
必ず守らなければならないというものではないが、適切な規約にのっとって記述されたコードは読みやす
いだけではなく、保守性や再利用性にも優れるので活用して欲しい。
1.4 プログラミング言語の種類
プログラミング言語の種類は多岐にわたる。また一つの言語が二つ以上のパラダイムを実装していたり、
バージョンによってパラダイムが変わる言語もあるため、プログラミング言語の種類は一意には決められ
ない。以下、あくまで参考までにプログラミング言語を類別する。
プログラム言語は、大きく分けて手続き型と関数型に分けられる。手続き型の代表は Fortranや、Cなどであり、関数型では Haskellや LISPなどが有名である4。手続き型の言語は主に構造化プログラミン
グを指向して設計されたが、その概念をさらに発展させたのがオブジェクト指向プログラミングであり、
Javaなどに代表される。オブジェクト指向プログラミング言語は、さらにクラスベースとプロトタイプベースに分けられる。Javaはクラスベース、JavaScriptはプロトタイプベースだが、Flashに用いられるActionScriptは Ver 1.0ではプロトタイプベースだったが、Ver 2.0以降ではクラスベースとなった。また、現在使われている手続き型言語のほとんどが言語仕様を拡張し、オブジェクト指向的にプログラム
が可能となっている。たとえば、C++やObjective-Cは、C言語のオブジェクト指向拡張であり、Fortranも Fortran 90からオブジェクト指向が言語仕様として盛り込まれた。
Pascalは主に教育用に用いられた手続き型言語だが、その拡張である Object Pascalを元に Delphiという言語が作られた。C#は.NET Frameworkで動作する純粋なオブジェクト指向言語で、その言語仕様は Javaに似ているが、実は Delphiを経由して Object Pascalの仕様が数多く盛り込まれている5。
4関数型言語は、変数の値の書き換えを許すかどうかにより、さらに純粋型関数型言語と非純粋型関数型言語に分けられる。Haskelは純粋型、LISP は非純粋型言語である。
5Delphi は Borland 社が開発した、主に Windows 向けの統合開発環境であり、その C++版である Borland C++ Builderとともにヒットした。Delphi の開発を行っていた Anders Hejlsberg がマイクロソフトに移籍後、C#の開発に関わっている。
5
2 Java言語の基礎
2.1 Javaとは
Javaとは Sun Microsystems社が開発したプログラム言語であり、以下の様な特徴を持っている。
オブジェクト指向言語 Javaは純粋なオブジェクト指向に基づいた言語であり、文法に従うことで自然にオブジェクト指向プログラミングを実践することができる。Javaにおいてはオブジェクトはクラスによって表現される。Javaプログラミングとは、クラスを記述することに他ならない。
可搬性 Javaは、環境に依存しない。ソースファイルをコンパイルするとバイトコードと呼ばれる中間コードが生成される。その中間コードをインタプリタが実行することでプラットフォーム非依存を実現
する。たとえば OSがWindowsであるかMacであるかといった環境の違いはインタプリタが吸収する。
静的型付け Javaはコンパイル時に強力な型チェックを行うことで、プログラム実行時に問題が起きないようにする。対となる概念に「動的型付け」というものがあるが、大半のスクリプト言語が動的型
付けの立場をとる。
ガベージコレクション Javaの実行環境はガベージコレクション機能を持つ。これは、ユーザーが確保したメモリを自動的に開放する機能であり、ユーザーは自分でメモリを管理する必要が無くなる。こ
の機能により、メモリリークの心配も無くなる6。
ネイティブなグラフィック環境 Javaはネイティブにグラフィカルユーザーインタフェース(Graphical UserInterface, GUI)を構築するためのツールキット (AWTや Swing)を備えている。C言語等に提供されるグラフィックライブラリは環境に強く依存することが多いが、Javaでは環境非依存にGUIアプリケーションを作成することができる。
2.2 コンパイルと実行
Javaはクラスベースのオブジェクト指向言語であり、プログラムは、クラス (Class)という単位から
出来ている7。原則としてファイル一つにクラスの定義が一つである。クラスは、フィールド(データ)と、メソッド(手続き)から構成される。プログラムは複数のクラスから構成されるが、プログラムについて一つだけ public static void main(String args[])というメソッドを持ち、一番最初にそのメソッドが呼ばれることでプログラムが実行される。
Javaのプログラムを作成、コンパイル、実行する方法を簡単な例で見てみよう。まずは「Hello World」を画面に出力するプログラムを見てみよう。以下のリストを「Hello.java」というファイル名で作成する。ファイル名の大文字、小文字も区別されるので注意。
List 1: Hello World¨ ¥import java.lang.*;class Hello {public static void main(String args[]){System.out.println("Hello World");
}}§ ¦作成したファイルを javacというプログラムでコンパイルする。6現在広く使われているスクリプト言語 (Perl, Python, Ruby 等) は、ほぼガベージコレクション機能を持つ。それに対し、
C/C++や Fortran といった言語にはガベージコレクションは無く、ユーザーがメモリを管理する必要がある。7オブジェクト指向言語は、主にクラスベースとプロトタイプベースに分けられる。前者では C++や Java、後者では JavaScript
や ActionScript などが有名である。
6
$ javac Hello.java
すると、Hello.classというファイル (バイトコード)が生成されるため、インタプリタによって実行する。
$ java Hello
この際、「java Hello.class」ではなく「java Hello」と入力することに注意したい。正しく実行されれば画面に Hello Worldの文字列が表示されるはずである。この例ではフィールドを持たず、唯一つのメソッド mainを持つクラス Helloを作成した。java Hello
と実行すると、インタプリタはまず Hello.classのmainという名前のメソッドを探して実行する。JavaはC言語と同様に手続きの書き方が自由であって、上から順に実行されるわけではない。そこで、プログラムで一番最初に実行される手続き (メソッド)を public static void main(String args[])という名
前で作ることが約束されており、そのメソッドからすべてが始まるのである。
2.3 プログラムの構成要素
2.3.1 クラス
Java言語のソースコードには、クラス定義が書いてある。クラスは、オブジェクトの仕様書のようなものであり、必要なときにクラスからオブジェクトを作る。この時作成されたオブジェクトをそのクラスの
インスタンス (Instance)と呼ぶ。
クラスは、フィールドとメソッドから構成される。フィールドとは、そのクラスの状態を表すデータで
あり、メソッドとは、そのオブジェクトに対する操作である。
例を挙げよう。
List 2: クラスの例¨ ¥import java.lang.*;
class Test {int a = 10; // フィールド定義void doubleA() { // メソッド定義a = a * 2;
}public static void main(String args[]){Test t = new Test();System.out.println(t.a);t.doubleA();System.out.println(t.a);
}}§ ¦これは Testという名前のクラス定義である。慣習により、クラス名は大文字、フィールド、メソッド名
は小文字で始める。Testというクラスは、整数型の変数 aというフィールドと、doubleAというメソッドが存在する。doubleAは、単に aをそのときの値の 2倍にする機能を持つ。プログラムは、まず mainから実行される。最初に Test型の変数 tが宣言され、 new演算子によって Testクラスのインスタンスが作られる。tというオブジェクトの aにアクセスするためには、t.aのように、「オブジェクト.メンバ名」とする。メソッド呼び出しも同様に、t.doubleA()のように、「オブジェクト.メソッド名」とする。このメソッドを呼び出すと aの値が二倍になるので、もう一度 aの値を表示させると今度は 20と表示される。以上をまとめると、
• Javaプログラミングとはクラスの定義の記述である
• クラスは、フィールドとメソッドから構成される
7
• フィールドはそのクラスの状態を表す
• メソッドはそのクラスへの操作を表す
• クラスは定義であり、new演算子によってその実体が与えられる。この実体をクラスのインスタンスと呼ぶ
• メンバやメソッドへのアクセスは、obj.aもしくは obj.method()などとする。
となる。見知らぬ単語が多数出てきて戸惑うかもしれないが、これから解説するので今は分からなくても
良い。
2.3.2 コメント
Javaは、C/C++言語と同じ形式でコメントを入れることができる。コメントとはソースコードに記述する注釈のことで、単一行と複数行の二種類の書き方がある。
単一行コメント スラッシュを二つ書くと、それ以降行末までコメントとみなされる。
複数行コメント 「/*」から「*/」で挟まれた領域は全てコメントとみなされる
コメントはコンパイル時には無視されるため、何を書いても良い。通常、クラスの説明やメソッドの注釈、
将来への覚書などを記述する。コメントは長すぎても短すぎてもよくない。長いコメントを必要とするコー
ドは、設計が悪くないか再考すべきである。また、良く設計されたプログラムはコメントの必要があまり
無いといわれる。
Javaには、ソースコードからプログラムの仕様書を作成する Javadocというソフトウェアがある。これは、プログラムの仕様を特殊なコメントの形で埋め込むものである。「/**」から「*/」で囲まれたコメントが Javadoc用のコメントとみなされる。Javadocは、そのコメントを解釈し、ソースコードからHTML形式の仕様書を作成する。
2.3.3 import宣言
Javaは豊富なクラスライブラリが用意されており、それらを利用することでソフトウェアの開発を容易に行うことができる。それらのクラスはパッケージと呼ばれる単位でまとめられており、名前がぶつかる
のを防いでいる。これは、C++言語に見られる名前空間 (namespace)と同じ発想であり、Javaの import宣言は、C++の using namespace宣言と似ているが、名前空間にはなんら構造は無いのに対して、Javaのパッケージは階層構造になっている8。
クラスは全て自分が属するパッケージが存在し、そのパッケージ名も含めたフルネームによりその
クラスが特定される。たとえば、Java アプレットを作るための基本クラス「JApplet」のフルネームは「javax.swing.JApplet」である。しかし、いちいちこのフルネームを指定するのは面倒であるために、ソースコードの最初に¨ ¥import javax.swing.JApplet;§ ¦と指定しておけば、「JApplet」と指定するだけでこのクラスを使うことが出来る。この時「javax.swing.JApplet」を完全限定名、「JApplet」を単純名と呼ぶ。あるパッケージ全てのクラスを参照したい場合は、¨ ¥
import javax.swing.*;§ ¦8URL のドメイン名、サブドメイン名もパッケージ名と同様に階層構造によって名前の衝突を避ける仕組みである。
8
と、ワイルドカード「*」を指定する。これにより javax.swingに属すクラス全てが単純名で指定できるようになる。ワイルドカードを指定した場合、
なお、java.lang.*パッケージはデフォルトでインポートされるため、明示的にインポート宣言しなく
ても良い。
9
3 変数
3.1 変数とは
プログラムは、データと、それを処理する手続きからなる。データを扱うための仕組みが変数 (Variable)
である。変数は、整数であったり実数であったり、クラスのインスタンスであったりするだろう。このよ
うに変数が表現するデータの性質をデータ型 (Data Type)、もしくは単に型と呼ぶ。また、変数は宣言
された場所によってフィールド、ローカル変数、メソッド引数の三種類に分けることができる。この種類
により、主に変数のスコープ (Scope)が変わる。スコープについては後述することにして、まずは変数
の型について解説する。
3.2 基本データ型
3.2.1 有効範囲
変数とは、データを保管したり、データへの操作を提供するものである。すべての変数には型が存在す
る。たとえば¨ ¥int a;§ ¦と宣言された変数 aは、整数 (integer)の型を持ち、整数の値しかとることができない9。Javaに言語仕様として最初から用意されている型などを基本データ型 (primitive type)と呼ぶ。基本データ型には以下
のようなものがある。
boolean 真偽値。trueか falseの値をとる。
byte 8ビット整数。(−128 ∼ 127)
short 16ビット整数。(−32768 ∼ 32768)
int 32ビット整数。(−2147483648 ∼ 2147483648)
long 64ビット整数。(−9223372036854775808 ∼ 9223372036854775808)
char 文字。(0 ∼ 65535)
float 32ビット浮動少数点数。(±1.40239846 × 10−45 ∼ 3.40282347 × 1038)
double 64ビット浮動少数点数。(±4.94065645841246544 × 10−308 ∼ 1.79769313486231570 × 10324)
整数と浮動小数点数をあらわす型が多数存在するが、単に「3」などと整数を書いた場合には int 型と、「3.0」などと実数を書いた場合には double型と解釈される。
C++や Javaといった言語は変数がどのデータ型であるかを宣言しないと使うことができないため、プログラム設計時に変数の型が決定する。このような言語を静的型付け言語と呼ぶ。逆に、実行するまで型
が確定しないような言語を動的型付け言語と呼び、Rubyなどのスクリプト言語に多い10。静的型付け言語
は、コンパイル時に型のチェックを行うため、型の不整合が起きるとコンパイルエラーや警告を出す。こ
れによってバグを未然に防ぐことができる。9より正確に言えば、データ型とはコンピュータのメモリ上にあるデータをどのように扱うかの宣言である。たとえば int型なら
通常4バイトの情報を持つが、その情報を整数として翻訳して扱うということを意味する。したがって、同じデータであっても、それを整数と認識するか、実数として認識するかで値は大きく食い違う。これは Fortranなどの型チェックの甘い言語でバグの原因となる。たとえば関数の引数として REAL のデータを渡したのに、関数側で DOUBLE PRECISION で受け取ったりすると値がおかしくなるが、コンパイルエラーが出ないために発見しづらい。
10動的型付け言語には「型が無い」とたびたび誤解されるようである。動的型付け言語にも当然ながら型は存在する。静的と動的の違いは、変数の型チェックがコンパイル時に行われるか、実行時に行われるかの違いである。
10
変数には有効範囲がある。たとえば byte型なら、-128から 127までしか表現できない。したがって、それ以上の数値を与えると結果がおかしくなる。
たとえば、プログラムを実行してからの経過時間を byte型で返す関数が elapsedTimeがあるとしよう。それを用いて、10秒停止するコードを意図して¨ ¥byte start = elapsedTime();while(elapsedTime() -start <10 ){//何もしない
}§ ¦というコードを書いたとする。このとき、プログラムを実行して 120秒目にこのコードが実行されたとすると、startの値は 120であり、elapsedTimeは-128から 127までの数字しか返さないのだから、このループは永遠に停止しないことになる11。
3.2.2 実数と丸め誤差
プログラムでは浮動小数点数をほぼ実数として扱っているが、計算機の上では離散的なデータとして扱
われている。これにより、有効桁数や丸め誤差といった問題が生じる。
たとえば、Javaでは IEEE 754の仕様に基づいて実数を表現する。doubleは 64ビットで表現されるが、そのうち 1ビットを符号、52bitを仮数部、11bitを指数部として扱う。このうち、仮数部が実際の有効数字を表現するため、10進数になおすと、およそ 15桁に対応する。したがって、15桁以上の有効数字は無意味となる。
また、doubleは内部では二進数で表現されているため、計算のたびに精度に丸め誤差が生じる。たとえば 0.1を 10回足してもぴったり 1にならない12。このような理由から、doubleなどの実数における等号比較は無意味であるので注意したい。たとえば以下のようなコードは停止しない。¨ ¥double x = 0;while(1){x += 0.1;if(x == 1.0)break;
}§ ¦このようなことを防ぐため、実数の比較は不等号を使うのが基本である。ただし、¨ ¥double x = 0;while(1){x += 0.1;if(x > 1.0) break;
}§ ¦余談コラム 1 ~ 静的型言語と動的型言語 ~
プログラム言語には、大別して静的型言語と動的型言語がある。静的の例としては Fortranや COBOL、C++や Javaが挙げられ、動的としては Lisp、Smalltalk、Python、Rubyなどが挙げられよう。静的型言語は原則として変数は宣言しないと使うことが出来ないが、動的の場合は変数は宣言せずとも使うこと
ができる。一般に静的の方が文法が厳しく、大規模なプログラムに向くとされる反面、柔軟性に欠け、仕
様変更時に変更箇所が多くなる傾向にある。対して動的型言語は柔軟性に富む分、最適化が難しいとされ
てきたが、近年の計算機の速度向上によりその弱点が小さくなりつつある。言語の優劣を議論するのは無
益であることが多いが、個人的な経験では動的型言語の方が生産性が高いと感じている。
11こういうコードは実際の製品に存在する。ずいぶん昔になるが、友人がグラフィックボードの不具合に苦しんでいた。結局、デバイスドライバが本文のようなコードを書いていて、そのために起きていた不具合であることが判明した。2000 年問題もこの種の不具合に分類されよう。
12十進法における 0.1 は二進数では循環小数になるため。
11
などとすると、変数 xの値が最終的に 1.0になるか 1.1になるかは実装に依存する。今回のようにあらかじめ足される数字が分かっている場合には¨ ¥double x = 0;while(1){x += 0.1;if(x > 1.05) break;
}§ ¦などとして防ぐことができるが、実際には、¨ ¥double x = 0;for(int i=0;i<10;i++){x += 0.1;
}§ ¦と、比較の場所 (今回は i<10)には整数を用いるのが安全である。
実数の比較には気をつける
3.2.3 キャスト
原則として異なる型同士の変数の代入は許されないが、自動で変換が可能であれば代入されることもあ
る。たとえば、¨ ¥int a = 3;double b = a;§ ¦というコードでは、実数型を持つ変数に整数型を持つ変数の値が代入されている。この時、一度整数 (この場合は 3)が実数に (この場合は 3.0)に変換されて代入が実行される。このような型変換をキャストと呼ぶ。上記の例は特に暗黙的キャストと呼び、エラーも警告も出ない処理系が多いが、暗黙的なキャストは
バグの温床であるので注意したい。
たとえば、1/3を表現したくて¨ ¥double b = 1/3;§ ¦と書いたとしよう。この時、1も 3も型は明示されていないが、整数型として扱われる。すると、整数同士の演算は整数にするという原則から、1/3が実行されると整数 0となる。その後、整数 0が暗黙的に実数にキャストされ、最終的に bの値として 0.0が代入される。このプログラムは bに 0.3333 · · ·を代入することを意図して書かれているから、これはバグとなる。このような簡単な例なら発見できるかも知れな
いが、¨ ¥double energy = (vx*vx/2 + vy*vy/2)*sin(omega) + tan(theta + 1/2);§ ¦というコードにおいて実際には 1/2が 0として扱われていることに気がつくのは難しい。暗黙的なキャストによる問題を防ぐには、¨ ¥int a = 3;double b = (double)a;§ ¦と、整数型変数を実数として扱うことを明示したり、¨ ¥double b = (double)1/(double)3;§ ¦と、まず 1や 3を実数として扱うことを宣言し、その後割り算を実行するように指示する。このようなキャストを明示的キャストと呼ぶ。定数の場合は
12
¨ ¥double b = 1D/3D;§ ¦と、数字の後ろに Dをつけて、この数字が double型であることを宣言したり、より簡単に、¨ ¥double b = 1.0/3.0;§ ¦と小数点をつけて、最初から定数を実数として使う方法もある。いずれにせよ、常に型を意識し、キャス
トが必要な場合には明示的に行う癖を普段からつけておかなければならない。
キャストは明示的に
3.3 定数
多くのプログラム言語には定数を宣言するための修飾子が存在する。Javaでは final修飾子がそれにあたる13。これらは型修飾子 (Type Qualifiers)の一種であり、続けて宣言された変数を定数として扱う。
たとえば、¨ ¥final int L = 10;§ ¦とすると、Lは定数となり、以降その値を変更することができなくなる。プログラム中で定数に値を代入しようとするとコンパイルエラーが起きる。¨ ¥final int L = 10;L = 3; //コンパイルエラー§ ¦プログラミングにおいては (最初のコーディングでバグを入れないことは当然として) 将来の仕様変更で
バグが入らないように設計するのがもっとも大切なことである。そのために、数字を生のまま使わない、
というのはその第一歩である。
たとえば、大きさ 10の整数の配列を宣言する場合を考える。普通に宣言するなら、¨ ¥int array[] = new int[10];§ ¦で文法上間違いではない。しかし、実際のプログラミングにおいては¨ ¥static final int SIZE = 10;int array[] = new int[SIZE];§ ¦などと必ず一度定数を用いて配列サイズを宣言しておく。定数で宣言しておかないと、後で配列のサイズ
に依存する処理をしようとするときに¨ ¥int array[] = new int[10];
for (int i=0; i < 10; i++){array[i] = 0;
}§ ¦のように、配列のサイズ 10が二箇所に出現することになる。このように、生のままの数字のことをマジックナンバーと呼ぶ。後で配列のサイズを変更しようとした場合、プログラムのすべての場所において配列
のサイズに依存する数値の変更をしなくてはならず、変更忘れはバグに直結する。このように、配列のサ
イズをマジックナンバーで宣言するという行為はプログラムに時限爆弾を埋め込むことに他ならない。そ
こで以下のように14
13C/C++においては、定数は const 修飾子で宣言できるが、#define 宣言によっても同等な機能を得ることができる。しかし、#define で宣言された変数は単に文字列として展開されてしまって型チェックが行われず、しかもスコープの概念も無い。特別な理由が無い限り const 修飾子を用いるべきである。
14ここはあくまで例であり、Java の場合は array.size() とすれば配列のサイズを得ることができるため、このような記述は必要ない。
13
¨ ¥const int SIZE = 10;int array = new int[SIZE];
for (int i=0; i < SIZE; i++){array[i] = 0;
}§ ¦と定数を使って宣言すれば、最初の宣言部分のみ変更したら他の場所もそれに伴って適当に変更されるこ
とが保証される。むしろ、そう保証されるようにコーディングするのである。このように、同じ情報、同
じ手続きを複数回記述しないという原則を DRY原則(Don’t Repeat Yourself)と呼び、プログラム設計の基本の一つである。
さらに、意味の無い情報であった「10」という数字が意味のある文字「SIZE」によって置き換えられたことに注意したい。「10」だけを見て、これが何を意味する数字であったかを判断するのは難しい。コードを組んだ直後ならともかく、一ヵ月もすれば意味を忘れてしまっているだろう。それに対して、「SIZE」としてあれば、これが何かのサイズをあらわすことが分かる。さらに、「ARRAY SIZE」や「BUFFER SIZE」など、詳しい内容がわかる名前はより望ましい15。
マジックナンバーの代わりに定数を使う(定数との比較における if文の書き方)
3.4 スコープ
変数にはスコープ(Scope)という概念がある。スコープとは、変数や関数の有効範囲のことであり、通
常は宣言されたブロック内でのみ有効となる。ブロックとは、簡単に言えば中括弧 {}で囲まれた領域である。
一般にブロックの外側で定義されるほどスコープが広くなる。スコープが広ければ広いほどその変数に
アクセスできる範囲が広がり、結果的にバグが入りやすくなるため、スコープは必要最小限にとどめるの
が望ましい。
3.4.1 ローカル変数
メソッドの中で定義された変数のことをローカル変数 (local variable)と呼ぶ。これらは一時的に使
用される変数であり、メソッドの処理が終わると同時に保持するデータは消える。
ローカル変数のスコープは、基本的に「ブロックの中」に制限される。たとえば、¨ ¥if (something) {int a = 10;System.out.println(a);
}§ ¦というコードでは、ローカル変数 aにアクセスできるのは if文の中のみである。逆に ifブロックの中からは、より外の変数にもアクセスできる。したがって、¨ ¥int a = 20;if (something) {a = 10;System.out.println(a);
}System.out.println(a);§ ¦15ここで、慣習に従って定数の名前をすべて大文字で書いている (命名規約の一種)。
14
とすると、最初に定義した aの値が変更される。また、¨ ¥int a = 20;if (something) {int a = 10;System.out.println(a);
}System.out.println(a);§ ¦とすると、既に定義された変数 aを再定義しようとするためエラーとなる。さらに、¨ ¥if (something) {int a = 10;System.out.println(a);
}int a = 20;System.out.println(a);§ ¦これはエラーにならない16。ifブロックが始まった段階では変数 aは定義されておらず、ifブロックの外で定義された時には ifブロックで宣言された aの有効範囲は終わっているからである。
3.4.2 メソッド引数
引数 (ひきすう)とは、メソッドを呼び出す際に必要な値のことである。そのスコープは、そのメソッド内で有効である。引数と名前と同じ名前のローカル変数をメソッド内で定義することはできない。
ただし、スコープとはメソッドの引数としてつけられた名前の有効範囲であって、その変数の値の有効
範囲ではない。変数の値がメソッド終了後に保持されるかどうかは、その引数が参照渡しか値渡しかに依
存する。
3.4.3 フィールド
フィールドとは、クラス定義の中では最も外側のブロックで定義された変数のことである。フィールドの
スコープを制御するのに、アクセス修飾子 (access modifier)を使用する。アクセス修飾子には private、public、protectedの三種類が存在する。
private そのメンバが定義されたクラスの中のみからアクセス可能。サブクラスからも見ることが出来
ない、最も厳しいアクセス制限である。ただし、同じクラスから作られたインスタンスはお互いの
privateメンバにアクセスできる。
protected 同一パッケージとサブクラスからアクセス可能。
public 全てのクラスからもアクセス可能。
省略 アクセス指定子を省略すると、同一パッケージからのアクセスが可能となる。
ここでパッケージに関するスコープが出て来たが、大きなプログラムでなければパッケージをまたいだコー
ドを作成しないであろうから、いまは覚えなくても良いだろう。
一般的に、フィールドのスコープは狭ければ狭いほど良い。特に変数は定数でない限り publicにすべきでない。特別な理由が無い限り privateをつけることを推奨する。他のクラスからその値を参照したり変更したりしたい場合は、参照用のメソッドと変更用のメソッドを作成して公開し、そのメソッドを通して
参照、変更を行う。これらのメソッドはそれぞれ getter/setterと呼ばれる。このようにフィールドを隠蔽し、公開メソッドを通してのみ値の変更を許す手法はカプセル化と呼ばれる手法の一種である。フィール
16これらはあくまで説明のために書いたコードであり、良くないコード例なので決してマネをしてはいけない。
15
ドをカプセル化することにより、オブジェクトの状態が勝手に書き換えられることを防ぐ。また、状態が
変わった時に適切な処理を行うことができる。
3.4.4 変数の隠蔽
Javaでは、フィールドと同じ名前のローカル変数を定義することが出来る。たとえば以下のコードを考える。
List 3: ローカル変数によるフィールド隠蔽の例¨ ¥class Test{int x = 1;
void method(){int x = 2;
}
public static void main(String args[]) {Test t = new Test();t.method();System.out.println(t.x);
}}§ ¦このコードはエラーにならずコンパイルされ、実行すると結果は 1と表示される。この際、methodというメソッド内では、ローカル変数のスコープが優先され、フィールドのスコープは隠蔽される (アクセスできない)。この場合、明示的にフィールドにアクセスしたい場合は、thisキーワードを使って this.xと
すればよい。しかし、このようなフィールドの隠蔽はバグの元である。処理の中で、xがローカル変数なのかフィールドなのかが分からなくなるため絶対にやってはいけない。
同様に、メソッドの引数にフィールドと同じ名前を指定することができる。
List 4: メソッド引数によるフィールドの隠蔽の例¨ ¥class Test{int x = 1;
void method(int x){System.out.println(x);
}
public static void main(String args[]) {Test t = new Test();t.method(2);System.out.println(t.x);
}}§ ¦この場合もメソッド引数のスコープが優先され、フィールドは隠蔽される。同様にバグの元であるから、
これもやってはいけない。スコープを隠蔽してもメリットが無いため意識的に同じ変数名を使うことは無
いと思うが、コンパイルエラーが出ないために気づきにくい。そのため、フィールドに iや aなど、ロー
カル変数に使いそうな名前を使うことは避けるべきである。
スコープの上書きをしないなお、フィールドに特別な名前をつけることでローカル変数と名前の衝突をさける手法がある。たとえ
ば C++で良く使われるハンガリアン記法ではフィールドに m_という特別なプレフィックスをつけ、続く
名前を大文字からはじめる。これを用いるとフィールドの名前は m_Sizeや m_Dataなどとなる17。また、17このような記法をシステムハンガリアンと呼ぶ。
16
Javaで良く使われる記法では、フィールドをアンダースコアで始めて名前を小文字とする。これを用いると_sizeや_dataなどとなる。
3.4.5 C言語における staticキーワード
Javaでは禁止されているが、C言語にはローカル変数に staticキーワードをつけることができる。Javaにおける staticは後述するが、C言語における staticキーワードについて簡単に解説しておく。
C/C++言語において、ローカル変数は原則としてスタック領域に確保される。これは関数が再帰できるようにするためである。スタック領域は関数を抜けるときに解放されるため、次に関数を呼んだときに
は変数に値は残されていない。しかし、ローカル変数に staticキーワードをつけると、その変数用のメモリはスタックではなくヒープに取られる。この static変数は最初に一度だけ初期化されたあと、関数を抜けても値が保持される。
たとえば、以下のような関数を作ったとしよう。¨ ¥intfunc(void){int a = 0;a++;return a;
}§ ¦この関数は、何度呼んでも 1を返す。しかし、ローカル変数 aに staticキーワードをつけると¨ ¥intfunc(void){static int a = 0;a++;return a;
}§ ¦この関数の返り値は呼ばれるたびに 1,2,3,· · ·と値が増えていく。これは、関数の呼び出し回数を数えたいが、さりとてそのカウンタをグローバル変数に取りたくない場合などに良く使われる。
しかし、こういう staticな変数を持つ関数は、内部状態を持つことになる。内部状態をもつ関数は、同じ引数を与えても異なる値を返す可能性がある18。これは、バグが発生した場合の問題箇所の特定を困難
にするため、あまり多用すべきではない。
C言語における staticキーワードの重要な使い方をもう一つ上げておこう。それは、大きな配列をローカル変数として定義する場合である。たとえば、¨ ¥voidfunc(void){double temp[100000000];//必要な処理
}§ ¦のように、一時的に必要な大きな配列をローカル変数で定義すると、その配列用のメモリもスタックに確
保される。一般にヒープ領域に比べてスタック領域は小さいため、あまり大きな配列を宣言しようとする
と実行時にスタックオーバーフローを起こしてプログラムが異常終了する。数値計算において、小さい規
模の計算を行っている時には正しく動作していたのに、大きくしたときに coreを吐いて死ぬようになったなどのときには、この種の問題を疑う必要がある。
これを防ぐには、staticキーワードをつけてヒープに確保するか、もしくは配列を動的に確保する。
List 5: 静的に確保¨ ¥void
18同じ引数を与えたら必ず同じ値を返す関数を「参照透過性がある」と呼ぶ。詳しくは Haskell などの関数型言語を参照のこと。
17
func(void){static double temp[100000000];//必要な処理
}§ ¦List 6: 動的に確保¨ ¥
voidfunc(void){double *temp = new double[100000000];//必要な処理delete [] temp;
}§ ¦静的に確保した場合、配列の値は関数を抜けても保持されるので十分注意しなくてはならない。動的に確
保した場合には、関数を抜ける際に確保したメモリを解放する処理を忘れないこと。忘れるとメモリリー
クを起こしてシステムが不安定となる。
なお、Fortranはデフォルトですべての変数がヒープに確保されるため、この種の問題は生じない。ただし、関数を抜けた時に、変数の値が保持されるかどうかは処理系に依存するため、変数の値が残ること
を利用したコードを書くべきではない19。
19Fortranで関数内のローカル変数の再利用を行うコードは、特に OpenMPを用いた並列化などで問題となる。普段から処理系に依存しないコーディングを心がけるべきである。
18
4 メソッド
4.1 メソッドとは
オブジェクト指向プログラミングにおいては、オブジェクト同士がメッセージをやり取りすることで全
体として目的の機能を実現する。Javaにおいてはこのメッセージのやりとりを実現する手段としてメソッド (Method)が用意されている。たとえば、あるオブジェクト objが有効な値を持っているか知りたいとする。この時、オブジェクト objに isValidというメソッドを用意しておき、有効な値を持っている場合には obj.isValid()が真を返すようにプログラムを設計する。obj.isValid()を実行したオブジェクトはメッセージの送信者 (sender)であり、objはメッセージの受け手 (reciever)である20。
メソッドは、クラスの定義の中に次のように宣言する。
[修飾子] 戻り値のデータ型 メソッド名 (引数のデータ型 引数名, ...)
それぞれについて以下で解説する。
4.2 スコープ
修飾子は、前述のアクセス修飾子 (private, protected, public)などが含まれる。アクセス修飾子の意味はフィールドの場合と同じである。それぞれについてどのように使うかまとめておく。
private privateが指定されたメソッドは、そのクラスの外部から呼ぶことができない。プライベートメソッドは、クラスの内部的な処理を行うのに使われる。
protected protectedが指定されたメソッドはサブクラスにも継承される。クラスの外には公開されないため内部処理に使われるのはプライベートメソッドと同じだが、サブクラスにも継承すべきメソッ
ドには protectedを指定する。また、メソッドのオーバーライドの対象となる。オーバーライドに関しては後述する。
public すべてのクラスからアクセス可能となる。パブリックメソッドは、外部からのメッセージを受け
取る窓口である。メソッドのオーバーライドの対象となる。
変数の場合と同様に、メソッドもやたらと公開すべきではない。どのメソッドを非公開とし、どのメソッ
ドを公開すべきかはよく考える必要がある。また、公開するメソッドはその目的 (機能)がわかりやすい名前をつけるとよい。Javaのメソッドは「動詞 (+目的語)」の形をとることが多い。たとえば、コンポーネントを描画する「paint」、色を指定する「setColor」、現在の色を得る「getColor」などである。特に「set」や「get」がついているメソッドはオブジェクトの内部状態の変更や取得のために公開されているメソッドであり、それぞれ「setter」「getter」と呼ばれる。Javaには「set***」「get***」の形をしたメソッドが多数存在する。
4.3 値渡しと参照渡し
メソッドには、その実行に必要な情報を引数 (argument)として与えることができる。情報の渡し方
には値渡し (passed by value)と参照渡し (passed by reerence)がある。渡される引数が基本データ
型の場合は値渡しとなり、参照データ型の場合は参照渡しとなる。
値渡しとは、メソッドが呼ばれる際に変数の値がコピーされて、その値だけが渡される方式である。基
本データ型を引数として渡す場合には値渡しとなる。値渡しでは値がコピーされるため、メソッド内でそ
の引数の値を変更しても呼び出し元の変数の値は変更されない。20メッセージの受け手は、通常カタカナで「レシーバ」と呼ばれることが多い。
19
List 7: 値渡しの例¨ ¥void methodA(int value) {value = 1;System.out.println(value);
}
void methodB() {int i = 0;methodA(i); // 1が表示されるSystem.out.println(i); // 0が表示される (値は変更されない)
}§ ¦参照渡しとは、メソッドが呼ばれる際に引数として参照 (ポインタ)が渡される方式である。基本データ
型以外のデータ (クラスのインスタンスや配列など)は参照渡しで渡される。そのため、メソッド内部で引数の値を変更すると、呼び出し元でもその影響を受けるために注意が必要となる。
List 8: 参照渡しの例¨ ¥void methodA(int[] value) {value[0] = 1;System.out.println(value[0]);
}
void methodB() {int i[] = new int[1];i[0] = 0;methodA(i); // 1が表示されるSystem.out.println(i[0]); // 1が表示される (値が変更されている)
}§ ¦4.4 返り値
メソッドは return文によって呼び出し側に何か値を返すことができる。その値を返り値 (return value)、
もしくは戻り値と呼ぶ。値を返すためには、メソッドの宣言時に返す値の型を指定しておく必要がある。
return文で返すことができる変数は指定した型と整合するものでなければならない。値を返さないメソッドの場合は voidを指定する。返り値を void以外にしたメソッドは、必ず return文により値を返さなくてはならない。返り値を指定しているのに return文がなかったり、返り値の型と整合しない型の変数を返そうとしたりするとコンパイルエラーとなる。
List 9のクラス Absは引数の絶対値を返すメソッドを持っている。
List 9: 絶対値を扱うクラス¨ ¥class Abs {
public bool isPositive(int value) {if (value <0 ) {return false;
}else {return true;
}}
public int getAbsoluteValue(int value) {if (value <0 ) {return -value;
}else {return value;
}}
}§ ¦20
メソッドは、あたかも返した値の変数のように使用することができる。¨ ¥Abs obj = new Abs();int n = -10;int n_abs = obj.getAbsoluteValue(n); //返り値を変数に代入
System.out.println(n_abs); // 10が表示される
if (obj.isPositive(n)) { //返り値を if文の真偽値に利用System.out.println("正の数です");
}else{System.out.println("負の数です");
}§ ¦なお、異なる引数の型、数を持つ同じ名前のメソッドを作成することができる。これをオーバーロード
と呼ぶが、詳しくはコンストラクタの項にて説明する。
4.5 カプセル化
オブジェクトの内部仕様に関する部分を隠蔽し、外部にはインタフェースのみ公開することでオブジェ
クトの独立性、再利用性を高める手法をカプセル化 (encapsulation) と呼ぶ。カプセル化の典型例は
getter/setterメソッドである。次のようなクラスを考えよう。
List 10: フィールドが公開されたクラス¨ ¥class Class {public int value;
}§ ¦このクラスは、フィールド valueを持つ。この変数は公開されているため、外部から¨ ¥Class obj = new Class();obj.value = 2;§ ¦などのように直接値を代入することが許される。
ここで、ある理由によってこのフィールドは正の値しか許されないとしよう。フィールドを公開してし
まうと、この条件を保証できない。そこで、次のようにする。
List 11: カプセル化の例¨ ¥class Class {private int value;public void setValue(int v) {if (v<0) {
余談コラム 2 ~ オブジェクト指向設計と責任移譲 ~
プログラム設計においては、「管理するオブジェクト」と「管理されるオブジェクト」という構図がよくあ
らわれる。この時、なるべく「管理する側」が「管理される側」の詳細を知らないほうが望ましい。たと
えばウィンドウオブジェクトは自分にどんなコンポーネントが乗る可能性があるかをあらかじめ知ってお
く必要はない。それぞれのコンポーネントは自分の描画方法や大きさなどを自分で知っているので、ウィ
ンドウは彼らに描画を任せることができる。これによって将来コンポーネントの仕様が変更されたり種類
が増えたりしてもウィンドウクラスを変更する必要が無くなる。このように「なるべく責任を下の方に」
というのがオブジェクト指向プログラミングの気持ちである。管理するオブジェクト (上司)が管理されるオブジェクト (部下)の仕事をなんでも知っている状態は設計上よくない。このあたり、現実社会に通じるものがありそうである。
21
v = 0;}value = v;
}}§ ¦このクラスでは、フィールド int valueは非公開とされ、その変数を設定するメソッド setValue(int)
が公開されている。したがって、このクラスの外部から obj.value の値は直接変更できず、いちいち
obj.setValue(int)経由で値を設定してやらなければならない。もし負の値が代入されようとしても、そ
れを検出して適切な処理をすることができる。
オブジェクトの状態が変更された場合に適切な処理を施す必要がある場合もカプセル化が有効である。
たとえば、ボタンやラベルなどのグラフィックコンポーネントの描画を考える。なんらかの処理によって、
コンポーネントの大きさが変更されたら直ちに再描画されなくてはならない。一般に「○○する際には必
ず△△すること」という事項が多いほど、すなわちプログラマが意識すべき約束ことが多いほどバグを生
む可能性が高い。もし、コンポーネントの幅や高さと言った変数が publicとして公開されていたら、再描画は呼び出し側で保証してやらなければならない。それに対して、幅や高さがカプセル化されていれば、
再描画の保証はコンポーネント側で行えばよい。このようにカプセル化は、「そのオブジェクトを利用す
るプログラマが意識すべきこと」を減らすための手法である。
22
5 クラス
5.1 オブジェクトとインスタンス
既に述べたように、オブジェクト指向言語ではプログラムの構成要素をオブジェクトとして考え、オブ
ジェクト同士が通信することで全体として機能を実現する。Javaはクラスベースのオブジェクト指向言語であり、すべてのオブジェクトはクラスのインスタンスとして生成される。クラスとは、オブジェクトの
仕様設計書や雛形のようなものである。このように、一度クラスとして仕様を決めてからオブジェクトを
生成することで、どのオブジェクトがどんなメンバやメソッドを持っているかがコンパイル時にすべて決
定される21。したがって、Javaは実行前にクラス同士の関係がすべてわかっていることになり、これがプログラムの堅牢性につながっている。クラスの関係のうち、もっとも重要な関係がクラスの親子関係であ
る。Javaでは、あるクラスを親として、その機能を受け継いだクラスを作成することができる。これをクラスの継承 (inheritance)と呼び、オブジェクト指向の肝となる概念である。
なお、Javaではクラスのインスタンスとしてオブジェクトを生成しているが、オブジェクト指向言語において必ずしもオブジェクトがクラスから作成される必要はない。たとえば Selfや JavaScriptなどのプロトタイプベースのオブジェクト指向言語においてはオブジェクトは別のオブジェクトのクローンとして作
成される。
5.2 クラス変数とクラスメソッド
クラスはオブジェクトの雛形であるため、newキーワードによってインスタンスを作らなければフィールドやメソッドにはアクセスできない。しかし static修飾子が指定された変数、メソッドはインスタンスを作らなくてもアクセスができる。それらをそれぞれクラス変数 (class variable)、クラスメソッド (class
method)と呼ぶ22。インスタンス変数は、インスタンスごとに異なった値を持つが、クラス変数は、同
じクラスから作られたインスタンスで共通した値を持つ。
List 12: クラス変数の例¨ ¥class ClassA {static public int class_variable;public int instance_variable;
}
class ClassB{static void main(String arg[]) {ClassA.class_variable = 3; // インスタンスを作らずともアクセスができるClassA obj1 = new ClassA();ClassA obj2 = new ClassA();obj1.class_variable = 10; //インスタンス変数と同様に扱うこともできる。System.out.println(obj2.class_variable); //10が表示される。ClassA.instance_variable = 3; //エラーが出る
}}§ ¦上記の例では、クラスClassAがクラス変数int class_variableと、インスタンス変数int instance_variable
を持つ。クラス変数にアクセスするには、ClassA.class_variableと「クラス名.クラス変数名」とすればよい。クラス変数は、インスタンス変数と同様に扱うこともできるが、全てのインスタンスについて値
を共有するので注意が必要である。
21Rubyは Javaと同じくクラスベースの言語であるが、動的にメソッドやメンバの追加、削除ができるため、実行前にはオブジェクトの詳細はわからない。
22静的変数 (static variable)、静的メソッド (static method) とも呼ぶ。
23
一般に、staticなフィールドはむやみに作るべきではない。グローバル変数と同様にいつ誰に修正されるかわからない上に、別のインスタンスが値を修正したら、他のインスタンスも影響を受けるからである23。
ただし、定数は staticとして宣言したほうが都合が良いことが多い。Javaでは、色は java.awt.Colorクラスが担当しているが、いちいち必要な色をインスタンスを作成して使用する、たとえば¨ ¥
public void paint(Graphics g) {Color c = new Color(0,0,0); //黒色を作成g.setColor(c); //カレントカラーを黒に設定
}§ ¦とするのは面倒である。そこで、Colorクラスには良く使う色を staticかつ finalな定数として宣言してあり、¨ ¥
public void paint(Graphics g) {g.setColor(Color.black); //カレントカラーを黒に設定
}§ ¦のように使うことができる。他には、円周率なども Mathクラスのクラス変数 Math.PI として定義されて
いる。
クラス変数と同様に、static修飾子がついたメソッドはクラスメソッドとなり、インスタンスを作らずとも使うことができる。クラスメソッド内では、インスタンス変数やインスタンスメソッドにアクセスす
ることができない。
List 13: クラスメソッドの例¨ ¥class Class {static int class_variable;int instance_variable;
void instance_method(){class_variable = 0; //アクセスできるinstance_variable = 0; //アクセスできる
}
static void class_method(){class_variable = 0; //アクセスできるinstance_variable = 0; //アクセスできない
}}§ ¦一般にクラスメソッドは、そのクラスに共通な振る舞いを記述するというよりは、そのクラスが意味する
概念に関連する処理を行うのに使われる。たとえば、整数を文字列に変換するためには、Integerクラス
の toString(int)メソッドを使う。¨ ¥int a = 10;String s = Integer.toString(a) + "days";System.out.println(s); // "10days" と表示される}§ ¦他にも、sinや cosといった三角関数も Mathクラスのクラスメソッドとして定義されている。
クラス変数、クラスメソッドは、呼び出す際にクラス名を必要とするだけで実質上は旧来のプログラム
のグローバル変数、グローバル関数と変わらない。グローバル変数、グローバル関数はモジュールの依存
関係を密にしやすく、ひとつの修正が他に波及しやすくなる。したがってクラス変数と同様に、クラスメ
ソッドも濫用を避けるべきである。クラスメソッドは「そのクラスの意味する概念に直結した機能であり、
今後変更の必要がないもの」に限るのが良い。文字列と整数の相互変換や三角関数などはその一例である。
23何度も強調するが、バグの少ないプログラムを組む基本は、何かを修正した際になるべく影響が他に広がらないようにすることである。
24
なお、main関数はプログラム実行時に一番最初に呼ばれる関数であり、staticかつ publicでなくてはならない。たとえば java Testを実行した際には、Testクラスのクラスメソッドである Test.main()が
呼ばれているのである。main関数はクラスメソッドであるから、たとえ同じクラスに定義されていてもインスタンス変数にはアクセスできない。そこで、一度自分自身のインスタンスを作成してアクセスする
必要がある。¨ ¥class Class {int a;
void method() {a = 0; //インスタンスメソッドからはアクセスできる
}
static void main(String arg[]) {a = 0; //アクセスできないClass obj = new Class();obj.a = 0; //アクセスできる
}}§ ¦クラスがmain関数内で自分のインスタンスを作る手法は、Javaアプレットをスタンドアローンプログラムとしても実行できるようにするのによく使われる。
List 14: アプレットを単独でも実行可能にする¨ ¥import javax.swing.*;
class MyApplet extends JApplet{static final int WIDTH = 300;static final int HEIGHT = 300;
public void paint(Graphics g) {// ここにアプレットとしての処理を書く}
public static void main(String argv[]){JFrame f = new JFrame("MyApplet");MyApplet a = new MyApplet();a.init();f.getContentPane().add(a);f.setSize(WIDTH, HEIGHT);f.show();
}}§ ¦上記のソースから作られたバイトコードは、アプレットとしても実行可能であり、コマンドラインから
java MyAppletとしても実行可能である。アプレットとして実行する場合は描画領域はブラウザが用意す
るが、単独で実行する際は描画領域を自分で用意する必要がある。コマンドラインから実行した場合は、
まず自分のインスタンスを作成し、まず描画先となるウィンドウを JFrameのインスタンスとして作成してから自分のインスタンスを JFrameのインスタンスに addすることで描画先のウィンドウの表示を行っている。
5.3 コンストラクタ
インスタンスを作る際、new クラス名と指定したが、実際には括弧がついて new クラス名 ()と関数呼
び出しの形になっていた。これはコンストラクタ (constructor)と呼ばれる特殊なメソッドが呼ばれてい
る。コンストラクタはオブジェクトが扱うデータの初期化などを行うメソッドである。
25
コンストラクタを作るには、クラス名と同じ名前のメソッドを宣言する。ただし、返り値やアクセス修
飾子を指定してはならない。
List 15: コンストラクタの例¨ ¥class Circle{private int radius;Circle(int r) { //コンストラクタradius = r;
}static void main(String arg[]) {Circle c = new Circle(10); //半径 10の円を生成
}}§ ¦なお、コンストラクタを宣言しなかった場合は、引数無しで何もしないコンストラクタが自動的に用意さ
れる。¨ ¥class Class{static void main(String arg[]) {Class obj = Class(); // 何もしないコンストラクタが用意されている
}}§ ¦しかし、引数があるコンストラクタを宣言した場合に引数無しのコンストラクタを呼ぼうとするとエラー
となる。¨ ¥class Circle{private int radius;Circle(int r) { //コンストラクタradius = r;
}static void main(String arg[]) {Circle c = new Circle(); //エラー
}}§ ¦コンストラクタは、オブジェクトの初期化をするための特別なメソッドである。しかし、初期化処理は
コンストラクタを使わなくても実装することができる。先ほどの Circleクラスの例なら¨ ¥class Circle{private int radius;public void setRadius(int r) {radius = r;
}
static void main(String arg[]) {Circle c = new Circle();c.setRadius(10);
}}§ ¦とすれば List 15と同じ機能を実現できる。それではなぜコンストラクタを使うかと言えば、初期化忘れを防ぐためである。クラスのインスタンスを作るためには、必ずコンストラクタを呼ばなければならない。
そこで初期化処理に必要な情報を要求し、かつ初期化をすることでプログラマが初期化されていない不正
なオブジェクトを使うことを防ぐのである。バグのないプログラムを組むコツは、「○○する際には必ず
××すること」とか「○○は、△△までに必ず××されていなければならない」といった暗黙の約束事を
なるべく減らすことである。
引数の数やタイプが異なった複数のコンストラクタを宣言することもできる。¨ ¥class Ellipse{
26
private int left;private int top;private int width;private int height;
Ellipse(int l,int t, int w, int h) {left = l;top = t;width = w;height = h;
}
Ellipse(int x, int y, int r) {left = x - r;top = y - r;width = r * 2;height = r*2;
}
static void main(String arg[]) {Ellipse e = Ellipse(0,0,10,10); // 中心 (5,5) 半径 5の円が作られるEllipse c = Ellipse(5,5,5); // 中心 (5,5) 半径 5の円が作られる
}}§ ¦コンストラクタに限らず、異なる引数の型、数をもった同じ名前のメソッドを定義することができる。メ
ソッドが呼ばれる際、引数の型の順番、数が一致するメソッドが実行される。これをメソッドのオーバー
ロード (overload)と呼ぶ24。メソッドの名前と引数の型、順番、数をまとめて、そのメソッドのシグネ
チャ (signature)と呼ぶ。同じ名前でも、引数の型などが異なれば別のメソッドと認識される。同じシグ
ネチャのメソッドを複数宣言することはできない。
24日本語では多重定義とも呼ぶが、オーバーロードの方が一般的だと思われる。
27
6 継承と多態性
6.1 動的結合
オブジェクト指向言語の定義はさまざまだが、一般にはカプセル化、継承、多態性の三要素を持つ言語
のことをオブジェクト指向言語と呼ぶ25。このうち、カプセル化は既に解説した。以下では継承と多態性
について説明する。
オブジェクト指向プログラミングにおいては、オブジェクト同士がメッセージをやりとりすることで全
体として機能を発現する。オブジェクトがメッセージを受け取ったとき、どのメソッドを実行するか決め
ることを結合 (binding)と言う。結合がコンパイル時に (実行前に)決定されることを静的結合 (static
binding)、実行時まで決定されないことを動的結合 (dynamic binding)と言う (図 2を参照)。Javaは静的な型を持つ言語であるが、後に述べる継承とオーバーライドによって動的結合をサポートしている。
���������
� ����� �
methodA
methodB
methodC
����������
���
図 2: メッセージと結合。オブジェクトがメッセージを受け取ったとき、対応するメソッドを実行することを結合という。動的結合では、オブジェクトがメッセージを受けた時にどのメソッドを実行するかを動的に決める。したがって、コンパイル時にはどのメソッドを実行するかは決定されない。
6.2 継承とは
クラスを定義する際、別のクラスから機能を受け継ぐことができる。これを継承 (Inheritance)、もしく
は派生と呼び、継承元のクラスをスーパークラス (super class)、継承先のクラスをサブクラス (subclass)
という。スーパークラスを親クラス、サブクラスを子クラスとも言うので、継承関係を親子関係というこ
とも多い。
クラスの継承を行うには、クラス定義の際に extends キーワードを使って次のように宣言する。¨ ¥class クラス名 extends 親クラス名 {//クラスの定義
}§ ¦サブクラスは、スーパークラスの privateではないメンバ、メソッドをすべて受け継ぐ。以下は継承の例である。
25Fortran は古い言語であるため、たびたび大幅な拡張がなされた。特に F90 に大幅な近代化が行われ、モジュールの導入によりカプセル化が可能となったが、継承と動的結合は導入されなかった。現時点で最新の仕様である Fortran 2003 には継承と多態性が導入され、本格的なオブジェクト指向化がなされたようだ。
28
List 16: 継承の例¨ ¥class SuperClass {protected int value;protected void printValue() {System.out.println(value);
}}
class SubClass extends Superclass {void method () {System.out.print(value); //親クラスのフィールドを使うことができるprintValue(); //親クラスのメソッドも使うことができる
}}§ ¦複数の親から機能を継承できることを多重継承、1つの親しか持つことができないことを単一継承と呼
ぶ。C++は多重継承、Javaは単一継承である。多重継承は強力な表現手段を提供するが、コードが混乱し、バグが潜みやすいなどの問題がある。そこで Javaは単一継承を選択するかわりに interfaceという方法を使って多重継承の機能の一部を実現している。
6.3 オーバーライド
スーパークラスと同じ名前のフィールドをサブクラスで定義した場合、スーパークラスのメンバは隠蔽
される。このとき、スーパークラスの値にアクセスするためには superキーワードを用いる。また、自分
の変数であることを明示的に指示する場合は thisキーワードを用いる。以下に例を挙げる。
List 17: メンバの隠蔽¨ ¥class SuperClass {int value = 1;
}
class SubClass {int value = 2; //親クラスのメンバを隠蔽
void method() {System.out.println(value); // 2が表示されるSystem.out.println(super.value);// 1が表示されるSystem.out.println(this.value);// 2が表示される
}}§ ¦フィールドと同様に、スーパークラスと同じ名前のメソッドをサブクラスで再定義することができる。
これをオーバーライド (override)と言う26。
List 18: メソッドの上書き¨ ¥class SuperClass {public void method() {System.out.println("SuperClass");
}}class SubClass extends SuperClass{public void method() {super.method();System.out.println("SubClass");
}public static void main(String args[]){SubClass obj = new SubClass();
26オーバーライドのことを再定義とも言う
29
obj.method();}
}§ ¦上記の実行結果は、
SuperClass
SubClass
となる。
6.4 カレントインスタンス
クラスは、(staticなクラスを除いて)実行時にはインスタンス化されているはずである。thisキーワードは、インスタンス化されたオブジェクト、すなわち自分自身を表す。これをカレントインスタンス (current
instance)と言う。オブジェクトが他のオブジェクトにメッセージを送る際、「レシーバ.メソッド名」とレシーバは明示的に指定されるが、センダーは指定されない。thisキーワードを用いれば、自分自身を
引数としてメソッドを呼び出すことができる。また、直接のスーパークラスのインスタンスを superキー
ワードで指定することができる。
あるクラスをインスタンス化した場合、そのスーパークラスも同時にインスタンス化される。したがっ
て、フィールドを同じ名前で上書きしていても、スーパークラスとサブクラスのそれぞれの値が別々に保
持されていることになる。
SuperClass
value: int
SubClass
value: int
�������� � �
�������
this.value
super.value
this.value
super.value
12
10
3
6
図 3: カレントインスタンスとスーパークラス。クラスがオブジェクトとしてインスタンス化される際、それぞれのスーパークラスのインスタンスも作成されている。したがって、それぞれのオブジェクトにおいて this
と superキーワードの参照する場所 (メモリ)は異なっている。
Javaにおいては、クラス定義の中や、メソッドの内部でもクラスを定義することができる。これを内部クラス (Inner Class)と呼ぶ。内部クラスにおいては、カレントインスタンスが複数存在するため、thisキーワードの使用には注意が必要となるが、本稿では詳細に立ち入らない。内部クラスは、特に匿名クラ
スとしてイベント処理によく用いられる。
30
6.5 多態性
Javaは静的型付け言語であり、変数は宣言された型の値しかもつことが許されない。したがって、あるクラス ClassA型の変数 objAを定義したら、その変数 objAは ClassAのインスタンスの値しかとることができない。しかし、宣言されたクラスのサブクラスの値を持つことはできる。¨ ¥SuperClass obj;obj = new SuperClass(); //SuperClass型の変数は代入可能obj = new SubClass(); //SuperClassのサブクラスのインスタンスも代入可能§ ¦ただし、サブクラスの型として定義された変数に親クラスのインスタンスを代入することはできない。
キャストすることもできない。¨ ¥SubClass obj;obj = new SuperClass(); //コンパイルエラーobj = new (SubClass)SuperClass(); //実行時エラー§ ¦また、スーパークラスの型を持つ変数にサブクラスのインスタンスを代入した場合、スーパークラスが持
つメソッドやフィールドにしかアクセスすることはできない。
スーパークラスのメソッド method()を、サブクラスで上書きしているとする。この時、スーパークラ
スの型を持つ変数にサブクラスのインスタンスを代入して、そのインスタンスのメソッドを呼び出すと、
サブクラスのメソッドが実行される。これを多態性 (Polymorphism)と呼ぶ27。
List 19: 多態の例¨ ¥class SuperClass{void method(){System.out.println("I’m SuperClass.");
}}class SubClass extends SuperClass{void method(){System.out.println("I’m SubClass.");
}}
class OtherClass{static void main(String args[]) {SuperClass obj = new SubClass();obj.method(); // "I’m SubClass."と表示される。
}}§ ¦
余談コラム 3 ~ 継承と多態性 ~
多態性は、形式的にはあるクラスのメソッドを呼び出したつもりが、実際にはそのサブクラスのメソッド
が実行されることと説明した。しかし一般的には、あるオブジェクトにメッセージを送った際の挙動がメッ
セージの送り側ではなく受け手 (レシーバ)によって定まることを多態性と呼ぶ。クラスの継承はこの多態性を用いるために使うといっても過言ではない。たまに「コードの再利用をするのに継承を用いる」とい
う表現を見かけるがコードの再利用のための継承は悪手であることが多い。継承で対応すべきか、別の方
法を用いるべきかは、二つのクラスを「is-a」の関係にすべきか「has-a」の関係にすべきかによって判断する。詳しくはクラス設計の項で触れるであろう。
27なお、C++言語で同様なことをやるためには、仮想関数を用いなければならない。仮想関数の宣言には virtual キーワードが必要となる。Java のすべてのメソッドは仮想関数であると判断されるため、そのような宣言は必要ない。
31
6.6 多態の使い方
オブジェクト指向設計において、なぜ多態が必要となるかを考えよう。ウィンドウクラスと、そこに表示
されるコンポーネントクラスを考える。ウィンドウが表示されたとき、ウィンドウクラスはコンポーネン
トを全て描画する必要がある。この時、もし多態がなければ、ウィンドウクラスは自分が表示すべきコン
ポーネントを全て把握していなければならない。言い換えれば、ウィンドウクラスはコンポーネントクラ
スをいかに描画すべきかを知っていなくてはならない。表示されるべきコンポーネントが増えると、ウィ
ンドウクラスのコードも修正される必要がある。
しかしここで多態を使って、ウィンドウクラスが表示すべきコンポーネントは、全て Componentクラス
から派生しているとする。ここで、Componentクラスは paintメソッドを持ち、派生クラスは全て paint
メソッドを適切に上書きしているとしよう。すると、ウィンドウクラスは描画が必要な際、Componentク
ラスのインスタンスの paintメソッドを呼ぶ。すると実際には派生クラスのメソッドが呼ばれ、適切に描
画が行われることになる。ここで、ウィンドウクラスから見れば、自分が管理すべきクラスは Component
クラスのみであることに注意したい。将来コンポーネントの種類が増えても、適切な設計が行われていれ
ばウィンドウクラスのコードは修正する必要がなくなる。
プログラムにおいては、あるクラスが多くのクラスのインスタンスを管理する、という場合がよく出て
くる。先ほどのウィンドウクラスとコンポーネントの描画や、アンドゥ管理クラスと個々のアンドゥ実行
クラスなどが典型例である。このようなコードを多態を用いて書くのがオブジェクト指向プログラミング
の定石である。多態を用いることで、全体の設計がわかりやすくなる、個々の瑣末な変更が全体に波及し
づらくなる、などのメリットが生じる。
32
7 構造化例外処理
7.1 エラー処理について
プログラムの実行中、存在しないファイルを開こうとしたり、データ内容に矛盾があるなど、様々なエ
ラーが生じる可能性がある。小さいプログラムならばエラーのたびにメッセージを表示して実行を中止し
てもかまわないが、大きなシステムではエラー内容をユーザーに伝え、かつ処理を続けるためにプログラ
ムを正常な状態に復帰する必要がある。これがエラー処理 (Error Handling)である。
実行中にエラーが起きたときにどうすべきか考えよう。一般的にプログラムは階層化されており、ユー
ザーから遠いところで問題が生じた場合はなんらかの手段でそれをユーザーに伝えなくてはならない。そ
の最も簡単な手段は、メソッドの引数でエラーが起きたことを伝えることである。すなわち、全てのメソッ
ドの返り値を「true (成功)」と「false (失敗)」にしておき、falseが帰ってきたらメソッドの実行が失敗したと判断して、メソッドの呼び出し側で対応するという方法である。返り値のある関数では、返り値に特
別の値を用意することでエラーを表現することもできる。たとえば、C言語でファイルを開く関数 fopenは、ファイルオープンに成功するとファイルポインタを、失敗すると NULLを返す仕様になっているため、その返り値をチェックすることでエラーを処理することができる。
List 20: エラー処理の例 (C言語の場合)¨ ¥FILE fp =fopen(filename, "r");if (fp == NULL) {//ファイルオープンに失敗printf("Cannot open file %s \n", filename);exit(EXIT_FAILURE);
}else{// ファイルオープンに成功
}§ ¦ただし、この方法ではメソッドが失敗したことはわかってもどのように失敗したかがわからない。そこで、
たとえばWindows APIでは全てのエラーにエラー番号を用意し、LastError (最後におきたエラー)というグローバル変数に起きた問題に対応するエラー番号を代入する。メソッドの呼び出し側は、メソッドが
失敗したときにはこのグローバル変数を参照することで何が起きたかを知ることができる。Windows APIでファイルを開く関数 CreateFileは、ファイルオープンに失敗すると INVALID HANDLE VALUEを返す。エラー内容は GetLastErrorで取得できる。
List 21: エラー処理の例 (Windows APIの場合)¨ ¥//ファイルを書き込みモードで新しく作成するHANDLE hFile = CreateFile(lpFilename, GENERIC_WRITE, 0, NULL, CREATE_NEW,
FILE_ATTRIBUTE_NORMAL , NULL);if (hFile == INVALID_HANDLE_VALUE) {//エラー処理
if (GetLastError() == ERROR_ALREADY_EXISTS) {//ファイルが既に存在する
}}else{//ファイルオープンに成功}
}§ ¦しかし、この手法は多くの問題をかかえていることがすぐわかるであろう。まず、起き得るエラーがあ
らかじめ全てわかっていないといけない。もしエラー内容が増えたらそのエラーに新たに番号を振る必要
がある。また、エラー番号がグローバル変数に格納されていることも問題である。たとえばメソッドが失
敗したことを検出してからエラー番号を参照する間に、別のスレッドがそのエラー番号を変更している可
能性もある28。また、この方式ではエラーに関する情報が強く制限されてしまう。プログラマに通知され28Windows でプログラムを組んでみると分かるが、これはかなり頻繁に起きる。エラーというのはどこかで問題が起きると連鎖的に起きるものである。エラーが起きるたびにグローバル変数が書き換えられてしまうため、同じコードなのにタイミングによってエラー番号が変わるなどということが起きる。このような場合のデバッグは容易ではない。
33
るのは「どんな種類のエラーが生じたか」のみであり、そのエラーがどのように起きたかの詳細な情報を
得る手段が無い。そしてなにより、エラー処理と通常の処理が混在することによってソースコードが読み
づらくなっている。
以上のような問題を解決するために Javaでは構造化例外処理 (Structured Exception Handling)
と呼ばれる、エラー処理を構造化する手段を提供している。例外 (Exception)とは、正常でない処理、
予想外のできごとという意味で、例外は通常のプログラム実行とは別の枠組みで処理される。
7.2 例外処理の仕組み
プログラムの実行中にエラーがおきると、Javaは例外オブジェクトを作成する。例外オブジェクトは、すべて Exceptionクラスのサブクラスのインスタンスとして作成される。例外オブジェクトを作成して例外処理を依頼することを「例外を投げる」という。例外を発生する可能性のあるメソッドを使用する際は、
例外が発生した場合にどうするかをあらかじめ指定しておく必要がある。発生した例外は、その場で処理
(catch)するか、さらに上で投げる (throws)かのどちらかを指定する。例外を上に投げるとは、例外が発生したメソッドの呼び出し元メソッドの、さらに呼び出し元に処理を依頼することである。呼び出しの連
鎖が続く限りいくらでも処理の上流に例外を投げることができるが、どこかで必ず処理されなくてはなら
ない。例外を処理するブロック (例外処理が記述された部分)を「例外ハンドラ (Exception Handler)」と呼ぶ。
���� GUI� � �
��������
� �� � � � � �� � � � �
��������
����������������
� � � � � ���� ���� ���� ���� �
� �
図 4: メソッド呼び出しと例外処理の流れ。メソッド呼び出しのネストの深いところでおきた例外を、ユーザーに近いところで処理することができる。
オブジェクト指向設計では、ユーザーに近い上流側クラスから実際の処理を行う下流側のクラスまでク
ラスが階層化されていることが多い。最下層において、どう処理するかユーザーの判断が必要であるよう
な例外が発生したとしよう。このとき、プログラムはユーザーにダイアログを出すなどして判断を促す必
要があるが、そのためにはユーザーに近いところまでエラーの情報を伝達する必要がある29。構造化例外
処理は、このような伝達手段を実現する。
例外には大きく分けてとチェック例外 (checked exception)30とランタイム例外 (run-time excep-
tion)31の二種類がある。チェック例外オブジェクトは Exceptionクラスのサブクラスのインスタンス、ランタイム例外オブジェクトは RuntimeExceptionクラスのサブクラスのインスタンスである。なお、メモリ不足などの重大なエラーが起きた場合はErrorクラスのサブクラスのインスタンスが生成される。Errorクラスは回復不可能なエラーを表現しており、プログラマはこれに対する処理をおこなってはならない。
29これは、ダイアログの表示に親ウィンドウのウィンドウハンドルが必要になるためである。エラーダイアログが出ている間、ユーザに他の操作をされては困る場合が多い。そこで、自分が表示されている間、どのウィンドウを操作禁止にするかをダイアログに教えるためにウィンドウの情報が必要となるのである。詳しくは「モード付きダイアログ (Modal Dialog)」について調べてみること。
30チェック済例外、検査例外とも呼ばれる。31実行時例外、非チェック例外 (unchecked exception) とも呼ばれる。
34
7.3 チェック例外
ランタイム例外以外の例外はチェック例外と呼ばれ、必ず例外処理を記述しなくてはならない。チェッ
ク例外を起こす可能性のあるメソッドを呼び出す際には、例外を投げるか処理するかを選択しなければな
らない。Javaはコンパイル時に適正に例外処理がなされているかをチェックを行い、処理されない可能性のある例外がある場合はコンパイルエラーを出す。
例外をさらに上に投げる場合は、投げる可能性のある例外の種類をメソッド宣言に throws節によって指定する。throws節にはカンマで区切ることで投げる例外をいくつでも指定することができる。以下に例を示す。
List 22: 例外をさらに上に投げる¨ ¥void throwMethod() throws IOException {BufferedWriter writer = new BufferedWriter(new FileWriter(FILENAME));writer.write("Hello World");writer.writeln();writer.close();
}§ ¦throwMethodには入出力例外 IOExceptionを投げる可能性があるメソッド BufferedWriter::writeの呼
び出しが含まれる。もし IOExceptionが発生した場合は、throwMethodを呼び出したメソッドにその例外をそのまま投げ、処理を依頼する。したがって、throwMethodを呼び出すメソッドはすべて IOExceptionにどう対応するか記述しなければならない。
例外を上に投げず、自分で処理するためには try–catchブロックを用いる。以下に例を示す。
List 23: 例外を処理する¨ ¥void catchMethod() {try{BufferedWriter writer = new BufferedWriter(new FileWriter(FILENAME));writer.write("Hello World");writer.writeln();writer.close();
}catch (IOException e) {//例外処理
}}§ ¦まず、try{· · ·}ブロックで、例外が発生する可能性がある箇所を囲む。二つ以上の例外発生箇所を囲んでもかまわない。その後、catchブロックによって、処理すべき例外と、その処理を記述する。二つ以上の
例外が発生する可能性がある場合は、例外オブジェクトのインスタンスにマッチする例外クラス処理が見
つかるまで順番にチェックされ、初めてマッチしたところで処理される。
チェック例外は Exceptionクラスのサブクラスとして定義されているが、後述するランタイム例外はRuntimeExceptionクラスのサブクラスとして定義されている。RuntimeExceptionはExceptionクラスのサブクラスとして定義されているため、すべての例外オブジェクトは Exceptionクラスをスーパークラスに持つ32。したがって catch(Exception e)などとすれば、ランタイム例外を含むすべての例外をキャッ
チできるが、このようなプログラムは組むべきではない。
tryブロックで例外が発生してもしなくても、必ず行って欲しい処理がある場合は finallyブロックに
記述する。たとえば tryブロックでファイルを開いて何かを出力する場合、例外がおきても必ずファイルを閉じなくてはならない。もしくはソケットを開いて通信する場合、通信が正常に終了した場合でもエラー
が生じた場合でもソケットは閉じなければならない。そこで、finallyブロックにファイルを閉じる処理
32さらに、Exception クラスは Throwable という interface をインプリメントしている。interface については GUI プログラミングの節で解説する。
35
を記述しておけば、必ずファイルが閉じられることが保証される33。try–catch–finallyブロックについて以下にまとめておく。
List 24: try–catch–finallyブロック¨ ¥try{//例外が発生する可能性のある処理
}catch (例外クラスA インスタンス名){//例外クラス Aに対応する処理
}catch (例外クラスB インスタンス名){//例外クラス Bに対応する処理
}finally{//例外発生有無にかかわらず実行される処理
}§ ¦try–catch(–finally)ブロックを用いることにより、通常の処理とエラー処理を分けて書くことができる。
メソッドが成功したか失敗したかを if文によってチェックする方式と比較すれば、エラー処理が構造化されたことが実感できるであろう。
7.4 ランタイム例外
ランタイム例外とはランタイムシステムによって検出される例外で、ゼロ除算や配列の範囲外アクセス、
ヌルポインタアクセスなどがある。ランタイム例外はどこでも起きる可能性があるため、処理しなくても
良い。むしろ、多くの場合において処理しないほうが望ましいとされる。ランタイム例外が発生し、かつ
その例外が処理されなかった場合、Javaはその例外が発生した状況を標準出力に出力する。以下にランタイム例外を起こすソースコード例を挙げる。
List 25: ランタイム例外発生例¨ ¥1 class RESample {2
3 void myMethod1() {4 myMethod2();5 }6 void myMethod2() {7 myMethod3();8 }9 void myMethod3() {
10 int[] a = new int[10];11 a[10] = 10; //ここで配列外アクセス例外がおきる12 }13 public static void main(String args[]){14 RESample obj = new RESample();15 obj.myMethod1();16 }17 }§ ¦このソースの実行結果は以下のとおり。
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
at RESample.myMethod3(RESample.java:11)
at RESample.myMethod2(RESample.java:7)
at RESample.myMethod1(RESample.java:4)
at RESample.main(RESample.java:15)
33なお、catchブロックや finallyブロックの中で例外が発生するような処理を行った場合は、さらにその中で tryブロックなどで処理する必要があるが一般にこのようなプログラムは組むべきではない。しかしファイルをクローズするメソッド closeは IOExceptionを投げる可能性があり、悩ましい。
36
この出力には例外が発生したメソッド名とその場所、そのメソッド呼び出したメソッド名とその場所、さら
にそのメソッドを呼び出した・・・とメソッド呼び出しの順番 (スタック・トレースと呼ばれる)がすべて含まれる。今回の例では、mainスレッドにおいて配列外アクセス (ArrayIndexOutOfBoundsException)がおきたが、その場所は RESampleクラスのmyMethod3メソッドで、それは RESample.javaの 11行目であり、myMethod3を呼び出したのは RESampleクラスのmyMethod2メソッドで、それは RESample.javaの 7行目であり・・・と以下一番最初のメソッド (すなわちmain)までの情報が表示されている。プログラマはこの情報からどこでどのようにエラーが起きたかを知ることができる。
ランタイム例外とは、デバッグの済んだプログラムでは発生しないはずの例外である。したがって、ラ
ンタイム例外がおきると Javaの実行環境はエラーを出して終了する。しかし、後に述べるGUIプログラム、より正確にいえばイベントドリブンによるプログラムにおいては、ランタイム例外が生じても処理が
続行される。そのため、Javaプログラマはランタイム例外を無視しがちであるが、ランタイム例外がおきるということは、何か問題が生じているということを忘れてはならない。ランタイム例外を放置してお
くと、必ず後で痛い目に合う。ボタンを押すたびに何かコンソールにエラーが大量に表示されているのに
「とりあえず動いているからいいや」などと思わないようにして欲しい。
7.5 独自例外の定義
Exceptionクラスから派生させることで、例外を自分で定義することもできる。定義の方法は通常のクラスと同様である。
List 26: 例外の定義¨ ¥class MyException extends Exception{MyException () {//デフォルトコンストラクタ}MyException (String msg) { //メッセージ付きコンストラクタsuper(msg);
}}§ ¦Exceptionクラスは、コンストラクタとして文字列を受け取るとそれを getMessageメソッドの返り値とする。そこで、ここで定義したMyExceptionは、親クラス Exceptionのコンストラクタを明示的に呼び出すことでその機能を使っている。
定義した例外は、throw文で投げることができる。
List 27: 独自例外を投げるメソッド¨ ¥void throwMethod() throws MyException {if(hoge){//例外を投げたい状況が発生throw new MyException();
}}§ ¦なお、あらかじめ throws節でその例外を投げる可能性があることを宣言しておく必要がある。これにより、メソッド throwMethodを呼び出すメソッドは、例外MyExceptionをさらに上に投げるか処理するかのどちらかをしなくてはならない。
IOExceptionなど、Javaにはさまざまな例外クラスが定義されているが、独自の例外を既存のクラスのサブクラスとして定義するのは避けたほうが良い。たとえば IOException例外を処理するメソッドに、IOExceptionのサブクラスの例外オブジェクトを投げると、IOExceptionとして処理されてしまい、混乱のもととなる。
また、RuntimeExceptionのサブクラスとして例外クラスを定義すると、その例外は catchも throwもしなくてよい。しかし、RuntimeExceptionはあくまで Javaの実行環境 (ランタイム)で発生する、回復不可能な例外であり、それをプログラマが独自に定義すべきではない。
37
8 インタフェースとイベント処理
8.1 GUIプログラミング
通常パソコンで触れるアプリケーションは、ウィンドウの中にボタンなどが配置され、キーボードに加え
てマウスでも操作が可能となっている。このようなインタフェースをGraphical User Interface, GUI
と言う。対義語はCommand User Interface, CUIである。
GUIプログラミングを行うためには、ウィンドウやボタンなどの描画、マウス入力などのイベント処理などさまざまなコードを書く必要があるが、Javaはそれらの作成を容易にするためのパッケージを提供している。GUIを構成するための部品をツールキット (Toolkit)、もしくはコンポーネント (Component)
と呼ぶ。Javaは当初GUI部品として Abstract Windowing Toolkit (AWT)を提供していたが、後に改良された Swingパッケージが提供された。現在、AWTも Swingも使えるが、以下では Swingについてのみ解説する。なお、慣習として Swingコンポーネントのクラス名は大文字の Jからはじまる。
GUI アプリケーションは、ボタンやチェックボックスなどのコンポーネント (部品) と、それらを収納するウィンドウやパネルなどのコンテナ (容器) から構成される。Swing のコンポーネントはすべてjavax.swing.JComponentのサブクラスであるが、コンテナである JFrameや JPanelは java.awt.Containerのサブクラスである。コンテナの中でも、最上位のものはトップレベルウィンドウと呼ばれる。
例を挙げよう。
List 28: ウィンドウを表示するだけのサンプル¨ ¥import java.awt.*;import javax.swing.*;
public class FrameTest extends JFrame {public static void main(String args[]){FrameTest f = new FrameTest();f.setSize(100,100);f.setVisible(true); // f.show()は非推奨
}}§ ¦このソースをコンパイル、実行すれば、まず JFrameクラスのサブクラスである FrameTestのインスタンスが作成され、その幅と高さを 100ピクセルに設定し、f.setVisibleによってウィンドウが表示され
る34。ここで、f.setVisibleの実行後、すなわち main関数の実行が終了してもプログラムが終了しない
ことに注意したい。JFrameクラスのインスタンスは、一度表示されるとイベント待ち状態になる。また、ウィンドウサイズを変更したり、最小、最大化の状態を変化させたり、ドラッグして位置を変更したりす
ることもできる。これらは当たり前のように思うかもしれないが、GUIプログラムを一から書いた場合にはすべてプログラマが責任を持たなければならないことである。このプログラムは右上の×印をクリック
してウィンドウを消しても終了しないため、コマンドラインで「Ctrl+C」を押すことで終了する。次に、JFrameに何かコンポーネントを配置してみよう。JFrameはコンポーネントを置くための場所を
JRootPaneのインスタンスとして保持している。したがって、JFrameにコンポーネントを配置するためには、まず JRootPaneクラスのインスタンスを取得し、そこに追加するという処理が必要になる。先ほどのウィンドウにボタンをひとつ追加する例を挙げよう。
List 29: ボタンを配置するサンプル¨ ¥import java.awt.*;import javax.swing.*;
public class ButtonTest extends JFrame {public static void main(String args[]){
34古いテキストなどでは、ウィンドウの表示を f.show() としているが、これは後に非推奨 API となったので、コンパイルすると「FrameTest.java は推奨されない API を使用またはオーバーライドしています。」という警告が出る。
38
ButtonTest f = new ButtonTest();f.getContentPane().add(new JButton("OK"));f.pack();f.setVisible(true);
}}§ ¦
JFrameクラスのルートペインを getContentPane()により取得し、そのインスタンスの addメソッドに JButtonクラスのインスタンスを渡すことでボタンをフレームに追加している。f.pack()とは、配置
されたコンポーネントを表示しつつ、もっともウィンドウサイズが小さくなるように整理するメソッドで
ある。
8.2 イベントドリブン型プログラミング
GUIプログラミングにおいては、ユーザーからの入力に柔軟に対応するため、イベントドリブン (Event-
driven)型プログラミングという手法を用いる。通常の処理は上から下へ順番に流れていく (フロー型)のに対して、イベントドリブンでは実行するとイベント待ちの状態になり、その際に起きたイベントに対
して応答を返すことでプログラムが機能する。イベントは、あるキーが押された、離された、マウスがク
リックされたなどのユーザーからの入力が主だが、一定時間たった (タイマーイベント)、OSがアプリケーションの終了を問い合わせてきた、などのシステムからの入力も含む。イベントドリブン型プログラムと
は「このようなイベントが起きたらこんな反応をする」という処理を記述していくことである。イベント
を発生させる可能性のあるオブジェクト (ボタンやテキストエリアなど)をイベントソース (Source)と呼
ぶ。イベントを受け取って処理するオブジェクトをイベントリスナー (Listener)、イベントリスナーの
中で、イベント処理をするメソッドをイベントハンドラ (Event Handler)という。適切にイベントが処
理されるためには、イベントソースがあらかじめイベントが起きた際にどのイベントリスナーのイベント
ハンドラを実行すればよいかを教えておかなければならない35。なお、イベント待ちの状態から、入って
きたイベントを実際に処理することをイベントディスパッチ (Event Dispatch)、その処理を担当するス
レッドをイベントディスパッチスレッド (Event Dispatch Thread, EDT)と呼ぶ。これらは、マルチ
スレッドプログラミングをしない場合はあまり意識しなくても良い36。
JavaではイベントリスナーをListenerインタフェースという手法で実現する。以下ではまずインタフェースについて解説する。
余談コラム 4 ~ GUIとCUI ~
アイコンやメニューなどをマウスでクリックして使うGUIに比べて、すべてキーボードから操作するCUIは敷居が高いことが多い。しかし、慣れてしまえばCUIの方が圧倒的に生産性が高くなる。たとえばCUIエディタである viにはコマンドモードと編集モードの区別があるためにユーザが戸惑うことが多い。しかし、慣れてしまえば Ctrlや Altといった修飾キーを多用する emacsに比べて viのキーバインドが良く考えられた使い易いものであることに気がつくはずだ。また、CUIに慣れると全ての作業がキーボードだけで済むためにマウスに手を移動させる必要がなくなり、机に肘がついたまま作業が行うことができる。こ
れは肩凝り防止に非常に有効である。タッチタイプができれば視線移動の回数が減り、疲労軽減効果はさ
らに高くなる。
35C 言語などではこのような仕組みをコールバック関数と呼ばれる手法で実現する。36逆に言えば、マルチスレッドプログラミングをする際にはイベントディスパッチについて意識する必要があるということである。特に GUI プログラムはマルチスレッドで書かれることが多いので、スレッドセーフなプログラミングをしなければならない。
39
���� � � � � � �
��(����� � � � )
� �
� � � � �
� � � � � � �
� � � � �
� � � � �
� � � �
�� �
����������������� �� �� �� �� � � � � � � �
図 5: イベント処理の仕組み。イベントが発生すると、イベントソースからあらかじめ登録されたイベントリスナーのイベントハンドラ (メソッド)が呼び出される。イベントハンドラにはイベント発生時の処理を記述しておく。
8.3 インタフェース
イベントソースはイベントリスナーのオブジェクトをフィールドとして保持しておき、イベントが発生
した際にそのオブジェクトのイベントハンドラを呼び出すことでイベントを処理する。したがって、イベ
ントソースは発生したイベントを伝える相手であるイベントリスナーをあらかじめ知っている必要がある。
しかし、どんなクラスのインスタンスでもイベントリスナーとなりうるが、それらすべてのクラスがイベ
ントハンドラをメソッドとして持っていなければならない。そのためには、リスナーとなりうるクラスは
イベントハンドラをメソッドに持つようなクラスのサブクラスでなくてはならない。そこで、Listenerクラスというイベントハンドラメソッドを持つ抽象クラスを用意し、リスナークラスは Listenerクラスから派生してイベントハンドラをオーバーライドすればよさそうに思える。
ところが、一般にリスナークラスは別のクラスのサブクラスであることが多く、Javaは多重継承を禁止しているため、同時に Listenerクラスのサブクラスとなることはできない。そこで Javaはインタフェース (Interface)という特別なクラスを用意することで異なる継承系統に属すクラスに共通の振る舞いを持
たせている
インタフェースは、通常のクラスと同様にメソッドとフィールドで宣言されている。¨ ¥interface Interface {int VALUE = 0;void interfaceMethod();
}§ ¦ただし、インタフェース内メソッドには実体はかかない。また、必ず publicかつ abstract37とみなされるため、privateなどのアクセス修飾子などをつけてもいけない。インタフェースのフィールドは publicかつ staticかつ finalとみなされる。すなわちグローバル定数となる。インタフェースで定義されたメソッドを組み込むことを実装する (implement)と言う。インタフェー
スを実装するには、クラス宣言の直後に implements宣言をする。¨ ¥class ClassName extends SuperClassName implements Interface {
public void interfaceMethod(){//Interfaceのメソッドをオーバーライド
}}§ ¦インターフェースを実装するクラスは、そのインタフェースが持っているメソッドをすべてオーバーライ
ドしなければならない。また、必ず publicを指定する。なお、インタフェースは、カンマで区切ることで何個でも実装することができる。
37ここでは abstract 修飾子の詳しい説明はしない。書籍を参照すること。
40
例を挙げよう。以下はボタンを押したときに行動を起こすサンプルである。
List 30: ActionListenerのサンプル¨ ¥import java.awt.*;import java.awt.event.*;import javax.swing.*;
public class ActionTest extends JFrame implements ActionListener {
ActionTest(){JButton button = new JButton("OK");button.addActionListener(this);getContentPane().add(button);pack();
}
public void actionPerformed(ActionEvent e){System.out.println("Clicked!");
}public static void main(String args[]){ActionTest f = new ActionTest();f.setVisible(true);
}}§ ¦ActionTest クラスは JFrame のサブクラスであるが、ActionListener インタフェースを実装している。ActionListenerを実装したクラスは、必ず actionPerformed(ActionEvent)というメソッドをオーバーライドし、その中にイベント処理を書く。ここでは標準出力にメッセージを表示している。なおイベントを
扱うため、java.awt.event.*をインポートする必要がある。
このサンプルでは ActionTestクラスがイベントリスナーを兼ねており、イベントハンドラをメソッドとして実装している。しかし、簡単な処理でよければ、イベントリスナーをActionListenerのインスタンスとして直接定義することができる。
List 31: 匿名クラスの例¨ ¥import java.awt.*;import java.awt.event.*;import javax.swing.*;
public class Anonymous extends JFrame {
Anonymous(){JButton button = new JButton("OK");button.addActionListener( new ActionListener(){public void actionPerformed(ActionEvent e){System.out.println("Clicked");
}});getContentPane().add(button);pack();
}
public static void main(String args[]){Anonymous f = new Anonymous();f.setVisible(true);
}}§ ¦この例では、JButtonのイベントリスナーを、JButtonの addActionListenerの中で定義してしまっている。以下のように、ActionListenerは普通にクラスとして実装し、そのインスタンスをイベントソースに渡すことも可能である38。
38この例ではメソッド中にクラスを定義している。これをローカルクラス (Local Class) と呼ぶ。ローカルクラスも匿名クラス
41
¨ ¥class MyActionListener implements ActionListener()public void actionPerformed(ActionEvent e){System.out.println("Clicked");
}}button.addActionListener( new MyActionListener());§ ¦しかし、一度しかインスタンスが作られないクラスをわざわざ名前をつけて定義するのは面倒であるので、
メソッド引数の中で直接クラスを定義してしまうのである。このようなクラスは名前がつけられないこと
から匿名クラス (Anonymous Class)と呼ばれ、主にイベントリスナー、イベントアダプタの実装に用
いられる。匿名クラスはどこでも使えるが、無節操に使用するとスコープが混乱しやすくなり、なにより
ソースが見づらくなるので、多用は禁物である。
8.4 アダプター
インタフェースに定義されているメソッドには実体がない。したがってインタフェースを実装するクラス
は、そのインタフェースが持つすべてのメソッドをオーバーライドしなくてはならない。たとえばマウスの
クリックイベントを表すMouseListenerにはmouseClicked, mouseEntered, mouseExited, mousePressed,MouseReleasedの 5つのメソッドがある。このうちクリックイベントだけを受け取りたいのに、わざわざ他のメソッドをオーバーライドするのは面倒である。この煩雑さを避けるため、二つ以上のメソッドを持
つイベントリスナーにはアダプタークラスが用意されている。アダプタークラスはリスナークラスに用意
されたメソッドに「何もしない」という実体を与えているため、必要なメソッドのみをオーバーライドす
ればよい。マウスイベントを処理する例を挙げよう。
List 32: MouseAdapterの例¨ ¥import java.awt.*;import java.awt.event.*;import javax.swing.*;
public class MouseSample extends JFrame {
public MouseSample(){addMouseListener(new MouseAdapter(){public void mouseClicked(MouseEvent e){
System.out.println(e);}
});}public static void main(String args[]){MouseSample f = new MouseSample();f.setSize(100,100);f.setVisible(true);
}}§ ¦この例ではトップレベルウィンドウである JFrameのマウスイベントのうち、クリックイベントを処理している。実行してウィンドウをクリックすると、以下のように表示される。
java.awt.event.MouseEvent[MOUSE_CLICKED,(71,69),absolute(71,69),button=1,
modifiers=Button1,clickCount=1] on frame0
java.awt.event.MouseEvent[MOUSE_CLICKED,(71,69),absolute(71,69),button=1,
modifiers=Button1,clickCount=2] on frame0
java.awt.event.MouseEvent[MOUSE_CLICKED,(71,69),absolute(71,69),button=1,
も、内部クラス (Inner Class) の一種である。
42
modifiers=Shift+Button1,extModifiers=Shift,clickCount=1] on frame0
java.awt.event.MouseEvent[MOUSE_CLICKED,(71,69),absolute(71,69),button=1,
modifiers=Ctrl+Button1,extModifiers=Ctrl,clickCount=1] on frame0
java.awt.event.MouseEvent[MOUSE_CLICKED,(70,68),absolute(70,68),button=3,
modifiers=Meta+Button3,clickCount=1] on frame0
表示されているのは、イベントリスナーに引数として渡されたMouseEventの内容であり、どこで発生したか (絶対座標と相対座標)、どのボタンが押されたか (button=1なら左クリック)、一緒にシフトキーやコントロールキーなどの修飾キー (modifiers)が押されているか、などの情報を含んでいる。なお、アダプタークラスは対応するインタフェースのすべてのメソッドに実体を与えているため、スペ
ルミスなどにより正しくイベントハンドラを記述していなくてもコンパイルエラーが生じない。たとえば
mousePressedとすべきところを、mousePresedと打ち間違えていても、そのような新しいメソッドが定義されたと解釈され、イベントが処理されない。コンパイルエラーも例外も発生しないバグなので気をつ
けたい。これを防ぐには、@Overrideアノテーションを用いる。
43
9 グラフィックスの基礎
9.1 描画の仕組み
シングルスレッド、シングルウィンドウのプログラムにおける描画は、単に画面にたいして描画命令を
発行すればよかった。しかし現在のアプリケーションはマルチスレッド、マルチウィンドウ環境で実行さ
れるため、常に他のウィンドウによって画面を書き換えられる可能性がある。そこで、GUIプログラムではイベントドリブンの考え方によって描画を行う。すなわち、プログラムの起動時、他のウィンドウによっ
て隠されていた領域が表示された、最小化されていたウィンドウが元に戻された、などの状態をイベント
として捕らえ、そのイベントを処理することで描画を行う。Javaのコンポーネントは、描画イベントのイベントハンドラとして paintメソッドを備えている。paintメソッドには、引数としてGraphicsクラスのインスタンスが渡される。Javaにおけるすべての描画はGraphicsオブジェクトを通して行われる。以下はすべて Swingコンポーネントについて説明するが、AWTコンポーネントと Swingコンポーネントでは描画の仕様が異なる場合があるので注意して欲しい39。
OK����
(� � � � )
JVM
���� � �
��
Java�������� � �� ��( � �� �� � )
paint � � (� � � � � � � )
� �� ��( � �� �� � )
paint � � ( � � � � � � � )
JFrame JButton
図 6: 描画の仕組み。JVMが描画の必要性を検出すると、トップレベルウィンドウに描画を依頼する。描画はイベント処理と同じ枠組みで処理される。描画依頼がイベント通知であり、イベントリスナはウィンドウ、paintメソッドがイベントハンドラに対応する。ウィンドウオブジェクトは、自らが管理するコンポーネントにも描画を通知する。この描画の通知は再帰的に行われる。
すべての Swingコンポーネントは paintメソッドを備えているため、プログラマは paintメソッドを上書きすることで描画を行うことになる。以下は描画の簡単な例である
List 33: paintメソッドの使い方¨ ¥import javax.swing.*;import java.awt.*;
class DrawSample extends JFrame{public void paint(Graphics g) {g.setColor(Color.black);g.drawLine(0,0,100,100);
}public static void main(String args[]){DrawSample f = new DrawSample();f.setSize(100,100);f.setVisible(true);
}}§ ¦この例では、まず JFrameを継承する際に paintメソッドを上書きしている。その中で受け取った Graphicsクラスのインスタンスを使って、カレントカラーを黒にしてから、座標 (0,0)から (100,100)に向かって直
39具体的には、AWT は再描画時に背景色で領域がクリアされるが Swing はクリアされないので自分でクリアしてやらなくてはいけないなど、update、repaint、paint などのメソッドの振る舞いが異なる。今は意識する必要は無いが、将来問題が起きたときのために頭の片隅に入れておくと良い。
44
線を描画している。ここで、プログラム中では paintメソッドを明示的に呼び出していないことに注意したい。paintは描画が必要になったときに JVM40から呼び出される。
もともとの JFrameの paintメソッドではコンポーネントの領域を背景色で塗りつぶしていたのだが、それを上書きしてしまったため、他のウィンドウから隠されると、そのウィンドウの痕跡が残ってしまう
ことがある41。これを防ぐためには、背景色の塗りつぶしも明示的に指定してやらなければならない。塗
りつぶしには Graphicsクラスの fillRectメソッドを用いて次のようにすれば良い。¨ ¥public void paint(Graphics g) {g.setColor(getBackground());g.fillRect(0,0,getWidth(),getHeight());g.setColor(Color.black);g.drawLine(0,0,100,100);
}§ ¦getBackgroundで背景色を取得し、それをGraphicsのカレントカラーに設定してから fillRectによって全体を塗りつぶしている42。こうすることによって他のウィンドウの痕跡を消しつつ、目的のイメージ (この場合は直線)を描画することができる。
Graphicsクラスには、直線 (drawLine)や長方形 (drawRect, fillRect)の他にも楕円 (drawOval,fillOval)、円弧 (drawArc)、多角形 (drawPolygon)や文字 (drawString)といった基本的なグラフィックスを描画するメソッドが用意されている。
9.2 グラフィックスコンテキスト
一般に、描画には多数のパラメタを指定する必要がある。背景色に前景色、線の太さやフォントなど、
描画に必要なパラメタをまとめて描画属性と呼ぶ。これらを毎回指定するのは不便であるし、また設定す
るオーバーヘッドも無視できない。その問題を解決するため、一般に描画プログラムではグラフィックス
コンテキスト (Graphics Context, GC)と呼ばれる仕組みを提供している。GCは、描画属性を保持し、かつカプセル化し、描画に対するペンやブラシの役割を担う。それに対して描画する対象 (ウィンドウやイメージなど)をグラフィックスデバイスと呼ぶ。グラフィックスデバイスはキャンバスの役割を果たす。Javaにおける java.awt.Graphicsは、GCをあらわすクラスである。Javaの描画はすべてGraphicsオブジェクトを通して行われる。
Javaにはガーベジコレクション機能があるため、プログラマはメモリの開放については通常はあまり意識しなくても良い。しかし、グラフィックスコンテキストはOSのシステムリソースを使う場合が多い。OSのシステムリソースは使用できる数に制限があるため、Graphicsオブジェクトを作ったままにしているとリソースを占有してしまう可能性がある。小さなプログラムなどではあまり気にしなくても良いが、
大きなアプリケーションを使う場合には、Graphics.dispose()を明示的に呼び出すことでリソースの開放を行う必要がある。
9.3 ダブルバッファリング
Swingコンポーネントは、再描画の必要があるたびに paintメソッドが呼ばれる。そのため、paintメソッド内で背景のクリアから描画をやりなおしていると、毎回背景のクリアが見えることになり、ちらつ
きの原因となる。また、時間のかかる処理を paintで行うと、描画が重くなる原因となる。これを防ぐのがダブルバッファリング (double-buffering)と呼ばれる処理である43。
40Java 仮想マシン (Java Virtual Machine) のこと。平たく言えば Java のバイトコードのインタプリタである。41これは処理系に依存する模様。Java のバージョンにより痕跡が残ったり残らなかったりするので注意されたい。42ちなみに clearRect メソッドを使えば背景色で塗りつぶしてくれるので、わざわざカレントカラーを変更しなくても良い。43単にダブルバッファと呼ばれることが多い。
45
List 34: ダブルバッファを使わない例¨ ¥import javax.swing.*;import java.awt.*;
class DBSample1 extends JFrame{
static final int WIDTH=500;static final int HEIGHT=500;
DBSample1(){setSize(WIDTH,HEIGHT);
}
void draw(Graphics g) {g.setColor(getBackground());g.fillRect(0,0,getWidth(),getHeight());g.setColor(Color.red);for(int i=0;i<1000;i++){int x = (int)(Math.random()*getWidth());int y = (int)(Math.random()*getHeight());int r = 6;g.fillOval(x-r,y-r,r*2,r*2);
}}
public void paint(Graphics g) {draw(g);
}
public static void main(String args[]){DBSample1 f = new DBSample1();f.setVisible(true);
}
}§ ¦コンポーネントに直接描画すると、その描画の過程が見えてしまう。これを防ぐため、まずオフスクリー
ンバッファと呼ばれる、見えない裏のスクリーンを用意しておき、そこに描画する。表のスクリーンの描
画が必要になった際には、裏のスクリーンから表のスクリーンにイメージをコピーする。これにより、描
画の過程がユーザー見えなくなり、また、複雑な描画を毎回やり直す必要がなくなる。ゲームなど、アニ
メーションのあるプログラムにおける基本の技術である。
例を挙げよう。まず、ダブルバッファを使わないコードを List 34に示す。この例では、適当な大きさのJFrameを用意し、paintメソッドの中でランダムに赤い円を描画している。再描画のたびにランダムに赤い円が再配置されるため、どこが描き換えられたかがわかるだろう。また、描き換える必要のある場所の
みを描き換えていることもわかる。
次に、ダブルバッファを使った例を List 35に示す。
List 35: ダブルバッファを使った例¨ ¥import javax.swing.*;import java.awt.*;
class DBSample2 extends JFrame{
static final int WIDTH=500;static final int HEIGHT=500;Image offImage;
DBSample2(){
46
setSize(WIDTH,HEIGHT);}
void draw(Graphics g){g.setColor(getBackground());g.fillRect(0,0,getWidth(),getHeight());g.setColor(Color.red);for(int i=0;i<1000;i++){int x = (int)(Math.random()*getWidth());int y = (int)(Math.random()*getHeight());int r = 6;g.fillOval(x-r,y-r,r*2,r*2);
}}
public void paint(Graphics g) {if(offImage==null){offImage = createImage(WIDTH,HEIGHT);draw(offImage.getGraphics());
}g.drawImage(offImage,0,0,this);
}
public static void main(String args[]){DBSample2 f = new DBSample2();f.setVisible(true);
}}§ ¦まず、オフスクリーンイメージのために Imageクラス型の変数である offImageを用意している。Imageクラスのインスタンスを得るには、Componentクラスのメソッドである createImageを使う。しかし、このメソッドは Componentクラスのオブジェクトがグラフィックスを与えられた後でないと使えないため、最初に描画される際に呼ばれている。先ほどの例では JFrameに直接描画されていた赤い円は、オフスクリーンイメージが作成された時に一度だけ offImageに描画される。以後、描画が必要になるたびに (paintが呼ばれるたびに)offImageのイメージをコピーすることで描画する。
余談コラム 5 ~ オブジェクト指向の考え方 ~
オブジェクト指向プログラミングにおいては、オブジェクト同士がメッセージをやりとりすることでプロ
グラムが機能する。オブジェクト指向言語である Javaでは、すべてがこの枠組みにしたがって設計されている。たとえばグラフィックスでは、描画の必要があると、コンテナからその管理下にあるコンポーネン
トに描画依頼のメッセージが渡される。これはイベント処理として実現されており、コンテナがイベント
ソース、コンポーネントがイベントリスナ、paintメソッドがイベントハンドラである。さらに、イベント処理はイベントソースであるオブジェクトがメッセージのセンダー、イベントリスナであるオブジェクト
がレシーバである。このように、プログラミングをオブジェクト指向的に捕らえる感覚を養って欲しい。
47
10 ソフトウェアの開発手法
これまで、Java言語を題材にオブジェクト指向の考え方を学んできた。この章ではより一般的に、メンテナンス性および拡張性に優れたソフトウェアを開発するための方法論を学ぶ。
10.1 命名規約
10.1.1 命名規約とは
プログラムを書く際には、メソッドやフィールドの名前のつけかたはプログラマに一任されている。し
かし、名前を適当につけると可読性や再利用性が低くなり、結果としてバグの温床となりやすい。そこで、
なんらかの一貫した名前の付け方の規則を決め、その規則にしたがってプログラムを書こうという発想
が生まれる。クラス、メソッド、フィールドなどの名前のつけかたについての約束を命名規約 (Naming
Conventions)と呼ぶ。より一般的に、プログラムのインデントや改行をどのように書くべきかも含めて
約束することをコーディング規約 (Coding Conventions)と呼ぶ。命名規約はコーディング規約の一種
である。
命名規約には、大きく分けて
• 規則にしたがってシステマティックに名前の付け方を決めることで可読性を高める。
• 特別な名前の付け方をすることでなんらかのミスを防ぐ。
の二つの役割を持つ。
10.1.2 Javaにおける命名規約
一般的に、それぞれのプログラム言語において「こういう記述はこう書く」という約束事が存在する。
たとえば Javaに慣れたプログラマなら「こういったクラスにはこういう名前のメソッドがあるはずだ」という感覚を身につけているのが普通である。逆に、クラスの設計者はその感覚に沿うように名前をつけな
ければ、他のプログラマがそのクラスを利用しづらいだけではなく、似たような用途のメソッドの乱立を
招き、思わぬバグの温床となることもある。以下では Javaにおける名前のつけかたの約束を個別に紹介するが、全体を通して、名前はフルスペルの英語で記述し、略語やローマ字を使わないという原則がある。
クラス クラス名は大文字から始め、以後小文字で続ける。英単語の区切りごとに大文字からはじめる。
また、抽象クラスでは最初に「Abstract」をつける。
○ Vector, KeyStroke, AbstractClass
× Keystroke, kStroke, abstractclass
メソッド メソッド名は小文字からはじめ、以後は英単語の区切りごとに大文字からはじめる。さらに、
オブジェクトが三人称単数の主語となるように一般的に「動詞」もしくは「動詞+目的語」「動詞+補語」
という形にする。特に、boolean値を返すメソッドの動詞「is」「has」、場合によっては「can」などを用いる。この時、trueを返す場合を名前とする。たとえば「isValid」なら、「obj is Valid」という命題が真である場合に trueを返すように設計する。主語が三人称単数であるから、「contains」や、「hasNext」のように動詞もそれに対応した形を取る。
○ clear, clearAll, isValid, hasNext, setName, getName,
48
インタフェース インタフェースは、一般的に「~able」という名前にする。そのインタフェースを実装したクラスができるようになることの名前をつける。また、インタフェースもクラスの一種であるから、
大文字から初めて小文字で続ける。継承では親クラスと子クラスの間に「is-a」の関係がある。たとえばJButtonクラスは JComponentクラスのサブクラスであり、「JButton is a JComponent.」が成立する。インタフェースでは、実装クラスとインタフェースの間に「is」の関係があることが多い。たとえば JAppletに Runnableインタフェースを実装した場合「JApplet is Runnable.」という関係が成り立つのがわかるだろう。また、Throwableインタフェースの実装である Exceptionクラスは「Exception is Throwable.」が成り立つ。
○ Runnable, Throwable
× Interface1, Interface2
定数 static finalで定義された定数は、全て大文字とし、単語の区切りはアンダースコア「 」をつける。
○ ARRAY SIZE, BUFFER SIZE
× i, test (←こんな名前は論外である)
フィールド フィールドを、別の変数と区別したい場合には、頭にアンダースコアをつける。たとえば、
名前を変更するメソッド setNameにおいて、¨ ¥String _name;
public void setName(String name) {_name = name;
}§ ¦などとして、引数との衝突を防ぐ。同じ名前にしても thisキーワードをつければ引数とフィールドの区
別をつけることができるが、それは避けるべきである。
10.1.3 アプリケーションハンガリアン
前述の Javaの規約は、プログラマの間で共通の名前の付け方を決めることで可読性を高めることが目的だった。ここでは、命名規約によって思わぬバグを防ぐ「アプリケーションハンガリアン」という手法
を紹介する4445 。
ウェブにおいてユーザーの入力を受けつけ、それを適宜表示するというプログラムを考えよう。たとえ
ばインターネットの掲示板などがこれにあたる。たとえば、JTextField(tfName)にユーザーの名前を入力させ、それを表示させるとき、¨ ¥String name = tfTextField.getText();System.out.println("こんにちは" + name + "さん");§ ¦などというプログラムを書いたとしよう。単にユーザーの入力を出力としただけのこのコードはクロスサ
イトスクリプティング (Cross Site Scripting, XSS)という脆弱性を持つことになる46。たとえば、イ44この手法はハンガリー出身のプログラマによって考案されたためにハンガリー記法と呼ばれている。ハンガリー記法は本来バグを防ぐための命名規約であったが、変数の型を明示する手法と誤解されてひろまった。Java のような強い型付け言語においては変数の型の明示は役に立たない。一般に「ハンガリー記法」と言うと、誤解されて広まった役に立たない手法を指すため、その手法を「システムハンガリアン」、もともと提案者が意図した手法を「アプリケーションハンガリアン」と呼んで区別する。
45ここで紹介する事例は www.joelonsoftware.com というサイトの「間違ったコードは間違って見えるようにする」という記事(http://www.joelonsoftware.com/articles/Wrong.html) からの引用である。この記事には邦訳もあるので、興味のある人は検索されたい。
46インターネットに限らず、ユーザからの入力は常に不正を疑う必要がある。ここで言う不正とは、悪意のあるユーザによる操作に限らない。たとえば、入力として整数を期待しているフォームに小数点や日本語を入力されたらサーバが落ちる、といったプログラムを組むべきではない。
49
ンターネットの掲示板においてユーザの入力をそのまま表示してしまうと、悪意あるユーザは名前と偽っ
て HTMLのタグを書いたり、JavaScriptを書いたりするだろう。すると、ウェブサイトを乗っ取ったり、クレジットカードの番号などの秘密情報を盗んだりすることが可能となる。
これを防ぐためには、「<>」などのタグを一度 HTMLエンコードする必要があるだろう。HTMLエンコードとは、「<」を「<」などに変更することである。これにより「<H1>Hello</H1>」などはそのま
ま「<H1>Hello</H1>」と表示されるようになり、ユーザがタグを使うことはできなくなる。そのエンコー
ドするメソッドが Encodeであるとすると、¨ ¥String name = Encode(tfTextField.getText());System.out.println("こんにちは" + name + "さん");§ ¦などとすればよいことがわかる。それでは、入力された文字列を即座にエンコードして、以後エンコード
された文字列を扱うことにすればいいかというと、そうもいかない。たとえば名前の長さをカウントした
い場合に誤動作を起こすし、ソートなどでも問題を生じるだろう。そもそも、データはユーザが入力した
とおりに保持しておき、表示する直前にエンコードして出力するのが正しい設計であろう。
そこで、表示する際には必ずHTMLエンコードする、すなわち encodePrintというメソッドを使い、通常の printの代わりに使うことを考える。すなわち、¨ ¥String name = tfTextField.getText();
//どこか別の場所でencodePrint("こんにちは" + name + "さん");§ ¦これなら生の print文を見つけ次第、encodePrintに書き換えればよいため、問題が解決したように思える。しかし、この方法ではプログラマがHTMLタグを使いたいときに使えない、という問題が生じる。たとえば名前を太字で表示したいときに、¨ ¥encodePrint("<B>" + name + "</B>");§ ¦とすると、「<B>」は勝手に「<B>」に変換されてしまい、出力が「<B>名前</B>」になってしまう。
これでは困る。
これを解決するのが命名規約である。HTMLエンコードした文字列を格納した変数は encodedの「e」、まだエンコードされていない文字列は unencodedの「u」というプレフィックスをつけることにする。すると、コードはたとえばこんな感じとなる。¨ ¥String uName = tfTextField.getText();//ずっと後で、eName = Encode(uName);//さらにずっと後で、System.out.println(eName + "さんこんにちは");§ ¦この規則に従えば、たとえば¨ ¥
String eName = tfTextField.getText();§ ¦これが誤りであることが一目でわかる (エンコードされていない生の文字列を「e」で始まる変数名に格納している)。他にも、¨ ¥String uFullName = uFirstName + " " + uFamilyName;§ ¦これはただしい。¨ ¥String uFullName = uFirstName + " " + eFamilyName;§ ¦これは間違っている。¨ ¥System.out.println(uName + "さんこんにちは");§ ¦
50
これも間違っている。
以上のように、間違った代入や間違った処理がその行だけで判別できる。一般に、アプリケーションハ
ンガリアン記法は型だけでは判別できない誤った代入を防ぐのに効果的である (むしろ、そのように記法を定める)。たとえば、通貨を整数型であらわすことにする。米ドルをあらわす変数には「dollor」、日本円をあらわす変数には「yen」というプレフィックスをつければ、¨ ¥dollorBuy = yenSell;§ ¦といった誤った代入を即座に見つけることができる (レート変換されていない通貨の代入は誤り)。他にも、Microsoft Excelのソースにおいては rwと colというプレフィックスが使われている。どちらも整数型で、それぞれ「行 (row)」と「列 (column)」を表している。行列を転地するといった特別な用途以外では、行に列を代入するのは意味が無い47。
一般的に、プログラムが正しい動作をするかどうかを判定するのに意識しなくてはならない範囲が狭け
れば狭いほど、そのプログラムはメンテナンス性に優れる。グローバル変数を減らしたり、スコープをな
るべく局所的にするのもその一例である。
10.2 設計モデル
10.2.1 分析と設計
実際のソフトウェア開発では、まずやりたいこと (目的)が与えられ、その目的を実現するためのモジュールはいかにあるべきかを分析し、最終的にクラスを設計していくという手順をとる。やりたいことを実現
するためにはどんな機能が必要かを分析したものを分析モデル、それらの機能をどのように実現するかを
設計したものを設計モデルと呼ぶ。分析および設計モデルの作成方法には長い歴史があり、数多くの手法
が提案されているが、ここでは設計モデル、特にクラス設計について紹介するにとどめる。設計モデルを
一般的に定義することは難しいが、ここではある目的を実現するためのクラス設計のことである、と定義
することにしよう。
クラスベースのオブジェクト指向言語にとって、プログラミングとはクラス設計とほぼ同義である。一
般に大きなソフトウェアほど、それを実装するためのクラスの数も増える。また、長く使われるにしたがっ
てシステムもどんどん肥大化し、それにともなってクラス同士の関係は複雑化してしまいやすい。しかし、
クラスの関係が複雑であるような設計は「良くない設計」であることが多い。具体的には一部の変更が広
範囲に影響を及ぼしたり、機能を追加したら思いもよらぬところが動かなくなった (いわゆる地雷)などという症状がおきやすい。したがって、システムを開発する際には、まず見通しの良い設計を行うことが
必須である。見通しの良い設計とは、クラスの間の関係がすっきりしていることである。クラスの関係で
もっとも強いのは親子関係であるが、そのほかにも様々な関係がありうる。
余談コラム 6 ~ プログラムの質と上司 ~
一般的に上司に恵まれないのは不幸であるが、特にプログラマの上司がおかしな「常識」を持っていたり
すると大変なことになる。たとえば、「メソッドの名前はすべてmethod001,method002,· · ·と連番にせよ」ということを真顔で言う人がいる。関数が全部で何個あるかすぐにわかるからだそうだ。しかも、メソッ
ドの引数もmethod001(int method001arg001,double method001arg002,· · ·)と型や機能とは無関係に連番で定義するのだそうだ。私がもしこのような会社に入ってしまったらすぐに転職を考える。また、こうい
う会社が基幹システムを作っていたりすることがあるかと思うとぞっとする。
47これらをより厳しくチェックするには、クラスによる型チェックを利用する。詳しくはデザインパターンを勉強せよ。
51
ClassA
ClassB
����
ClassB�ClassA�� � � � � �
ClassC
ClassD
ClassC�ClassD � � � �
� � � � ��
Class�
ClassF
ClassF�ClassE�� � � � � �
ClassG
ClassH
ClassH�ClassG���
�� �� ��
図 7: クラス同士の関係を表した UML図。左から「親子関係」「合成」「集約」「依存」を表現しており、この順番でクラスの関係が弱くなる。矢印の向きは依存関係を表しており、矢印の始点があるクラスは、終点があるクラスの情報がないとコンパイルできない。
あるクラスがフィールドとして他のクラスのインスタンスを持っている場合、つまりクラスに包含関係
が成り立つ場合、これを集約 (aggregation)と呼ぶ。所有クラスが被所有クラスとライフタイムを共有
する場合、つまり所有クラスが作成された場合には必ず被所有クラスが作成され、所有クラスが消滅する
まで被所有クラスが消滅しない場合、特に合成 (composition)と呼ぶ。あるクラスが別のクラスに依存
するが、親子関係も包含関係もないこともある。これは単に依存関係 (dependency)と呼ばれ、あるク
ラスのメソッド引数に別のクラスのインスタンスが渡されるときなどに良く見られる。クラス同士の関係
の一部を図 7に示す。一般に、クラス同士の関係が弱ければ弱いほど仕様変更に強いコードとなる。特に、あるクラスの仕様を変更した場合に、影響がでるクラスが最小限で、かつどのクラスに影響が出るかすぐ
に分かるような設計が望ましい。
10.2.2 継承と委譲
すでにあるクラス Aの機能を使うクラス Bを作る場合、クラス Bをクラス Aのサブクラスとするべきか、それともクラスAのインスタンスをフィールドとして持つべきかを考える必要がある。このとき、二つのクラスの関係が is-aであるか、has-aであるかを考えるとうまく設計できることが多い。たとえば、ボタンを表すクラス Button があるとする。いま、アニメーションをするボタンのクラス
AnimateButtonを作りたいとき、Buttonを継承して AnimateButtonを作るのが良いであろう。このとき、二つのクラスの間には「AnimateButton is a Button」という関係が成り立つ。これを「is-a」の関
係と呼ぶ。一般に、親クラスである SuperClassと、その子クラスである SubClassには「SubClass is asSuperClass」という関係が成り立つ。「Button is a Component」、「MouseEvent is a Event」など、Javaで親子関係にあるクラスは、ほぼ「is-a」の関係を持つ。では、ウィンドウクラスにレイアウトマネージャをつける場合はどうであろうか。一般にウィンドウクラス
にコンポーネントを載せるとき、自動でレイアウトを行うレイアウトマネージャが働く。ウィンドウクラス
52
をWindow、レイアウトマネージャをLayoutManagerとすると、明らかに「Window is a LayoutManager」は成り立たない。むしろ、「Window has a LayoutManager」とすべきである。たとえばアンドゥ機能を持つエディタを設計するなら、「Editor has a Undo」であって、「Editor is an Undo」でないことは明白であろう。これを「has-a」の関係と呼ぶ。こういう場合、Windowクラスは、フィールドとして LayoutManagerのインスタンスを持つ。実際の Javaのコードとことなるが、抽象的にコードを書けば¨ ¥class Window{private LayoutManager layout;
public doLayout(){layout.doLayout();
}}§ ¦といった感じになる。レイアウトの必要があるとき、自分のフィールドである LayoutManagerのインスタンスにレイアウトを頼む形となっている。一般に、あるクラス (ここではWindow)にある処理 (レイアウト)を依頼したとき、そのクラス自身は処理の方法を知らないが、その処理の方法を知っているクラスを知っており、そのクラスのインスタンスに処理を依頼することで処理が行われるとき、この一連の処理
を「委譲 (delegate)」と呼ぶ。
また、この例で言うWindowクラスと LayoutManagerクラスのように強い所有関係があり、特に委譲元オブジェクトが委譲先オブジェクトをフィールドとして持っている場合、この関係を「クラスの集約
(Aggregation)」と呼ぶ。委譲にはもう一つのパターンがある。フィールドにインスタンスを持つのではなく、メソッド引数とし
てインスタンスを渡す方法である。たとえば、ウィンドウクラスに自分で作成したレイアウトマネージャ
を使って欲しい場合、以下のようなコードとなるだろう。¨ ¥class Window{public doLayout(LayoutManager layout){layout.doLayout();
}}§ ¦Windowクラスはレイアウトマネージャのインスタンスをフィールドとしてもっていないが、レイアウトが必要なときにはレイアウトマネージャが渡されることになっており、それを使ってレイアウトを実行する。
クラスの間には、結合の強さが存在する。もっとも強い関係が親子関係で、次にクラスが別のクラスを
フィールドとして持つ場合 (合成)、もっとも弱いのがメソッド引数として渡される場合である。たとえば、コンポーネントは自分を描画する必要があるとき、「筆とキャンバス」である Graphicsク
ラスのインスタンスを必要とする。しかし、コンポーネントは一般に Graphicsクラスのインスタンスをフィールドとして持っておらず、必要なときに paintメソッドに Graphicsオブジェクトが渡される形になっている。
一般的に、クラスの関係が弱いほど独立性が高く、バグが入りにくく、かつ仕様変更に強いコードとな
る。あるクラスの機能が必要だからといって、安易に継承を用いると、基底クラスに変更があった場合、
その変更は派生クラスに波及する。かといって、いつも委譲を用いればよいかというとそうでもない。ク
ラス設計をする際、委譲を用いるか、継承を用いるかは今後の仕様変更もにらみ、慎重に決定する必要が
ある。
10.2.3 MVCモデル
ユーザーからの入力により対話的に処理をするソフトウェアをGUIアプリケーションと呼ぶ。このGUIアプリケーションを「モデル (Model)」「ビュー (View)」「コントローラ (「Controller」)」の三つに分けて考える設計手法をMVC設計モデル、あるいは単にMVCと呼ぶ。モデルはデータの実体の保持と処理
53
の中核を担当し、ビューはモデルをどのように表示すべきかを担当し、コントローラはユーザからの入力
に応答してその内容をモデルに伝える役割を担当する。「MVCの分離」というと、これら三つの要素を分けて考え、実装することである。
���
��
� � � � ��(Controller)
�(Model)
� �(View)
� � � � �
� �
���� �
� � �� �
� � �
�
��
� �
図 8: MVC分離の概念図。ユーザからの入力をコントローラが処理し、その情報をモデルに伝える。モデルは指示にしたがってデータを変更し、ビューに描画を依頼する。ビューは受け取ったデータを可視化し、ユーザに提供する。
MVCの三要素のうち、モデルとビューの分離は世の中でよくみられる一般的な手法である。たとえば、ウェブサイトを記述するHTML言語とスタイルシートがMV分離の例となっている。HTMLが文書構造(モデル)をあらわし、その構造をどのように表示するか (ビュー)をスタイルシートが記述する。HTML言語では、文書にタグを埋め込むことによって章立てや脚注といった構造情報や強調や引用といった意味
情報を表現する。スタイルシートは、たとえば「章」のタイトルのフォントやサイズを指定したり、強調
文をゴシック体にしたり引用文をイタリック体にするなど、与えられた文書をどのように表示するかを指
定する。このようにデータと表示方法を分離しておくことによって、後で「強調文を赤色で表示する」と
いう表示方法の変更や、「引用文だけ探したい」といった意味情報の検索が容易となる。
逆にMVの分離がされていない例がWYSIWYG48系のワープロソフトである。ワープロソフトでは、
たとえば引用文をマウスで選択して、イタリック体にすることが簡単にできる。また、見えている通りに
印刷されるために、結果をイメージしながら編集しやすいといった特徴がある。しかし「引用文のフォン
トを変えたい」と思ったとき、イタリック体になっている場所を延々目で探していく必要がある。また、
印刷した原稿を見てみたら脚注のフォントの大きさがバラバラになっていた、といった経験がある人もい
るだろう。
このように、表示方法と意味情報の混在は、その後のメンテナンス性を著しく損なう。そこで、ソフト
ウェアを設計する際、モデルとビュー、コントローラの三つの実体に分けて考える。
Perl言語による CGIを例に MVC分離を考えてみよう。CGIは、ユーザからの入力を適当に処理し、HTMLの書式で出力することで処理を完了する。このとき、よくあるタイプの CGIスクリプトは次のような形をとる。¨ ¥print "Content-type: text/html\n";#ヘッダ部分 の出力print "<html><body>\n";
//ここでデータを読み込む
48「What You See Is What You Get」の略で、直訳すれば「見えている通りに得られる」。主に画面に表示されているとおりに印刷されることをさす。
54
//データの表示部print "<table>\n";print "<tr><td>お名前</td><td>コメント</td></tr>";for($i = 0; $i < $articlecount;$i++){$name = $nameData[$i];$comment = $commentData[$i];print "<tr><td>$name[$i]</td><td>$comment</td></tr>";
}print "</table>\n";
#フッタ部分 の出力print "</body></html>";§ ¦データは別のファイルに保存されているとする。HTMLを出力する部分が分断されており、実行結果が想像しづらいのがわかるだろう。たとえばテーブルを駆使したレイアウトを使っていたような場合、後で表
示するページのレイアウトを変更するのは大変な手間となる。また、ヘッダ部分やフッタ部分がプログラ
ムに直接埋め込まれていることも問題である。このようなプログラムをハードコード、このようなプログ
ラムを書くことをハードコーディング (Hard Coding)と呼び、書いてはいけないプログラムの典型例で
ある49。
MVCの観点からは、コントローラとビューが混在していることが問題である。そこで、ビューを分離することを考えよう。あくまでデータ処理と表示は分離されるべきである。そこで、HTMLは別にテンプレートファイルとして用意しておく。テンプレートファイルには特別な記述 (たとえば@data)を用意しておき、CGIファイルはテンプレートファイルを読み込み、特別な記述をデータで置換する50。以上のよう
にすれば、コントローラとビューが分離したことがわかるだろう。たとえば、Perl言語はわからないが、HTMLはわかる人がレイアウトだけ変えたい、と思ったときに、テンプレートファイルだけを修正すればよい。テンプレートはそのままブラウザで見ることができるので、実行結果も想像しやすい。
10.3 リファクタリング
プログラムは、開発期間よりも保守期間の方が長い。保守期間の間には、継続して機能が追加されてい
くだろう。このとき、設計にゆがみが生じやすくなるため、適宜修正していく必要がある。この修正のこ
とをリファクタリング (Refactoring)と呼ぶ。個人的な経験では、オブジェクト指向的な考え方がもっ
とも身につきやすいのはリファクタリングをする時である。ソースコードを読んでいて「この部分は気持
ち悪いな」と感じるようになれば、それは質の高いプログラムを組むための一歩を踏み出したということ
である。リファクタリングには数多くの手法や定石があるが、ここでは一例として、継承関係を委譲関係
で置き換えることでクラス間の結合度を弱くし、かつ拡張性を高める方法を紹介する。
回路設計をするアプリケーションを作成したとしよう。回路には様々な種類があるため、Circuitクラスから派生させ、ContainerクラスはCircuitクラスを管理する。画面表示のため、基底クラスであるCircuitに drawWindowメソッドを用意しておき、派生先で実装する。さらに回路図をビットマップファイルとして保存するため、Circuitクラスに drawBitmapクラスを定義し、派生先で実装してある。この状態で、さらに回路図を EPSファイルとして保存する機能を実装するにはどのようにすべきであろうか。一つの方法は、ビットマップファイルの保存と同様に基底クラスに drawEPSFileメソッドを定義し、派
生先で実装することであろう。しかしそれでは現在存在するすべての派生クラスのメソッドを実装する必
要があり、さらに将来別のファイルタイプ (たとえばメタファイルなど)で保存したい場合に同様な作業が必要となってしまう。
49ハードコーディングとは、特定の環境、状況を決めうちして書くプログラムである。たとえば定数の項で出てきたマジックナンバーもハードコードの一種である。
50ここでは簡単のために置換を用いた実装を例に挙げたが、ID 属性によるマッチングや XML+XSLT などを用いたテンプレートライブラリを使う方がより好ましい。
55
一般に、仕様変更や機能追加のたびに基底クラスを書き換える必要があるのは悪い設計である。そこで、
描画という動作を抽象化し、描画を別のクラスに委譲することで拡張性を高めることを考える。
具体的には、抽象的な描画を担当するクラス、AbstractDrawを定義する。Circuitクラスには drawメソッドのみを定義しておき、drawメソッドの引数にはAbstractDrawクラスのインスタンスを受け取るようにしておく。描画の必要がある場合にはAbstractDrawを適切に継承したクラスのインスタンスを渡すことで、Circuitクラスに「いま自分がどんなファイルフォーマットで描画しているか」を意識させないことが可能となる。図 9にリファクタリングのクラスの関係を、図 10にリファクタリング後のコード例を示す。
リファクタリング前は、ファイルフォーマットを追加するにはCircuitから派生したすべてのクラスの実装を行う必要があった。リファクタリング後は、新たなファイルフォーマットに対応するのにAbstractDrawクラスを適切に継承するだけで良く、Circuitクラスと派生クラスを修整する必要がない。
Containerクラスは描画が必要な時、欲しいファイルフォーマット対応する描画クラス (AbstractDrawクラスの派生クラス)のインスタンスを作成し、それを管理している Circuitクラスに渡す。Circuitクラスは自分を描画するのに、指定された描画クラスを使う。つまり、描画という動作を描画クラスに委譲し
ている。Circuitクラスは、より抽象化された「線を引く」「文字を描画する」といった動作を描画クラスに依頼し、描画クラスは対応するファイルフォーマット用に実際の描画を行う。このような設計にするこ
とで、今後対応するファイルフォーマットが増えた場合でも Circuitクラスとその派生クラスはなんら変更する必要がなくなる。
Circuit
drawWindowdrawBitmap
CircuitA
drawWindowdrawBitmap
CircuitB
drawWindowdrawBitmap
Circuit
draw
CircuitA
draw
CircuitB
draw
AbstractDraw
drawLinefillRect....
WindowDraw
drawLinefillRect....
BitmapDraw
drawLinefillRect....
EPSDraw
drawLinefillRect....
図 9: リファクタリングにおけるクラス図の変化。抽象クラスや抽象メソッドは斜字体になっている。左がリファクタリング前、右がリファクタリング後。
56
void drawWindow(){WindowDraw drawer= new
WindowDraw()for(int i=0;i<Circuits.size();i++){
Circuits[i].draw(drawer);}
}
void saveAsEPS(string filename){EPSDraw drawer = new EPSDraw();for(int i=0;i<Circuits.size();i++){
Circuits[i].draw(dw);}drawer.saveToFile(filename);
}
void saveAsBitmap(string filename){BitmapDraw drawer = new BitmapDraw();drawer.bitmapType = BitmapDraw.DIB;for(int i=0;i<Circuits.size();i++){
Circuits[i].draw(dw);}drawer.saveToFile(filename);
}
abstract public void draw(AbstractDraw drawer);
public void draw(AbstractDraw drawer){drawer.setColor(Color.black);drawer.drawRect(x,y,width,height);drawer.drawString(x,y,name);...
}
CircuitA
CircuitContainer
public void draw(AbstractDraw drawer){drawer.setColor(Color.red);drawer.drawCircle(x,y,r);drawer.drawString(x,y,name);...
}
CircuitB
図 10: リファクタリング後のコード例。
57
11 終わりに
Java言語を題材に、「良いプログラム」を書く方法を駆け足で学んだ。ただし、いくら良いプログラム設計手法を知っていても、実際にプログラムが組めなければ意味が無い。英文法がいくら完璧でもボキャ
ブラリが不足していれば英作文ができないように、ライブラリを使いこなせないプログラマは役に立たな
い。Javaには強力なクラスライブラリが多数用意されている。興味があれば、java.utilパッケージなどを眺めてみると良い。ハッシュやベクタ、スタックなどのデータ構造や、カレンダーや通貨といった概念を
扱う有用なクラスが多数定義されている。また、Windowsプログラミングを志すなら多数のAPIやMFC(Microsoft Foundation Class)に精通する必要があるし、STL (Standard Template Library)を使えなければ C++を使う意味はあまりない。分析、設計にはUML (Unified Modeling Language)の知識が必要となるがほとんど触れられなかった。これも必要となったら参考書にあたって欲しい。
良いプログラムを書くための原則は「いま苦労することで後々の苦労を軽減する」ということに尽きる。
くれぐれもちょっとしたキーストロークの労を厭って地雷を埋め込むといったことの無いようにされたい。
58