コンピュータ基礎および演習II

講義資料

オブジェクト指向プログラミング(3)

今回は少し視点を変え、 実際にプログラムを書く上で必要になる知識と注意すべきポイントを示しながら、 オブジェクト指向プログラミングの感覚をつかんでいくことにします。 復習の内容もありますので、必要に応じて読み飛ばしても構いません。

変数の宣言、初期化、代入 (復習)

変数の宣言

Javaの変数には「型」があります。 この型によって変数にどのような種類のデータを格納できるかが決まります。 例えば、「ウサギ」型の変数に「キリン」を格納するようなことはできません。 「ウサギ」と「キリン」では大きさも形も能力も行動も違います。

int 型 (整数) として宣言された変数には double 型 (浮動小数点数) の データを格納することは適していません。無理に格納すると精度が下がり、 小数点以下の情報が失われます (それを分かった上で故意に行うことはあります) 。

変数使うためには、 まずどんな型のどんな名前の変数を作るか決めなければなりません。 変数を作ることを「宣言」と言います。

次の変数宣言を見てみましょう。

int counter;

変数への代入

宣言によって用意された変数に実際にデータを入れることを代入と呼びます。 代入は、入れたい変数の名前と値をイコールで結ぶことで行うことができます。

次の例は、変数 counter に 5 を代入する処理を示しています。

counter = 5;

代入では、イコールではさんだ式の両辺で型が一致している必要があります。 これは、さきほど「ウサギ」型の変数に「キリン」を代入できないという ルールからも理解できるはずです。

上の例では counter は int 型の変数であり、 5 も int 型の値であるため、正しい代入であると言えます。

変数の宣言と初期化

最初に変数を用意すると同時に 決められた値を設定しておきたいことがあります。 Java では変数宣言と同時に値を入れておくことができます。

次の例は int 型の変数 counter を宣言し、 同時に counter の値を 5 に初期化する処理を示しています。

int counter = 5;

クラスを作成する際に考えること

クラスを作成する際に必要なことは、 「このプログラムにはどのような物や事が関わっているか」 という視点で考えることです。 次に、そのような物や事にどのような「情報」があって、 どのような「機能」や「役割」があるかを考えます。

Javaでは物事に必要な情報のことを「属性」として変数に入れておくようにし、 物事が果たすべき機能や役割を「メソッド」として 処理の手順を書いていきます。

インターネットでのオンラインショッピングやスーパマーケットでの ショッピングカートについて考えてみましょう。

ショッピングカートには「カートの内容」という情報があります。 そして、「カートに商品を加える」、「カートから商品を取り除く」、 「カートの内容をレジで清算する」という機能があるとします。

ショッピングカートのクラスの設計は次のようになります。

class ShoppingCart {
    // カートの内容
    cartContents;

    // カートに商品 (item) を加える
    addToCart(item) {
        ....
    }

    // カートから商品 (item) を取り除く
    removeFromCart(item) {
        ....
    }

    // カートの内容を清算する
    checkOut() {
        ....
    }
}

ここでの cartContents を「属性」と言います。 属性の値を変えることでオブジェクトの性質を さまざまに変えることができます。 また、addToCart(), removeFromCart(), checkOut() を「メソッド」と言います。 クラスを作る際には、属性とともに、 クラスにどのような機能を持たせるかを考えます。 それをメソッドとして機能の内容を書くのです。

属性の値はメソッドによって読み込んだり、書き換えられたりします。 例えば、 addToCart(item) では、item に指定された商品を 属性である cartContents に加える処理をすることになります。

以上のように、クラスでは属性とメソッドが重要な役割をするのです。

クラスとオブジェクトの違い

上で定義したクラスは、Java がオブジェクト (インスタンス) を作るための設計図として使われます。 実際にオブジェクトが作られるときには、 クラスの属性の値として特定の値が書き込まれます。 クラスを定義した時点ではショッピングカートの中身に何が入っているかは 決まっていませんが、 オブジェクトを作成して、そこに商品情報を書き込むことで 具体的なショッピングカートがプログラムの中で使えることになるのです。

オブジェクトの作成と利用

では、実際にショッピングカートを作り利用してみましょう。 自分で定義したクラスは、これまでの「型」と同じように使うことができます。 クラスを用いた変数の宣言とオブジェクトの生成は、 次のように書くことができます。

