【未経験からのプログラミング講座】STEP3.クラスとオブジェクト2

2. 継承とインターフェース

2.1. クラスの利用

2.1.1クラスの代入

同じ型のクラスオブジェクトは代入処理できます。

ただし、個別の値が代入されるのではなく、クラスの参照がコピーされるので注意が必要です。

注)参照の概念を理解しましょう。

サンプル

public class ABData {

    private int a;
    private String b;

    public void setA(int a) {
        this.a = a;
    }

    public void setB(String b) {
        this.b = b;
    }

    public int getA() {
        return this.a;
    }

    public String getB() {
        return this.b;
    }
}

public class Sample9_1 {
    public static void main(String[] args) {
        ABData obj1 = new ABData();
        obj1.setA(100);
        obj1.setB("サンプル");

        // obj1をobj2に代入
        ABData obj2 = obj1;
        System.out.println("--- obj2 = obj1 を実行して両者の内容を表示。---");
        System.out.println("obj1: a=" + obj1.getA() + " b=" + obj1.getB());
        System.out.println("obj2: a=" + obj2.getA() + " b=" + obj2.getB());

        obj1.setA(500);
        obj1.setB("テスト");

        System.out.println("--- obj1 の内容を変更して両者の内容を表示。---");
        System.out.println("obj1: a=" + obj1.getA() + " b=" + obj1.getB());
        System.out.println("obj2: a=" + obj2.getA() + " b=" + obj2.getB());}
    }
}

2.1.2. 名前の隠蔽

javaではスコープが異なるときは、同一の識別子名(変数)を使うことが出来ます。

例えばメソッド1の中で変数aを使い、メソッド2の中でも同じ名前の変数aを使うことが出来ます。

この識別子はスコープの相互関係により、ある名前が別の名前で隠される事があります。

これを名前の隠蔽(シャドーイング)といいます。

サンプル

public class Concealment {

    private int a;     // ①
    private String b;  // ②

    public void setA(int x) {	// ③
        int a;		// ④
        a = x;		// ⑤
    }

    public void setB(String b) {// ⑥
        b = b;             // ⑦
    }

    public void setAB(int x, String y) {
        a = x;
        b = y;
    }

    public void disp() {
        System.out.println("a=" + a + " b=" + b);
    }
}

public class Sample9_2 {

    public static void main(String[] args) {

        Concealment obj = new Concealment();

        System.out.println("--- setABメソッドで値を設定 ---");
        obj.setAB(100, "サンプル");
        obj.disp();
	
        System.out.println("--- setA() setB()メソッドで値を設定 ---");
        obj.setA(500);
        obj.setB("テスト");
        obj.disp();
    }
}

上述のプログラムでは、2種類のシャドーイングの例を示しています。

setAメソッドの中で、①のメンバ変数(フィールド変数)と同じ名前のローカル変数「int a;」を宣言しています。この場合①のメンバ変数aは隠され、ローカル変数が優先されるので⑤の式は、④のローカル変数aへの代入になります。

つぎにsetBメソッドでは、②のメンバ変数名と同じ名前の仮引数「String b」が使われています。この場合も、②のメンバ変数bは隠され、⑦の式は、「仮引数b = 仮引数b」となります。

このように、スコープの外側で宣言されている識別子と同じ名前が、スコープの内側で宣言されているときは、内側で使われている名前が優先されます。そのため、thisキーワードを使うことによって同一名識別子を使い分けることができます(参考:2.1.1. のサンプルのクラスABData)。ただ、ソースを読むときにどのように変数が参照しているか解らなくなるため、同じ変数名を使用することは極力控えましょう。

問題 3-2-1

以下の条件に合うプログラムを作成してみよう。

dispという名前のメソッドをもつクラスを作成
dispメソッドは、int型の引数を2つもちます
dispメソッドの処理内容は、上記引数の掛け算の結果を表示
dispメソッドとは、別クラスを作成。dispメソッドを実行するメソッドをもつ。
・実行メソッド内の処理として、変数を宣言してdispメソッドを持つクラスのインスタンスを代入。dispメソッドを呼び出す。

2.2. 継承

2.2.1. スーパークラスとサブクラス

元になるクラス → スーパークラス

継承して作成される新クラス → サブクラス

