Step 8: データの隠蔽とカプセル化

さて、最後になります。
なんかずいぶん長かったよね?(いつも通りこの色は美樹ちゃん)
ほほほほ、あまり気にしてはいけない。さっさと始めよう!
は〜い。 (ごまかしてる、ごまかしてる...)
オブジェクト指向プログラミングというと、再利用というのがあります。これは、いちいち全てを始めから作るのではなく、以前作ったものや誰かが作ったもので利用できるモジュールは再利用する。このことで、開発期間を短縮するというメリットがあります。ここで重要になるのがデータの隠蔽とカプセル化です。
不必要な情報を隠して簡単かつ安全に利用できるようにするわけでしょう?
その通り。あとは、カプセル化によってブロックで組み立てるようにプログラミングできるようになれば理想的だね。
C 言語のライブラリーもそれなりに部品化されているような気がするけどどうなの?
そうだね、C や Pascal といった言語では、複雑な処理を簡単な処理に分解して、それぞれを関数や手続きに分担させているよね(その関数の集まりがライブラリー)。このプログラミングスタイルでは、これらの部品を個々に作成、試験してからそれらを組み立てて全体のプログラムとなります。そのおかげで、複数のプログラマが各部品(関数)を分担して作れるので大きくて複雑なプログラムの作成も可能になりました。しかし、各プログラマが作ったプログラムの部品間で受け渡すデータの構造はなどはプログラマの間で頻繁な話し合いが必要で、構造が変化した場合その度に関わる部品を全て作りなおし、全体の動きを再度試験しなおさなければならず効率的とはいえません。そこを、Javaのようなオブジェクト指向言語によるプログラミングでは、処理ではなくデータを中心として部品化するので、ある程度自由な部品が作れるし、データの構造が変化しても各部品のつながりに変化がないので部品を入れ替える感覚で修正がすみます。
そうか、全体に悪い影響が出ないように悪い所や拡張したい所だけを入れ替えられるような感覚で部品化することが重要なのね。
そういうこと。その時に、部品にある程度アクセスを制限したり、むやみに不必要な情報を漏えいしないようにする仕組みが必要になってきます。

Section 1: パッケージ

クラスの数が増えると、区別するために長い名前を付ける必要があったり、他人の作ったクラス名と同じ名前を付けないように注意する必要がでてきます。Javaでは、名前の衝突を避けたり、部品となるクラスやインターフェースをまとめておくために、パッケージというクラスの集まりを作ることができます。パッケージが異なれば、同じ名前のクラスが存在していてもかまわないので、簡潔な名前を使えるようなります。一般に、クラスは パッケージ名.クラス名 の形で指定できます。

特に、大規模なプログラムやクラスライブラリでは、管理や検索を容易にするためにパッケージ名を階層化します。例えば、標準クラスライブラリは先頭が java で始まり、その次に io, net, util, awtなどがきます。この時、Applet クラスはjava.applet パッケージに入っているので java.applet.Applet というふうに指定できます。パッケージの階層構造と、その中に入っているクラスの継承構造は関係ありません。自分でパッケージ名を付ける場合、インターネットのドメイン名をトップレベルドメイン(JP.co.foobar)から利用するように要求されています。しかし、ドメインを持っていなければ自分の名前でも、何らかの組み合わせでもかまいません。ただし、ユニークな名称である必要があるので、可能ならばインターネットのドメイン名を利用することをお勧めします。

Javaではプログラム実行時に必要なクラスファイルが自動的に使用されるが、検索のためにクラスファイルの位置はパッケージの階層構造に従っている必要があります。例えば、 java.applet.Applet クラスは

$CLASSPATH/java/applet/Applet.class 

を表すため、java/appletディレクトリーの下に Applet.classファイルを作って格納されています。$CLASSPATH は、コンパイルされた Java クラスやインターフェースがファイルシステム上のどこにインストールしたかを示すディレクトリーのリストです。

