戻る
前章では「クラスはデータ型である」というようにクラスをユーザー定義型の観点から説明した。
一方、クラスはオブジェクト指向プログラミングと大いに関係がある。
なぜなら、オブジェクト指向プログラミングは、クラスを単位にプログラムを構築していくプログラミングの方法論だからである。
オブジェクト指向プログラミングの主な特色は次の3つである。
・ データの隠蔽
・ 継承
・ 他態性
これらの特色のうち、本章では「データの隠蔽」について説明する。
プログラムを利用していくうちに業務が拡大するなどして、クラス内部のデータ構造を変更する必要が出てくる事がある。
その時、フィールドの型を変更したり、新しいフィールドを追加したりといった変更が発生する。
そのようなときは、当然ながらプログラムを書き換えなければならない。
その「書き換えの手間を最小で済ませ、安全に行うにはプログラムをどのように書けばよいか」という事を探っていく。
データの隠蔽
オブジェクト指向プログラミングの特色の1つ目であるデータの隠蔽は、プログラムを保守する手間を小さくする事を目的としている。
ここでいう「データ」とは、フィールド(クラス内部の変数)の事である。また、「隠蔽」というのは隠す事である。
何を隠すのかというと「フィールドを隠す」のであり、どこから隠すのかというと「クラスの外から」隠すのである。
つまり、データの隠蔽とは、「フィールドをクラスの外から隠す」事を言う。そのためには、次のような原則でプログラムを書く。
・ クラスの利用側は、クラスのフィールドを参照しない
・ クラスの利用側が参照するのはメソッドだけ
これがオブジェクト指向プログラミングで言うところのデータの隠蔽である。
もう少し具体的に説明する。例えば、次のようなクラスAがあったとする。
class A {
int value; ・・・・・・・・・・・フィールド
public void show( ) { ・・・メソッド
…
}
}
このクラスAを使う時には、次の点に注意してプログラムを書くようにする。
A a = new A( ); ・・・・・aはクラスAのインスタンス
a.value = 100; ・・・・・・×(フィールドを参照している)
a.show( ); ・・・・・・・・・○(メソッドを参照している)
このようにクラスの利用側は、クラスのフィールドを参照せず、メソッドだけを使うようにプログラムしていくのがデータの隠蔽である。
では、何故そのような制限をするのが良いのだろうか。
何故、メソッドを参照するのは良いが、フィールドを参照してはいけないのだろう。
それは、プログラムの書き換えをクラスの内部だけで済ませるようにするためである。
・ プログラムの変更は、クラスの内部だけを書き換える
・ クラスを利用する側は変更しない
例えば、次のように2つのフィールドを持つクラスBlackBoxがあったとする。
class BlackBox {
int field_1; ・・・・・フィールド1
int field_2; ・・・・・フィールド2
…
}
そして、データ隠蔽の原則を無視して、クラスの外からフィールドを参照したとする。
例えば、次のように2つのフィールドの値を使って何かを計算したとする。
BlackBox b = new BlackBox( );
…
…
int result = b.field_1 + b.field_2; ・・・・・直接フィールドを参照して計算する
しばらくしてクラスBlackBoxの使用を変更する必要が生じたとし、その結果、第3のフィールドが追加されたとする。
class BlackBox {
int field_1;
int field_2;
int field_3; ・・・フィールドを追加
…
}
この変更に伴い、先の;計算も変更しなければならない。
int result = b.field_1 + b.field_2;
↓ クラスの使用が変わるとクラスの利用側も変更しなければならない
int result = b.field_1 + b.field_2
+ b.field_3;
このようにクラスの外で直接フィールドを参照していると、後でクラスの変更があった場合にその部分の書き換えが必要になる事がある。
もしこのような変更を必要とする箇所がプログラムのあちこちに散在していたら―これは大変な手間となる。また、それだけ誤りを招く事になる。
プログラマーはそれら全ての部分を間違いなく書き換えなければならない。
…
…
b.field_1++; ←
… プログラムに散在するフィールドを参照している部分を
… 全て見つけて書き換えなければならない
x = b.field_1 * b.field_2; ←
…
メソッドを参照する
フィールドを参照する代わりにそのフィールドを参照しないでも済むようなメソッドを提供すると、
クラスの仕様が変更になってもプログラムの書き換えをクラスの内部だけにとどめる事が出来る。
例えば前項の例なら、プログラムの外で計算するのではなく、計算を行うメソッドを提供してやれば良いのである。
class BlackBox {
int field_1;
int field_2;
…
…
public int clac( ) { ・・・・・このメソッドを追加
return field_1 + field_2;
}
}
そして、クラスBlackBoxを利用する側は直接フィールドを参照する代わりにこのメソッドcalcを呼び出すようにする。
BlackBox b = new BlackBox( );
…
…
int result = calc( ); ・・・・・・・・・・・フィールドではなく、メソッドを参照
こうすればクラスBlackBoxの仕様が変更されても、この部分は変更する必要が無い。
クラス内部のメソッドの中身だけを変更すれば良いのである。
class BlackBox {
int field_1;
int field_2;
int field_3; ・・・フィールドを追加
public int calc( ) {
return field_1 + field_2 + field_3; ・・・・・ここだけを変更
}
…
…
}
この変更があってもメソッドの呼び出し方法は変わらないから、クラスを利用する側は書き換える必要が無い。
BlackBox b = new BlackBox( );
…
…
int result = calc( ); ・・・・・ここは元のまま
これが、フィールドを参照せず、メソッドだけを使うメリットである。
オブジェクト指向プログラミングのデータの隠蔽は、クラスの内部のデータをブラックボックス化し、プログラムの書き換えを容易にする事が出来る。
メンバーに対するアクセス制御(1)
前項では、データの隠蔽について説明した。すなわち
・ クラスを利用する時は、フィールドを参照してはいけない
・ 代わりにメソッドを参照する
ように説明した。これを守るか否かは、プログラマーの意思にかかっている。
ところが、これをプログラムで強制する事が出来る。それが以下で説明するアクセス制御である。
アクセス修飾子
アクセス制御というのは、アクセス修飾子というキーワードを使って名前が使える範囲を制御するものである。
アクセス修飾子には、次のものがある。
private ・・・・・この修飾子で修飾されたメンバーは他のクラスから参照する事は出来ない。(そのメンバーを外部に非公開にする)
public ・・・・・この修飾子で修飾されたメンバーは他のクラスから参照する事が出来る。(そのメンバーを外部に公開する)
これらのアクセス修飾子をクラスのメンバーにつけることにより、そのメンバーを外部に公開したり、非公開にする事が出来る。
例えば、次のようなフィールドがあったとする。
int value;
この宣言の前にprivateをつけると、このフィールドを外部に対して非公開にする事が出来る。
その場合、クラスの外からこのフィールドを参照しようとするとコンパイルエラーになる。
private int value; ・・・・・クラスの外から参照出来ない。
public int value; ・・・・・・クラスの外からも参照できる。
以上をテストする為のサンプル
サンプル;Private.java
//
//Private.java---public,privateのテスト
//
class BlackBox {
private int n1;
public int n2;
private void method1( ) {
System.out.println("method1");
}
public void method2( ) {
System.out.println("method2");
}
}
public class Private {
public static void main(String[ ]
args) {
BlackBox b = new BlackBox( );
b.n1 = 100;
b.n2 = 200;
b.method1( );
b.method2( );
}
}
ここに出てくるクラスBlackBoxには、2つのフィールドと2つのメソッドがある。
class BlackBox {
private int n1; ・・・・・privateフィールド
public int n2; ・・・・・・publicフィールド
private void method1( ); { ・・・・・privateメソッド
System.out.println("method1");
}
public void method2( ) { ・・・・・publicメソッド
System.out.println("method2");
}
}
このうち、n1とmethod1はprivateメンバーなので、外部のクラスから参照しようとするとエラーになる。
しかし、n2とmethod2はpublicメンバーなので、外部のクラスから参照する事が出来る。
b.n1 = 100; ・・・・・これは×
b.n2 = 200; ・・・・・これは○
b.method1( ); ・・・・・これは×
b.method2( ); ・・・・・これは○
次にこのアプリケーションをコンパイルしたところを示す。
$ javac Private.java
private.java:22: n1 は BlackBox で
private アクセスされます。
b.n1 = 100
^
private.java:25:method1( ) は BlackBox
で private アクセスされます。
b.method1( );
^
エラー2個
$ _
n1とmethod1を参照している箇所でコンパイルエラーになっているのが分かる。
データ隠蔽を守るために
アクセス修飾子を適切に指定する事により、データ隠蔽を推進する事が出来る。
・ クラスの利用側は、クラスのフィールドを参照しない
・ クラスの利用側が参照するのはメソッドだけ
この原則を強制するためには、アクセス修飾子を次のように使えば良い。
・ フィールドは外部の参照から保護するためにprivateメンバーにする
・ メソッドは、外部の参照を許可するためにpublicメンバーにする
例えば、前項のクラスBlackBoxの場合は、アクセス修飾子を次のよう;に使う。
class BlackBox {
private int n1; ・・・フィールドはprivateメンバーにする
private int n2;
public void method1( ) { ・・・メソッドはpublicメンバーにする
System.out.println("method1");
}
public void method2( ) {
System.out.println("method2");
}
}
ただし、メソッドについては例外がある。
ヘルパーメソッド
一般的にはメソッドは外部に公開するので、publicメンバーにする。しかし、中には非公開にするメソッドもある。
あるメソッドの下請け処理を担当するメソッドで、そのメソッドを外部に公開する必要が無い場合である。
例えば、次のようなメソッドがあったとする。
public void big_method8 ) {
処理1
処理2
…
…
処理n
}
このbig_methodはかなり大きなメソッドで、内部でたくさんの処理をしている。
このような時は、それぞれの処理を外部に独立させ、big_methodはそれらのメソッドを呼び出すようにする。
public help_1( ) { ・・・このメソッドは処理1に専念する
処理1
}
public help_2( ) { ・・・このメソッドは処理2に専念する
処理2
}
…
…
public void big_method( ) {
help_1( );
help2( );
…
helpn( );
}
このように、大きなメソッドを複数のメソッドに分解する事により、big_methodをすっきりと小さくまとめることが出来る。
問題は、それぞれを独立させたhelp_1、help_2、…である。
これらのメソッドはbig_methodの手伝いをするのが目的であり、外部から呼び出す必要は無い。
したがって、これらのメソッドはpublicではなく、privateめメンバーにする。
private help_1( ) { ・・・このメソッドは公開しない
処理1
}
…
…
private help_n( ) {
処理n
}
このように、あるメソッドの下請け処理を担当するメソッドをヘルパーメソッドという。
コンストラクタ
1つのクラスは、全体としてある「固有の処理」を担当する。例えば、Stringクラスは「文字列の処理」に専念する。
その為クラスを使う前に何らかの初期化―例えばフィールドの初期とを与えるといった処理が必要になる。
このような初期化を行うには、初期化専用のメソッドを作り、それを最初に呼び出せばよい。
class Foo { ・・・あるクラス
…
…
void init( ) {
…
}
}
Foo f = new Foo( ); ・・・Fooのインスタンスを生成
f.init( ); ・・・・・・・・・・・・最初に初期化を行う
しかし、もしプログラマーがクラスFooを使うにあたってinitを実行するのを忘れていたらどうなるだろう。
そのインスタンスは、初期化が行われていないので、悲惨な結果になるかもしれない。
そこで、javaではこのような事は行わない。なぜならクラスの初期化を自動的に行うための仕組みのコンストラクタが用意されているからである。
コンストラクタの自動実行
コンストラクタは、特殊なメソッドである。コンストラクタはメソッドなので、どんなコードを書いても良い。
しかし、通常はその中にあるクラスを初期化する為のコードを書く。
したがって、コンストラクタを実行することによって、オブジェクトを初期化する事ができる。
コンストラクタが他のメソッドと異なる点は、それが自動的に実行されるという事である。
クラスのコンストラクタを作る時、newを実行する。
Foo f = new Foo( ); ・・・Fooのインスタンスを生成する
コンストラクタは、このnewを実行する時に呼び出される。逆にコンストラクタを普通のメソッドと同じように呼び出してはいけない。
f.コンストラクタ( );
これは、エラーになってしまう。
コンストラクタの定義
コンストラクタはメソッドの一種であるが、普通のメソッドと同じように定義することが出来る。ただし、一般的なメソッドとは次の点で異なる。
・ コンストラクタの名前はクラス名と同じにする。
・ メソッドの型は指定しない。(voidもつけない)
・ ただしpublic修飾子は必要。(publicをつけなくてもエラーにはならない)
通常のメソッドは、自由な名前を付けることが出来るが、コンストラクタの名前はクラス名と同じにしなくてはならない。
コンストラクタは、return文で値を返す事が出来ない。
そもそもコンストラクタは自動的に実行されるので、値を返してもそれを受取る事が出来ないからである。
したがってコンストラクタの型はvoidである。ただし、コンストラクタを定義する時は、voidは指定しない。
例えば、次はAutoという名前のクラスのコンストラクタを定義したものである。
class Auto {
public Auto( ) { ・・・クラス名と同じ
… ・・・・・・・・・・・・ここに初期化のためのコードを書く
…
}
}
実際にコンストラクタを定義し、それが自動実行される様子を見るためのサンプルアプリケーションを次に示す。
サンプル:AutoTest.java
//
//AutoTest.java---コンストラクタの自動実行をテストする
//
class Auto {
public Auto( ) {
System.out.println("コンストラクタAuto");
}
public void use( ) {
System.out.println("クラスAutoを使う");
}
}
public class AutoTest {
public static void main(String[ ]
args) {
Auto a = new Auto( ); ・・・クラスAutoのインスタンスを生成し
s.use( ); ・・・・・・・・・・・・・・メソッドuseを実行
}
}
このアプリケーションは、クラスAutoのインスタンスを生成し、メソッドuseを実行しているだけである。
しかし、メソッドuseが実行される前にコンストラクタが実行される様子を見る事が出来る。
実行例
$ java AutoTest
コンストラクタAuto ・・・・・この表示はコンストラクタのもの
クラスAutoを使う ・・・・・・この表示はメソッドuseによるもの
$ _
引数を取るコンストラクタ
コンストラクタは、型を指定する事は出来ないが、引数を指定する事はできる。
例えば次は、int型の引数を取るコンストラクタFooを定義したところである。
public Foo(int n) { ・・・下線部は引数を取っている
…
}
このようなコンストラクタに引数を渡すには、newを実行する時に引数を指定する。
Foo f = new Foo(100); ・・・この引数がコンストラクタに渡される。
次に引数を取るコンストラクタを使ったサンプルアプリケーションを示す。
サンプル:ConsArg.java
//
//ConsArg.java---引数を取るコンストラクタ
//
class Foo {
private int value;
public Foo(int n) {
value = n;
}
public void show( ) {
System.out.println("値="
+ value);
}
}
public class ConsArg {
public static void main(String[ ]
args) {
Foo f1 = new Foo(100); ・・・・・・・f1のコンストラクタには100を渡す
Foo 2 = new Foo(999999); ・・・・・f2のコンストラクタには999999を渡す
f1.show( );
f2.show( );
}
}
実行例
$ java ConsArg
値=100
値=999999
$ _
コンストラクタの多重定義
1つのクラスに複数のコンストラクタを定義する事が出来る。これをコンスラクタの多重定義、あるいはコンストラクタのオーバーロードという。
コンストラクタを多重定義すると、いろいろな方法でインスタンスを初期化する事が出来る。
2つのコンストラクタを持つクラス
例えば次は、valueという名前のフィールドを持つクラスFooである。
class Foo {
int value; ・・・・・フィールド
…
…
}
このクラスを2つの方法で初期化出来るようにする。1つは直接int型のデータを与える方法である。
そのため、次のコンストラクタを定義する。
public Foo(int n) { ・・・・・引数を1つ取るコンストラクタ
value = n; ・・・・・・・・・・与えられた引数でフィールドvalueを初期化する
}
いま1つは、引数が全く与えられなかった場合である。その場合は、valueの初期値としてー1を与える事にする。
そのため、次のコンストラクタを定義する。
public Foo( ) { ・・・・・引数を取らないコンストラクタ
value = -1; ・・・・・・−1でvalueを初期化する
}
つまり、クラスFooは次の2つのコンストラクタを持つ事になる。
class Foo {
private int value; ・・・・・フィールド
public Foo( ) { ・・・・・・・引数を取らないコンストラクタ
value = -1;
}
public Foo(int n) { ・・・・引数を1つ取るコンストラクタ
value = n;
}
…
…
}
コンストラクタの選択
1つのクラスに複数のコンストラクタがあるとき、どちらのコンストラクタが実行されるかは、インスタンスを生成する時に与える引数で決まる。
例えば次のようにf1,f2の2つのインスタンスを作ったとする。
Foo f1 = new Foo( );
Foo f2 = new Foo(100);
f1の法は引数を与えていないので、引数を取らないコンストラクタFoo(
)が実行される。
また、f2の方は整数の引数を与えているので、引数を1つ取るコンストラクタFoo(int
n)が実行される。
このように1つのクラスに複数のコンストラクタがあるとき、その区別は引数によって行われる。
したがって、コンストラクタを多重定義する時は、次のように引数によって区別されるようにしなければならない。
・ 引数の数を変える。
・ 引数の数が同じなら、型を変える。
コンストラクタの多重定義をテストする為のサンプルアプリケーション
サンプル:ConsArg_2.java
//
//ConsArg_2.java---コンストラクタの多重定義
//
class Foo {
private int value;
public Foo( ) {
value = -1;
}
publicFoo(int n) {
value = n;
}
public void show( ) {
System.out.println("値="
+ value);
}
}
public class ConsArg_2 {
public static void main(String[ ]
args) {
Foo f1 = new Foo( );
Foo f2 = new Foo(100);
Foo f3 = new Foo(999999);
f1.show( );
f2.show( );
f3.show( );
}
]
実行例
$ java ConsArg_2
値=-1 ・・・・・・・引数無しのコンストラクタが実行された
値=100 ・・・・・・引数を1つ取るコンストラクタが実行された
値=999999 ・・・これも引数を1つ取るコンストラクタが実行された
$ _
デフォルトコンストラクタ
1つのクラスに定義できるコンストラクタの数は自由である。
複数のコンストラクタを持つクラスもあれば、コンストラクタを1つも持たないクラスもある。
クラスにコンストラクタを1つも持たないときでも、自動的にコンストラクタが作られる。これをデフォルトコンストラクタという。
デフォルトコンストラクタは、引数の無いコンストラクタである。
たとえばClassnameというクラスにコンストラクタが無い場合、次のコンストラクタが自動的に作られる。
public Classname( ) { ・・・・・引数はない
}
このクラスのインスタンスが作られる時は、この空のコンストラクタが実行される。
デフォルトコンストラクタの処理内容は、フィールドに初期値を与える事である。その初期値は次のようになっている。
| データ型 | 初期値 |
| 数値型 | 0 |
| 論理地 | false |
| クラス型 | null |
もし1つでもコンストラクタが定義されると、もはやそのクラスにはデフォルトコンストラクタが作られない。
class Foo {
private int value;
public Foo(int n); { ・・・コンストラクタ
value = n;
}
public voud show( );
System.out.println("値="
+ value);
}
}
例えば、このクラスFooの中には、明示的なコンストラクタが定義されている。したがって、Fooには、デフォルトコンストラクタは定義されない。
このためクラスFooのインスタンスを生成する時、次のように(デフォルトコンストラクタを使うつもりで)引数を指定しないとコンパイルエラーになる。
public static void main(String[ ] args)
{
Foo f = new Foo( ); ・・・・・引数を指定しない
}
この場合のコンパイルエラーを次に示す。
$ javac ConsArgs_11.java
ConsArg_11.java:18: シンボルを解釈処理できません。
シンボル: コンストラクタ Foo ( )
位置 : Foo のクラス
Foo f = new Foo( );
^
エラー1個
$ _
次へ