サブクラスはスーパークラスの機能を継承します。言い換えると、スーパークラスを内包するということです。

(サブクラスは、private属性を除く、スーパークラスのメソッド、メンバ変数をインスタンスなどを使用せずに、直接使用することが出来ます)

2.2.2. クラス継承の記述

class クラス名 extends スーパークラス名 {
}

サンプル

// スーパークラス
public class ClsX {
    public int x;                   // 変数xを管理
    public void disp_x() {          // x表示メソッド
        System.out.println("x=" + this.x);
    }
}

public class ClsXY extends ClsX {          // ClsXを継承してClsXYを作成
    public int y;                   // 変数yを追加管理
    public void disp_y() {          // y表示メソッド
        System.out.println("y=" + this.y);
    }
    public void disp_xy() {         // xy表示メソッド
        // スーパークラスの変数xを利用できる。
        System.out.println("x=" + this.x);
        System.out.println("y=" + this.y);
    }
}

public class Sample12 {
    public static void main(String[] args) {
        ClsXY cls = new ClsXY();    // ClsXYを宣言
		
        cls.x = 100;                // ClsXのメンバ変数を利用できる
        cls.y = 200;                // 自分のメンバ変数に設定
        cls.disp_x();               // ClsXのメソッドを利用できる
        cls.disp_y();               // 自分のメソッドを利用
        cls.disp_xy();              // 自分のメソッドを利用
    }
}

2.2.3. アクセス修飾

前述のサンプルは、メンバ変数をpublic属性のメンバですが、public属性はクラス外部から自由にアクセスできるという性格をもっています。実際のプログラミングでは、クラス変数はprivate指定するのが普通です。

安全性を考えたとき、private属性にすることは重要です。しかし、継承を行ったときスーパークラスのprivateメンバはサブクラスからアクセスできない。という制約があるので調整が必要です。通常このような場合「privateな変数にアクセスするためのpublicなメソッド」を作成します。

サンプル

public class Example {
    private double x;
    
    public Example () {
        this.x = 0.0;
    }

    // メンバ変数xに値をセットするメソッド
    public void setX(double x) {
        this.x = x;
    }

    // メンバ変数xに値を取得するメソッド
    public double getX() {
        return this.x;
    }
}

スーパークラスにあるメンバのアクセス修飾は、public にするとサブクラスからアクセスでき便利だが安全性が低くなる。privateにすると安全性は高くなるがサブクラスからアクセスできず不便である。という、排他関係を持ちます。この不便を解消するために、protectedというアクセス修飾子があります。

protected属性のメンバは、サブクラスからアクセス可能であるとともに、同一パッケージ内であるなら、他のクラスからもアクセス可能となります。

問題 3-2-2

サンプルプログラムClsXClsXYのメンバ変数xyをprivate属性に修正してみよう。
それに伴い必要ならClsXClsXYSample12のソースの修正を行いましょう。

2.3. クラス型変数

2.3.1. 継承クラス間のアクセス

クラスオブジェクトは同型の変数で指し示すことができます。あるクラス型の変数を使って、まったく別のオブジェクトを指すことは出来ません。しかし継承関係にあるクラス間では、多少の融通がききます。

サンプル

public class SubCls extends SprCls {……}  //この継承関係にある場合

    SprCls spr;            //SprClsの変数を宣言
    SubCls sub = new SubCls();  //SubClsオブジェクト
    spr = sub;             //SprCls変数でSubClsオブジェクトを指す

この代入処理をした場合、スーパークラスの変数でサブクラスオブジェクトを操作できるようになりますが、制約があります。サブクラス内に含まれるスーパークラス部分だけを操作することができるということです。

A.サブクラス拡張メンバのアクセス

スーパークラス変数を使ってサブクラスを操作する場合、サブクラスで独自に拡張追加されたメンバはアクセスできません。

B.オーバーロードメンバのアクセス

サブクラスで追加されたオーバーロード対象メソッドへのアクセスは出来ません。

(オーバーロードに関しては「2.8. メソッドのオーバーロード」を参照して下さい)

C.オーバーライドメンバのアクセス

アクセスできます。

(オーバーライドに関しては「2.7. メソッドのオーバーライド」を参照して下さい)

サンプル

public class SprCls {
    public void disp() {
        //何らかの処理
    }
}

