オブジェクト指向プログラミングa 第8回


●モジュールの作り方「Javaを学ぶ 第9回 (2003年1月 )」
 ※この資料は,「Javaを学ぶ 第9回 (2003年1月 )」の内容を簡単にしたもの。「Javaを学ぶ 第9回 (2003年1月 )」
  に基づいた資料はこちら

【はじめに】

正常に動作するには,変数(フィールド)を外部から守らなくてはならない。

●「正しく動作するプログラムにおける変数のライフタイム」再掲

(1)まず,変数の設計図である「データ型」がある。「データ型」は「そのデータが何を表現しているか,何バイト使うか」
  といった情報を持っている。たとえば,int型は「データは整数を表現していて,4バイト使う」という情報を持っている。
  この「データ型」を元に,メモリ上に数バイト使用して変数が誕生する。なお,
変数(variable)はインスタンス(instance)
  とも言われる
。インスタンスは「実例」とか「実体」という意味で,「データ型のひとつひとつの実例」というニュアンス
  である。
「住宅の一生」に例えれば,家の設計図を基に,メモリという敷地の一部をいくらか使って,その上に家を建てる
  段階に当たる

(2)変数(インスタンス)の誕生した直後は,生成される毎に中に入っている値が異なる。そのため,
変数を使用する前に,適切
  な初期値で変数を初期化しないとプログラムの正しい動作が期待できない
「住宅の一生」に例えれば,住宅のライフライ
  ン(電気・水道・下水・ガス等々)をひいたり,家具を揃えたりカーペットや畳を敷いたりして住めるように仕上げる段階に
  相当する

(3)変数が初期化されたら,いよいよその変数を使う(変数に値を代入したり,変数に入ってる値を使ったり)段階に入る。この
  とき,
不正な操作で変数の値が勝手にいじられると,プログラムが正しく動作しなくなってしまう。このようなことを防ぐ
  には,変数へのアクセスできるものを「正しい操作」だけに限定して,それ以外の「不正な操作・不正かもしれない操作」
  からアクセスされるのを防がなくてはならない
「住宅の一生」に例えれば,住宅の中のものを勝手にいじられないように
  セキュリティをかため,家族のメンバーだけが入れるようにし,家族以外の部外者の侵入を防ぐようにする
,ということで
  ある。
(4)変数が役目を終えるのは,その中に入っている値を最後に参照したときである。「住宅の一生」に例えれば,家の中を最後
  に利用したときとなる。
(5)役目が終わり,もう使用されることがない変数は,消滅してもらわなくてはならない。なぜなら,
使用されることがない変数
  がそのまま居残り続けると,メモリという敷地の一部を占領しつづけることになる。このような「居残り変数」が多くなると
  他の目的のために使用できるメモリ領域(空きメモリ)が不足してしまう。このように不必要な変数がメモリ上に居残り続けて,
  空きメモリが不足する現象を「
メモリリーク(memory leak,メモリ漏れ)」と言う。メモリリークはプログラムの動作不良
  の代表的な原因である。メモリリークを防ぐために,
必要の無くなった変数には消滅してもらい,その変数が占拠していた
  メモリ領域を明け渡すようにする必要がある
Javaでは,不必要になった変数(いわば「ゴミ」)は,ガベージコレクション
  (gabage collection,ゴミ集め)という仕組みによって自動的に調べられ,自動的に消滅する
ようになっている。「住宅の一生」
  に例えれば,要らなくなった住宅を壊さずにそのまま放置すると土地を無駄使いして土地不足になるので,不必要な住宅を
  壊して敷地を更地に直し,土地を他の用途に使えるようにすることに当たる


※今回は特に,上図(3)の「適切な操作による値の変更」について解説する。フィールドは,クラスの外
部から勝手にいじられると,プログラムが誤動作することが多い。そこで,クラスの中の特定のメンバを,
クラスの外部(=他のクラス)から隠すことが出来るようになっている。
 ・こうして,「隠されたメンバ」を,無理矢理,クラスの外部(=他のクラス)から利用しようとすると,
  コンパイル時にエラーになる。
 ・もちろん,「隠されたメンバ」は,そのクラスのメソッドからは自由に利用できる。
このように,クラスに公開された部分(公開部)と,隠された部分(非公開部)が出来ると,クラスは再利用
可能な部品(モジュール)としての性質を持ってくる。Javaでは,クラスはプログラムを構成する最少のモ
ジュールと言える。今回は,複数の関連素rくらすを集めた,より高レベルのモジュールである「パッケー
ジ」についても解説する。当然ながら,パッケージもモジュールであるからには,公開部と非公開部がある。



