...

プログラム設計概論

by user

on
Category: Documents
1

views

Report

Comments

Transcript

プログラム設計概論
平成 21 年 7 月 16 日
プログラム設計概論
渡辺宙志
東京大学情報基盤センター
概要
Java 言語を題材としてプログラムの設計手法を学ぶ。特にオブジェクト指向などの概念を通し、バグが無く、仕様変
更に強いコーディング手法を学ぶ。
目次
1
2
はじめに
1.1
1.2
目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3
1.4
ソフトウェアの開発手法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
プログラミング言語の種類 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Java 言語の基礎
2.1 Java とは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
6
コンパイルと実行 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
プログラムの構成要素 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
7
変数
3.1 変数とは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
10
基本データ型 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
スコープ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
13
14
メソッド
4.1 メソッドとは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
19
4.2
4.3
スコープ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
19
4.4
4.5
返り値 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2
3.3
3.4
4
5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
5
2.2
2.3
3
プログラムの保守性
3
3
3
定数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
値渡しと参照渡し . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
21
クラス
5.1 オブジェクトとインスタンス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2 クラス変数とクラスメソッド . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
23
23
コンストラクタ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
5.3
カプセル化
1
6
7
8
継承と多態性
6.1
動的結合 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2
6.3
6.4
継承とは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5
6.6
多態性 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
オーバーライド . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
カレントインスタンス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
多態の使い方 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
構造化例外処理
7.1 エラー処理について
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2
7.3
例外処理の仕組み . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.4
7.5
ランタイム例外 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
チェック例外
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
独自例外の定義 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
インタフェースとイベント処理
8.1
8.2
GUI プログラミング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
イベントドリブン型プログラミング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.3
8.4
インタフェース . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
28
28
29
30
31
32
33
33
34
35
36
37
38
38
39
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
40
42
グラフィックスの基礎
9.1 描画の仕組み . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
44
グラフィックスコンテキスト . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ダブルバッファリング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
45
10 ソフトウェアの開発手法
10.1 命名規約 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.2 設計モデル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
48
48
51
10.3 リファクタリング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
9
9.2
9.3
アダプター
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
Obj B
main
sub1
goto C:
goto A:
goto D:
Obj A
message
call sub1
message
sub2
call sub2
message
Obj C
goto B:
Spaghetti Programming
Structured Programming
Object Oriented Programming
図 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 は非純粋型言語である。
5 Delphi は Borland 社が開発した、主に Windows 向けの統合開発環境であり、その C++版である Borland C++ Builder
とともにヒットした。Delphi の開発を行っていた Anders Hejlsberg がマイクロソフトに移籍後、C#の開発に関わっている。
5
Java 言語の基礎
2
2.1
Java とは
Java とは Sun Microsystems 社が開発したプログラム言語であり、以下の様な特徴を持っている。
オブジェクト指向言語 Java は純粋なオブジェクト指向に基づいた言語であり、文法に従うことで自然に
オブジェクト指向プログラミングを実践することができる。Java においてはオブジェクトはクラス
によって表現される。Java プログラミングとは、クラスを記述することに他ならない。
可搬性 Java は、環境に依存しない。ソースファイルをコンパイルするとバイトコードと呼ばれる中間コー
ドが生成される。その中間コードをインタプリタが実行することでプラットフォーム非依存を実現
する。たとえば OS が Windows であるか Mac であるかといった環境の違いはインタプリタが吸収
する。
静的型付け Java はコンパイル時に強力な型チェックを行うことで、プログラム実行時に問題が起きない
ようにする。対となる概念に「動的型付け」というものがあるが、大半のスクリプト言語が動的型
付けの立場をとる。
ガベージコレクション Java の実行環境はガベージコレクション機能を持つ。これは、ユーザーが確保し
たメモリを自動的に開放する機能であり、ユーザーは自分でメモリを管理する必要が無くなる。こ
の機能により、メモリリークの心配も無くなる6 。
ネイティブなグラフィック環境 Java はネイティブにグラフィカルユーザーインタフェース(Graphical User
Interface, 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) と呼ぶ。
クラスは、フィールドとメソッドから構成される。フィールドとは、そのクラスの状態を表すデータで
あり、メソッドとは、そのオブジェクトに対する操作である。
例を挙げよう。
¨
import java.lang.*;
List 2: クラスの例
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.*;
§
8 URL
のドメイン名、サブドメイン名もパッケージ名と同様に階層構造によって名前の衝突を避ける仕組みである。
8
¥
¦
と、ワイルドカード「*」を指定する。これにより javax.swing に属すクラス全てが単純名で指定できるよ
うになる。ワイルドカードを指定した場合、
なお、java.lang.*パッケージはデフォルトでインポートされるため、明示的にインポート宣言しなく
ても良い。
9
変数
3
3.1
変数とは
プログラムは、データと、それを処理する手続きからなる。データを扱うための仕組みが変数 (Variable)
である。変数は、整数であったり実数であったり、クラスのインスタンスであったりするだろう。このよ
うに変数が表現するデータの性質をデータ型 (Data Type)、もしくは単に型と呼ぶ。また、変数は宣言
された場所によってフィールド、ローカル変数、メソッド引数の三種類に分けることができる。この種類
により、主に変数のスコープ (Scope) が変わる。スコープについては後述することにして、まずは変数
の型について解説する。
3.2
3.2.1
基本データ型
有効範囲
変数とは、データを保管したり、データへの操作を提供するものである。すべての変数には型が存在す
る。たとえば
¨
int a;
§
¥
¦
9
と宣言された変数 a は、整数 (integer) の型を持ち、整数の値しかとることができない 。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
13 C/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 では、フィールドと同じ名前のローカル変数を定義することが出来る。たとえば以下のコードを考
える。
¨
class Test{
int x = 1;
List 3: ローカル変数によるフィールド隠蔽の例
¥
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 がローカル変数な
のかフィールドなのかが分からなくなるため絶対にやってはいけない。
同様に、メソッドの引数にフィールドと同じ名前を指定することができる。
¨
class Test{
int x = 1;
List 4: メソッド引数によるフィールドの隠蔽の例
¥
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 変数は最初に一度だけ初期化されたあと、関数を抜
けても値が保持される。
たとえば、以下のような関数を作ったとしよう。
¨
int
func(void){
int a = 0;
a++;
return a;
}
§
¥
¦
この関数は、何度呼んでも 1 を返す。しかし、ローカル変数 a に static キーワードをつけると
¨
int
func(void){
static int a = 0;
a++;
return a;
}
§
¥
¦
この関数の返り値は呼ばれるたびに 1,2,3,· · · と値が増えていく。これは、関数の呼び出し回数を数えたい
が、さりとてそのカウンタをグローバル変数に取りたくない場合などに良く使われる。
しかし、こういう static な変数を持つ関数は、内部状態を持つことになる。内部状態をもつ関数は、同
じ引数を与えても異なる値を返す可能性がある18 。これは、バグが発生した場合の問題箇所の特定を困難
にするため、あまり多用すべきではない。
C 言語における static キーワードの重要な使い方をもう一つ上げておこう。それは、大きな配列をロー
カル変数として定義する場合である。たとえば、
¨
void
func(void){
double temp[100000000];
//必要な処理
}
§
¥
¦
のように、一時的に必要な大きな配列をローカル変数で定義すると、その配列用のメモリもスタックに確
保される。一般にヒープ領域に比べてスタック領域は小さいため、あまり大きな配列を宣言しようとする
と実行時にスタックオーバーフローを起こしてプログラムが異常終了する。数値計算において、小さい規
模の計算を行っている時には正しく動作していたのに、大きくしたときに core を吐いて死ぬようになった
などのときには、この種の問題を疑う必要がある。
これを防ぐには、static キーワードをつけてヒープに確保するか、もしくは配列を動的に確保する。
¨
void
List 5: 静的に確保
18 同じ引数を与えたら必ず同じ値を返す関数を「参照透過性がある」と呼ぶ。詳しくは
17
¥
Haskell などの関数型言語を参照のこと。
func(void){
static double temp[100000000];
//必要な処理
}
§
¦
List 6: 動的に確保
¨
void
func(void){
double *temp = new double[100000000];
//必要な処理
delete [] temp;
}
§
静的に確保した場合、配列の値は関数を抜けても保持されるので十分注意しなくてはならない。動的に確
保した場合には、関数を抜ける際に確保したメモリを解放する処理を忘れないこと。忘れるとメモリリー
クを起こしてシステムが不安定となる。
なお、Fortran はデフォルトですべての変数がヒープに確保されるため、この種の問題は生じない。た
だし、関数を抜けた時に、変数の値が保持されるかどうかは処理系に依存するため、変数の値が残ること
を利用したコードを書くべきではない19 。
19 Fortran で関数内のローカル変数の再利用を行うコードは、特に 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 は引数の絶対値を返すメソッドを持っている。
¨
class Abs {
List 9: 絶対値を扱うクラス
¥
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 メソッドである。次のようなクラスを考えよう。
¨
class Class {
public int value;
}
§
List 10: フィールドが公開されたクラス
¥
¦
このクラスは、フィールド value を持つ。この変数は公開されているため、外部から
¨
Class obj = new Class();
obj.value = 2;
§
¥
¦
などのように直接値を代入することが許される。
ここで、ある理由によってこのフィールドは正の値しか許されないとしよう。フィールドを公開してし
まうと、この条件を保証できない。そこで、次のようにする。
¨
class Class {
private int value;
public void setValue(int v) {
if (v<0) {
List 11: カプセル化の例
¥
余談コラム 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 と「クラス名. クラス変数名」とすれ
ばよい。クラス変数は、インスタンス変数と同様に扱うこともできるが、全てのインスタンスについて値
を共有するので注意が必要である。
21 Ruby は 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 修飾子がついたメソッドはクラスメソッドとなり、インスタンスを作らず
とも使うことができる。クラスメソッド内では、インスタンス変数やインスタンスメソッドにアクセスす
ることができない。
¨
class Class {
static int class_variable;
int instance_variable;
List 13: クラスメソッドの例
¥
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 アプレットをスタンドアローンプログラ
ムとしても実行できるようにするのによく使われる。
¨
import javax.swing.*;
List 14: アプレットを単独でも実行可能にする
¥
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
private
private
private
int
int
int
int
left;
top;
width;
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 ではないメンバ、メソッドをすべて受け継ぐ。以下は継承の例
である。
25 Fortran は古い言語であるため、たびたび大幅な拡張がなされた。特に F90 に大幅な近代化が行われ、モジュールの導入によ
りカプセル化が可能となったが、継承と動的結合は導入されなかった。現時点で最新の仕様である Fortran 2003 には継承と多態性
が導入され、本格的なオブジェクト指向化がなされたようだ。
28
¥
¦
¨
class SuperClass {
protected int value;
protected void printValue() {
System.out.println(value);
}
}
List 16: 継承の例
class SubClass extends Superclass {
void method () {
System.out.print(value); //親クラスのフィールドを使うことができる
printValue(); //親クラスのメソッドも使うことができる
}
}
§
¥
¦
複数の親から機能を継承できることを多重継承、1つの親しか持つことができないことを単一継承と呼
ぶ。C++は多重継承、Java は単一継承である。多重継承は強力な表現手段を提供するが、コードが混乱
し、バグが潜みやすいなどの問題がある。そこで Java は単一継承を選択するかわりに interface という方
法を使って多重継承の機能の一部を実現している。
6.3
オーバーライド
スーパークラスと同じ名前のフィールドをサブクラスで定義した場合、スーパークラスのメンバは隠蔽
される。このとき、スーパークラスの値にアクセスするためには super キーワードを用いる。また、自分
の変数であることを明示的に指示する場合は this キーワードを用いる。以下に例を挙げる。
¨
class SuperClass {
int value = 1;
}
List 17: メンバの隠蔽
¥
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
this.value
super.value
value: int
SubClass
12
10
3
this.value
super.value
6
value: int
図 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 。また、この方式ではエラーに関する情報が強く制限されてしまう。プログラマに通知され
28 Windows でプログラムを組んでみると分かるが、これはかなり頻繁に起きる。エラーというのはどこかで問題が起きると連鎖
的に起きるものである。エラーが起きるたびにグローバル変数が書き換えられてしまうため、同じコードなのにタイミングによって
エラー番号が変わるなどということが起きる。このような場合のデバッグは容易ではない。
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 はその例外が発生した状況を標準出力に出力する。
以下にランタイム例外を起こすソースコード例を挙げる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
¨
class RESample {
List 25: ランタイム例外発生例
¥
void myMethod1() {
myMethod2();
}
void myMethod2() {
myMethod3();
}
void myMethod3() {
int[] a = new int[10];
a[10] = 10; //ここで配列外アクセス例外がおきる
}
public static void main(String args[]){
RESample obj = new RESample();
obj.myMethod1();
}
}
§
¦
このソースの実行結果は以下のとおり。
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
のサブクラスである。コンテナの中でも、最上位のものはトップレベルウィンドウと呼ばれる。
例を挙げよう。
¨
import java.awt.*;
import javax.swing.*;
List 28: ウィンドウを表示するだけのサンプル
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 クラスのインスタンスを取得し、そこに追加するという処理が必要になる。先ほ
どのウィンドウにボタンをひとつ追加する例を挙げよう。
¨
import java.awt.*;
import javax.swing.*;
List 29: ボタンを配置するサンプル
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 プログラミングにおいては、ユーザーからの入力に柔軟に対応するため、イベントドリブン (Eventdriven) 型プログラミングという手法を用いる。通常の処理は上から下へ順番に流れていく (フロー型)
のに対して、イベントドリブンでは実行するとイベント待ちの状態になり、その際に起きたイベントに対
して応答を返すことでプログラムが機能する。イベントは、あるキーが押された、離された、マウスがク
リックされたなどのユーザーからの入力が主だが、一定時間たった (タイマーイベント)、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 に慣れると全ての作業がキーボードだけ
で済むためにマウスに手を移動させる必要がなくなり、机に肘がついたまま作業が行うことができる。こ
れは肩凝り防止に非常に有効である。タッチタイプができれば視線移動の回数が減り、疲労軽減効果はさ
らに高くなる。
35 C
言語などではこのような仕組みをコールバック関数と呼ばれる手法で実現する。
36 逆に言えば、マルチスレッドプログラミングをする際にはイベントディスパッチについて意識する必要があるということであ
る。特に GUI プログラムはマルチスレッドで書かれることが多いので、スレッドセーフなプログラミングをしなければならない。
39
(
)
図 5: イベント処理の仕組み。イベントが発生すると、イベントソースからあらかじめ登録されたイベントリ
スナーのイベントハンドラ (メソッド) が呼び出される。イベントハンドラにはイベント発生時の処理を記述
しておく。
8.3
インタフェース
イベントソースはイベントリスナーのオブジェクトをフィールドとして保持しておき、イベントが発生
した際にそのオブジェクトのイベントハンドラを呼び出すことでイベントを処理する。したがって、イベ
ントソースは発生したイベントを伝える相手であるイベントリスナーをあらかじめ知っている必要がある。
しかし、どんなクラスのインスタンスでもイベントリスナーとなりうるが、それらすべてのクラスがイベ
ントハンドラをメソッドとして持っていなければならない。そのためには、リスナーとなりうるクラスは
イベントハンドラをメソッドに持つようなクラスのサブクラスでなくてはならない。そこで、Listener ク
ラスというイベントハンドラメソッドを持つ抽象クラスを用意し、リスナークラスは Listener クラスから
派生してイベントハンドラをオーバーライドすればよさそうに思える。
ところが、一般にリスナークラスは別のクラスのサブクラスであることが多く、Java は多重継承を禁止
しているため、同時に Listener クラスのサブクラスとなることはできない。そこで Java はインタフェー
ス (Interface) という特別なクラスを用意することで異なる継承系統に属すクラスに共通の振る舞いを持
たせている
インタフェースは、通常のクラスと同様にメソッドとフィールドで宣言されている。
¨
interface Interface {
int VALUE = 0;
void interfaceMethod();
}
§
¥
¦
37
ただし、インタフェース内メソッドには実体はかかない。また、必ず public かつ abstract とみなされる
ため、private などのアクセス修飾子などをつけてもいけない。インタフェースのフィールドは public か
つ static かつ final とみなされる。すなわちグローバル定数となる。
インタフェースで定義されたメソッドを組み込むことを実装する (implement) と言う。インタフェー
スを実装するには、クラス宣言の直後に implements 宣言をする。
¨
class ClassName extends SuperClassName implements Interface {
¥
public void interfaceMethod(){
//Interface のメソッドをオーバーライド
}
}
§
¦
インターフェースを実装するクラスは、そのインタフェースが持っているメソッドをすべてオーバーライ
ドしなければならない。また、必ず public を指定する。なお、インタフェースは、カンマで区切ることで
何個でも実装することができる。
37 ここでは
abstract 修飾子の詳しい説明はしない。書籍を参照すること。
40
例を挙げよう。以下はボタンを押したときに行動を起こすサンプルである。
¨
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
List 30: ActionListener のサンプル
¥
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 のインスタン
スとして直接定義することができる。
¨
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
List 31: 匿名クラスの例
¥
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 この例ではメソッド中にクラスを定義している。これをローカルクラス
41
(Local Class) と呼ぶ。ローカルクラスも匿名クラス
¨
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 つのメソッドがある。このうちクリックイベントだけを受け取りたいのに、わざわざ
他のメソッドをオーバーライドするのは面倒である。この煩雑さを避けるため、二つ以上のメソッドを持
つイベントリスナーにはアダプタークラスが用意されている。アダプタークラスはリスナークラスに用意
されたメソッドに「何もしない」という実体を与えているため、必要なメソッドのみをオーバーライドす
ればよい。マウスイベントを処理する例を挙げよう。
¨
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
List 32: MouseAdapter の例
¥
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 。
Java
JVM
)
(
)
(
(
OK
(
)
)
)
paint
(
JFrame
paint
JButton
図 6: 描画の仕組み。JVM が描画の必要性を検出すると、トップレベルウィンドウに描画を依頼する。描画
はイベント処理と同じ枠組みで処理される。描画依頼がイベント通知であり、イベントリスナはウィンドウ、
paint メソッドがイベントハンドラに対応する。ウィンドウオブジェクトは、自らが管理するコンポーネント
にも描画を通知する。この描画の通知は再帰的に行われる。
すべての Swing コンポーネントは paint メソッドを備えているため、プログラマは paint メソッドを上
書きすることで描画を行うことになる。以下は描画の簡単な例である
¨
import javax.swing.*;
import java.awt.*;
List 33: paint メソッドの使い方
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 。
40 Java
仮想マシン (Java Virtual Machine) のこと。平たく言えば Java のバイトコードのインタプリタである。
のバージョンにより痕跡が残ったり残らなかったりするので注意されたい。
42 ちなみに clearRect メソッドを使えば背景色で塗りつぶしてくれるので、わざわざカレントカラーを変更しなくても良い。
43 単にダブルバッファと呼ばれることが多い。
41 これは処理系に依存する模様。Java
45
¨
import javax.swing.*;
import java.awt.*;
List 34: ダブルバッファを使わない例
¥
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 に示す。
¨
import javax.swing.*;
import java.awt.*;
List 35: ダブルバッファを使った例
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 エン
コードとは、「<」を「&lt;」などに変更することである。これにより「<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>」は勝手に「&lt;B&gt;」に変換されてしまい、出力が「<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
ClassC
Class
ClassG
ClassB
ClassD
ClassF
ClassH
ClassB ClassA
ClassC ClassD
ClassF ClassE
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 as
SuperClass」という関係が成り立つ。「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 の分離」というと、これら三つの要素を分
けて考え、実装することである。
(View)
(Controller)
(Model)
図 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
Circuit
drawWindow
drawBitmap
draw
CircuitA
CircuitA
drawLine
fillRect
....
CircuitB
WindowDraw
BitmapDraw
EPSDraw
CircuitB
draw
drawWindow
drawBitmap
AbstractDraw
draw
drawWindow
drawBitmap
drawLine
fillRect
....
drawLine
fillRect
....
drawLine
fillRect
....
図 9: リファクタリングにおけるクラス図の変化。抽象クラスや抽象メソッドは斜字体になっている。左がリ
ファクタリング前、右がリファクタリング後。
56
Container
Circuit
void drawWindow(){
WindowDraw drawer= new
WindowDraw()
for(int i=0;i<Circuits.size();i++){
Circuits[i].draw(drawer);
}
}
abstract public void draw(AbstractDraw drawer);
CircuitA
public void draw(AbstractDraw drawer){
drawer.setColor(Color.black);
drawer.drawRect(x,y,width,height);
drawer.drawString(x,y,name);
...
}
void saveAsEPS(string filename){
EPSDraw drawer = new EPSDraw();
for(int i=0;i<Circuits.size();i++){
Circuits[i].draw(dw);
}
drawer.saveToFile(filename);
}
CircuitB
public void draw(AbstractDraw drawer){
drawer.setColor(Color.red);
drawer.drawCircle(x,y,r);
drawer.drawString(x,y,name);
...
}
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);
}
図 10: リファクタリング後のコード例。
57
11
終わりに
Java 言語を題材に、「良いプログラム」を書く方法を駆け足で学んだ。ただし、いくら良いプログラム
設計手法を知っていても、実際にプログラムが組めなければ意味が無い。英文法がいくら完璧でもボキャ
ブラリが不足していれば英作文ができないように、ライブラリを使いこなせないプログラマは役に立たな
い。Java には強力なクラスライブラリが多数用意されている。興味があれば、java.util パッケージなどを
眺めてみると良い。ハッシュやベクタ、スタックなどのデータ構造や、カレンダーや通貨といった概念を
扱う有用なクラスが多数定義されている。また、Windows プログラミングを志すなら多数の API や MFC
(Microsoft Foundation Class) に精通する必要があるし、STL (Standard Template Library) を使えなけ
れば C++を使う意味はあまりない。分析、設計には UML (Unified Modeling Language) の知識が必要と
なるがほとんど触れられなかった。これも必要となったら参考書にあたって欲しい。
良いプログラムを書くための原則は「いま苦労することで後々の苦労を軽減する」ということに尽きる。
くれぐれもちょっとしたキーストロークの労を厭って地雷を埋め込むといったことの無いようにされたい。
58
Fly UP