最終更新日:2021/01/30 原本2017-

点クラス

ここでは,簡単なクラスの例を用いて,クラスの使い方を理解しましょう.
図形を扱うアプリケーションに必要な要素は何でしょう?
あらゆる図形は点の集合で構成されます. そこでまず,点,すなわち座標を表現するクラスを作って見ましょう. 座標は一般にn次元で定義できますが,ここでは簡単に2次元で考えましょう. まずは,下のサンプルを見てください.

Coordinate.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package jp.ac.utsunomiya_u.is;
 
public class Coordinate {
 
    /**
     * X座標
     */
    private double x = 0.0;
    /**
     * Y座標
     */
    private double y = 0.0;
 
    /**
     * コンストラクタ(デフォルト)
     */
    Coordinate() {
        x = y = 0.0;
    }
 
    /**
     * コンストラクタ
     *
     * @param x X座標
     * @param y Y座標
     */
    Coordinate(double x, double y) {
        this.x = x;
        this.y = y;
    }
 
    /**
     * セッタ for X座標
     *
     * @param x X座標
     */
    void setX(double x) {
        this.x = x;
    }
 
    /**
     * セッタ for Y座標
     *
     * @param y Y座標
     */
    void setY(double y) {
        this.y = y;
    }
 
    /**
     * セッタ for X-Y座標
     *
     * @param x X座標
     * @param y Y座標
     */
    void set(double x, double y) {
        this.x = x;
        this.y = y;
    }
 
    /**
     * ゲッタ for X座標
     *
     * @return X座標
     */
    double getX() {
        return x;
    }
 
    /**
     * ゲッタ for Y座標
     *
     * @return Y座標
     */
    double getY() {
        return y;
    }
}
1行目
"jp.ac.utsunomiya_u.is;"はパッケージ名です. パッケージ名はクラスを分類するために使われるもので,独自のクラスを作成する場合,独自のパッケージ名を付けておくほうが望ましいです. よく使われるパッケージ名の付け方は,所属のドメイン名を逆順につけていく方法です. パッケージ名を付けない場合,どのパッケージに属さないということにはならず,そのクラスは無名パッケージに属する事になります.
3-77行目
"class Coordinate"はクラス名です. クラス名とファイル名は一致している必要があります. "public"がついているので,このCoordinateクラスはどこからでもアクセスできます.
publicをつけることができるクラスは1つの.javaファイルで1つです.
クラスの最後は}で終わりにします(C++のように};ではありません).
8, 12行目
x-y軸上での2次元の座標を表現するために,x座標,y座標を表現する必要があります. そこで,double型のフィールド変数x,yを準備します.
フィールドは極力privateとし,クラス外からアクセス出来ないようにしましょう(カプセル化). 必要があれば,アクセサを用いましょう.
フィールドの宣言時に,規定の初期化が行われますが,初期値を明示しておく方が好ましいです.
17-19, 27-30行目
コンストラクタです.
30-70行目
アクセサ(セッタ,ゲッタ)でprivateのフィールド変数にアクセスします.
コンストラクタ,メソッドはすべてにアクセス修飾子なしに設定しています. アクセス設定はよく考えて,極力厳しいものにすることを心がけましょう.
Coordinate.javaと以下のCoordinateTest.javaをコピーして動作を確認してください.

CoordinateTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package jp.ac.utsunomiya_u.is;
 
class CoordinateTest {
 
    public static void main(String[] args) {       
        Coordinate coordinate = new Coordinate(1.0, 2.0);
        System.out.println("x = " + coordinate.getX() + " y = " + coordinate.getY());
        coordinate.setX(3.0);
        System.out.println("x = " + coordinate.getX() + " y = " + coordinate.getY());
        coordinate.set(4.0, 5.0);
        System.out.println("x = " + coordinate.getX() + " y = " + coordinate.getY());
    }
}

出力

x = 1.0 y = 2.0
x = 3.0 y = 2.0
x = 4.0 y = 5.0

コンストラクタ

コンストラクタはクラスがインスタンス(実体)化されるときに呼び出されます. インスタンス化される際に,まずフィールド(Coordinate.javaではxとy)が規定の値で初期化されます. なので,フィールド宣言で初期化しなくても良いですが,明示しておいた方がわかりやすい場合もあります.
フィールド宣言で明示的に初期化する事を心がけましょう.
クラスのインスタンスが生成されるときに最初に呼び出される特別なメソッドがコンストラクタです. コンストラクタ名は,クラス名と一致している必要があります. コンストラクタには戻り値は設定できません. コンストラクタは複数宣言でき,その引数で呼び出し時に区別されます(同じ引数のコンストラクタは宣言できません). クラスに1つもコンストラクタを書かなかった場合,引数なしで何もしないディフォルトコンストラクタが暗黙で呼ばれます. 逆に,引数付きのコンストラクタを1つでも自作した場合,暗黙のディフォルトコンストラクタは未定義になり,引数なしのでインスタンス化は出来ません. これはクラスの作り方としては親切ではありません.
なにも書くことがなくても,ディフォルトコンストラクタは書きましょう.
インスタンスが生成される時に,フィールドは既定値で初期化されますが,その値が必ずしも適切でない場合もあります. やはり,コンストラクタで明示的に初期化を行う事がミスの未然防止になる場合もあります.
全てのコンストラクタで適切な初期化を行う事を心がけましょう.