public class SubCls extends SprCls {
    public void disp() {
        //何らかの処理
    }
}
    //前述のサンプルの場合
    SprCls spr = new SprCls();
    spr.disp();                     //SprClsのdisp()を呼ぶ

    spr = new SubCls();
    spr.disp();                     //SubClsのdisp()を呼ぶ

宣言した時のクラス型でなく、現在指しているクラス型のオーバーライドメソッドを実行するということです。

D. キャストによるサブクラスメンバへのアクセス

(キャストに関しては「2.4. 代入とキャスト」を参照して下さい)

スーパークラスの変数がサブクラスオブジェクトを指しているときサブクラスメンバへのアクセスができます。 spr = new SubCls(); のような場合、SprCls型の変数sprをサブクラスにキャストすればサブクラス変数と同じように使用できるということです。

E.スーパークラス変数を仮引数に用いる

スーパークラス変数でサブクラスオブジェクトを操作できるという機能は、スーパークラスとサブクラスを同じ文脈で処理するような場合に使用すると便利です。

問題 3-2-3

以下の条件を満たすプログラムを作成しなさい。

・スーパークラスを作成する。
・サブクラスを作成する。
・スーパークラスにdisp()という名の表示メソッドを作成。
・メソッドdisp()は、サブクラスでオーバーライドされる。
・実行クラスを作成
・配列の中に両クラスを混在させて処理する。 (オーバーライドに関しては「2.7メソッドのオーバーライド」を参照して下さい)

2.4. 代入とキャスト

代入とは「STEP2の演算子と代入」の部分で習ったとおり、変数の宣言の後に代入文により明示的に初期化することです。

Int  oil ;		//変数の宣言
Oil  =  12;	//初期値の代入

もちろん、宣言と代入を同時に行うことも出来ます。

Int  oil  =  12	//変数の宣言と初期化

代入を思い出したところで、次にキャストについての説明を行います。

キャストとはある基本型を強制的に別の型に変換することです。

以下の文を見てみましょう。

double  x = 1.05;
int  nx = (int)x;

これはxという式の値に対して、小数点以下を無視して整数の値に変換しています。

キャストにはもう一つ、基本型の値を変換することの他に、あるクラスのオブジェクトの参照を別のクラスのオブジェクトに変換することもあります。書き方は同じでキャストしたいオブジェクトの参照の前に変換先のクラス名をカッコの前に入れて書けます。

Airplane  airplane  =  new Jet()
Jet  jet  =  (Jet) airplane;
※airplaneはJetのスーパークラスです。

キャストの目的は一時的に自分の型を忘れて指定した型のオブジェクトとして振舞うことです。 この場合、airplaneAirplaneオブジェクトとして定義されますが、Jetの変数にアクセスしたい場合はJetにキャストする必要があります。

ここで問題となるのが、スーパークラスとサブクラスの関係です。

サブクラスのオブジェクトをスーパークラスの変数に代入すると、通常のサブクラスオブジェクトを生成したときに比べ、求められる機能はスーパークラスの機能だけなのでコンパイルは可能です。

Airplane airplane2 = new Jet();	//OK
画像

注)AirplaneよりJetの方がオブジェクトの機能は多い。

逆にサブクラスの変数にスーパークラスのオブジェクトを代入するとスーパークラスよりも多くの機能を求めることになり、コンパイルエラーとなります。

Jet  jet2  =  new Airplane();	//エラー

また、実行時に最初の式である

Jet jet = (Jet) airplane;

airplaneJetオブジェクトとして宣言されていない場合はClassCastExceptionを発生します。

問題 3-2-4

次のソースはコンパイル時にエラーになる。このソースを正常に処理できるようにしてみよう。
(コンパイルできても実行時にエラーになる可能性があるのでtry-catchを使用せずに回避すること。)
(キャストはChildクラスを宣言するときに行う)

class Parent {
}

class Child extends Parent {
}

public class CastTest {

    public static void main(String[] args) {
        CastTest test = new CastTest();
        test.func();
    }

    void func() {
        Parent parent = new Parent();
        Child child = parent;
    }
}

2.5. superとthis

superとはスーパークラスのオブジェクトを表すキーワードです。このsuperキーワードを指定することによりsuperクラスのメソッドや変数を使用することができます。

