プログラミング/9

出典: CourseWiki

目次

クラス(その2)

(プログラミング/8#クラス(その1)を復習してから読んでください)

学生を表す Student クラスを定義してみます.Studentクラスは,一人の学生の

  • 名前
  • 点数(単純にするため1科目のみ)

を管理することにします.

以下が Student クラスのソースプログラムです. このファイルは Student.java という名前でセーブする必要があります.

/*
 * Studentクラスの定義
 * mainメソッドは定義していないので,このクラスから実行開始することはできない.
 */
public class Student {
    /*
     * インスタンス変数の定義
     * インスタンス変数はインスタンス毎に作られる.つまり,new される毎に作られる.
     * private の意味は後ほど.
     */ 
    private String name;    // 名前
    private int score;        // 点数
    
    /*
     * コンストラクタの定義
     * コンストラクタは,new されたときに自動的に呼ばれ,インスタンスの初期化を行う.
     * ここでは,引数から名前を設定し,点数を 0 にしている.
     * 
     * コンストラクタは,クラス名と同じ名前で定義しなければならない.
     * (publicの意味は後ほど)
     * コンストラクタの引数のパターン(ここではString)は,new の呼び出しと
     * 一致する必要がある.
     */ 
    public Student(String n) {
        // 以下のprintlnは,コンストラクタが呼ばれていることを示すため.
        System.out.println("コンストラクタが呼ばれました(" + n + ")");
        name = n;    // this.name = n と書いても良い
        score = 0;
    }
    
    /*
     * setScoreメソッドの定義
     * 引数で指定した点数をインスタンスにセットする
     */
    public void setScore(int score) {
        /*
         * ここでは,引数の score をインスタンス変数の score に代入したい.
         * 単に score と書くとこのメソッドの引数の score を指す.
         * インスタンス変数の score は this.score と書けばよい.
         * this は,操作対象のインスタンスを示す特別な変数.
         */
        this.score = score;
    }
 
    // この学生の取得する
    public String getName() {
        return name;
    }
 
    // この学生の点数を取得する
    public int getScore() {
        return score;
    }
}


次に,Studentクラスを使う StudentSample クラスを定義します. (このファイルは StudentSample.java という名前でセーブする必要があります)

/*
 * Studentクラスを使ったプログラム例
 */
public class StudentSample {
    public static void main(String[] args) {
        // インスタンスの生成は new により行う
        // new の引数はコンストラクタの引数とマッチする必要がある
        Student gw = new Student("George Washington");
        Student ja = new Student("John Adams");
 
        // インスタンスメソッドの呼び出し.インスタンスメソッドは,
        // Student型変数.メソッド名(引数...)で呼び出す.
        // 2人に別々の点数をつけて...
        gw.setScore(80);
        ja.setScore(90);
        // ちゃんと別々に管理されているかを確認
        System.out.println(gw.getName() + ": " + gw.getScore());
        System.out.println(ja.getName() + ": " + ja.getScore());
    }
}

StudentSampleクラスを見れば分かるように,自分で書いたクラスも, Javaで最初から用意されているクラス(Stringクラスしかやっていませんが)と同じように使うことができます.

StudentSample クラスを実行すると,以下のようになるはずです. (Studentクラスには main メソッドがないので実行できません)

コンストラクタが呼ばれました(George Washington)
コンストラクタが呼ばれました(John Adams)
George Washington: 80
John Adams: 90

クラス定義の解説

(一度にいろいろと解説すると混乱するので,)public,privateなどの解説は後回しにします. ここではインスタンス変数は private,コンストラクタとインスタンスメソッドは public に固定としておきます.

インスタンス変数

クラスを定義すると,new演算子によってそのクラスのインスタンスをヒープメモリに作ることができます.

クラスのインスタンス毎に保持する必要があるデータは,インスタンス変数(instance variable)として定義します.インスタンス変数は,クラスをインスタンス化するごとに作られます.つまり,new を呼ぶたびに新しいインスタンス変数の領域が確保されます.

上のプログラムを実行したときのメモリの状態を以下に示します.

画像:Student.png

インスタンス変数はクラス変数と似ていますが,クラス変数はクラス1つに対して1つだったのに対し,インスタンス変数はインスタンス1つに対して1つ生成されるところが違います.

インスタンス変数を定義するには,メソッドの外で(ただしクラスの中で),先頭に static を付けずに private だけを付けます.static を付けるとクラス変数になります. (実際には private を付けないインスタンス変数もあるのですが,いまのところこうしておきます).

public class Foo {
  // インスタンス変数
  private int num;
  private String name;
  // クラス変数
  private static boolean flag;
}

コンストラクタ

クラスを new でインスタンス化するとき,インスタンス変数などを初期化しておきたいことがよくあります.

このために,クラスではコンストラクタ(constructor)を定義することができます. コンストラクタは,クラスが new でインスタンス化されるときに,自動的に呼び出される特別なメソッドと考えるとよいでしょう.(上の実行例をみても,new によってコンストラクタが呼び出されていることがわかります.)

コンストラクタは,public の後にクラス名と引数を付けて定義します(返り値の型は不要). コンストラクタの引数は,インスタンスを適切な状態にするために用います (Studentクラスの例では,学生の名前をString型の引数で,インスタンス変数にコピーしています).

コンストラクタの引数のパターンと,new するときの引数のパターンは一致している必要があります.

インスタンス変数の初期化

(プログラミング/7#クラス変数の初期化も参照)

インスタンス変数は,new が呼ばれたときに(コンストラクタが呼び出される前に)初期化されます.

初期化式がついていればその値に初期化されます. 初期化式がついていなければ 0 (booleanの場合はfalse) で初期化されます. (配列やStringのようなオブジェクトを指す変数の場合は,null に初期化されます.nullについては後述).

public class Foo {
  private int foo = 100;  // 初期化式付きのインスタンス変数
  private int bar;        // 初期化式なしのインスタンス変数
  // コンストラクタ
  public Foo() {
    System.out.println(foo);     // 100が表示される
    System.out.println(bar);     // 0が表示される.
  }
}

インスタンスメソッド

クラスのインスタンスに対する操作は,インスタンスメソッド(instance method)で定義します.

いままで定義してきたメソッドは private static から始まっていましたが,このように static 修飾子が付くメソッドはクラスメソッド(class method)あるいはstaticメソッド(static method)と言います. クラスメソッドは,インスタンスがなくても実行できる(ある意味特別な)メソッドです.

mainメソッドはpublic static void main(String[] args) と書いていました. これは,Stringの配列を引数とするクラスメソッドです.

クラスメソッドとは違って,インスタンスメソッドは指定されたインスタンスに対して何らかの操作を実行するためのものですから,インスタンスがないと実行できません.

前回のプログラミング/8#メソッド呼び出しでStringクラスのメソッド(length, substringなど)を使いましたが,これらはStringクラスのインスタンスメソッドの例になります.

インスタンスメソッドを呼び出すためには,以下のように操作対象のインスタンスの後に .メソッド名(引数..) と書くのでした.

Student s = new Student("Tarou Kuidaore"); // インスタンスの生成
s.setScore(100);                           // インスタンスメソッドの呼び出し

このとき,呼び出されたメソッド(上の例ではsetScore)は,sの指すインスタンスに対して働きます.

クラスメソッドからクラス変数が見えたように,インスタンスメソッドからはインスタンス変数が見えます.この見えているインスタンス変数は,上のようにメソッド呼び出し時に指定されたインスタンス(上の例だとs)のインスタンス変数です.

public class Foo {
  private int num;
  public void incNum() {
    num++;   // インスタンス変数にアクセス
  }
}
public class Bar() {
  public static void main(String[] args) {
    Foo a = new Foo();
    Foo b = new Foo();
    a.incNum();
    // 上のようにincNumメソッドを呼び出すと,incNum()メソッドの中では
    // aの指すインスタンスのインスタンス変数が見えている   
    b.incNum();
    // 上のようにincNumメソッドを呼び出すと,incNum()メソッドの中では
    // bの指すインスタンスのインスタンス変数が見えている   
  }
}

ややこしく感じるかもしれませんが,「インスタンスメソッドの中では注目している1つのインスタンスのことだけを考えれば良い」と考えてください.インスタンスはnewで(メモリの許す限り)いくつでも生成できますが,インスタンスメソッドで注目すべきインスタンスは1つだけです.

ちなみに,インスタンスメソッドからクラス変数も見えます.ただし,クラスメソッドからはインスタンス変数は見えません(クラスメソッドはインスタンスと関係なく動作するので,当然).

クラス中のどこから何が見えるのかを表にまとめました.

場所 インスタンス変数 クラス変数
コンストラクタ
インスタンスメソッド
クラスメソッド ×

this

(上で述べたように,)コンストラクタやインスタンスメソッドが呼び出されるときは,必ず特定のインスタンスが対象になっています.その特定のインスタンスは,thisという特別な変数で参照することができます. thisは,コンストラクタやインスタンスメソッドが呼び出されるときに自動的に更新されます.

下の例では,setScore の中では this が s を指しています(this==s).

Student s = new Student("Tarou Kuidaore");
s.setScore(100); 

thisは,(当然)クラスメソッドでは使えません.

インスタンスメソッドで,インスタンス変数と同名の引数を使っている場合,this.インスタンス変数名 と書くことでインスタンス変数にアクセスできます.

public class Foo {
  private int num; // インスタンス変数 num
  // インスタンスメソッド
  public void setNum(int num) { // 同名の引数 num があるので,
    this.num = num; // インスタンス変数 num に引数の num を代入
  }
}

まとめ:

  • staticがつくメソッドはクラスメソッド
  • staticがつかないメソッドはインスタンスメソッド
  • インスタンスメソッドは,指定されたインスタンスに対して働く.そのインスタンスは this で参照できる.
  • クラスメソッドはインスタンスと関係なく動作する

null

オブジェクトを指す変数を総称して参照型変数あるいはオブジェクト変数といいます.例:

  • int[] data;
  • String str;
  • Student s;

参照型変数は基本的にヒープメモリ上にあるオブジェクトを指しているのですが, どこも指していないということもあり,これを null で表します.

参照型のクラス変数やインスタンス変数は,初期化式がない場合,null で初期化されます. (プログラミング/8#クラス変数の初期化#インスタンス変数の初期化参照)

null が入っている参照型変数に対して,インスタンスメソッドを呼び出すことはできません. 呼び出すと,NullPointerExceptionという実行時エラーになります.

次のPersonクラスは1人の人とその配偶者を表す単純なクラスです. spouseName が null かどうかで配偶者がいるかどうかを判別するようになっています.

/*
 * 一人の人と配偶者(もしいれば)を表すクラス Person
 */
public class Person {
    private String name;       // 自分の名前
    private String spouseName; // 配偶者の名前
    // コンストラクタ
    public Person(String name) {
        this.name = name;
    }
    // 結婚する.配偶者の名前をセットする
    public void marry(String spouseName) {
        this.spouseName = spouseName;
    }
    // 離婚する.配偶者の名前を null にしておく
    public void divorce() {
        // nullを代入する例
        this.spouseName = null;
    }
    // 配偶者の名前を返す.独身ならば null が返る.
    public String getSpouseName() {
        return spouseName;
    }
    // 独身かどうかを返す
    public boolean isSingle() {
        // nullとの比較の例
        return spouseName == null;
    }
}

演習(Diceクラス)

サイコロを表す Dice クラスを作成せよ.Dice クラスの仕様は以下の通り.

  • int型のインスタンス変数で,サイコロの目(1〜6)を記憶しておく.
  • Diceクラスがインスタンス化されると(newされると),乱数によってサイコロを振る.
  • public int getPip() メソッドによって,今のサイコロの目を得る.
  • public void throwDice() メソッドによって,サイコロを振り直す.

Diceクラスを使うために以下のDiceSampleクラスを使います.

public class DiceSample {
    public static void main(String[] args) {
        // サイコロを2つ作成
        Dice d1 = new Dice();
        Dice d2 = new Dice();
        for (int i = 0; i < 5; i++) {
            // 2つとも投げて...
            d1.throwDice();
            d2.throwDice();
            // 表示する
            System.out.println(i + ": " + d1.getPip() + ", " + d2.getPip());
        }
    }
}

以下のような結果がでれば OK です.

0: 3, 6
1: 4, 3
2: 3, 5
3: 1, 6
4: 6, 6

演習(Diceクラス)の回答例

分数クラス

float型やdouble型は,浮動小数点形式で値を保持するため,たとえば \frac{1}{10} = 0.1 を正確に表すことができません. (0.1 は2進数だと循環小数になり,有限ビット数では正確に表せない) このため,0.1 を加算し続けると誤差が現れてきます.

public class DoublePrecision {
    public static void main(String[] args) {
        double sum = 0.0;
        for (int i = 0; i < 50; i++) {
            sum += 0.1;
            System.out.println(sum);
        }    
    }
}

実行例(途中まで):

0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
1.2
1.3
1.4000000000000001
1.5000000000000002
1.6000000000000003
1.7000000000000004
1.8000000000000005
1.9000000000000006
2.0000000000000004
2.1000000000000005
2.2000000000000006
2.3000000000000007

このため,繰り返しを数えるために float や double を使うべきではありません.

そこで,分数を分数のまま扱うクラス Fraction を作ってみます. このクラスを使えば,有理数(整数と整数の比で表せる数)は正確に扱えるはずです.

Fractionクラスのソースプログラムを以下に示します. 分母(dominator),分子(numerator)は大きな値を扱えるように long 型にしています.

/*
 * 分数クラス (負の数には対応していない)
 */
public class Fraction {
    long numerator;    // 分子
    long dominator;    // 分母
 
    // コンストラクタ
    public Fraction(long n, long d) {
        numerator = n;
        dominator = d;
    }
 
    // コンストラクタ
    public Fraction(long n) {
        // 下の2行の代わりに this(n, 1); と書いてもよい
        // 上のコンストラクタを指定した引数で呼び出すことになる
        numerator = n;
        dominator = 1;
    }
 
    // doubleに変換する
    public double toDouble() {
        return (double)numerator / dominator;
    }
 
    // 分数の加算.thisに対してanotherで指定された分数を加算する
    public Fraction add(Fraction another) {
        // a/b + c/d = (ad+bc)/bd を使って加算する
        // 分母の計算
        // another.dominator は,anotherの指すインスタンスのインスタンス変数
        // のdominatorを意味する    
        long dom = dominator * another.dominator;
        // 分子の計算
        long num = numerator * another.dominator
            + dominator * another.numerator;
        dominator = dom;
        numerator = num;
        reduce();        // 約分する
        // this を return している理由は,
        // a = a + b + c を計算するとき,
        // a.add(b).add(c) のように書けるようにするため.
        return this;
    }
 
    // 分数の引き算
    public Fraction sub(Fraction another) {
        long dom = dominator * another.dominator;
        long num = numerator * another.dominator
            - dominator * another.numerator;
        dominator = dom;
        numerator = num;
        reduce();
        return this;
    }
    
    // 分数の乗算
    //public Fraction mul(Fraction another) {
    // !!! fill in here !!!
    //}
 
    // 分数の除算
    //public Fraction div(Fraction another) {
    // !!! fill in here !!!
    //}
    
    // 文字列に変換
    public String toString() {
        // 分母が1だったら,分子だけ表示
        if (dominator == 1) {
            return "(" + numerator + ")";
        }
        // そうでなければ,(a/b) の形
        return "(" + numerator + "/" + dominator + ")";
    }
 
    // 約分する
    private void reduce() {
        // 分母,分子を最大公約数で割る
        long gcd = gcd(numerator, dominator);
        numerator /= gcd;
        dominator /= gcd;
    }
 
    // ユークリッドの互除法を用いて最大公約数を求める
    // クラスメソッドにしてある
    private static long gcd(long a, long b) {
        while (a > 0 && b > 0) {  // a, bともに0より大きい
            if (a > b) {
                a = a % b;        // a > b のとき
            } else {
                b = b % a;        // a <= b のとき
            }
            if (a == 0) {         // a が 0 ならば答えは b
                return b;
            } else if (b == 0) {  // b が 0 ならば答えは a
                return a;
            }
        }
        return -1;
    }
}

Fractionクラスを使ったサンプル ((1/10)を50回加算する).

public class FractionTest {
    public static void main(String[] args) {
        // sum = 0;
        Fraction sum = new Fraction(0);
        // d = 1/10;
        Fraction d = new Fraction(1, 10);
        for (int i = 0; i < 50; i++) {
            // sum += d
            sum.add(d);
            // 分数形式と小数形式で表示
            System.out.println(sum + ", " + sum.toDouble());
        }
    }
}

実行例(途中まで):

(1/10), 0.1
(1/5), 0.2
(3/10), 0.3
(2/5), 0.4
(1/2), 0.5
(3/5), 0.6
(7/10), 0.7
(4/5), 0.8
(9/10), 0.9
(1), 1.0

解説

  • 以下の両方の書き方を許すためにコンストラクタを2つ定義しています.
 Fraction a = new Fraction(1, 2); // 2分の1 
 Fraction b = new Fraction(100);  // 100 

Javaでは引数のパターン(引数の数や型)さえ違えば,コンストラクタや同じ名前のメソッドを複数定義できます.

このため,あるクラスのメソッドを特定するためには,メソッド名と引数のパターンが必要です.このメソッド名と引数のパターンのことを,メソッドのシグネチャ(signature)と呼びます.

また,同一名のメソッドを複数定義することを,メソッドのオーバーロード(overload)といいます.

  • toString() メソッドは,分数クラスのインスタンスを文字列表現に変換するためのものです.

System.out.printlnなどでインスタンスを表示する場合, 当該インスタンスのtoString()メソッドが呼ばれ,その結果が表示されるようになっています. (System.out.printlnの内部でtoString()が呼ばれている)

Fraction a = new Fraction(1, 3);
System.out.println(a);   // a.toString() の結果が表示される

基本的に,クラスを定義するときは toString() メソッドを定義すべきです.

  • reduceメソッドは約分するためのものです.最大公約数を求めるために次のgcdメソッドを使っています.
  • gcdメソッドは毎度おなじみのユークリッドの互除法を用いて最大公約数を求めるものです.

インスタンスメソッドとクラスメソッドの使い分け

基本的に,ある特定のインスタンスに対して処理を行うメソッドはインスタンスメソッドに,そうでないメソッドはクラスメソッドとします.

インスタンス変数にアクセスしたり,他のインスタンスメソッドを呼び出す必要がある処理は,インスタンスメソッドになります.

上のgcdメソッドの処理(与えられた引数の最大公約数を求める)は,特定のインスタンスと関係ないのでクラスメソッドにしています.ただし,もし(与えられた引数ではなく)numeratorとdominatorの最大公約数を求めるメソッドにするならば,gcdメソッドはインスタンスメソッドにしなければなりません(numeratorとdominatorは特定のインスタンスのものなので).

インスタンス変数とクラス変数の使い分け

インスタンスごとに異なる可能性のある変数はインスタンス変数にします. 全てのインスタンスで共通の値で良い変数はクラス変数にします.

たとえば,あるクラスで使うマジックナンバーは,通常インスタンス毎に異なることはないのでクラス変数にします(final static int のように書いていたことを思い出してください).

課題9-1(分数クラス)

  • Fractionクラスで,コメントアウトされている mul (乗算), div (除算)メソッドを実装せよ.

それぞれ,分数を普通に計算する方法で実現すれば良い.

  • Fractionクラスに,逆数にするインスタンスメソッド public Fraction inverse() を追加せよ.

返り値は this でよい. また,inverse() が正常に動作することを示すサンプルプログラムも書け.

Fraction f = new Fraction(2, 3);  // f = (2/3) 
f.inverse();            // 逆数にする
System.out.println(f);  // (3/2)が表示される

2つの値a, bを入れ替えるには,一時的な変数(tmp)を使って以下のように書く.

long tmp;
tmp = a;
a = b;
b = tmp; 
  • Fractionクラスを使って次の式を計算し,結果を分数と小数で表示するプログラムを作成せよ.
    • 1+\cfrac{1}{2+\cfrac{1}{2}}

余裕のある人用1

\sqrt{2}は以下の連分数で計算できる.

\sqrt{2}=1+\cfrac{1}{2+\cfrac{1}{2+\cfrac{1}{2+\cfrac{1}{2+\cfrac{1}{2+\cfrac{1}{2+\cfrac{1}{\cdots}}}}}}}

Fractionクラスを使って適当な段数を計算し,結果と,結果の2乗を分数と小数で表示せよ.

ヒント: 繰り返しで,連分数の分母の下の方から順番に計算する.一番下の ... は 2 としてよい. 繰り返し数が大きすぎると,long型の制限を超えるので注意すること.

余裕のある人用2

Fractionクラスを負の数(-1/2など)に対応できるように改良せよ. numeratorが負なら負の値を表すことにし,dominatorは常に正になるようにしておくとよい.

Copyright (C) 2002-2015 Kota Abe, Osaka City University

ナビゲーション