ShoppingCart myCart = new ShoppingCart();

この文の内容を詳しく見ていきましょう。

  1. 変数の宣言
    ShoppingCart myCart = new ShoppingCart();
    

    これは、int a; と同じように ShoppingCart 「型」の変数 myCart を宣言したということです。 これによって、myCart にショッピングカートの 情報を入れることができるようになりました。

  2. オブジェクトの生成
    ShoppingCart myCart = new ShoppingCart();
    

    クラスから宣言された変数は、そのままでは使うことができません。 new という演算子でオブジェクトを生成する必要があります。 new ShoppingCart() で、実際にデータの出し入れやメソッドの実行が行える オブジェクトが生成されて、データを格納する領域が作られます。

  3. オブジェクトを変数に代入する
    ShoppingCart myCart  =  new ShoppingCart();
    

    イコールを使用して、2 で生成されたオブジェクトを変数 myCart に代入しま す。これで、 myCart がショッピングカートとして使えるようになりました。

オブジェクトにアクセスするためにはドット記号を使う

プログラムの中で、 オブジェクトにアクセスするためにはドット (.) を使います。 どのオブジェクトに対して操作を行うかをドットの左側に書き、 行いたい操作、つまりメソッド名をドットの右側に書きます。

ショッピングカートに対して商品の追加を行いたい場合は、 次のように書くことができます。

今回は、私のショッピングカート (myCart) にリンゴ (apple) を追加したいとします。

myCart.addToCart(apple);

上の例では myCart に商品を加えるために addToCart メソッドの実行を指示しています。 その際に、「どの商品をショッピングカートに追加するか」 という情報が必要になるため、引数 (カッコ内) に商品である apple を指定しています。

これで、めでたく私のショッピングカートにはリンゴが追加されました。

参照

クラスは「型」の一種であるという話をしましたが、 厳密にはクラスとして宣言された変数と int, double などとして宣言された変数の性質は異なります。

具体的には、例えば以下の 2 つの宣言による変数の性質は異なるということです。

int counter;
ShoppingCart myCart;

ここで、「型」の性質を述べるために以下の分類をします。

(基本型のことを「プリミティブ型」と呼ぶこともあります)

Java のルール

基本型
参照型

イメージ

基本 (プリミティブ) 型の変数を考えてみましょう。

int counter = 4;

この場合、変数 counter には「4」という値そのものが格納されます。

 

 

では、参照型ではどうなるのでしょうか。

ShoppingCart myCart = new ShoppingCart();

この場合、変数 myCart には ShoppingCart のオブジェクトの在りかを示す 参照 (オブジェクトを指し示す矢印のようなもの) が格納されます。

 

 

重要: ShoppingCart オブジェクトそのものが変数に格納されるのではありません。 格納されるのはその在りかに関する情報です。

参照を理解するための練習

参照型のイメージをつかみましょう (1): 参照型の変数の宣言とオブジェクトの生成、代入

次のような変数宣言とオブジェクトの生成を考えてみましょう。

Dog koro = new Dog();

 

  1. Dog koro = new Dog();
    

    これにより koro という名前の変数が作られます。 koro は Dog クラスのオブジェクトの在りかが格納される変数ということになります。 しかし、まだ、この時点では koro には具体的な在りかは 格納されていません。

  2. Dog koro = new Dog();
    

    これにより Dog クラスのオブジェクトが生成されます。

  3. Dog koro  =  new Dog();
    

    イコールを使用すると、 2 で生成したオブジェクトの「在りか」が koro に代入されます。 すなわち、 koro の指し示す先は、 2 で生成されたオブジェクトだということになります。

     

     

参照のイメージをつかみましょう(2): 変数の代入