サンプル

public class SuperClass{
    int a = 10;		//メンバ変数

    SuperClass(){		//コンストラクタ
    }
    
    SuperClass(int a){	//コンストラクタ
    }

    public void func(){	//動的メソッド
    }
}

public class ExampleSuper extends SuperClass{

    int a = 100;		//メンバ変数

    ExampleSuper (){	//コンストラクタ
        this(10);		//同じクラスの別コンストラクタの呼び出し
    }
    
    ExampleSuper (int a){	//コンストラクタ
        super(a);		//スーパークラスのコンストラクタの呼び出し
    }

    public void func(){	//動的メソッド
        int b = super.a;	//スーパークラスのメンバ変数を参照
        super.func();	//スーパークラスのメソッドを呼び出し
    }
}

super()を使用するとサブクラスのコンストラクタからスーパークラスのコンストラクタを呼び出すことができますが、コンストラクタの先頭行で行う必要があります。

※スーパークラスが2重、3重になっているときに、同一のメソッド、変数が存在する場合は直近のスーパークラスを参照します。

thisとは自分自身のオブジェクトを表すキーワードです。このthisキーワードを指定することによりメンバ変数やメソッドを指定して使用することができます。

サンプル

public class ExampleThis{
    int a = 10;		//メンバ変数

    ExampleThis (){		//同じクラスの別コンストラクタの呼び出し
        this(10);
    }
    
    ExampleThis (int a){	//コンストラクタ
        this.a = a; 		//メンバ変数を参照
    }

    public void func(){	//動的メソッド
        int b = 100;
        int c = this.a;	//メンバ変数を参照
        this.otherMethod();//別メソッドの呼び出し
    }
    
    public void otherMethod (){	//動的メソッド
    }
}

super()と同様にthis()はコンストラクタの先頭行で行う必要があります。

② 参照するときはまずオブジェクトを生成したクラス内で対象となる変数、メソッドを探しその内容を参照します。オブジェクトを生成したクラス内に存在しない場合、superと同様に直近のスーパークラスの内容を参照します。

superthisの違い

superは最初から対象がスーパークラスに対し、thisは最初はオブジェクトを生成したクラスを対象とし、ない場合はスーパークラスを参照します。

public class Parent { 
    public Parent(){
        this(10);
    }

    public Parent(int a) {
    }

    public void execute() {
    }
}

public class Model extends Parent {
    int a = 10;

    public Model () {
        super();
    }

    public Model (int a) {
        super(a);
    }

    public void execute() {
        int b = super.a;
    }
}

問題 3-2-5

次のソースをコンパイルするとエラーとなる。
スーパークラスに1行追加してコンパイルできるようにしてみよう。

class Parent {

    public Parent(){
        this(10);
    }

    public Parent(int a){
    }

    public void execute(){
    }
}

public class Model extends Parent{
    int a = 10;
    
    public Model () {
        super();
    }
    
    public Model (int a) {
        super(a);
    }
    
    public void execute(){
        int b = super.a;
    }
}

2.6. finalキーワード

2.6.1. finalフィールド

javaでは、定数の宣言にfinal修飾子を使用します。final修飾子は、それが最終形でありそれ以降の修正はできないという意味をもったキーワードです。

final int MAX_DATA = 10000;

2.6.2. finalメソッド

finalをつけたメソッドはその処理内容を変更できない。(つまり、オーバーライドできない)

(オーバーライドに関しては「2.7. メソッドのオーバーライド」を参照して下さい)

サンプル

public final void disp() {
    System.out.print("a = " + a );
}

2.6.3. finalクラス

finalをつけたクラスは継承することができない。(finalクラス内の全メソッドは暗黙でfinalメソッドとみなされる)

サンプル

public final class Sample {
    protected int x;
    
    public void setx(int n) {
        x = n;
    }
    
    public void dispx() {
        System.out.println("x= " + x );
    }
}

問題 3-2-6

以下の条件にあうプログラムを実装しなさい。

finalクラスを一つ作成する。
・上記のクラスには、一つ以上の定数と、メソッドをもつ。
・別クラス、public static void main(String[] args) ・・・のメソッド内でfinalクラスの定数とメソッドを呼び出す処理を記述する。

