Javaプログラミング基礎

講義資料

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

メソッドのオーバロード

1クラスの中には同じ名前のメソッドを重複して宣言することができます。 これをメソッドのオーバロード (over load) と呼びます。 次のプログラムは円を表すクラス定義です。 この円には x 座標と y 座標、半径、円周率という情報があり、 座標情報を登録するメソッド setCoordinate があります。

class Circle {
    int x, y;
    int radius;

    void setCoordinate() {
        x = 0;
        y = 0;
        radius = 1;
    }
    void setCoordinate(int a, int b) {
        x = a;
        y = b;
        radius = 1;
    }
    void setCoordinate(int a, int b, int r) {
        x = a;
        y = b;
        radius = r;
    }
}

class CircleUser {
    public static void main(String[] args) {
        Circle a = new Circle();
        Circle b = new Circle();
        Circle c = new Circle();

        // 引数をすべて省略:       座標(0,0) 半径1の円とする
        a.setCoordinates();

        // 座標情報のみ与える:     座標(5,4) 半径1の円とする
        b.setCoordinates(5, 4);

        // 座標情報と半径を与える: 座標(2,3) 半径2の円とする
        c.setCoordinates(2, 3, 2);

        ....
        ....
    }
}

このクラスにはメソッド setCoordinates の宣言が 3 回行われていますね。 このメソッドは引数の数によって異なった動作をします。

setCoordinate()
最初のメソッドは、 引数をすべて省略して setCoordinates を実行したときに、 実際に実行されるメソッドの内容を宣言しています。 メソッド内では、座標 (0, 0) の半径 1 の単位円、という属性を登録しています。
setCoordinate(int a, int a)
2番目のメソッドは、 int 型整数 2 つが引数に与えられたときに 実行されるメソッドの内容を宣言しています。 引数で与えられた値の座標にある半径 1 の単位円、という属性を登録しています。
setCoordinate(int a, int b, int r)
3番目のメソッドは、 int 型整数 3 つが引数に与えられたときに 実行されるメソッドの内容を宣言しています。 1 番目と 2 番目の引数で与えられた値の座標にある、 3 番目の引数で与えられた値の半径の円、という属性を登録しています。

このように、 同じ setCoordinates という名前のメソッドでも 引数の種類や数によって、 異なった仕事をするようにできます。 メソッドのオーバロードとは、 同じメソッド名に対して複数の実装を行うことを言います。 オーバロードを行ったメソッドは、 それぞれが独立した一つのメソッドのように動作します。