【モジュール】





【名簿クラス】

Person型オブジェクトを登録する名簿を考えてみよう。ここでは,名簿を表すクラスをMeiboクラスと
する(下リスト Meibo.java)。
Meibo.java

※Meiboクラスのmaxnumフィールドにはfinalという指定がついている。final指定付きで宣言された
 フィールドは,それ以降値の変更ができなくなる。つまり,final指定されたフィールドは定数にな
 るわけである。これを,一般に final定数と呼ぶ。

この名簿クラスをテストするクラス
MeiboTest01.java

テストの仕方:
 Meibo.javaとMeiboTest01.javaを同じフォルダに入れて,
  > javac MeiboTest01.java
 と,MeiboTest01.javaをコンパイルする。すると,Meibo.javaも自動的にコンパイルされる
 実行は,
  > java MeiboTest01
 として,MeiboTest01クラスのmain()メソッドを実行する。実行結果は以下の通り。




【クラス外からのアクセス禁止】
 この名簿クラスのメンバが外部から勝手にいじられると,名簿クラスは正常に動作しなくなってしまう。
AttackMeibo.java

 AttackMeibo.javaをMeibo.javaと同じフォルダに入れ,以下のようにコンパイルし,実行してみよ。
  > javac AttackMeibo.java
  > java AttackMeibo
実行した結果は以下のようになる。


●クラスの外部からメンバへのアクセスを禁じるには,メンバをprivate指定する


改良版Meibo.java

このように,フィールドをprivate指定した場合,AttackMeibo.javaの5行目
  m.num = 10;
という部分は,コンパイル時に以下のようなメッセージを出してエラーになる。
AttackMeibo.java:5: num は Meibo で private アクセスされます。
 m.num = 10; // 登録数をむりやり上限値に設定してしまう
  ^
この下にもエラーメッセージが続くが,それは次節で説明する
実際の試し方は以下の通り。
  (1) まず,必ず古い3つのクラスファイル( Meibo.class,Person.class,AttackMeibo.class )を消去すること。
    ※古いクラスファイルが残っていると,javacはコンパイル済みであると勘違いして,コンパイルを省略してしまう。その結果,内容を更新したはずの
       ファイルがコンパイルされなくなり,実行時には古いクラスファイルが実行されしまうことになる。
  (2) 古い Meibo.java を消去する。
  (3) 改良版Meibo.java をダウンロードして,AttackMeibo.java と同じフォルダに入れる。
  (4) 以下のように,AttackMeibo.java を再びコンパイルする。
    > javac AttackMeibo.java

●一難去ってまた一難?
 Miboクラスの全フィールドをprivatに指定したので,Miboクラスの全フィールドは勝手にアクセスされることはなくなった。
同様に,Personクラスの全フィールドもprivate指定されているので,Personクラスの外から勝手にアクセスされることは無い。
 しかし,まだ問題が残っている。Personのage,gender,nameフィールドをprivateにしたため,MeiboクラスのprintAll()メソッ
ドで,これらのprivateなフィールド
 pa[i].age,pa[i].gender,pa[i].name
にアクセスしようとしているところ(Meibo.javaの36行目)も以下のようなメッセージを出してコンパイルエラーになってしまうの
である(!)。
./Meibo.java:36: age は Person で private アクセスされます。
if( pa[i] != null ) System.out.println( pa[i].age + ", " + pa[i].gender + ", " + pa[i].name );
                                              ^
./Meibo.java:36: gender は Person で private アクセスされます。
if( pa[i] != null ) System.out.println( pa[i].age + ", " + pa[i].gender + ", " + pa[i].name );
                                                                 ^
./Meibo.java:36: name は Person で private アクセスされます。
if( pa[i] != null ) System.out.println( pa[i].age + ", " + pa[i].gender + ", " + pa[i].name );
                                                                                       ^
つまり,次のような矛盾が起こっているのである。
 ・Personクラスとしては,全フィールドを外部からの意図しないアクセスから守るにはprivateにしなければならない
 ・一方,Meiboクラスは,Person型の各フィールド値を表示するためにそれらのフィールドのアクセスしなければならない
このように,クラスの外部から privateなフィールドにアクセスする必要があるときは,どうすればよいのだろうか?
 実は,後述する「アクセッサ」というフィールドにアクセスするための専用のメソッドを用意することで解決できる。
アクセッサについて解説する前に,クラスのメンバが公開部分と非公開部分に分かれる意味を確認しておこう。

【公開と非公開の意味】