2.7. メソッドのオーバーライド

オーバーライドとはスーパークラスで定義されたメソッドと同じ名前、引数を持つメソッドを、サブクラスで再定義することです。メソッドのオーバーライドを行うと、スーパークラスのメソッドをサブクラスで再定義された機能に置き換えます。オーバーライドを行うことによって、同じメソッド名を使い、状況に応じて処理内容を変更することができます。

下記の例では、スーパークラスのthrowBallメソッドではストレートを投げる処理が実行されるが、サブクラスのthrowBallメソッドを実行すると、カーブやストレートを投げる処理が実行されます。

サンプル

public class SuperClass {
    public void throwBall(String str) {
        //ストレートを投げる処理
    }
}

public class SubClassAClass  extends SuperClass  {
    public void throwBall(String str) {         //オーバーライド
        //カーブを投げる処理
    }
}

public class SubClassBClass extends SuperClass  {
    public void throwBall(String str) {         //オーバーライド
        //シュートを投げる処理
    }
}

オーバーライドを定義する際には以下の規定があります。

1. オーバーライドする側はオーバーライドされる側と戻り値の型、メソッド名、シグニチャ(メソッドの引数の箇所)が同じでなければなりません。どれか一つでも異なる場合はオーバーライドとは見なされません。

2. アクセス修飾子は、アクセス制限を弱める方向でオーバーライドできます。即ち、protected メソッドは public に変更できますが、アクセス修飾子のないメソッドを private 宣言することはできません。

アクセス修飾子の規則の例外として、private 修飾されたメソッドはオーバーライドできません。private 修飾されたメソッドは継承されずに、全く別のメソッドとして実装されることになります。private メソッドは、サブクラスにおいて、同じシグネチャで実装しても、別のものと解釈され、オーバーライドにまつわる一切の制限を受けません。

3. static 修飾子が宣言されたメソッドは、オーバーライド時にも static 修飾子が必須です。その逆に、非staticなメソッドをstaticなメソッドにオーバーライドすることもできません。

4. final 修飾子が宣言されたメソッドはオーバーライドできません。final 修飾子が付与されたメソッドは、その処理内容を変更できないからです。

5. オーバーライドされる側のメソッドにabstract修飾子が付与されている場合、そのメソッドはサブクラスで必ずオーバーライドしなければなりません。オーバーライドしない場合はそのサブクラス全体がabstractクラスになります。

6. オーバーライドするメソッドでは、スーパークラスのオーバーライド対象メソッドがスロー(3. 例外処理を参照)できる例外以外をスローすることはできません。

問題 3-2-7

円の面積を計算して取得する意味を持つ抽象メソッドgetEnMenseki(double r)をもつクラスを作成してみよう。
また、このクラスを継承して円周率を3として面積を求めるメソッドをもつクラスを作成してみよう。
さらに、円周率を3.14として面積を求めるメソッドをもつクラスを作成してみよう。

2.8. メソッドのオーバーロード

オーバーロードとは同一クラス内でメソッド名が同一で引数の型、数、並び順が異なるメソッドを複数定義することを言います。

同じ機能を持つものは同じメソッド名とした方が、プログラムがきれいに見えます。Java では、メソッドを識別するのに、メソッド名と引数リストの組を使います。但し、戻り値の型は識別の対象には含まれません。つまり、戻り値の型は異なるけれども名前と引数の型が同じオーバーロードするメソッドを作成することはできません。

オーバーロード可能な例

int ex(int a)
int ex(double a)
int ex(int a, int b)
int ex(int a, double b)
int ex(double a, int b)

オーバーロード不可な例

int ex(int a)
int ex(int b)

int ex(int a)
double ex(int a)

メソッドを呼び出すときにも型変換は発生します。この場合も可能なのは拡張変換(サブクラスのオブジェクトをスーパークラスのオブジェクトに変換すること、もしくはint型をlong型へ変換するように基本参照型で有効桁数が少ない型を大きい型に変換すること)のみであり、縮小変換(拡張変換の逆)になる場合は明示的なキャストが必要となります。メソッドの呼び出し規則は以下のようになります。

1) 実引数型と一致する仮引数型がある時はそのメソッドを呼び出す。

2) 実引数を拡張変換すると一致する仮引数型がある時はそのメソッドを呼び出す。

