Java

図で理解するJavaのクラスとインスタンス

Javaプログラミングの学習を始めると、わりと序盤でクラスとインスタンスについての理解に迫られます。これ個人的に初学者にはいささかハードルが高いのではないかと思っています。参考書やネット記事では、しばしば

クラスは設計書でインスタンスは設計書を基に作られた実体である

というようなことが書いてあるのをよく見ますが、これで理解ができてしまうのはプログラミング経験者やセンスのある人だけのような気がします。Java学習当初の僕は、こういった概念的な説明では理解ができませんでした。

この記事はJava(この記事ではJava8を使用)を学び始めた時の僕と同じようにクラスとインスタンスについてあまり理解ができていない人に向けた記事になります。図に関してはあくまでイメージで、正確には表現しきれていないのであらかじめご了承ください。

インスタンス

classからnewすることでインスタンスを生成することができますね。Javaでは基本的にはインスタンスを生成して処理を実行します。以下はありがちですが、人間クラスからインスタンスを生成するプログラムを書いています。

class Human {

    String name;
    int age;

    Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    void greet() {
        System.out.printf("はじめまして、%sです。\n", this.name);
    }
}

public static void main(String[] args) {

    Human humanA = new Human("金田", 16);
    Human humanB = new Human("島", 15);

}

人間を表すHumanクラスから名前と年齢を持ったインスタンスを生成します、上記では二つのインスタンスを生成しています。これを図にしてみました。

WS000001.JPG

クラスからインスタンスを生成しメモリに展開します。そして、そのインスタンスの参照値(メモリ上のアドレス)を変数に代入します。では上記で生成した二つのインスタンスの挨拶メソッドを実行させてみます。

public static void main(String[] args) {

    Human humanA = new Human("金田", 16);
    Human humanB = new Human("島", 15);

    humanA.greet(); // はじめまして、金田です。
    humanB.greet(); // はじめまして、島です。
}

名前はインスタンスごとに違うので、出力結果も異なります。

WS000002.JPG

クラスからそれぞれ実体を作り出し動作させるのが、Javaでの基本的なプログラムの動きになります。

static 修飾子

staticは直訳すると静的という意味です。こいつが登場すると少しだけ話がややこしくなりますが、こいつを理解せずにプログラミングをするととんでもないバグに繋がることがあるので、必ず理解するようにしましょう。static修飾子が付けられた変数、メソッドをそれぞれクラス変数、クラスメソッドとも呼びます。先ほどの人間クラスにそれぞれ追加してみます。

class Human {

    static final String classification = "哺乳類"; // <- クラス変数
    String name;
    int age;

    Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    static final boolean isMammal() { // <- クラスメソッド
        return true;
    }

    void greet() {
        System.out.printf("はじめまして、%sです。\n", this.name);
    }
}

人間クラスにstatic変数として分類を意味するclassification、staticメソッドとして哺乳類確認をするisMammalを追加しました。final修飾子は、書き換え不可にする修飾子です。変数であれば値の変更を、メソッドであればオーバーライドを不可能にします。では、追加したメンバーにアクセスしてみます。

public static void main(String[] args) {

    Human humanA = new Human("金田", 16);
    Human humanB = new Human("島", 15);

    System.out.println(humanA.classification); // 哺乳類
    System.out.println(humanB.isMammal()); // true

}

staticという修飾子がついたメンバーは、クラスの持ち物になります。なので、インスタンスを経由してクラスにアクセスしていることになります。これも図にしてみました。

WS000003.JPG

ちなみにeclipseで上記のコードを書くと「...には static にアクセスする必要があります」という警告メッセージが出ると思います。Javaではクラスのメンバーは直接クラスにアクセスしましょうという暗黙的なルールがあるからです。インスタンスを経由すると、それがクラスの持ち物なのかインスタンスの持ち物なのか判別しにくいためですね。この場合は以下のように直接クラスにアクセスするのが正しいです。

System.out.println(Human.classification); // 哺乳類
System.out.println(Human.isMammal()); // true

つまりstatic修飾子は、インスタンスに依存しないメンバーに付与する必要があります。オブジェクト指向的な考え方で言うと、人間の分類が"哺乳類"なことは、どんなインスタンスを生み出そうとインスタンス間で異なることのない定義です。finalをつけたのは、その定義(人間が"哺乳類"だということ)が今後変わることがない想定だからです。

もし使い方を間違えると...

staticの使い方を間違えるとバグに繋がると言いましたが、実際にstaticの誤った使い方を書いてみます。上記の人間クラスに歳をとらせるgrowOldメソッドをstaticで追加してみます。歳をとるということが人間にとってどういうことかを考えながら見てください。