※フィールドは必ず private に指定する
※フィールドを参照したり操作する場合は,次に説明する専用のメソッド(アクセッサ, accessor)を用意すること


【アクセッサ】

 前述したように,フィールドは原則的にprivateにして,クラスの外部からアクセスできないようにすべきである。
そのため,クラスの外部からフィールドの値を変更したり,フィールドの値を参照したい場合には,そのための専用の
メソッドを用意する。このようなフィールドにアクセスするための専用のメソッドをアクセッサ(accessor)と呼ぶ。
特に,フィールドに値を設定するアクセッサをセッタ(setter),フィールドの値を返すメソッドをゲッタ(getter)
言う。
 次に,アクセッサの例を2つのソースで示す。最初のソース(Accessor1.java,下図)では,クラスAが定義されて
おり,0以上でなくてはならないint型フィールドiを持つ(下図(1))。iはフィールドなのでprivate指定していることに
注意。クラスAには,フィールドiへアクセスするためのアクセッサが2つ用意されている。
 メソッドgetI()は,iの値を返してくれるゲッタである。このように,ゲッタの名前は「getフィールド名」という
形をしていることが多い。
 また,メソッドsetI()は,iに値を設定するセッタである。setI()は,渡された実引数の値をiに代入するが,実引数が
負の値である場合は,iの値を変更しない。このようにして,setI()は「フィールドiの値は0以上でなくてはならない」
という条件が常に成立するように,クラスAのオブジェクトを護っているのである。なお,このsetI()のようにセッタ
の名前は,「setフィールド名」という形になっていることが多い。
 下図のAccessor1.javaのmain()メソッドを見てみよう。(4)では,セッタsetI()を使い,A型オブジェクトaのフィー
ルドiの値を10に設定している。(5)では,ゲッタgetI()を使用してフィールドiの値を取り出して表示するので,10と
表示される。(6)では,またセッタsetI()を呼び出してフィールドiに-10を設定しようとしているが,setI()は負の値が
実引数に渡された場合にはiの値を変更しないので,(7)でiの値を表示しているところでは,やはり10と表示される。
Accessor1.java


 次に,今まで頻出してきたPersonクラスにprivate指定とアクセッサを追加した例を示す(Accessor2.java下図)。
ちゃんと,フィールドageとgenderはいずれもprivateになっている(下図(1),(2))。これらのフィールドへのアク
セッサが下図(3)〜(6)である。2つのセッタはいずれも実引数の値が適切なときだけフィールドに実引数の値を代
入している。talk()メソッドのなかでは,2つのセッタを利用してフィールドの値を取り出している(下図(7),(8))。
Accessor2クラスのmain()メソッドでは,2つのセッタを使って各フィールドの値を変更しているが,不正な値は
検知されて無視される(下図(9),(10))。
Accessor2.java


・Meibo.javaのPersonクラスに適切なアクセッサを設けた例を以下に示す。黄色で示したところがアクセッサの定義である。
改良版その2Meibo.java

※PersonのセッタsetAge(),setGender()では実引数の値が適切かどうかをチェックしてからフィールド
 の値をセットしていることに注意。適切な値でない場合,エラーメッセージを表示するようにしても良い
 だろう。
※Personのコンストラクタの中でもセッタを利用している。これによって,初期化値が適切かどうかのチェ
 ックができる。

アクセッサを追加した 改良版その2Meibo.java の実際の試し方は以下の通り。
  (1) まず,もし古い3つのクラスファイル( Meibo.class,Person.class,AttackMeibo.class )があったなら,それらを必ず消去すること。
    ※前述したように,古いクラスファイルが残っていると,javacはコンパイル済みであると勘違いして,コンパイルを省略してしまうためである。その結果,
       内容を更新したはずのファイルがコンパイルされなくなり,実行時には古いクラスファイルが実行されしまうトラブルがおこる。
  (2) 古い Meibo.java も消去する。
  (3) 改良版その2Meibo.java をダウンロードして,AttackMeibo.java と同じフォルダに入れる。
  (4) 以下のように,AttackMeibo.java を再びコンパイルする。
    > javac AttackMeibo.java
すると,コンパイル時に次のようにエラーが1個表示されてコンパイルに失敗する。
AttackMeibo.java:5: num は Meibo で private アクセスされます。
 m.num = 10; // 登録数をむりやり上限値に設定してしまう
  ^

 


【パッケージ】

 クラスは,小さい『モジュール』であった。関連するクラスをまとめて,『パッケージ』というより大きいモ
ジュールにまとめることができる。