3) 拡張変換を行った結果、一致する仮引数型が複数ある時は、変換コストの低い方のメソッドを呼び出す。

サンプル

ex(999); 			//メソッド呼び出し側の実引数

public String ex(float f){ 	//呼び出される側の仮引数型がfloatとdoubleの場合
    ・・・・			//float型への変換コストが低い為、ex(float)の方が
}			//呼び出される。

public String ex(double d) {
    ・・・・
}

問題 3-2-8

引数に半径を渡すと円周率に3.14を使用して面積を求めるメソッドを作成してみよう。
また、引数に半径と円周率を指定して面積を求めるメソッドを作成してみよう。

2.9. 抽象クラス

2.9.1. 抽象メソッドと抽象クラスの記述

javaでは、オーバーライドして使用するメソッド本体を省略して定義することが出来ます。このようなメソッドを抽象メソッドといい、abstractキーワードを付与します。

abstract属性を付与した抽象メソッドは「あとで必ずオーバーライドしないと使えない」というメソッドです。また、クラスの中に抽象メソッドがあれば、そのクラスもまた抽象クラスにしなければならない。という約束があります。

このようなメソッドを含む抽象クラスは、クラスとして完全ではありません。(未定義部分があるということ)そのため、インスタンスを生成することは出来なくなります。

2.9.2. 抽象メソッドの意義

スーパークラス利用時の一貫性を維持する。ということです。これは、プログラマによってメソッド名や引数構成がバラバラにならないようにする為です。

サンプル

abstract class XYData { // 抽象クラスである
    protected double x;
    protected double y;
 
    public XYData() {
        this.x = 0.0;
        this.y = 0.0;
    }
 
    public void set_data(double x1, double y1) {
        this.x = x1;
        this.y = y1;
    }
 
    abstract public void disp();  // 抽象メソッドである
}

public class Square extends XYData {  // 抽象クラスを継承
 
    public Square() {
        this.x = 0.0;
        this.y = 0.0;
    }

    public void disp() {  // 抽象メソッドの本体

        System.out.println("x=" + x + " y=" + y);
        System.out.println("四角形の面積 = " + (x * y));
    }
}

public class Cube extends XYData {  // 抽象クラスを継承

    protected double z;
 
    public Cube() {
        this.x = 0.0;
        this.y = 0.0;
        this.z = 0.0;
    }
 
    public void set_data(double x1, double y1, double z1) {
        this.set_data(x1, y1);
        this.z = z1;
    }
    
     public void disp() {  // 抽象メソッドの本体
        System.out.println("x=" + x + " y=" + y + " z=" + z);
        System.out.println("立方体の体積 = " + (x * y * z));
    }
}

public class Sample10 {
    public static void main(String[] args) {
        Square obj1 = new Square();
        Cube obj2 = new Cube();

        obj1.set_data(30, 40);
        obj2.set_data(50, 60, 70);

        obj1.disp();
        obj2.disp();
    }
}

問題 3-2-9

以下の条件に合うプログラムを作成してみよう。

・抽象クラスを作成
・抽象クラスはメンバ変数を2つもつ
・メンバ変数は各々private属性 protected属性である。
・抽象クラスに抽象メソッドを1つ作成
・抽象クラスを継承したクラスを作成。
・抽象メソッドの処理内容は、上記で作成した2つのメンバ変数を表示する。
・上記条件を満たした実行クラスの作成

2.10. インターフェース

2.10.1. インターフェースの記述

interface 名前 {
    型  フィールド名 = 初期値;    (任意)
    型  メソッド名(引数ならび);  (任意)
}

インターフェースのルール

・ いかなる実装もふくまれない。

・ メソッドは暗黙のうちにabstractであり、publicである。

・ メソッドは定義のみでき、実装することはできない。

・ 定数だけ定義できる。メンバ変数(1.3. メンバ変数の定義と初期化を参照)は定義できない。

・ インスタンス を生成できない。

2.10.2. インターフェースの意義

Javaは、言語構造の簡潔さを志向しているため、同時に二つのスーパークラスをもつ多重継承を記述することができません。しかし、インターフェースを使うと多重継承に近い処理を行うことができます。

implementsキーワード以下に複数個つなげることが出来ます。