CLASSPATH=/usr/java/lib:/usr/local/java/lib:.

である場合、/usr/java/lib/java/applet/Applet.class, /usr/local/java/lib/java/applet/Applet.class, ./java/applet/Applet.class の順番で Applet クラスを検索します。(. は、カレントディレクトリー)

パッケージとして集められたクラスは、上記のように(サブ)ディレクトリーに配置されるだけでなく、jar という階層構造を保持したアーカイブファイルとして提供される場合もあります。クラスがどのパッケージに属するのか package というキーワードを使用して明確にしておきます。package文は、ソースファイルの先頭(コメントを除く)で一度だけ宣言することができます。

package パッケージ名;

パッケージの指定例:

package java.applet;      // このソースで宣言されたクラスは、パッケージ java.applet に属する
 や
package com.foo.shape;    // このソースで宣言されたクラスは、パッケージ com.foo.shape に属する

ピリオド "." によって階層構造をきちんと整理しています。

Package (class) ディレクトリー
package com.foo.misc;
 
public class Memo {
 ...
}
($CLASSPATH) -+- com -+- foo -+- misc  -+- Memo.class
                              |
                              +- shape -+- Shape.class
                                        |
                                        +- Circle.class
                                        |
                                        +- Rectangle.class
package com.foo.shape;
 
public class Shape {
 ...
}
package com.foo.shape;
 
public class Circle {
 ...
}
package com.foo.shape;
 
public class Rectangle {
 ...
}

現在のファイルにパッケージ化されたクラスやインターフェースをインポートするには import というキーワードを使用します。import 文もまた、ソースファイルのはじめで宣言されていなければなりません。

import java.applet.Applet;       // パッケージ java.applet に属するクラス Applet をインポート
import com.foo.shape.Circle;     // パッケージ com.foo.shape に属するクラス Circle をインポート
import com.foo.shape.Rectangle;  // パッケージ com.foo.shape に属するクラス Rectangle をインポート

個々のクラスをインポートするほかに、下記のようにすることで、すべてのクラスをインポートすることもできます。

import パッケージ名.*;

しかし、これは指定したパッケージ直下の public クラスのみなので、さらに階層がある場合複数行記述しなければなりません。

import java.awt.*;
import java.awt.image.*

プログラム中で他のパッケージのクラスを参照する場合、通常、パッケージ名.クラス名 の形で指定します。しかし、import を使用することでパッケージ名を省略することができます。以下のようなプログラムの場合、

public class HelloWorld extends java.applet.Applet {
    public void paint(java.awt.Graphics g) {
        g.drawString("Hello world!", 50, 25);
    }
}

import を使用すると

import java.applet.Applet;
import java.awt.Graphics;
 
public class HelloWorld extends Applet {
    public void paint(Graphics g) {
        g.drawString("Hello world!", 50, 25);
    }
}

というふうに記述できます。

また、何かの偶然でパッケージ内のクラス名が他のパッケージ内のクラス名と同じだった場合、クラスの初めにパッケージ名を付加して名前を明確にしなければなりません。 たとえば、自分の作った shape パッケージで Rectangle と呼ばれるクラスを定義しました。java.awt パッケージにも同様に Rectangle クラスが含まれています。もし両方の shapejava.awtの両方をインポートした場合、次の行(および Rectangle クラスを使用しようとするその他のコード)はあいまいになります。

Rectangle rect;        // あいまい

このような場合、正確にどの Rectangle クラスにするのかを明確に指定して示さなくてはなりません。

shape.Rectangle rect;    // 明確

このように、クラス名の初めにパッケージ名を付加し、ピリオドでパッケージ名とクラス名を分ける。

Section 2: アクセス制御

C言語では、プログラムのどこからでも参照できるグローバル変数と、特定の関数やブロック内でのみ参照可能なローカル変数の区別があります。 アクセス制御は、不必要な情報を隠すために必要です。C言語のローカル変数にあたる取り扱いは Java でも同じようになります。