参照型の変数には、 そのオブジェクトへの参照が格納されます。 次のプログラムを見てみましょう。

  1. Dog pochi = new Dog();
    Dog koro  = new Dog();
    

    Dog クラスのオブジェクトを示す変数を 2 つ宣言し、 Dog のオブジェクトを生成しています。 2 つの変数にはそれぞれ Dog オブジェクトへの参照を代入しています。

     

     

  2. Dog myFavorite = pochi;
    

    変数 myFavorite を新たに宣言し、 変数 pochi の値を代入しています。 これにより myFavorite に pochi の情報がコピーされることになります。

    ここで重要なことは、 myFavorite には pochi と同じオブジェクトへの 参照が格納されているということです。 myFavorite と pochi は同じオブジェクトを指し示すことになります。

    元々 pochi の指し示していたオブジェクトは、 myFavorite という変数を通じても使うことができます。

     

     

  3. pochi = koro;
    

    おや、 koro の値を pochi に代入してしまいました。 こうすると、pochi の指し示すオブジェクトが koro と同じものになります。 元々 koro の指し示していたインスタンスは、 pochi という変数を通じても使うことができます。 (同じ犬なのに pochi と koro の 2 つの名前を持つことになりました)

     

     

    ここで myFavorite について考えてみましょう。 pochi の指し示す先が変わっても、 myFavotite の指し示す先は変わりません。

参照のイメージをつかみましょう(3) オブジェクトの生と死

  1. Dog pochi = new Dog();
    Dog koro  = new Dog();
    

    Dog クラスのオブジェクトへの参照を格納できる変数を2つ宣言し、 Dog クラスのオブジェクトを2つ生成しています。 2 つの変数にはそれぞれオブジェクトへの参照を代入しています。

     

     

  2. pochi = koro;
    

    koro の値を pochi に代入しています。 pochi の指し示すオブジェクトが koro と同じものになります。 元々 koro の指し示していたオブジェクトは、 pochi という変数を通じても使うことができます。

    さて、ここで元々 pochi の指していたオブジェクトはどうなるのでしょう。

    このオブジェクトは、どの変数からも参照されていない状態となります。 Java では、どの変数からも参照されていない状態となったオブジェクトは、 プログラムの中で不要になったものだとして自動的に消滅させられます。 この機能をガーベージコレクション (garbage collection) と呼びます。

     

     

    (CやC++には、ガーベージコレクションの機能はありません。 プログラムの中で不要になったオブジェクトやデータは 明示的に領域の開放を行う必要があります。 きちんと開放されないと永久にメモリ上に存在することになります。 このようなことから起こるプログラム不具合をメモリリークと呼びます。)

  3. pochi = null;
    

    pochi に null を代入しています。 null とは、「何もオブジェクトを指し示していない」ことを表す特別な値です。 pochi はどのオブジェクトも指し示していないので、 pochi を使ってメソッドを実行するなどの意味のある仕事はできません。

    一方、koro の指し示すオブジェクトは、 まだ変数 koro によって参照されています。 koro の指し示すオブジェクトがガーベージコレクションによって 消去されることはありません。

     

     

クラスの設計

クラスはオブジェクトの設計図のようなものです。 オブジェクトはクラスのプログラムに従って作られます。 属性には、「そのオブジェクトがどのような情報を持っているか」 というデータが収められます。 属性にはそのオブジェクトがどのような状態であるかという情報も 入れることができます。

そして、属性はメソッドの動作に影響を与えることができ、 またメソッドでは属性の値を書き換えることができます。 ここでは、属性とメソッドの関係について詳しく見ていきましょう。

属性の値はメソッドの機能に影響を与える

属性がメソッドの動作に影響を与える例を見てみましょう。

以下のような属性とメソッドを備えた Dog クラスがあるとします。 bark メソッドの動きが年齢によって変わるのが分かるはずです。

class Dog {
    int years;
    String name;

    void bark() {
        if (years >= 10)
            System.out.println("クンクン");
        else if (years >= 5)
            System.out.println("ワンワン");
        else
            System.out.println("キャンキャン");
    }

    ....
    ....
}

class DogTestDrive {
    public static void main(String[] args) {
        .....
        .....
        .....
        //Dogクラスのオブジェクトで10歳のハチ公があったとして

        hachiko.bark();     //→ 「クンクン」と鳴く

        //Dogクラスのオブジェクトで5歳のツンがあったとして

        tsun.bark();        //→ 「ワンワン」と鳴く

    }
}

属性の値はインスタンスごとに独立である

上の例では name や years という属性を持った 「犬」クラスを定義しました。 ここで次のように2つのオブジェクトを生成したとします。

Dog hachiko = new Dog();
Dog tsun = new Dog();