●クラスのパッケージへの編入の仕方(package宣言)
 ソースファイルの先頭で
  package パッケージ名;
というようにパッケージ宣言を行う。サブパッケージに編入したい場合は,
  package パッケージ名.サブパッケージ名1;
とか,さらに階層の深いパッケージなら
  package パッケージ名.サブパッケージ名1.サブパッケージ名2;
などと宣言する。パッケージ宣言をすると,そのソースファイル内で定義されているクラスはすべて指定された
パッケージに編入される。
 パッケージ名は,
  ・javaそのものに付属しているものは java.〜javax.〜という名前
  ・個人や会社が作ったものは,会社名のURLの逆.〜という名前にすることになっている
   (例:com.kaishamei.〜)
※パッケージ宣言を行っていないソースファイルのクラスは,すべて名前のない特別な『デフォルトパッケージ』
 に編入される。

●別のパッケージ内のクラスを使う(import宣言)
 同一パッケージ内のクラスは特別に何もしなくても使用することが出来る。しかし,別のパッケージ内のクラス
を使うには,ソースファイルの先頭で
  import パッケージ名.クラス名1;
  import パッケージ名.クラス名2;
  import パッケージ名.クラス名3;
        …

という具合にimport宣言を行わなければならない。階層の深いパッケージの場合は,
  import パッケージ名.サブパッケージ名1.サブパッケージ名2.クラス名;
というように宣言することになる。基本的にimportしたい各クラスごとにimport宣言を書くのが良いとされている
が,実際にはなかなかたいへんになるので,
  import パッケージ名.*;
と書くと,そのパッケージ内の全クラスをいっぺんにimportすることができるようになっている。
 また,特にimport宣言をしなくても,java.langの下にあるクラスは自動的にimportされることになっている。
println()メソッドを使いたいときに,java.lang.System.out.println()と書かずに,System.out.println()とだけ
書けばよいのは,このためである。



【パッケージとアクセス指定】

●クラスのアクセス指定
(ex.1) class A {} // アクセス指定無しクラス
(ex.2) public class A {} // publicクラス

  指定無しクラス publicクラス
同一パッケージ内のクラスから
別のパッケージ内のクラスから ×
※つまり,publicクラスへはどこからでもアクセスできる
※ひとつのソースファイルの中でpublicなクラスはひとつだけしか存在できない

●メンバのアクセス指定
  privateメンバ 指定無し(デフォルトアクセス)メンバ publicメンバ
同一クラス内から
同一パッケージ内のクラスから ×
別のパッケージ内のクラスから × ×
※その他にprotectedというメンバアクセス指定があるがここでは省略する
※つまり,publicメンバはどこからでもアクセスできる

●図で見るprivateメンバ/デフォルトアクセスメンバ/publicメンバ


【パッケージとディレクトリ構成】

 パッケージに属するクラスは,パッケージ構成を反映したディレクトリの中になくてはならない。
たとえば,Meibo.javaのMeiboクラスとPersonクラスは,tuisjava.comというドメインを所有して
る会社が開発した人事評価用ソフトの一部だとしよう。この2つのクラスを,com.tuisjava.jinjiとい
うパッケージにまとめる場合,「com」ディレクトリの中の「tuisjava」ディレクトリの中の「jinji」
というディレクトリの中に,Meibo.classとPerson.classを置かなくてはならない(下図Fig.10参照)。
 このとき,comディレクトリを置くディレクトリ(名前は自由につけても良いが,Fig.10の例では
myclassesという名前になっている)
の場所を,コンパイラコマンドjavacや,実行コマンドjavaに,
知らせてやる必要がある。その方法には2種類あるが,それについては後ほど解説する。


では,実際に下記の演習でMeibo.javaをパッケージ化してみよう。

●演習
 手順0)先ほど作成した
      Meibo.java,Meibo.class ,Person.class,MeiboTest01.class
     を削除せよ。これらは,パッケージ化する前のものなので,もう必要がない。
     (というよりも,有ると以降の演習の邪魔になる)

 手順1)下図(Fig.10a)のように,「マイドキュメント」(Z:¥MyDocuments)ディレクトリの下に,
     myclassesディレクトリ以下,jinjiディレクトリまでを作成せよ(綴りを間違えないように
     気をつけること)


 手順2)Person.java(package宣言無しバージョン)Meibo.java (package宣言無しバージョン) を,
     上図のjinjiディレクトリに保存し,下図のように
      ・それぞれ1行目にpackage宣言を書き加え
      
Personクラス自体とのPersonのコンストラクタをpublicに指定
      