class Human {
    static final String classification = "哺乳類";
    String name;
    int age;

    Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    static void growOld(){
        age++; // <- コンパイルエラー
    }

    /* 中略 */
}

追加したgrowOldメソッド内で「非 static フィールド age を static 参照できません」というコンパイルエラーになります。

WS000001.JPG

非staticフィールド、つまりインスタンスのフィールドなのでクラスメソッドからはアクセスできませんよと言うことです。こうなると歳をとるgrowOldメソッドをstatic定義していることが間違いだと気づきますが、親クラスやフレームワークの関係でメソッドからstaticを消すことができない場合があります。そんなときは、そもそもフィールドにアクセスしようとする設計がおかしいと思いましょう。ごくまれに変数(この場合で言う年齢 age)をstaticにしてしまう馬鹿者がいますが、むやみに変数をstaticにしてはいけません。全ての人間が同じ年齢で同時に歳をとるなんてことはありえません。staticメンバーを定義する場合は、設計レベルから考え直すようにしましょう。

変数とメソッド以外にもstaticを付与することができるメンバーで内部クラスがあります。この記事では内部クラスについては触れませんが、変数やメソッドとは若干意味合いが違うので注意してください。

継承

Javaでは他のクラスを継承することができ、継承したクラスのメンバーにアクセスすることができます。継承されるクラスを、継承するクラスの親クラス、またはスーパークラスとも言います。次は、他クラスを継承しているクラスからインスタンスを生成してみたいと思います。下記は、上記の人間クラスを継承した日本人クラスとアメリカ人クラスを新たに定義し、それぞれのインスタンスから挨拶メソッドを実行しています。

class Japanese extends Human {

    Japanese(String name, int age) {
        super(name, age);
    }

    @Override
    void greet() {
        super.greet();
        System.out.println("日本人です。");
    }

}

class American extends Human {

    American(String name, int age) {
        super(name, age);
    }

    @Override
    void greet() {
        super.greet();
        System.out.println("アメリカ人です。");
    }

}

public static void main(String[] args) {

    Japanese japanese = new Japanese("くみ", 15);
    American american = new American("エミリー", 15);

    japanese.greet();
    // はじめまして、くみです。
    // 日本人です。

    american.greet();
    // はじめまして、エミリーです。
    // アメリカ人です。

}

子クラスは親クラスの持つメンバーをそのまま引き継ぎます。つまり子インスタンスは親クラスのメンバーをそのまま使えます。なので子の挨拶メソッドの中で親の挨拶メソッドへのアクセスが可能となるのです。
WS000000.JPG

オーバーライド

ところで、日本人もアメリカ人も人間を継承しているので、インスタンスが人間として振舞うことができます。下記は人間という括りでリストにまとめて、順番に挨拶をさせています。

List<Human> humans = new ArrayList<>();
humans.add(new Japanese("くみ", 15));
humans.add(new Japanese("けん", 15));
humans.add(new American("エミリー", 15));
humans.add(new American("ビリー", 15));
humans.forEach(h -> h.greet());
// はじめまして、くみです。
// 日本人です。
// はじめまして、けんです。
// 日本人です。
// はじめまして、エミリーです。
// アメリカ人です。
// はじめまして、ビリーです。
// アメリカ人です。

ここで注目したいのは挨拶の内容です。人間として挨拶をしていますが日本人とアメリカ人とでそれぞれ子クラスの挨拶が実行されています。このように親クラスのメソッドを子クラスのメソッドで上書きすることをオーバーライドと言います。

WS000000.JPG

ちなみに下記の場合、"大和魂"は日本人のメンバーなので人間型変数ではアクセスできません。日本人インスタンスだとしても日本人のメンバーは日本人として振舞っていなければ使うことができません。

class Japanese extends Human {

    String japaneseSoul = "大和魂"; // <- 日本人の魂

    Japanese(String name, int age) {
        super(name, age);
    }

    @Override
    void greet() {
        super.greet();
        System.out.println("日本人です。");
    }

}

Japanese japanese = new Japanese("くみ", 15);
Human human = new Japanese("けん", 15);

System.out.println(japanese.japaneseSoul); // 大和魂
System.out.println(human.japaneseSoul); // <- コンパイルエラー

(日本人固有の持ち物に良い例が思い浮かばず、申し訳ない)

最後に

Javaはプログラミング入門の言語としてよく用いられ、多くのシステムで採用されている言語です。しかしクラスとインスタンスの関係を理解をしていないと、正しいJavaプログラミングはできませんし、オブジェクト指向云々の理解もできません。これが少しでも困っている方の理解に繋がれば幸いです。