hachiko の名前や年齢は、 tsun の名前や年齢と異なるのは当り前ですね。 name や years などの属性は オブジェクトごとに独立したものになります。 属性にはオブジェクト固有の状態を示す値を格納することができます。

一方、複数のオブジェクトで1つの属性の値を共有する方法もありますが、 詳しくは後の回で改めて述べることにします。

メソッドに値を渡す

外部からメソッドに値を渡してオブジェクトに仕事をさせることもできます。 例えば、 Dog オブジェクトに対して「3 回鳴け」ということを指示するために、 メソッドに渡す値は以下のように ( ) の中に書きます。

hachiko.bark(3);

メソッドに渡す値のことを引数 (ひきすう) と呼びましたね。

この引数の値がメソッドに伝わると、 メソッドの内部では、変数を通じて引数の値を使うことができます。 次の例は bark メソッドの中身です。

class Dog {
    ....
    void bark(int numberOfBarks) {

        // numberOfBarks 回鳴く処理がここに書かれる

    }
}

hachiko.bark(3) で指定された引数の「3」という値は、 bark メソッドの内部では numberOfBarks という変数を 通じて取り出し使うことができます。

この変数のことを実引数 (じつひきすう) と呼びましたね。 実引数のことを「パラメータ (parameter)」と呼ぶこともあります。

 

 

メソッドには複数の値を渡すことができる

メソッドには複数の実引数を宣言することもできます。 その場合、引数をひとつずつカンマ (,) で区切って書きます。 また、メソッドを呼び出す側の引数の型と順番は、 メソッドの内部で宣言された型と順番にあわせる必要があります。

Dog hachiko = new Dog();
hachiko.bark(3, "ワン");

...

class Dog {
    ....

    void bark(int numberOfBarks, String roar) {

        // numberOfBark 回 「roar」と鳴く処理がここに書かれる

    }
    ....
}

メソッドは値を戻す

メソッドは値を受け取るだけでなく、「値を戻す」こともできます。 ただし、メソッドの戻せる値は 1 つだけです。

メソッドの中身を決めるときには、 そのメソッドがどんな型の値を戻すのかを書く必要があります。

下の例では int 型の値を戻す compute メソッドと check メソッドの利用と、 メソッドの宣言を示しています。

Simple simpleObject = new Simple();
result = simpleObject.compute() - simpleObject.check();
....

class Simple {
    int compute() {
        return 5 * 8;
    }

    int check() {
        return 5 - 2;
    }
}

 

 

return の式の計算結果がメソッド自身の計算結果になる

上のプログラムのこの式の result の値はいくつになりますか。

result = simpleObject.compute() - simpleObject.check();

「5 * 8 - 5 - 2」で 33 というのは間違いです。 上の式の「simpleObject.compute()」の計算結果が 40 となり、 「simpleObject.check()」の計算結果が 3 となり、 結果として「40 - 3」の計算が行われます。答えは 37 です。

引数として指定された値の渡され方

Javaではメソッドの引数として与えた値は、 いったんコピーされてからメソッドに伝わります。 これを「値渡し」と言います。

int x = 100;
bottle.drunk(x);

class PETBottle {
    ....
    void drunk(int volume) {

        ここでの変数 volume とメソッドの呼出し元の x は
        独立した別の変数 (値がコピーされているだけ)

    }
}

ところが、参照型の変数の場合は少し様子が異なります。 思い出してください。参照型の変数の場合、 変数に直接オブジェクトが格納されているのではなく、 変数にはオブジェクトの「在りか」だけが入っているのです。

メソッドの引数では、「在りか」の情報だけがコピーされることになります。 結果として、変数の指している実体であるオブジェクトは同一のものなのです。

Juice orangeJuice = new Juice();
PETBottle bottle = new PETBottle();
bottle.fill(orangeJuice);

class PETBottle {
    ....
    void fill(Juice juice) {

        ここでの変数 juice とメソッドの呼出し元の orangeJuice は
        同じオブジェクトを指し示している。

    }
}

効果的な属性の値の変更と取り出し

メソッドの有効な使い方を紹介します。 get メソッドと set メソッドです。 (getter/setter と呼ばれたり、アクセサと呼ばれることもあります。)