C言語
Java
int gi;     /* グローバル変数 */
 
void main(int argc, char *argv[] )
{
  int li;   /* ローカル変数(関数内のみ有効) */
  while (1) {
    int bi; /* ローカル変数(ブロック内のみ有効) */
  }
}
package access; // 所属パッケージの宣言
public class AccessTest {
  /** クラス内でのみ有効なフィールド */
  private int ci;
 
  /** クラス外部にも公開するメソッド */
  public static void main(String argv[]) {
    int li;     // ローカル変数(メソッド内のみ有効)
    while (1) {
      int bi;   // ローカル変数(ブロック内のみ有効)
    }
  }
}

しかし、Java では、クラスの存在が重要な働きをするためクラスを考慮したアクセス制御が必要になります。C言語では、グローバル的な存在の関数でも、Java のメソッドやコンストラクタはアクセス制御の対象になります。また、宣言した場所でアクセス可能な有効範囲が決定されるのではなく、もっと柔軟なアクセス制御が必要になります。そこで、クラス、メソッドと変数に対する他のクラスからのアクセスレベル(可視性)を示すために、private、protected、public というキーワードが用意されています。これらをうまく使用することで、無節操な外部クラスからデータをしっかり隠すことができます。

private 宣言したクラスのみ
protected 宣言したクラス、宣言したクラスのサブクラス、宣言したクラスと同一のパッケージ内のクラス
public すべてのクラスからアクセスできます。
指定無し(friendly) 宣言したクラス、宣言したクラスと同一のパッケージ内のクラス

サンプルプログラムを使ってアクセス制御を確認してみます。

packeage access;
public class Access {
  private   int pri;
  protected int pro;
  public    int pub;
            int def;
}
packeage access;  // 同一パッケージ
public class AccessTest {
  void accessMethod() {
    Access data = new Access();
    data.pri = 10;   // エラー
    data.pro = 10;   // OK
    data.pub = 10;   // OK
    data.def = 10;   // OK
  }
}
packeage access;  // 同一パッケージ
public class AccessTest extends Access {
  void accessMethod() {
    Access data = new Access();
    data.pri = 10;   // エラー
    data.pro = 10;   // OK
    data.pub = 10;   // OK
    data.def = 10;   // OK
  }
}
packeage access2;  // 異なるパッケージ
import access.Access;
public class AccessTest {
  void accessMethod() {
    Access data = new Access();
    data.pri = 10;   // エラー
    data.pro = 10;   // エラー
    data.pub = 10;   // OK
    data.def = 10;   // エラー
  }
}
packeage access3;  // 異なるパッケージ
import access.Access;
public class AccessTest extends Access {
  void accessMethod() {
    Access data = new Access();
    data.pri = 10;   // エラー
    data.pro = 10;   // OK
    data.pub = 10;   // OK
    data.def = 10;   // エラー
  }
}

サンプルプログラムでは、フィールドについて例を挙げましたがメソッドやクラスに対しても同じことが適用されます。これらをまとめると以下の表になります。

  
宣言したクラス
パッケージ内の
サブクラス
他のパッケージの
サブクラス
パッケージ内の
クラス
他のパッケージの
クラス
private
×
×
×
×
protected
×
public
指定無し(friendly)
×
×

あるパッケージpppの中のクラスCCCprotectedを指定した変数ivが定義されているとします。

package ppp;
class CCC {
  ...
  protected int iv;
  ...
}
この変数はパッケージppp内の任意のクラスから参照することができます。
次に、パッケージpppの外にあるクラスCCCの子孫であるサブクラスSubCCCがあると します。
class SubCCC extends CCC {
  ...
  void foo(CCC objOfCCC) {
      this.iv = 1;       // これはOK
      objOfCCC.iv = 2;   // これはエラー!
  }
}
クラスSubCCCの中では、SubCCCおよびその子孫のオブジェクトの ivを参照することができますが、親であるスーパークラスCCCのオブ ジェクトのivを参照することはできません。