Meiboのコンストラクタと,put()メソッド,printAll()メソッドをpublicに指定して
     
保存し直せ。






今,状況は次のようになっている。



 手順3)先ほどのMeiboTest01.java (import宣言無しバージョン) に,下図(1)のように1行目にimport宣言を付けて
     保存する。保存場所はどこでもいいが, Z:¥ooprog など,いつもjavaのプログラムを作成しているディレク
     トリでよだろう。


      次に,このMeiboTest01.javaをコンパイルするわけだが,このプログラムはimport宣言にあるように,
      com.tuisjava.jinji パッケージのMeiboクラスを利用する。
      前述したように,パッケージ化したクラスを利用するには,コンパイル時とプログラム実行時に,
       ・パッケージの起点となるディレクトリ(Fig.10の例ではmyclassesディレクトリ)の場所
     を,javacコマンドや,javaコマンドに教えておく必要がある。(ちなみに,コンパイルやプログラムの実行時
     に必要になるクラスの場所を示すファイルパス(file path name)のことを,クラスパス(class path)と呼ぶ。ここ
     で指定するパッケージの起点となるディレクトリの場所もクラスパスの一種である)
      パッケージの起点となるディレクトリの場所をjavac/javaコマンドに教える方法には,次の2つがある。
       i) コンパイル時やプログラム実行時に,そのつどパッケージの起点となるディレクトリの場所を指定する
       ii) 環境変数CLASSPATHに,パッケージの起点となるディレクトリの場所を追加する
     ここでは,i)の方法を紹介する。(ii)の方法についてはこちらを参照せよ)

     コンパイルは,次のように行う。
       (1) コマンドプロンプトを表示して,MeiboTest01.java を保存したディレクトリにcdコマンドで移動。
         例:もし,MeiboTest01.java を Z:¥ooprog に保存しているなら,
            > cd Z:¥ooprog
           とする。
       (2) 次のように,パッケージの起点となるディレクトリの場所を指定してMeiboTest01.java をコンパイルする。
         > javac -cp "Z:\MyDocuments\myclasses" MeiboTest01.java
               ※-cpは,javacコマンドのクラスパスを指定するためのオプションである。javacコマンドのオプションについては
                 > javac -help
                とすれば概要が表示される。

     コンパイルがうまくいかない場合は,
       ・ファイル配置が,手順2のFig.10のようになっているか(ディレクトリ名の間違いなどにも注意)
       ・Meibo.javaに,正しくpackage宣言が書かれているか
       ・MeiboTest01.javaに,正しくimport宣言が書かれているか
     を確認せよ。

     コンパイルがうまくいくと,Meibo.classとPerson.classが jinji ディレクトリの中に出来ているはずであるので,
     確認せよ(下図)。


     ここで起こったことを順序立てて書くと次のようになる。
      1)javacコマンドがMeiboTest01.javaをコンパイルするとき,必要になるMeiboクラスが見つか
        らないので,-cpオプションで指定されたファイルパスによりパッケージ階層の起点であるディ
        レクトリ(この場合,Z:\MyDocuments\myclasses)を割り出す。
      2)次いで,パッケージ階層の起点ディレクトリ(myclasses)とimport宣言の内容から
          Z:\MyDocuments\myclasses\com\tuisjava\jinji
        ディレクトリにクラスファイルMeibo.classがあるのではないかと推測する。
      3)しかし,そのjinjiディレクトリ内には,コンパイル済みのMeibo.classはまだ無い。そこで
        javaコンパイラは,Meiboクラスを定義しているはずのソースファイルMeibo.javaを探す。そ
        うして,jinjiディレクトリ内にMeibo.javaを見つけたjavaコンパイラは,Meibo.javaを
        コンパイルしてjinjiディレクトリの中に,Meibo.classとPerson.classを生成したのである
        (上図の矢印)。Personクラスについても同様である。

   手順6)手順の5でコンパイルしたMeiboTest01.classを実行してみよう。コマンドプロンプトの現在位置
       (カレントディレクトリ)にMeiboTest01.classがあることを確認せよ。実行コマンドは以下のよう
       になる。クラスパスに,MeiboTest01.classの在処であるカレントディレクトリ(.)を追加している
       ことに注意。なお,カレントパスの区切り文字はWindowsではセミコロン(;),Unixではコロン(:)が
       使われる。
        > java -cp "Z:\MyDocuments\myclasses;." MeiboTest01
               ※-cpは,javaコマンドのクラスパスを指定するためのオプションである。javaコマンドのオプションについては
                 > java -help
                とすれば概要が表示される。