この例では以下の 3 つのメソッドの宣言がありました。

  • void setCoordinates()
  • void setCoordinates(int a, int b)
  • void setCoordinates(int a, int b, int c)
  • 引数の型、引数の数の組み合せをメソッドのシグネチャ (signature) と呼びます。 同じ名前のメソッドとして宣言されていても、 これらが 1 つでも異なれば、別々のメソッドとして扱われます。 オーバロードは、 同じメソッド名でシグネイチャが異なるメソッドについて行うことができます。

    次の例ではオーバロードが可能なメソッド宣言の先頭部分を示します。

    int myMethod()
    int myMethod(int a)
    double myMethod(int a, double x)
    double myMethod(double x, int a)
    

    特に最後の 2 つは、 いずれも int 型と double 型の引数を 1 つずつ取るメソッドですが、 引数の宣言順が異なるため、異なったシグネチャとなりオーバロードが可能です。

    次の例はオーバロードの間違った例です。

    double myMethod(int a, int b)
    double myMethod(int x, int y)
    

    シグネチャは引数の型と数だけで決定されます。 上の例では変数名が異なっていますが、いずれも int 型であるため、 オーバロードできません。

    ポリモフィズム (多態性)

    オブジェクト指向の考え方では、 オブジェクトに対して仕事をさせるためにメッセージを送るという考え方をします。 メッセージは、オブジェクトに対して処理を依頼するという意味があります。 Javaではメッセージはメソッドの実行として実現されています。

    現実世界での犬と猫というオブジェクトについて考えてみます。 これらのオブジェクトに「鳴け」というメッセージを送った場合、 犬であれば「ワン」と鳴き、猫であれば「ニャン」と鳴きます。

    また、コンピュータの世界では、 ファイルというオブジェクトをダブルクリックして「開け」というメッセージを送った場合、 それぞれのファイルの種類によって、 それに適した異なるアプリケーションが起動します。

    つまり、オブジェクトに対して「鳴け」や「開け」というメッセージを送れば、 細かいことはオブジェクトが自動的に判断して、 その場の状況に合わせて適当な方法で目的の仕事をやってくれるということです。 メッセージの送り主は、 オブジェクトが所期の仕事を行ってくれることを信頼して、 あとはオブジェクトに任せてしまえば良いのです。

    このように、同じメッセージであっても、 そのメッセージの送り先やパラメータの内容により、 その場に応じて異なる挙動をする、という考え方があります。 これを、ポリモフィズム (polymorphism) または多態性と言います。

    Javaでポリモフィズムを実現する機能の一つが、メソッドのオーバロードです。 オーバロードを用いると、同じ意味の仕事を行うメソッドでも、 引数の数や組み合わせなどの状況に合わせて適切な動作を行うように動きを変えることができます。

    メソッドの仕事の一部を別のメソッドに下請けさせる

    先ほどの例題、クラス Circle の3つのメソッド setCoordinate は、 中身が良く似ています。x, y, radius の 3 つの 属性に何らかの値を登録しているという意味では同じ仕事と見なせます。

    class Circle {
        int x, y;
        int radius;
    
        void setCoordinate() {
            x = 0;
            y = 0;
            radius = 1;
        }
        void setCoordinate(int a, int b) {
            x = a;
            y = b;
            radius = 1;
        }
        void setCoordinate(int a, int b, int r) {
            x = a;
            y = b;
            radius = r;
        }
    }
    

    ここで3つめの setCoordinate に注目しましょう。 3つの引数の値を順番に、x座標, y座標, 半径に登録するという働きをしていますね。 引数の組み合わせ (シグネイチャ) が異なれば、 名前は同じでも別のメソッドと扱われるため、 3つめのメソッドを使って上の2つのメソッドを次のように書き換えることができます。

    void setCoordinate() {
        setCoodinate(0, 0, 1);    // 3番目のメソッドを0,0,1を引数として実行
    }
    void setCoordinate(int a, int b) {
        setCoodinate(a, b, 1);    // 3番目のメソッドをa,b,1を引数として実行
    }
    
    void setCoordinate(int a, int b, int r) {
        x = a;
        y = b;
        radius = r;
    }
    

    このように、クラス内のメソッドから、 自分自身のクラスにあるメソッドを実行することもできます。 その場合、

    this.setCoodinate(0, 0, 1);
    

    のように、メソッド名の前に this というキーワードをつけることになっていますが、 特別な場合を除いてthis自体も省略することができます。

    コンストラクタ

    コンストラクタを用いたプログラム例

    Person medio = new Person();
    

    上のプログラムのようにインスタンスを生成しただけでは、 その属性には何も値が入っていない状態です。 しかし、場合によっては、インスタンスの内部の情報を初期化したい場合があります。 コンストラクタ (constructor) は、 クラスからインスタンスを生成するときに初期化を行うものです。

    コンストラクタを使ったプログラムの例を以下に示します。

    Person medio = new Person("電大 メディ男", "千代田区神田錦町2-2");
    

    コンストラクタを適切に用意すれば、 このプログラムのように、インスタンスの生成と同時に 名前、住所情報の初期化を行うことができて便利です。 また、初期化をうっかり忘れることを防ぐことができます。

    上のような初期化を行うためのクラス Person のプログラムを以下に示します。

    class Person {
        String name;
        String address;
    
        // 名前と住所の2つの引数を受け取り初期化を行うコンストラクタ
        Person(String n, String a) {
            name = a;
            address = a;
        }
    
        // 何も引数が与えられなかったときのコンストラクタ
        Person() {
            name = "";
            address = "";
        }
    
    
        // 通常のメソッドの宣言
        ....
        ....
        ....
    }
    

    コンストラクタはメソッドの一種のような書き方をします。 クラス名と同名のメソッドとして、結果の型を書かずに宣言します。 コンストラクタの中身にインスタンスの初期化処理を書きます。 return 文で値を返すことはできません。

    上のプログラムでは引数の組み合わせの違うコンストラクタを2つ宣言しています。 これもオーバロードを利用しているというわけです。 引数を2つ与えられたときの初期化の方法と、 何も引数を与えられなかったときの初期化の方法の2つを用意しています。

    2つのコンストラクタを用意したため、 クラスPersonのインスタンスの生成は次の2とおりの方法で行うことができます。

    Person medio = new Person("電大 メディ男", "神田錦町2-2"); // 最初のコンストラクタが使われる。
    Person newCommer = new Person();                           // 2番目のコンストラクタが使われる。
    

    newCommerは引数を与えずにインスタンスを生成しているため、 クラス内部のnameaddressは空 ("") の文字列に 初期化されることになります。

    コンストラクタの文法

    文法をまとめておきます。 コンストラクタの宣言を含んだクラス定義の文法は次のようになります。

    class クラス名 {
        属性の宣言
        ......
    
        クラスと同じ名前 ( 引数, ... ) {
            コンストラクタで行う処理の中身
        }
    
        メソッドの宣言
        ......
    }
    

    暗示的コンストラクタ

    前回までのプログラムでは、コンストラクタを宣言しませんでした。 コンストラクタがなくてもプログラムはきちんと動きます。

    Javaでは、コンストラクタを用意していないクラスには、 自動的にコンストラクタが作られます。 これを、暗示的コンストラクタ (implicit constructor) と呼びます。

    暗示的コンストラクタの主な働きは属性の値を初期化することです。 整数や実数の属性はすべて 0 にし、 文字列、配列やインスタンスの属性は空 (null, ナル) の状態にします。 空の状態とは、 変数が対象となるデータを指し示していない状態と考えてください。

    クラス定義の中に1つでもコンストラクタの宣言を行うと、 暗示的コンストラクタは作られなくなります。

    デフォルトコンストラクタ

    前回までのプログラムでは、コンストラクタに引数を与えずに インスタンスを生成してきました。 ここで使われていた引数のないコンストラクタを、 デフォルトコンストラクタ (default constructor) と呼びます。

    Circle circle = new Circle();
    

    そして前回までのプログラムでは、コンストラクタを書かなかったため、 デフォルトコンストラクタが暗示的に (目には見えないけれども) 自動作成されプログラムは正しく動いていました。

    しかし、今回学んだ方法でクラス定義の中に1つでもコンストラクタを書くと、 暗示的コンストラクタは自動生成されません。 以下の例のように、デフォルトコンストラクタが自動的に用意されることを期待して プログラムを書くとエラーになります。

        ....
        // エラーとなる。
        Circle circle = new Circle();
        ....
        ....
    
    class Circle {
        int x, y;
    
        // 引数なしのコンストラクタ (デフォルトコンストラクタ) を用意しない。
        Circle(int a, int b) {
            x = a; x = b;
        }
    }
    

    このような場合、デフォルトコンストラクタを自分で用意する必要があります。

        ....
        // エラーとならない。
        Circle circle = new Circle();
        ....
        ....
    
    class Circle {
        int x, y;
    
        Circle(int a, int b) {
            x = a; x = b;
        }
    
        // 引数なしのコンストラクタ (デフォルトコンストラクタ) を用意する。
        Circle() {
            x = 0; y = 0;
        }
    }
    

    デフォルトコンストラクタは、 インスタンスを生成する最も一般的な方法であることから、 用意できる場合は用意しておくように気を配るのが良いと言えます。 逆に、初期化が必須のクラスの場合、 わざとデフォルトコンストラクタを用意しないことで、 まちがったクラスの使われ方を防ぐこともできます。 クラスを定義するときには、 自分の定義したクラスが他の場所で (あるいは、他の人によって) 再利用されるということを意識すると良いでしょう。

    コンストラクタのオーバロードを用いた例題

    前章ではメソッドのオーバロードについて説明しました。 コンストラクタのオーバロードの典型的な例を説明します。

    先ほどの例題、クラス Circle をコンストラクタを用いて書き換えてみます。

    class Circle {
        int x, y;
        int radius;
    
        Circle() {
            x = 0;
            y = 0;
            radius = 1;
        }
    
        Circle(int a, int b) {
            x = a;
            y = b;
            radius = 1;
        }
    
        Circle(int a, int b, int r) {
            x = a;
            y = b;
            radius = r;
        }
    
    }
    

    先ほどメソッドsetCoordinateから、 別のsetCoordinateを実行したように、 コンストラクタの中で、別のコンストラクタを実行することができます。

    x座標、y座標、半径を登録する働きをする3つ目のコンストラクタを、 他のコンストラクタからも利用するようなプログラムを考えてみます。 このような場合、thisキーワードを用います。

        Circle() {
            this(0, 0, 1);
        }
    
        Circle(int a, int b) {
            this(a, b, 1);
        }
    
        Circle(int a, int b, int r) {
            x = a;
            y = b;
            radius = r;
        }
    

    this キーワードは、 クラス定義の中で自分自身を表すのに用います。 上の例のように this( 引数, ... ) と書くと、 自分自身のコンストラクタを実行せよ、という意味になります。

    2つのオブジェクトの相互作用

    ここでは、いくつか実践的なプログラムを示し考えることで、 オブジェクト指向プログラミングに対する理解を深めていきます。

    例題: 2つの点の距離を求める

    class DistanceBetweenPoints {
        public static void main(String[] args) {
            Point p1 = new Point(0,0);
            Point p2 = new Point(7,7);
    
            System.out.println("2つの点の距離: " + p1.calcDistance(p2));
        }
    }
    
    class Point {
        int x;
        int y;
    
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        int getX() {
            return x;
        }
        int getY() {
            return y;
        }
    
        /** 他の点との距離 */
        double calcDistance(Point p) {
            return Math.sqrt((x - p.getX())*(x - p.getX()) +
                             (y - p.getY())*(y - p.getY()));
        }
    }
    

    このプログラムは2次元平面上の「点」をクラスとした プログラムです。

    まず、キーワード this について説明する必要があります。 下のコンストラクタでは引数であるローカル変数に 変数 x と変数 y が使われています。 一方、座標を表わす属性にも x と y があります。 このように変数名が重なってしまった場合、 単に xy と書くと、 一番近いブロックで宣言された変数の意味になり、 変数名の前に this をつけると属性としての変数という意味になります。

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    

    次にメソッド calcDistance の実行の様子を見てみます。

    System.out.println("2つの点の距離: " + p1.calcDistance(p2));
    
    ....
    ....
    
    double calcDistance(Point p) {
        return Math.sqrt((x - p.getX())*(x - p.getX()) +
                         (y - p.getY())*(y - p.getY()));
    }
    

    p1p2 は、独立したオブジェクトであることを思い出してください。 p1p2 は「点クラスの仲間」という性質は共通ですが、 内部に持つ情報は別のものです。 ここで、p1.calcDistance(p2) と書くと、 p1 に関する calcDistance が実行されます。

    このメソッドの引数は、クラス Point のオブジェクト p です。 これまで、メソッドの引数には主に int 型や double 型の値を使ってきましたが、 オブジェクトもメソッドの引数に使うことができるのです。

    このメソッド内部の計算を詳しく見てみましょう。

    Math.sqrt((x - p.getX())*(x - p.getX()) +
              (y - p.getY())*(y - p.getY()));
    

    Math.sqrt( ..... ) は、あらかじめ Java に備わっている メソッドの一種です。 引数に指定されたの値の平方根 (ルート) を計算することができます。

    さて、この式での xy は、 言うまでもなく p1 の座標です。

    では、 p.getX(), p.getY() はどうでしょうか。 メソッドcalcDistance内の p は、 メソッドの実行元の引数 p2 のことです。 p2 に対して getX(), getY() を実行した結果、 すなわち、p2の x 座標と y 座標を求めているのです。

    p1p2 の座標が分かれば、 2つの点の間の距離を計算するのは簡単です。

    メソッド calcDistance の働きを整理してみましょう。 このメソッドは、自分の点と引数に指定された別の点との距離を計算するメソッドだ、 と言うことができます。 このように、メソッドでは引数に別のオブジェクトを受け取り、 自分自身の属性と、引数に指定された別のオブジェクトの情報を使って 計算を行うこともできます。

    例題: ある点から見て2つの点のうち近い方を選ぶ

    もう1つ、2次元平面上の点を題材にしたプログラムを示します。 自分の点から見たとき2つの点のうち近い点を選ぶプログラムです。

    class CloseBetweenPoints {
        public static void main(String[] args) {
            Point origin = new Point(0,0);
            Point p1 = new Point(7,7);
            Point p2 = new Point(5,9);
            Point closePoint;
    
            closePoint = origin.chooseCloser(p1, p2);
    
            System.out.println("近い方の点の座標は (" +
                               closePoint.getX() + ", " + closePoint.getY() + ")");
        }
    }
    
    class Point {
        int x;
        int y;
    
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        int getX() {
            return x;
        }
        int getY() {
            return y;
        }
    
        /** 2つの点から近い方の点を返す */
        Point chooseCloser(Point a, Point b) {
            double distanceToA = Math.sqrt((x - a.getX())*(x - a.getX()) +
                                           (y - a.getY())*(y - a.getY()));
            double distanceToB = Math.sqrt((x - b.getX())*(x - b.getX()) +
                                           (y - b.getY())*(y - b.getY()));
            if (distanceToA < distanceToB)
    	    return a;
            else
                return b;
        }
    }
    

    メソッド chooseClose は、 引数に与えられた 2 つの点オブジェクトと自分自身の座標から距離を計算し、 近い方の点オブジェクトを返す働きをするメソッドです。 このように、オブジェクトそのものを return 文で返すことができます。 chooseClose メソッドは、Point close( .... ) のように、 Point クラスのオブジェクトを返すように宣言しておきます。

    メソッド main 内の変数 closePoint の宣言を見てみましょう。 この変数は new 演算子で新たなオブジェクトを生成する必要はありません。 メソッド close から返される、p1p2 のいずれかの点オブジェクトを代入するために使うからです。