クラス型インスタンスの配列

基本データ型だけでなく,クラスのインスタンスの配列も利用したい場合があります.
ここではインスタンスの配列を利用するときの注意点についてついて学習します.
CoordinateTest.javaのmainメソッドにに以下を追記して,実行してみましょう.

CoordinateTest.javaのmainメソッドへの追加

1
2
3
4
5
6
7
Coordinate[] coordinates = new Coordinate[4];
for (int i = 0; i < coordinates.length; ++i) {
    coordinates[i].set(i, i);
}
for (int i = 0; i < coordinates.length; ++i) {
    System.out.println("x = " + coordinates[i].getX() + " y = " + coordinates[i].getY());
}
この追記部分はコンパイルは通りますが,3行目で実行時エラーが発生します. この原因を考えてみましょう.
1行目
Coordinateクラスの配列をcoordinatesという配列名で宣言しています.
2行目
coordinates.lengthは配列の長さ取得することができます(ここでのlengthはメソッドでもフィールドでもない特別な構文です).
3行目
coordinatesのi番目の要素にsetメソッドでx-y座標を設定しています.
これは一見正しそうに見えますが
クラス型インスタンスの配列はの宣言の後に,個々の要素のインスタンスを生成しなければなりません.
上のプログラムの3行目を

CoordinateTest.javaのmainメソッドの修正

1
coordinates[i].set(i, i); -> coordinates[i] = new Coordinate(i, i);
のように修正して,実行してみましょう.
この修正により,クラス型インスタンスの配列の各要素ごとにインスタンス化し,同時にコンストラクタで初期化も行うことができます.
その他にも

1
Coordinate[] coordinates = {new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0), new Coordinate(2.0, 2.0), new Coordinate(3.0, 3.0)};

1
2
Coordinate[] coordinates;
coordinates = new Coordinate[]{new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0), new Coordinate(2.0, 2.0), new Coordinate(3.0, 3.0)};
のような方法でクラス型インスタンスの配列を利用できます. また,多次元配列も同様で,2次元配列の場合,

1
2
3
4
5
6
Coordinate[][] coordinateses = new Coordinate[3][2];
for (int i = 0; i < coordinateses.length; ++i) {
    for (int j = 0; j < coordinateses[i].length; ++j) {
        coordinateses[i][j] = new Coordinate(i, j);
    }
}
などのようにして利用します.

オブジェクトのコピー

Javaにおけるインスタンスのコピーは注意が必要です.

1
2
3
4
5
6
7
Coordinate coordinate1 = new Coordinate(1.0, 1.0);
Coordinate coordinate2 = coordinate1;
System.out.println("x1 = " + coordinate1.getX() + " y1 = " + coordinate1.getY());
System.out.println("x2 = " + coordinate2.getX() + " y2 = " + coordinate2.getY());
coordinate1.setX(2.0);
System.out.println("x1 = " + coordinate1.getX() + " y1 = " + coordinate1.getY());
System.out.println("x2 = " + coordinate2.getX() + " y2 = " + coordinate2.getY());
のようなコードを考えてみましょう. 2行目でインスタンスcoordinate1をcoordinate2に代入しているので,cxoordinate1とcoordinate2は同一となり,どちらかの変更が両者に影響します. 例えば,5行目でcoordinate1のみのx座標を変更したつもりが,coordinate2のx座標も同じ影響を受けます. これは,オリジナルのコピーを目的とするプログラマには意図しない事になるかもとしれないので注意が必要です.

出力

x1 = 1.0 y1 = 1.0
x2 = 1.0 y2 = 1.0
x1 = 2.0 y1 = 1.0
x2 = 2.0 y2 = 1.0
上記のようなコードでコピーする場合の対処方法はいくつかありますが,例えば,Coordinateクラスに以下のようなコンストラクタを追加します.

Coordinate.javaの追加部分

1
2
3
4
5
6
7
8
/**
 * コンストラクタ(コピー)
 * @param coordinate 点クラスインスタンス
 */
Coordinate(Coordinate coordinate) {
    this.x = coordinate.getX();
    this.y = coordinate.getY();
}
このコンストラクは,渡されたインスタンスcoordinateのxとyを新たに生成するインスタンスのxとyにそのまま代入します. そして,coordinate2のインスタンスを生成する時,