Section 3: 修飾子

Java では、public, protected, private といったアクセス制御をおこなう修飾子以外にも多くの修飾子があります。

3.1 static

static として宣言されたフォールドは、オブジェクトごとに生成されません。そのクラスのオブジェクトすべてに共通して利用されます。static なフィールドは、それ以外のフィールドよりも先に初期化されます。
クラス内の、static なフィールドしか扱わないメソッドは、staticメソッドとして宣言できます。staticメソッドでは、staticでないメソッドやstaticでないフィールドを利用することができません。
クラス定義内に、メソッドとは別に static な名前のないメソッドの手続きを記述することができます。これは、主に static フィールドの初期化に利用されます。

public class StaticTest {
  // static フィールド
  static int si;
 
  // 名前のない static メソッド
  static {
    si = 10;
  }
 
  // static メソッド
  static public void main(String argv[]) {
    System.out.println("si = " + si);
  }
}

クラスの定義やコンストラクタ、ローカル変数に static 修飾子を付けることには意味がありません。また、static なフィールドやメソッドは、オブジェクトを生成しなくても利用できます。

3.2 final

final は、ローカル変数やフィールドに対して宣言すると、値の変更できないことを意味します。メソッドに対して宣言すると、指定気を許可しないことを意味します。クラスに対して宣言すると、サブクラスの定義を許可しないことを意味します。

ローカル変数やフィールドに対する final 宣言は、C言語や C++言語の const を同じ機能を提供します。しかし、finalのフィールドはサブクラスで再定義できます。

定数を定義する目的で static かつ final なフィールドを宣言します。これらのフィールド名は、すべて大文字で記述することが習慣になっています。

public static final int BUFSIZ = 1024;

3.3 abstract と native

abstract と native は、いずれもクラス内に実装を持たないメソッドを宣言するための修飾子です。abstract や native なメソッドは、名前のみで実装部分を定義するブロックが存在しません。

abstract public void addMethod();
native public void callMethod();

abstractは、サブクラスでメソッドが定義され実装される予定であることを意味し、nativeは、Java以外の言語によるメソッドの実装が存在していることを意味します。

abstract メソッドを一つでも持つクラスは、抽象クラスとなります。よって、そのクラスに abstract 修飾子を付けなくてはなりません。ただしインターフェースのメソッドは常に abstract なので省略できます。

3.4 synchronized と volatile

synchronized と volatile は、複数のスレッドによって同一のデータが非同期にアクセスされ処理に混乱や矛盾が生じるのを防ぐための修飾子です。

synchronized は、メソッドや特定のオブジェクトとともに指定されたブロックを対象とします。ブロックの場合 synchronized の指定を待つ処理が終わるまで、指定されたオブジェクトにロックがかかります。synchronized の指定を待つメソッドは、そのメソッドを定義したオブジェクト自身にロックをかけ、メソッドの処理が終わるまで他のスレッドから内部のデータに同時にアクセスされることを防ぎます。

volatile は変数に対して指定します。volatile の指定を受けた変数は、メモリからのロードと格納の順序に制限がかけられます。それによって複数のスレッドの処理による混乱から保護されます。

3.5 transient

transient は、フィールドを Serializationの対象とするかどうか指定します。transientを宣言されたフィールドは、オブジェクトが Serializationされるときの情報としては含まれません(Serializationの対象にならない)。

Serializationとは、 Javaのオブジェクトを、それが保持している各種インスタンス変数の値ごとファイルにダンプする機能のことをいいます。 ダンプされたオブジェクトは再びファイルから読み込むことで、 インスタンス変数の値を復元してくれるのでそっくり同じオブジェクトとして復活させることができます。

[Prev] [Next]