class クラス名 implements インターフェース名, インターフェース名, ・・・・・・ {
}

サンプル

interface XYdtIntr {                           // xyデータ用インターフェース
    double NUL_DATA = -1.0;                   // 定数を設定
    public void setX(double x);                // 以下抽象メソッドを記述
    public void setY(double y);
    public double getX();
    public double getY();
    public void set(double x, double y);
    public void disp();
}

public class Square implements XYdtIntr {    // XYdtIntrを実装する。
    private double x;
    private double y;
    
    public Square() {                //コンストラクタ
        this.x = 0.0;
        this.y = 0.0;
    }
    
    // 以下インターフェース指示に対応
    public void setX(double x) {
        this.x = x;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double getX() {
        return this.x;
    }

    public double getY() {
        return this.y;
    }

    public void set(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public void disp() {
        System.out.println("x=" + this.x + " y=" + this.y);
        System.out.println("四角形面積=" + (this.x * this.y));
    }
}

public class Sample11 {

    public static void main(String[] args) {

        Square sq = new Square();
        
        sq.set(10, 20);
        sq.disp();
        
        sq.setX(30);
        sq.setY(40);
        
        System.out.println("x=" + sq.getX());
        System.out.println("y=" + sq.getY());
        
        sq.setX(Square.NUL_DATA);
        
        if (sq.getX() == Square.NUL_DATA) {
            System.out.println("sq のデータは無効です。");
        } else {
            System.out.println("sq のデータは有効です。");
        }
    }
}

問題 3-2-10

以下に示されたプログラムを作成してみよう。

・インターフェース(XYZIntr)を作成
XYZIntrは、メソッドsetXsetYsetZdisp(戻り値なし)をもつ。
.  また、getXgetYgetZ(戻り値:double型)をもつ。
・クラス(Volume)を作成
.  XYZIntrを継承する。
.  dispは、メンバ変数の値を表示する。
.  dispは、体積(x*y*z)の値を表示する。
・クラス(Example11)を作成
.  Volumeを実行するメソッドを持つ。(static void main・・・)
.  の値を10の値を20の値を5とする。

2.11. カプセル化と多様性

2.11.1. カプセル化

各オブジェクトは、それぞれ独立していますので、その各オブジェクトの状態を表す必要があります。四角形で言うと、大きさの情報をそれに例えることができます。

public class Square {
    public int height;
    public int width;
}

これらは四角形の大きさを求める為の情報【高さ(height)】【横幅(width)】を表すフィールドを持っています。

このような情報(フィールド)をインスタンスフィールドといい、オブジェクトはインスタンスフィールドによりそのオブジェクトの状態を表しています。そしてその状態を設定(メソッド)により変更することによりオブジェクトの状態を変えるわけです。メソッドを使用しなくても直接インスタンスフィールドのデータにアクセスして変えることも可能ですが、通常メソッドを使用してアクセスします。この方法をカプセル化といいます。

サンプル

public class Test {
    public static void main(String[] args) {
        Square sq = new Square();
        // 高さの設定
        sq.height = 10;
        // 横幅の設定
        sq.width = 5;
    }
}

上記の方法は直接、インスタンスフィールドへアクセスしています。

しかし、この方法はカプセル化に反する方法なので、オブジェクトの変数にアクセスする場合には、設定(メソッド)を使用します。

サンプル

public class Square {
    private int height;
    private int width;

    // 高さを設定する。
    public void setHeight (int height) {
        this.height = height;
    }
    // 高さを取得する。
    public int getHeight () {
        return this.height;
    }
    // 横幅を設定する。
    public void setWidth (int width) {
        this.width = width;
    }
    // 横幅を取得する。
    public int getWidth () {
        return this.width;
    }
    // 面積を取得する。
    public int getArea () {
        return this.height * this.width;
    }
}

今回は、インスタンスフィールドをprivateに設定し、外部ファイルから参照出来ないようにし、また、各インスタンスフィールドに設定/取得メソッド(アクセッサメソッド)を定義しています。

こうする事により、インスタンスフィールドへのデータが隠蔽され、メソッドがどのように情報を処理しているかなどを隠すことができます。

今回新たに面積を取得するメソッド(getArea)を作りましたが、内部の処理が途中で変更になった場合でも、アクセス方法さえ変わらなければ再利用できるということがあります。カプセル化は、オブジェクト指向プログラミングにおいて非常に重要な技法です。

2.11.2. 多様性(ポリモーフィズム)

「犬」「猫」「馬」クラスは「動物」クラスを継承しているとします。すると例えば、「呼吸する」という機能は「犬」「猫」「馬」すべてに共通のものなので、「動物」クラスで定義されます。

それでは、「鳴く」という機能はどうでしょうか?「鳴く」という行為は「犬」「猫」「馬」どれも似ています。しかし、鳴き声は全く違います。「犬」クラスのインスタンスに「鳴け」と命令した場合(つまり「鳴く」メソッドを実行した場合)は「わん!」と鳴くでしょうし、「猫」クラスのインスタンスなら「にゃ!」と鳴くでしょう。そこで、それぞれのクラスについて、共通の目的と独自の挙動をもつメソッドを定義する必要があるわけです。このようなメソッドはクラス階層のどこで定義されるべきでしょうか?

o 「鳴く」というメソッドは「犬」「猫」「馬」どのクラスのインスタンスでも実行可能でなければなりません。だとすると、「鳴く」メソッドは「動物」クラスで定義されるべきもののように見えます。

o 一方鳴き声は「犬」「猫」「馬」各クラスで異なります。だとすると、「鳴く」メソッドは「犬」「猫」「馬」それぞれのクラスで定義されるべきもののように見えます。

下図参考

もっと深く観察しましょう。

「鳴く」というメソッドが「犬」「猫」「馬」どのクラスのインスタンスでも実行可能であるということは、このメソッドに関して、「犬」「猫」「馬」の各クラスが同じメソッドを持っているのだということです。一方、鳴き声が「犬」「猫」「馬」各クラスで異なるということは、「鳴く」メソッドの実装は共通ではないということです。

つまり、「動物」クラスでメソッドを定義し、「犬」「猫」「馬」各クラスでメソッドを実装するのが自然です。

//動物クラスを定義
abstract class Animal{

//「呼吸する」メソッドの実装
public void breathe (){
	//動物は全て呼吸するので、スーパークラスで実装 
        System.out.println("空気を吸って、吐く");
}

//「鳴く」メソッドの定義
abstract public void cry();
}

//犬クラスを実装
public class Dog extends Animal { 
    //「鳴く」メソッドの実装
    public void cry () {
        //犬なので「わん!」と表示する。
        System.out.println("わん!わん!わん!");
    }
}

//猫クラスを実装
public class Cat extends Animal { 
    //「鳴く」メソッドの実装
    public void cry () {
        //猫なので「にゃ!」と表示する。
        System.out.println("にゃ!にゃ!にゃ!");
    }
}

//馬クラスを実装
public class Horse extends Animal { 
    //「鳴く」メソッドの実装
    public void cry () {
        //馬なので「ヒヒーン!」と表示する。
        System.out.println("ヒヒーン!ヒヒーン!");
    }
}

メソッドが「動物」クラスで定義されると、そのサブクラスである「犬」「猫」「馬」クラスのインスタンスを扱うときに、それが実際にどのサブクラスに属するのかを知る必要がなくなります。それがどのクラスのインスタンスであろうと「動物」インスタンスの一種なので、「動物」クラスで定義されている「鳴く」メソッドを共通的に実行できるのです。そしてインスタンス自身は自分がどのクラスに属しているか知っているので、自分専用の実装を使用して鳴き声を出します。

public class Crying{
    public static void main(String[] args) {
        //この部分で犬クラスか猫クラスか馬クラスの
        //オブジェクトを生成する。
        Animal animal = new Dog();
        cry(animal);
    }

    public static void cry(Animal animal) {

        //cryメソッドの呼び出し
        //Animalオブジェクトの実装はここでは判らない
        animal.cry();
    }
}

このように、メソッドがひとつでも、オブジェクトが属するクラスによって挙動が変わる仕組みのことを多様性(ポリモーフィズム)と言います。この仕組みが、オブジェクト指向の要です。

問題 3-2-11

【底辺】、【高さ】のインスタンスフィールドを持つ三角形のクラスを作成し、実際に使ってみよう。

タイトルとURLをコピーしました