CoordinateTest.javaのmainメソッドの修正

1
Coordinate coordinate2 = new Coordinate(coordinate1);
とすることで,本来意図した動作を実現する事が出来ます.
Coordinate.javaに上記のコンストラクタを追加し,CoordinateTest.javaのmainメソッドを修正し動作を確認してみましょう.

出力

x1 = 1.0 y1 = 1.0
x2 = 1.0 y2 = 1.0
x1 = 2.0 y1 = 1.0
x2 = 1.0 y2 = 1.0

オーバーロード

Coordinateクラスにdouble型の引数を2つとるコンストラクタが既にありました. Work4で引数をCoordinateクラスとするコンストラクタを作成しました. このように同じ名前での引数の異なるメソッドを利用可能です. これは「オーバーロード」(多重定義とも呼ばれます)と呼ばれます. JavaでもC++と同じようにオーバーロードが利用出来ます. この例では,コンストラクタのオーバーロードですが,一般のメソッドでもオーバーロード可能です.
引数の型,個数が全く同じで,その順序も全く同じメソッドは定義できません. これ以外であれば,戻り値の型や修飾子に依存せずオーバーロードできます.
今まで何度も使って来たprintlnメソッド(PrintStreamクラス)もオーバーロードを利用しています.

線クラス

これまで2次元空間上での座標を扱うクラスを考えてきました. では,次に線を扱うクラスを考えてみましょう. 線分を表現するためには,その両端の点の座標が必要です. そこで,次のようなクラスを作ってみましょう.

Line.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package jp.ac.utsunomiya_u.is;
 
public class Line {
 
    /**
     * 始点
     */
    private Coordinate begin = null;
    /**
     * 終点
     */
    private Coordinate end = null;
 
    /**
     * コンストラクタ(デフォルト)
     */
    Line() {
        begin = end = null;
    }
 
    /**
     * コンストラクタ
     *
     * @param begin 始点
     * @param end 終点
     */
    Line(Coordinate begin, Coordinate end) {
        this.begin = new Coordinate(begin);
        this.end = new Coordinate(end);
    }
 
    /**
     * コンストラクタ(コピー)
     *
     * @param line 線クラスのインタンス
     */
    Line(Line line) {
        this.begin = new Coordinate(line.begin);
        this.end = new Coordinate(line.end);
    }
 
    /**
     * セッタ for 始点
     *
     * @param begin 始点
     */
    void setBegin(Coordinate begin) {
        this.begin = new Coordinate(begin);
    }
 
    /**
     * セッタ for 終点
     *
     * @param end 終点
     */
    void setEnd(Coordinate end) {
        this.end = new Coordinate(end);
    }
 
    /**
     * セッタ for 始点 & 終点
     *
     * @param line Lineクラスのインスタンス
     */
    void set(Line line) {
        this.begin = new Coordinate(line.begin);
        this.end = new Coordinate(line.end);
    }
 
    /**
     * ゲッタ for 始点
     *
     * @return 始点
     */
    Coordinate getBegin() {
        return begin;
    }
 
    /**
     * ゲッタ for 終点
     *
     * @return 終点
     */
    Coordinate getEnd() {
        return end;
    }
 
    /**
     * 長さを計算
     *
     * @return 長さ
     */
    double getLength() {
        return Math.sqrt(Math.pow(begin.getX() - end.getX(), 2.0) + Math.pow(begin.getY() - end.getY(), 2.0));
    }
}
8, 11行目
線分は方向はありませんが,ここではフィールドとしてbegin, endとしています. nullで初期化しています.
17-19行目
ディフォルトコンストラクタです. フィールドのbeginとendをnullで初期化しています.
27-30行目
コンストラクタで線分の両端の座標を与えて初期化しています. 引数で与えられたbeginとendを用いて,新しいCoordinateクラスのインスタンスとして初期化しています.
37-40行目
コピーコンストラクタで,与えられたLineのインスタンスのbeginとendを用いて,新しいCoordinateクラスのインスタンスとして初期化しています. コピーコンストラクタの中でやっている事は,setメソッドと同じなので,コピーコンストラクタでsetメソッドを呼ぶようにすれば,共通化が可能と考えらます. しかし,ここでお話する継承によりオーバーライドされてしまうと予期せぬ挙動となるかもしれません.
コンストラクタ内でオーバーライド可能なメソッドを呼んではいけません.
47-49, 56-58, 65-68行目
セッタもコンストラクタと同様に,新しいCoordinateクラスのインスタンスとして保持しています.
94-95行目
線分の長さを取得するメソッドとしてgetLengthを用意しています. このメソッドの中でMathクラスを利用しています. Mathクラスはjava.lang.Objectクラスを継承していますが,java.langはimportしなくてもコンパイラによって自動的にimportされます. importはC言語の#includeのようなもので,後述します.
Line.javaと以下のLineTest.Javaをコピーして動作を確認してください.