get メソッドはオブジェクトの属性の値を取り出すために使うメソッド、 set メソッドはオブジェクトの属性に新たな値を登録するために使うメソッドです。

get メソッドでは戻り値の型を、 set メソッドでは引数の型を、 対象の属性の型と同じにしておきます。

class PETBottle {
    Juice content;
    int volume;

    Juice getContent() {
        return content;
    }
    void setContent(Juice juice) {
        content = juice;
    }
    int getVolume() {
        return volume;
    }
    void setVolume(int v) {
        if (v >= 0 && v <= 500) {
            volume = v;
        }
        else {
            System.out.println("PETBottle には 0〜500cc の容量しか設定できません");
        }
    }
}

なぜ、属性の値を扱うためにわざわざ get と set の 2 つの メソッドを用意するのでしょうか。

setVolume メソッドを見てください。 このメソッドでは volume に設定できる値の範囲を制限しています。 このように、メソッド経由で値を設定するようにしておけば、 属性に意図しない値が設定されたり、 不正なアクセスが行われることを防ぐことができるのです。

属性とローカル変数

メソッド内で宣言する変数をローカル変数と呼びます。 属性もローカル変数も変数であることには変わりません。

属性はクラスの中のメソッドの外で宣言します。 ローカル変数はメソッドの中で宣言します。

class Horse {
    int power;
    int weight;

    double computePWR() {
        double powerWeightRatio;

        powerWeightRatio = weight / power;

        return powerWeightRatio;
    }
}

属性の宣言と初期化

すでに述べたとおり、変数を宣言するには、 必ず型と名前を決定しなければなりませんでしたね。

int size;
String name;

また、宣言と同時にその変数の初期値を代入することもできました。

int size = 50;
String name = "Donny";

では、もし、属性の値を初期化せずに値を調べると 変数には何が入っているのでしょうか。 次のプログラムで確かめてみましょう。

class PoorDog {
    int size;
    String name;

    int getSize() { return size; }
    String getName() { return name; }
}

class PoorDogTester {
    public static void main(String[] args) {
        PoorDog dog = new PoorDog();
        System.out.println("Initial dog's size: " + dog.getSize());
        System.out.println("Initial dog's name: " + dog.getName());
    }
}

実行すると以下のようになりましたね。

Initial dog's size: 0
Initial dog's name: null

オブジェクトの属性では何も代入しないと、 数値のような基本型は 0 が、 オブジェクトや文字列や配列のような参照型では null が 入っていることになっています。

ローカル変数の宣言と初期化

ローカル変数の初期化をしないまま値を調べるとどうなるのでしょうか。

class LocalVariableTester {
    public static void main(String[] args) {
        int x;
        String name;

        System.out.println(x);
        System.out.println(name);
    }
}

おや、コンパイルエラーになりましたね。 ローカル変数は必ず値を入れてから使わなければならないという決まりがあるのです。

また、次のようなプログラムも誤りです。

class ErrorProgram {
    public static void main(String[] args) {
        int x, y;

        y = x * 2;  // x がまだ初期化されていないのでコンパイルエラーとなる
    }
}

スコープ

変数がプログラム中のどの範囲で使えるのか、 その有効範囲のことを「スコープ」と言います。 属性とローカル変数の有効範囲について説明します。

属性はクラスの定義の範囲内で自由に使うことができます。 一方、ローカル変数は少し複雑です。 原則として、宣言した位置から一番近い外側の中カッコ ({ }) の範囲内でのみ有効となります。

メソッドの中で、 if 文や while 文などの中で、 中カッコ ({ }) で囲まれた範囲をブロックまたは複文と呼びます。

下のプログラムで、どの変数がどの範囲で有効か考えてみてください。

class Dog {
    String name;
    int years;

    void something(int x) {
        int i;
        .....

        if (x > 0) {
            int j;

            ....
        }
        .....

        for(int k = 0; k < x; k++) {
            .....
        }
    }

    void someMatter() {
        String name = "New name";
        .....
        this.name = name;
    }
}

さて、 someMatter メソッドを見ると、 ローカル変数 name と属性 name の変数名が重複しています。 このようなときは、最も内側のブロックで宣言された変数名が優先されます。 ローカル変数の name ではなく、 属性の name を使いたい場合は this というキーワードを使います。