LineTest.java

1
2
3
4
5
6
7
8
9
10
11
12
package jp.ac.utsunomiya_u.is;
 
class LineTest {
    public static void main(String[] args) {
        Coordinate begin = new Coordinate(1, 1);
        Coordinate end = new Coordinate(2, 2);
        Line line = new Line(begin, end);
        System.out.println("begin.x = " + line.getBegin().getX() + " begin.y = " + line.getBegin().getY());
        System.out.println("end.x = " + line.getEnd().getX() + " end.y = " + line.getEnd().getY());
        System.out.println("length = " + line.getLength());      
    }
}

三角形クラス

ここまでで,点クラス,線クラスが作成できました. では次の図形のクラスとして,三角形クラスを考えていきましょう. 線クラスを参考に三角形クラスのめ作成を考えると,コンストラクタで3点を指定して,セッタ,ゲッタを作りましょう. 更に,面積を求めるメソッドも実装しましょう.

Triangle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package jp.ac.utsunomiya_u.is;
 
public class Triangle {
 
    /**
     * 三角形の三頂点を保持するためのフィールド
     */
    private Coordinate[] coordinates = new Coordinate[3];
 
    /**
     * コンストラクタ(ディフォルト)
     */
    Triangle() {
        coordinates = new Coordinate[3];
    }
 
    /**
     * コンストラクタ
     *
     * @param coordinates 3点座標
     */
    Triangle(Coordinate[] coordinates) {
        for (int i = 0; i < Integer.min(this.coordinates.length, coordinates.length); ++i) {
            this.coordinates[i] = new Coordinate(coordinates[i]);
        }
    }
 
    /**
     * コンストラクタ(コピー)
     *
     * @param triangle
     */
    Triangle(Triangle triangle) {
        for (int i = 0; i < Integer.min(this.coordinates.length, triangle.coordinates.length); ++i) {
            this.coordinates[i] = new Coordinate(triangle.get()[i]);
        }
    }
 
    /**
     * セッタ
     *
     * @param coordinates 3点座標
     */
    void set(Coordinate[] coordinates) {
        for (int i = 0; i < Integer.min(this.coordinates.length, coordinates.length); ++i) {
            this.coordinates[i] = new Coordinate(coordinates[i]);
        }
    }
 
    /**
     * ゲッタ
     *
     * @return 3点座標
     */
    Coordinate[] get() {
        return coordinates;
    }
 
    /**
     * 面積
     *
     * ヘロンの公式 3辺の長さがa,b,cである三角形の面積は (s(s-a)(s-b)(s-c))^(1/2) where s = (a+b+c)/2
     *
     * @return 面積
     */
    double getArea() {
        double a = new Line(coordinates[0], coordinates[1]).getLength();
        double b = new Line(coordinates[1], coordinates[2]).getLength();
        double c = new Line(coordinates[2], coordinates[0]).getLength();
        double s = 0.5 * (a + b + c);
        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
    }
}
8行目
三角形の3頂点の座標を保持するためのフィールドです. 3頂点あるので,サイズは3に固定です.
22-26行目
コンストラクタで,与えられたCoordinateの配列の中身で初期化しています. 与えられた配列の長さは3とは限らないので,短い方に合わせるためにminメソッドで比較して要素数の小さい方の分だけフィールドのcoordinateに代入しています.
33-37行目
コピーコンストラクタで上のコンストラクタと同様の事をしています.
44-48行目
セッタで上のコンストラクタと同様の事をしています.
35-37行目
3頂点の座標を返します.
46-52行目
ヘロンの公式を使って面積を計算し,それを返します.
Triangle.javaと以下のTriangleTest.Javaをコピーして動作を確認してください.

TriangleTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package jp.ac.utsunomiya_u.is;
 
public class TriangleTest {
 
    public static void main(String[] args) {
        Coordinate[] coordinates = {new Coordinate(0.0, 0.0), new Coordinate(2.0, 0.0), new Coordinate(1.0, 1.0)};
        Triangle triangle = new Triangle(coordinates);
 
        int i=0;
        for (Coordinate coordinate : triangle.get()) {
            System.out.println("[" + i + "] x = " + coordinate.getX() + " y = " + coordinate.getY());           
            i++;
        }
        System.out.println("area = " + triangle.getArea());
    }
}
6行目
インスタンスを生成しながらの配列を初期化しています.
10-13行目
拡張forを使って頂点の値を出力
拡張for

1
for(型 変数名 : コレクション) {}
の形式でforループを構成出来ます.