独り言

プログラミングの講師をしています。新人研修で扱う技術の解説と個人の技術メモ、技術書の紹介など

【Java】Java入門(オブジェクト指向編)

目次

  • オブジェクト指向概要
  • 継承
  • Objectクラス
  • 抽象クラス
  • インターフェース
  • パッケージ
  • アクセス修飾子
  • 例外

ここでは主にJavaにおいてのオブジェクト指向関連のテーマを扱います。

それ以前のJavaの基本については以下の記事を参照ください。

case10.hateblo.jp


オブジェクト指向概要

ここではオブジェクト指向の概要について説明します。
オブジェクト指向は、一言で言ってしまえばプログラミングを効率よく行うための考え方のことです。
別の記事でも説明しましたが、コンピュータは機械語(0と1)しか認識できません。
それを人間でも読み書きができるようにするためにプログラミング言語が誕生しました。
それ以降、もっと分かりやすく、そしてもっと効率よく、という思想のもとにどんどん新たな言語が誕生していきました。
オブジェクト指向という考え方が生まれる前の言語(例えばC言語など)でも、今現在も使用されているものもあります。
しかし、オブジェクト指向が使われていない言語で規模の大きなシステムを開発する場合には、効率や分かりやすさといった点でいくつかの問題点がありました。(グローバル変数の問題など) その問題を解決するために生まれたのがオブジェクト指向という考え方です。

つまり、オブジェクト指向がなくてもプログラミングすることはできるのですが、 規模の大きなプログラムを効率よく、かつ分かりやすく開発するために あると便利なものがオブジェクト指向だと思ってください。

オブジェクト指向を学ぶ上で知っておくべき基本的な用語があります。

以下ではこれらの用語について解説してきます。

オブジェクト

オブジェクト指向は、その名の通り「オブジェクト」と呼ばれるものに着目した考え方です。
ですので先にオブジェクトについて説明します。
オブジェクトは直訳すると「モノ」です。
それだけだと漠然としていますが、例えば、目の前のパソコンやスマホ、食べ物や飲み物、自分、などなど。
つまり、認識できるモノは全てオブジェクトということになります。
プログラミングの世界では、「クラス」を元に作られるものがオブジェクトとなります。

インスタンス

オブジェクトと同じ意味で使用されます。
つまり、「クラス」を元に作られたものがインスタンスです。
Javaではクラスから作られたもの、つまりオブジェクトのことをインスタンスと呼びます。
※厳密にはオブジェクトの方がインスタンスよりも広い概念です。
Javaではクラスから作られたものはインスタンスと呼ぶケースの方が多いでしょう。

クラス

オブジェクトやインスタンスの説明でも出てきました。
また、今までプログラムを作る際にも必ず作ってきました。
ここでようやくクラスについての説明です。
クラスとは、インスタンスを作る元になる設計書のようなものです。
クラスという言葉を直訳すると分類という意味になります。

例えば、自分という存在を何かに分類すると、「人間」「哺乳類」「生き物」「社会人」などに分類できます。
「私」という人間はオブジェクトですが、そのオブジェクトを含む分類、「人間」「生き物」「社会人」などがクラスに相当します。
他、例えば普段使用しているスマホは、「スマホ」「iPhone」「コンピュータ」などに分類できます。
なんとなくイメージできたでしょうか。
つまり、具体的なもの(「私」や「今使用しているスマホ」など)がオブジェクトで、
そのオブジェクトを抽象的なものとして分類したもの(「人間」や「スマホ」など)がクラスです。

フィールド

続いてはフィールドについてです。
フィールドとは、オブジェクト(インスタンス)の状態を表すものです。
例えば、人間を例として考えたとき、人間には名前や生年月日、性別や国籍など、様々な情報を持っています。
また、その情報は人によって異なります。
このように、人間(クラス)が全員持っていて、個人個人(オブジェクト)によって内容が違うもの。
このデータを保持するものがフィールドです。
実際はクラスの中に宣言された変数です。

※厳密にはフィールドはクラスフィールドとインスタンスフィールドの2つがあります。
ここで説明しているのはインスタンスフィールドのことです。
クラスフィールドについては後の章で説明します。

コンストラク

クラスからインスタンスを生成するタイミングで呼ばれる処理のことをコンストラクタと呼びます。
詳しくは後の章で説明します。

メソッド

オブジェクトの振る舞い(処理)のことをメソッドと呼びます。
例えば、人間は「歩く」「走る」「話す」などの動作ができます。
しかし、歩く速さや歩き方などは人によって異なります。
このような、オブジェクトによって動作が異なる処理をクラスの中にメソッドとして定義します。

※厳密にはメソッドはインスタンスメソッドとクラスメソッドに分かれます。 ここで説明しているのはインスタンスメソッドの方です。
今まで作成していたmainメソッドなどはクラスメソッドになります。
これらの違いについては後で説明します。

クラスの定義

ではクラスの定義方法についてみていきます。
クラスを定義する場合はクラス名は必須です。
それ以外のフィールド、コンストラクタ、メソッドは必要に応じて定義していきます。

構文

public class クラス名 {
    フィールド

    コンストラクタ

    メソッド

}

以下は人間を表すクラスの例です。
Ningen.java

package app;

// オブジェクト作成用のクラス
public class Ningen {
    // フィールド
    String name; // 名前
    int age;     // 年齢

    // メソッド
    // ここではstaticはつけない
    public void greet() {
        System.out.println("Hello!");
    }

    // メソッドの中でフィールドの値を使うことも可能
    public void greet2() {
        System.out.println("名前は" + name);
    }
}

以下は人間クラスの動作を確認するプログラム。
Main.java

package app;

// 動作確認用
public class Main {

    public static void main(String[] args) {
        // インスタンスの生成
        // クラス名が型になる
        // newというキーワードを使用する
        Ningen n = new Ningen();

        // フィールドの値を参照できる
        // 何もセットしていない場合は初期値がセットされる
        System.out.println(n.name);    // String型(参照型)の場合はnull
        System.out.println(n.age);     // int型の場合は0

        System.out.println("----------");

        // フィールドに値をセットする
        n.name = "Alice";
        n.age = 20;

        // フィールドの値を確認
        System.out.println(n.name);
        System.out.println(n.age);

        System.out.println("----------");

        // メソッドを使用
        n.greet();
        n.greet2();
    }
}

結果

null
0
----------
Alice
20
----------
Hello!
名前はAlice

解説

まず、クラスを利用する際は、そのクラスを型として変数を用意します。
そして、インスタンスを作成する際には「new」というキーワードを使用します。
newによってインスタンスを作成し、そのクラスの型の変数に代入することによって、インスタンスが利用可能になります。
また、フィールドやメソッドを利用する場合は、「変数名.フィールド名、変数名.メソッド名」で利用することが可能です。

構文

クラス名 変数名 = new クラス名();
変数名.フィールド名;  // フィールドへのアクセス
変数名.メソッド名();    // メソッドの利用

Ningen n = new Ningen();
n.name = "Alice"; // フィールドに値をセット
n.greet();  // メソッドの利用

また、フィールドは使い方としては変数と同じですが、 メソッドの中で宣言した変数(このような変数をローカル変数と呼びます)とは異なる点があります。
ローカル変数の場合、変数の値を初期化していない状態で使用を試みるとコンパイルエラーになりますが、 フィールドの場合、値を初期化しなくても、インスタンスが生成された時点で初期値がセットされる仕組みになっています。
数値の場合は0、真偽値の場合はfalse、参照型の場合はnull
の値がセットされる仕組みになっていますので、覚えておきましょう。

コンストラク

ここでコンストラクタについて説明します。
コンストラクタは、インスタンス作成時(newを行った場合)に呼び出される特殊なメソッドです。
主にインスタンスの初期化の目的で作成されます。
コンストラクタにはいくつかのルールがあります。
以下にそれを示します。

  • メソッド名はクラス名
  • 戻り値はなし(voidも書かない)
  • オーバーロードできる(引数が異なればコンストラクタはいくつでも定義できる)
  • コンストラクタを一つも定義していない場合、デフォルトコンストラクタが作成される

    デフォルトコンストラクタは、処理も引数もないコンストラクタで、
    ソースコード上コンストラクタが定義されていない場合に、コンパイル時に自動で作成される。
    逆に、コンストラクタを一つでも定義している場合は、デフォルトコンストラクタは作成されない。

以下はコンストラクタを使ったプログラムのサンプルです。

Ningen.java

package app;

// オブジェクト作成用のクラス
public class Ningen {
    // フィールド
    String name; // 名前
    int age;     // 年齢

    // コンストラクタ
    public Ningen() {
        name = "名無し"; // フィールドの名前を設定
        age = 20; // フィールドの年齢を設定
    }

    // メソッド
    public void greet() {
        System.out.println("名前は" + name + "です。年齢は" + age + "歳です。");
    }
}

Main.java

package app;

// 動作確認用
public class Main {

    public static void main(String[] args) {
        Ningen n = new Ningen();  // この時コンストラクタが呼ばれる
        n.greet(); // メソッドを呼び出してフィールドの値を確認
    }
}
名前は名無しです。年齢は20歳です。

newを実行したときにコンストラクタの処理が呼ばれていることが分かります。

続いて引数ありのコンストラクタを定義した場合の例です。

Ningen.java

package app;

// オブジェクト作成用のクラス
public class Ningen {
    // フィールド
    String name; // 名前
    int age;     // 年齢

    // コンストラクタ
    public Ningen(String n, int a) {
        name = n;
        age = a;
    }

    // メソッド
    public void greet() {
        System.out.println("名前は" + name + "です。年齢は" + age + "歳です。");
    }
}

Main.java

package app;

public class Main {

    public static void main(String[] args) {
        // コンストラクタの引数に値が渡される
        Ningen n = new Ningen("Alice", 20); 

        // この時引数なしでnewを使用とするとコンパイルエラー
        // Ningen n = new Ningen();  

        n.greet(); 
    }
}

結果

名前はAliceです。年齢は20歳です。

コンストラクタに引数を定義した場合、newする際にクラス名の後で引数に値を渡してあげる必要があります。
引数ありのコンストラクタのみを定義すると、引数なしの状態でnewを行うことができなることに注意しましょう。

this

メソッドやコンストラクタの中でthisというキーワードを使用することができます。
これは、「自分自身」を表すキーワードです。
this.フィールド名 this.メソッド名 のように、フィールド名やメソッド名につけて使用します。
また、this単体で使用することもでき、その場合はコンストラクタを表します。

Ningen.java

package app;

public class Ningen {
    // フィールド
    String name; // 名前
    int age;     // 年齢

    // コンストラクタ
    // 引数の変数名をフィールドと同じにすると区別がつかなくなる
    public Ningen(String name, int age) {
        // これだとフィールドに値が入らない
        // name = name; 
        // age = age;   

        // thisを付けることでフィールドに値がセットされる
        this.name = name;  
        this.age = age;
    }

    // 引数なしのコンストラクタ
    public Ningen() {
        // 引数ありのコンストラクタを呼び出す
        this("名無し", 20);
    }

    // メソッド
    public void sayHello() {
        System.out.println("Hello!");
    }
    public void greet() {
        this.sayHello();  // 自分自身のメソッドの呼び出し。
        System.out.println("名前は" + name + "です。年齢は" + age + "歳です。");
    }
}

Main.java

package app;

// 動作確認用
public class Main {

    public static void main(String[] args) {
        Ningen n1 = new Ningen("Alice", 20);  
        Ningen n2 = new Ningen();  
        n1.greet(); 
        n2.greet();
    }
}

結果

Hello!
名前はAliceです。年齢は20歳です。
Hello!
名前は名無しです。年齢は20歳です。

クラスメンバとインスタンスメンバ

フィールドとメソッドのことをまとめてメンバと呼ぶことがあります。
この講義ではここの単元でしか使用しませんが、用語として覚えておきましょう。
メンバにはクラスメンバとインスタンスメソッドがあります。

それぞれの使い方として、

インスタンスメンバは
インスタンス名.フィールド名(メソッド名)で使用する

クラスメンバは
クラス名.フィールド名(メソッド名)で使用する
(インスタンス名でもアクセスすることはできるが、通常はしない)

以下はクラスメンバとインスタンメンバの違いを確認する例です。

Studnet.java

package app;

public class Student {
    // インスタンスフィールド
    String name;  // 名前
    int score;    // スコア

    // クラスフィールド
    public static int highScore;

    // コンストラクタ
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    // インスタンスメソッド
    // 自分のスコアが最高得点よりも高い場合は更新する
    public void updateHighScore() {
        if (score > highScore) {
            highScore = score;
        }
    }

    // クラスメソッド
    public static void dispHighScore() {
        System.out.println("最高点は" + highScore + "点です。");
    }
}

Main.java

package app;

// 動作確認用
public class Main {

    public static void main(String[] args) {
        // クラスフィールド参照
        System.out.println(Student.highScore);

        // クラスメソッドの使用
        Student.dispHighScore(); // 最高点の表示

        // インスタンス作成
        Student s1 = new Student("Aさん", 75);
        s1.updateHighScore();

        // クラスメソッドの使用
        Student.dispHighScore(); // 最高点の表示

        // インスタンス作成
        Student s2 = new Student("Bさん", 85);
        s2.updateHighScore();

        // クラスメソッドの使用
        Student.dispHighScore();  // 最高点の表示
    } 
}

結果

0
最高点は0点です。
最高点は75点です。
最高点は85点です。

ここではStudentは一つの教室内の生徒を表すクラスだと思ってください。
生徒の名前やテストの点数はそれぞれ生徒によって異なります。
なのでこの場合インスタンスフィールドとなります。
一方で、テストの最高得点というのはクラス全体で1つだけです。
このように、それぞれの生徒と関係なく、クラス全体として一つの値しか持ちえないものはクラスフィールドとなります。
これはメソッドの場合でも同じです。

また、インスタンスメソッドからは、クラスフィールドやクラスメソッドにアクセスることが可能です。
一方、クラスメソッドからはインスタンスフィールドやインスタンスメソッドにアクセスすることはできません。
合わせて覚えておきましょう。

Studnet.java

public class Student {
    // インスタンスフィールド
    String name;  // 名前
    int score;    // スコア

    // クラスフィールド
    public static int highScore;

    // インスタンスメソッド
    public void updateHighScore() {
        if (score > highScore) {
            highScore = score;
        }
    }

    // インスタンスメソッド
    public void dispScore() {
        System.out.println(name + "のスコアは" + score + "点です。");
        dispHighScore(); // クラスメソッドの呼び出しは可能
    }

    // クラスメソッド
    public static void dispHighScore() {
        // クラスメソッドからインスタンスメソッドは呼び出せない
        // dispScore();
        System.out.println("最高点は" + highScore + "点です。");
    }
}

定数

通常、変数は一度定義すれば何度でも再代入して値を上書きすることができます。
しかし、初期化された後に再代入ができない(値の変更ができない)変数を使用したい場合があります。
Javaではそのような再代入できない変数を定義することができ、定数とよびます。
変数を定数にしたい場合には宣言時にfinalというキーワードを付けます。

Student.java

public class Student {
    public static final int MAX_SCORE = 100;
    public static final int MIN_SCORE = 0;

    // 以下略
}

Main.java

public class Main {

    public static void main(String[] args) {
        System.out.println(Student.MAX_SCORE);
        System.out.println(Student.MIN_SCORE);
    }
}

結果

100
0

定数を作成する場合は、一般的にクラスフィールドにします(staticを付ける)。
インスタンスごとに値が変わらないため、クラスで一つだけ持てばよいものです。
そのためstaticを付けてクラスフィールドとするのが一般的です。
また、定数名は、アルファベット大文字で単語をアンダースコアで区切るのが一般的です。
(このような名前の付け方をスネークケースと呼びます)
通常の変数と区別するために、このような書き方が推奨されています。

オブジェクト指向概要まとめ

  • オブジェクト指向とはプログラミングを効率化するための考え方
  • オブジェクトとは認識できるモノ。クラスとはオブジェクトを抽象化したもの
  • クラスから作成されたオブジェクトをインスタンスと呼ぶ
  • クラスにはフィールド、メソッド、コンストラクタが定義できる
  • フィールドはインスタンスが持つ情報
  • メソッドはフィールドの振舞い(処理)
  • コンストラクタはインスタンスが作成されたときに呼ばれる特殊なメソッド。主に初期化を目的とする
  • thisは自分自身を表すキーワード
  • フィールド(メソッド)にstaticを付けるとクラスフィールド(メソッド)になる
  • クラスフィールド(メソッド)はクラスが一つだけ持つフィールド(メソッド)
  • 再代入できない変数を定数という
  • Javaでは定数はfinalを付け、大文字のスネークケースで変数名を定義する

継承

継承はオブジェクト指向の三大要素と呼ばれるものの一つです。
継承は、既に存在しているクラスの機能を引き継いで、新しいクラスを作成するための仕組みです。
継承される側のクラスをスーパークラス、親クラス、などと呼びます。
継承する側のクラスと、サブクラス、子クラス、などと呼びます。

継承する場合は「extends」というキーワードを使用します。
継承を行うと、スーパークラスが持つフィールドやメソッドを受け継いで使用することができます。

以下はイメージです。 「>」 が継承を表していると考えてください。

スーパークラス > サブクラス
生き物 > 哺乳類 > 人間
乗り物 > 車 > 電気自動車
キャラクター > 魔法使い
キャラクター > 戦士

哺乳類は生き物なので、生き物としての性質を兼ね備えています。
生き物としての性質を持ちつつ、哺乳類としての性質が加わっています。
このような場合、生き物クラスを継承して哺乳類クラスが作成されたイメージです。 人間は哺乳類なので、人間は哺乳類としての性質を持ちつつ、人間としての特性が加わります。 この場合は、哺乳類クラスを継承して人間クラスが作成されます。 他、乗り物 > 車 も考え方は同じです。

以下は継承の例です。
ゲームのキャラクターを作ることを想定した例です。

Character.java

package app;

// スーパークラス
// キャラクター
public class Character {
    public String name;
    public int HP;

    public void attack() {
        System.out.println("パンチ");
    }
}

Wizard.java

package app;

// サブクラス
// 魔法使い
public class Wizard extends Character {
    int MP;

    public void magic() {
        System.out.println("魔法");
    }
}

Main.java

package app;

// 動作確認
public class Main {

    public static void main(String[] args) {
        // サブクラスのインスタンス作成
        Wizard w = new Wizard();
        // スーパークラスのフィールドは参照可能
        w.name = "maho";
        w.HP = 500;

        // スーパークラスのメソッドも使用可能
        w.attack();

        // サブクラスのメソッドも使用可能
        w.magic();
    }
}

結果

パンチ
魔法

キャラクタークラスを定義して、そのクラスを継承して魔法使いクラスを作成しました。
魔法使いクラスは、キャラクタークラスが持つフィールドやメソッドを持ちつつ、魔法使い独自の新しいフィールドやメソッドを定義しており、両方のフィールドやメソッドを使用することができます。

継承では、あるクラスを継承したサブクラスを更に継承して新しいサブクラスを作成することも可能です。

Vehicle.java

package app;

// 乗り物
public class Vehicle {

}

Car.java

package app;

// 車 // 乗り物クラスを継承
public class Car extends Vehicle {

}

ElectricCar.java

package app;

// 電気自動車 // 車クラスを継承
public class ElectricCar extends Car {

}

多重継承

1つのクラスが複数のクラスを継承することを多重継承といいます。
Javaでは多重継承は禁止されています。
理由は、継承元のクラスに同じ名前のメソッドがあった場合、 区別ができなくなってしまうからです。

public class A {

}

public class B {

}

// 多重継承は禁止
// 以下のように書くとコンパイルエラーになる
// public class C extends A, B {
// }

super

自分自身を表すとき、thisというキーワードを用いましたが、スーパークラスのメンバを参照したい場合は「super」というキーワードを使用します。

Character.java

package app;

// スーパークラス
// キャラクター
public class Character {
    public String name;
    public int HP;

    public Character(String name, int HP) {
        this.name = name;
        this.HP = HP;
    }

    public void attack() {
        System.out.println("パンチ");
    }
}

Wizard.java

package app;

// サブクラス
// 魔法使い
public class Wizard extends Character {
    int MP;

    public Wizard(String name, int HP, int MP) {
        super(name, HP); // スーパークラスのコンストラクタの呼び出し
        this.MP = MP;
    }

    // 引数なしのコンストラクタ
    // 処理を書かなかった場合、引数なしのスーパークラスのコンストラクタを呼ぼうとするが、
    // スーパークラスのコンストラクタに引数なしのコンストラクタは存在しないため、エラーになる
    // public Wizard() {
    //}

    public void doubleAttack() {
        for (int i = 0; i < 2; i++) {
            super.attack(); // スーパークラスのメソッドの呼び出し
        }
    }

    public void magic() {
        System.out.println("魔法");
    }
}

Main.java

package app;

public class Main {

    public static void main(String[] args) {
        Wizard w = new Wizard("maho", 500, 300);
        System.out.println("name:" + w.name);
        System.out.println("HP:" + w.HP);
        System.out.println("MP:" + w.MP);
        w.doubleAttack();
    }
}

結果

name:maho
HP:500
MP:300
パンチ
パンチ

継承を使う場合、コンストラクタで色々と制約があります。
サブクラスのコンストラクタではスーパークラスのコンストラクタを呼び出すのは処理の1行目でしかできません。
また、明示的にスーパークラスインスタンスの呼び出しを記述しなかった場合、 処理の1行目で暗黙的に引数なしのスーパークラスのコンストラクタが呼ばれるしくみになっています。

参照型の型変換

基本型の場合、大きい型に小さい型の変数の値を代入することが可能でした。
参照型でも同様のことができます。
スーパークラスの型の変数に、サブクラスの型のインスタンスを入れることができます。
ただし、その場合アクセスできるフィールドやメソッドは、スーパークラスで定義されているものだけです。
また、継承関係にある型同士であれば、キャスト演算子を使用することが可能です。

Character.java

package app;

// スーパークラス
// キャラクター
public class Character {
    public String name;
    public int HP;

    public void attack() {
        System.out.println("パンチ");
    }
}

Wizard.java

package app;

// サブクラス
// 魔法使い
public class Wizard extends Character {
    public int MP;

    public void magic() {
        System.out.println("魔法");
    }
}

Main.java

package app;

// 動作確認
public class Main {

    public static void main(String[] args) {
        // サブクラスのインスタンスをスーパークラスのインスタンスに代入
        Character c = new Wizard();
        c.attack();  // スーパークラスのメソッドは使用可能
        // c.magic();   // サブクラスのメソッドは使用不可

        // キャストすることが可能
        Wizard w = (Wizard)c;
        w.magic();  // サブクラスのメソッドも使用可能

    }
}

結果

パンチ
魔法

オーバーライド

オーバーライドとはメソッドの上書きのことです。
サブクラスでは、スーパークラスのメソッドを再定義して、処理の中身を上書きすることができます。
メソッドをオーバーライドするには、メソッド名、引数、戻り値の型が一致している必要があります。

Character.java

package app;

// スーパークラス
// キャラクター
public class Character {
    public String name;
    public int HP;

    public Character(String name, int HP) {
        this.name = name;
        this.HP = HP;
    }

    public void attack() {
        System.out.println("パンチ");
    }
}

Wizard.java

package app;

// サブクラス
// 魔法使い
public class Wizard extends Character {
    int MP;

    public Wizard(String name, int HP, int MP) {
        super(name, HP); // スーパークラスのコンストラクタの呼び出し
        this.MP = MP;
    }

    // メソッドのオーバーライド
    @Override
    public void attack() {
        System.out.println("魔法で攻撃");
    }
}

Main.java

package app;

// 動作確認
public class Main {

    public static void main(String[] args) {
        Wizard w = new Wizard("maho", 500, 300);
        w.attack();  // サブクラスのメソッドが呼ばれる
    }
}

結果

魔法で攻撃

結果から同じメソッド名で処理の内容が上書きされていることが分かります。
オーバーライドは言ってしまえばメソッドの処理内容を上書きするだけです。
それだけで何の意味があるのかはこの時点では感じるかもしれませんが、 オブジェクト指向を活用して開発効率を上げることができるのには、 このオーバーライドの機能がかなり大きく貢献しています。
詳しくはポリモーフィズムで説明しますが、オーバーライドは継承において非常に重要な機能であることを知っておきましょう。

オーバーライドとオーバーロード

メソッドの章でオーバーロードという言葉が出てきたのを覚えているでしょうか。
オーバーライドとオーバーロードは言葉が似ていて、どちらもメソッドが関係しているという点でも似ていますが、機能としては全く別もので何の関係もありません。
慣れないうちは紛らわしく感じるかもしれませんが、混乱しないように注意しましょう。

アノテーション

オーバーライドしているメソッド定義に「@Override」というものが付いています。
この「@」から始まるものを、「アノテーション」と呼びます。
日本語では注釈という意味で、コンパイラJVMなどにお知らせする機能を持ちます。
「@Override」はつけなくてもオーバーライドすることは可能ですが、 付けることで、オーバーライドのミスを防ぐことができます。
「@Override」を付けると、このメソッドはオーバーライドしています。
ということを明示的にお知らせします。
その際、スーパークラスのメソッドとメソッド名が異なっていたり、引数が異なっていた場合、コンパイルエラーになります。
アノテーションを付けていない場合、メソッド名が異なっていも、違うメソッドだと解釈されてしまうので、 コンパイルエラーにならず、思わぬバグになる可能性があります。
できる限りアノテーションを付けるようにしましょう。

ポリモーフィズム

先に説明したオーバーライドについて、メソッドを上書きするという説明だけではどんなメリットがあるのか分からない方も多いかと思います。
ポリモーフィズムがあることで、オーバーライドの重要性が分かります。
ポリモーフィズムは日本語では多態性とも呼ばれます。
これは、同じメソッドの処理内容がインスタンスによって異なる機能のことです。

Character.java

package app;

// スーパークラス
// キャラクター
public class Character {
    public String name;
    public int HP;

    public Character(String name, int HP) {
        this.name = name;
        this.HP = HP;
    }

    public void attack() {
        System.out.println("パンチ");
    }
}

Wizard.java

package app;

// サブクラス
// 魔法使い
public class Wizard extends Character {

    public Wizard(String name, int HP) {
        super(name, HP);
    }

    // メソッドのオーバーライド
    @Override
    public void attack() {
        System.out.println("魔法で攻撃");
    }
}

Warrior.java

package app;

// サブクラス
// 戦士
public class Warrior extends Character {

    public Warrior(String name, int HP) {
        super(name, HP);
    }

    // メソッドのオーバーライド
    @Override
    public void attack() {
        System.out.println("武器で攻撃");
    }
}

Player.java

package app;

// 動作確認
public class Main {

    public static void main(String[] args) {
        Character c1 = new Wizard("maho", 400);
        Character c2 = new Warrior("sen", 600);
        play(c1);
        play(c2);
    }

    public static void play(Character c) {
        c.attack(); // サブクラスで定義したメソッドの中身が実行される
    }
}

結果

魔法で攻撃
武器で攻撃

上記がポリモーフィズムの例です。
Character型の変数でメソッドを呼び出していますが、
実際に処理されているのは、変数に入っているインスタンスの型で定義したメソッド (オーバーライドされたメソッド)が呼ばれていることが分かるかと思います。
ポリモーフィズムは、オブジェクト指向を習いたてのころはメリットがいまいちわかりにくい仕組みの一つです。

ポリモーフィズムのメリットは、呼び出し側の処理を共通化できるという点です。
上記の例では、Characterを継承したクラスを新しく追加した場合でも、 Playerクラスのplayメソッドは変更する必要がありません。
引数に渡させた変数のインスタンスによって自動的に中の処理が変わるためです。

メソッドは呼び出される処理を共通化する仕組みですが、 ポリモーフィズムは逆に呼び出す側の処理を共通化する仕組みです。

最初はピンとこないかもしれませんが、 クラスライブやフレームワークの中でもよく使用されているものなので、 開発に慣れていくことで徐々に理解を深めることができるかと思います。

is-a関係

継承の機能を使用するにおいて、押さえておきたい概念があります。
それは「is-a」関係と呼ばれるものです。
A is a B
と書くと、AはBである。
という日本語約になります。
継承を使用するときは、is-a関係が成り立っているかどうかが大事となってきます。

例えば
「車は乗り物である。」「人間は生き物である。」という文章は違和感なく成立するので、
生き物クラスを継承して人間クラスを作成したり、乗り物クラスを継承して車クラスを作成することには問題はありません。

しかし、例えば、
パソコンクラスとテレビクラスを作成しようとした場合、パソコンとテレビはどちらもディスプレイに表示する機能を持っている。
なので、効率よく開発するためにパソコンクラスを継承してテレビクラスを作成したとする。
これはプログラミング的には問題なく動作するが、is-a関係に当てはめると「テレビはパソコンである」となる。
これはis-a関係が成り立っていないので、このような継承は推奨されません。

has-a関係

先に説明したパソコンクラスとテレビクラスを効率よく作成したい場合、has-a関係と呼ばれるものを使うとうまくいきます。
A has a B
と書くと、AはBを持っている、という意味になります。
パソコンは画面を持っているし、テレビも画面を持っています。
このような構造はhas-a関係を利用するとうまくいきます。
具体的には、AクラスのフィールドでBクラスのインスタンスを保持するような形になります。

以下はhas-a関係の例。

PC has a Display
TV has a Display
を表しています。

Display.java

package app;

public class Display {
    public String disp() {
        return "画面に表示";
    }
}

PC.java

package app;

public class PC {
    public Display display;

    public PC(Display display) {
        this.display = display;
    }

    public void disp() {
        System.out.println("PCの" + display.disp());
    }
}

TV.java

package app;

public class TV {
    public Display display;

    public TV(Display display) {
        this.display = display;
    }

    public void disp() {
        System.out.println("TVの" + display.disp());
    }
}
package app;

public class Main {

    public static void main(String[] args) {
        PC pc = new PC(new Display());
        TV tv = new TV(new Display());
        pc.disp();
        tv.disp();
    }
}

final

フィールドにfinalというキーワードを付けると定数になりました。
実はfinalというキーワードはクラスやメソッドにも付けることができます。
メソッド宣言にfinalを付けるとオーバーライドができなくなります。
また、クラス宣言にfinalを付けるとそのクラスを継承することができなくなります。
使う頻度は少ないですが覚えておくようにいましょう。

ちなみに、Stringクラスは継承ができないようにクラス宣言にfinalが付いています。

継承のまとめ

  • 継承とはクラスの特徴(機能)を受け継ぐこと
  • 継承されるクラスのことをスーパークラス(親クラス)、継承したクラスのことをサブクラス(子クラス)と呼ぶ
  • extendsというキーワードで継承が行える
  • サブクラスではスーパークラスの機能が使え、更にフィールドやメソッドを新しく追加できる
  • Javaでは多重継承は禁止されている
  • スーパークラスを参照するときはsuperというキーワードで参照できる
  • コンストラクタでは先にスーパークラスのコンストラクタが処理される必要がある
  • クラスを継承してメソッドを上書きすることをオーバーライドという
  • オーバーライドする際に@Overrideというアノテーションを使用することでミスを減らすことができる
  • スーパークラスの型の変数にサブクラスのインスタンスを格納することができる
  • スーパークラスの型にサブクラスのインスタンスを格納すると、メソッドはオーバーライドしたものが呼ばれる。これをポリモーフィズムと呼ぶ
  • 継承をする場合、is-a関係が成り立っているかどうかが大事
  • クラスにfinalを付けると継承ができなくなる
  • メソッドにfinalを付けるとオーバーライドができなくなる

Objectクラス

Javaの標準ライブラリの中にObjectクラスというクラスがあります。
これはJavaの中でも特別なクラスで、全てのクラスのスーパークラスになります。
自分で新しくクラスを作成した際に、extendsを使用しなかった場合は、 暗黙的に「extends Object」と書かれた状態と同じになります。

public class A {

}

// 上と同じ
public class A extends Object {

}

全てのクラスは暗黙的にObjectクラスを継承しているため、 Objectクラスで定義されているメソッドはどのクラスでも使用可能ということです。
どんなメソッドがあるか、詳しくはJavaAPI リファレンスを参照してください。
ここでは代表的なメソッドをいくつか紹介します。

toStringメソッド

そのクラスの文字列表現を表すメソッドです。
コンソールに出力するためのprintメソッドやprintlnメソッドの引数に 何かしらのインスタンスを入れた際は、そのクラスのtoStringメソッドが実行される仕組みになっています。

Ningen.java

package app;

public class Ningen {
    String name;
    int age;

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

Main.java

package app;

// 動作確認
public class Main {

    public static void main(String[] args) {
        Ningen n = new Ningen("Alice", 20);
        System.out.println(n);  // ObjectクラスのtoStringメソッドの実行結果が表示される
    }
}

結果

Ningen@15db9742

Ningen.java

package app;

public class Ningen {
    String name;
    int age;

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

    @Override
    public String toString() {
        return "名前:" + name + ", 年齢:" + age;
    }
}

Main.java

package app;

// 動作確認
public class Main {

    public static void main(String[] args) {
        Ningen n = new Ningen("Alice", 20);
        System.out.println(n);  // オーバーライドしたtoStringメソッドの実行結果が表示される
    }
}

結果

名前:Alice, 年齢:20

これはポリモーフィズムが活用されている例になります。
printlnメソッドは、引数にObject型のインスタンスを受け取ります。
全てのクラスはObjectクラスを継承しているため、どのクラスのインスタンスでも引数に渡すことができます。
そして、引数で受け取ったインスタンスのtoStringメソッドを呼び出しているため、ポリモーフィズムが適用され、オーバーライドしたメソッドが呼ばれます。

equalsメソッド

インスタンス同士が等しいかどうかを判断するためのメソッドです。
オーバーライドしない場合、インスタンスのアドレスを比較する(==で比較した場合と同じ)。
フィールドの値が同じ場合は同じインスタンスとみなしたい、 という場合、はequalsメソッドをオーバーライドします。
文字列を比較する場合にもequalsメソッドを使用したのを覚えているでしょうか。
Stringも参照型なので、文字列同士を==で比較した場合はインスタンスのアドレスが比較されてしまうわけですが、Stringクラスの中で、equalsメソッドがオーバーライドされており、文字が全て等しいかどうかを確かめています。 そのため、equalsメソッドを使用することで文字列の比較できるようになっているわけです。

Ningen.java

package app;

public class Ningen {
    String name;
    int age;

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

Main.java

package app;

// 動作確認
public class Main {

    public static void main(String[] args) {
        Ningen n1 = new Ningen("Alice", 20);
        Ningen n2 = new Ningen("Alice", 20);
        System.out.println(n1 == n2);       // false
        System.out.println(n1.equals(n2));  // false オーバーライドしていない場合は==と同じ
    }
}

結果

false
false

Ningen.java

package app;

public class Ningen {
    String name;
    int age;

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

    @Override
    public boolean equals(Object o) {
        if(o != null) {
            if (o instanceof Ningen) {
                Ningen n = (Ningen)o;
                // nameとageが同じなら同じとみなす
                if(name.equals(n.name) && age == n.age) {
                    return true;
                }
            }
        }
        return false;
    }
}

Main.java

package app;

// 動作確認
public class Main {

    public static void main(String[] args) {
        Ningen n1 = new Ningen("Alice", 20);
        Ningen n2 = new Ningen("Alice", 20);
        System.out.println(n1 == n2);       // false
        System.out.println(n1.equals(n2));  // true
    }
}

結果

false
true

Objectクラスのまとめ

  • Objectクラスは全てのクラスのスーパークラス
  • extendsを使用しなかった場合は自動的にObjectクラスが継承される
  • ObjectクラスはtoStringメソッドやequalsメソッドなどのメソッドを持つ

抽象クラス

継承を有効に活用するためのクラスとして抽象クラスと呼ばれるクラスがあります。
抽象クラスは継承されることを前提としたクラスになります。
「abstract」というキーワードを用いることで抽象クラスが作成できます。
そのため、抽象クラスのインスタンスを作成することはできません。
抽象メソッドと呼ばれる処理のないメソッドを定義し、サブクラスでオーバーライドします。
インスタンスはサブクラスで作成することになります。
うまく使うことで、開発上のミスを減らし、効率よく開発を行うことができるようになります。
抽象クラスとの対比で、抽象メソッドのない通常のクラスを「具象クラス」と呼ぶことがあります。

以下は抽象クラスを使用した継承の例です。

Character.java

package app;

// 抽象クラスにはabstractを付ける
public abstract class Character {

    // 抽象メソッドはabstractを付ける
    // 処理と中かっこは書かず、セミコロンを付ける
    public abstract void attack();

    // 通常のメソッドも定義できる
    // その中で抽象メソッドを使用できる
    public void doubleAttack() {
        for (int i = 0; i < 2; i++) {
            attack();
        }
    }
}

Wizard.java

package app;

// 抽象クラスを継承したクラス
public class Wizard extends Character{

    @Override
    public void attack() {
        System.out.println("魔法で攻撃");
    }
}

Main.java

package app;

// 動作確認
public class Main {
    public static void main(String[] args) {
        // 抽象クラスではインスタンスの作成はできない
        // Character c = new Character();

        Character w = new Wizard();
        w.attack();
        System.out.println("----------");
        w.doubleAttack();
    }
}

結果

魔法で攻撃
----------
魔法で攻撃
魔法で攻撃

抽象クラスの存在意義

抽象クラスを用いずとも、通常のクラスで、処理の中身のないオーバーライド前提のメソッドを作成し、
サブクラスでオーバーライドすることで同じことを実現することはできます。
しかし、その場合、サブクラスでオーバーライドをし忘れる可能性があります。
また、スーパークラスインスタンスを作成されてしまう可能性もあります。
抽象クラスを有効に使うことで、実装し忘れのミスを防ぐことができます。
これは、チームで開発を行うときに特に重要となる仕組みです。

抽象クラスのまとめ

  • 抽象クラスにはクラス宣言で「abstract」を付ける
  • 抽象クラスには抽象メソッドと通常のメソッドが書ける
  • 抽象メソッドにはメソッド宣言で「abstract」を付け、処理や中かっこは書かない
  • 抽象メソッドが一つでも定義されているクラスは抽象クラスになる
  • 抽象クラスはインスタンスを作成することができない
  • 抽象クラスを継承した具象クラスは、抽象メソッドをオーバーライドしなければいけない
  • 抽象クラスを継承して抽象クラスを作成することもできる。その場合は抽象メソッドをオーバーライドする必要はない

インターフェース

抽象クラスに似た概念で、インターフェースと呼ばれるものもあります。
インターフェースでは、メソッドは抽象メソッドしか定義できません。
(通常のメソッド(具象メソッドとも呼ばれる)は定義できない)
また、通常のフィールドも定義できず、定数のみ定義できます。

使い道がイメージしにくいかもしれません。
インターフェースは、is-a関係が成り立たない(つまり、継承関係にない)が、異なるインスタンスを同一視したい場合に使用します。

canDisp.java

package app;

// 画面インターフェース
public interface canDisp {

    // インターフェースでフィールドを定義すると定数になる
    // public static final が暗黙的に定義される
    int SIZE = 100;

    // 抽象メソッド
    // インターフェースに定義したメソッドは、暗黙的にpublicが定義される
    void disp();
}

PC.java

package app;

// PCクラス
// インターフェースの場合、implementsを付ける。
// 継承ではなく、実装と呼ぶ
public class PC implements Display {
    
    @Override 
    public void disp() {
        System.out.println("PCの画面を表示");
    }
}

TV.java

package app;

// TVクラス
public class TV implements Display {

    @Override 
    public void disp() {
        System.out.println("TVの画面を表示");
    }
}

Main.java

package app;

// 動作確認
public class Main {
    public static void main(String[] args) {
        // インターフェースも型として使用できる
        Display pc = new PC();
        pc.disp();

        Display tv = new TV();
        tv.disp();
    }
}

インターフェースの補足

インターフェース同士で継承を行うことができます。
クラスがインターフェースを利用する場合は実装と言いますが、インターフェース同士では継承となります。
Javaでは多重継承ができないと書きましたが、インターフェースの場合は多重継承が可能です。
インターフェースの場合は抽象メソッドしか定義できないためメソッドの処理を実装できません。
そのため、メソッド名が重複しても不都合がなく、多重継承が可能となっています。
また、インターフェースを実装する際は、複数のインターフェースを実装することも可能です。
また、クラスを継承しつつインターフェースを実装することも可能です。

public interface A {

}

public interface B {

}

// インターフェースを継承して新しいインターフェースを作成可能
// インターフェースの場合は多重継承が許可される
public interface C extends A, B {

}

public class D {

}

// クラスの継承しながらインターフェースの実装も可能
// また、インターフェースの実装は複数可能
public class E extends D implements A, B {

}

インターフェースを初めて学習した段階では使用するメリットは感じにくいかもしれません。
しかし、クラスライブラリやフレームワークの中ではインターフェースを用いたポリモーフィズムは多く使用されている仕組みです。

defaultメソッド

インターフェースで定義できるのは基本的に定数と抽象メソッドでしたが、 Java 8 からはdefaultメソッドというのが定義できるようになりました。
defaultメソッドでは、メソッドにdefaultという修飾子を付けることで、処理を定義することができます。

Greet.java

package app;

public interface Greet {

    // defaultメソッド
    default public void hello() {
        System.out.println("hello");
    }
}

defaultメソッドを定義した場合、そのインターフェースを実装するクラスでメソッドをオーバライドしていなかったとしてもコンパイルエラーになりません。
そのままそのメソッドを使用することができます。

Main.java

package app;

public class Main implements Greet{
    public static void main(Striing[] args) {
        Main m = new Main();
        m.hello();
    }
}

ただし、2つ以上のインターフェースを実装する場合に、インターフェース同士でメソッドが重複した場合にはオーバーライドを促される場合もあります。

Greet2.java

package app;

public interface Greet2 {

    public void hello();

}

Main.java

public class Main implements Greet, Greet2{
    public static void main(Striing[] args) {
        Main m = new Main();
        m.hello();
    }

    @Override
    public void hello() {
        // defaultメソッドを使用する場合はsuperを使用する
        Greet.super.hello();
    }
}

インターフェースのまとめ

  • インターフェースに定義できるのは定数と抽象メソッド
  • Java8からはdefaultメソッドが定義できる
  • クラスがインターフェースを使用する場合は「実装」という。implementsというキーワードを使用する。
  • インターフェースからインターフェースを継承できる。
  • インターフェースの場合は多重継承が可能

パッケージ

パッケージとは、簡単にいえばフォルダ(ディレクトリ)のことです。
パソコンやスマホでデータを管理する場合、写真は写真用のフォルダ、動画は動画用のフォルダ、音楽は音楽用のフォルダ と、フォルダを分けて管理したほうが分かりやすくなります。

これはプログラミングでも同じです。
プログラムの規模が大きくなると、ファイルの数が多くなって、そのままだと管理が難しくなります。
プログラムの機能によってフォルダを分けてあげることで管理がしやすくなり、 プログラムの把握が容易になります。

また、同じフォルダ内で同じ名前のファイルを複数作ることはできませんが、 フォルダが分かれていれば同じファイル名でファイルを作成することができます。
Javaでも同じクラス名を作成したい場合に、管理する場所を分けておけば、同じ名前が使用できます。

つまりパッケージとは

  • カテゴリごとにファイルを整理して分かりやすくする
  • 同じ名前のクラスを作成しても困らないようにする

という目的で存在します。

今までのソースコードの中に

package app;

という記述が含まれていましたが、これがパッケージの宣言です。 「app」というパッケージに含まれるソースコードということになります。

クラスを作成する際、どのパッケージにも属さない状態でクラスを作成することも可能です。
どのパッケージにも属さないクラスを「デフォルトパッケージ」のクラスと呼びます。
通常開発を行う際には、デフォルトパッケージのクラスは推奨されていません。
基本的に役割に合ったパッケージを作成して、そこに属するようにしておきましょう。

Utility.java

// パッケージ宣言はクラス宣言の上に書きます
package util;

public class Utility {
    public static int add(int a, int b) {
        return a + b;
    }
}

別パッケージのクラスの利用

パッケージ名から指定することで利用可能です。
この指定方法を完全修飾名といいます。

Main.java

package app;

public class Main {
    public static void main(String[] args) {
        // パッケージ名.クラス名.メソッド名
        int num = util.Utility.add(10, 20);
        System.out.println(num);
    }
}

毎回完全修飾名でクラスを指定するのは面倒です。
importの機能を使えば、クラス名だけでの使用が可能になります。

Main.java

package app;

import util.Utility; // パッケージ名.クラス名 とすることで、そのクラスが使用可能となる

public class Main {
    public static void main(String[] args) {
        int num = Utility.add(10, 20); // クラス名でアクセス可能
        System.out.println(num);
    }
}

1つのパッケージの中に複数のクラスが含まれている場合があります。
その場合に「」を使用すると一括でインポートすることができます。
しかし、どのクラスを使用しているのか分かりにくいため、推奨されません。
また、VS Codeeclipseなどを使って開発している場合、importされていない他パッケージのクラスを使用すると、自動的にインポートの候補を表示して補完してくれる機能もあります。
そのため、import文を書くことに時間がかかることはほとんどありません。
なので「
」は使用せず、各クラス名を指定してimportする方がよいでしょう。

Main.java

package app;

import util.*; 

public class Main {
    // 以下略
}

注意点としてJavaのパッケージには階層構造はないということを知っておきましょう。

package a;
public class ClassA {

}
package a.b;
public class ClassB {

}

上記のようなパッケージとクラスがあったとします。
OS上からソースを確認しようとすると「a」というフォルダの中にClassA.javaというファイルと「b」というフォルダがあります。
そして「b」というフォルダの中にClassB.javaというファイルがあります。
OS上から見れば「a」と「b」は階層構造にありますが、Javaのパッケージでは「a」というパッケージと「a.b」というパッケージは全く別物です。
インポート宣言部で

import a.*

と書いたとしても、実際にインポートされるのはClassAだけで、
「a.b」パッケージに含まれるクラスはインポートされないということを知っておきましょう。

パッケージ名の付け方

パッケージ名は、自社のドメイン名を逆にしたものを使用するのが一般的です。
ドメインが「xxxx.co.jp」の場合、パッケージ名は「jp.co.xxxx」のようになります。
どのプロジェクトで使用したクラスかが分かるように、プロジェクト名をパッケージに付ける場合もあります。
いずれにしても、インターネット上で被らない名前を付けることが望ましいでしょう。

java.langパッケージ

パッケージが異なるクラスを利用するためには、クラスをインポートするか、完全修飾名を使用するのどちらかが必要になります。 今まで、文字列を使用する際に何気なくStringというクラスを使用していました。
Stringクラスは普段自分たちがつくるプログラムとは別パッケージにありますが、インポート宣言や完全修飾名を使用しなくても利用することができました。
これはどういう仕組みかというと、Javaでプログラムを開発する際、インポート宣言を書かなくても暗黙的にインポートされるパッケージがあります。
それが「java.lang」というパッケージなのですが、実はStringクラスはこのクラスに含まれています。
そのため、何の意識もすることなくStringクラスが使用できたのです。

java.langパッケージには、Stringクラス以外にも ObjectクラスやMathクラス、Integerクラスなど、 普段からよく使用するクラスがまとめられています。
他にどんなクラスがあるのかはAPIリファレンスを参照ください。

パッケージのまとめ

  • パッケージとはクラスを役割毎にフォルダ分けする仕組み
  • パッケージには階層構造はない
  • パッケージ宣言は先頭に書く
  • パッケージ名はドメインを逆から書くのが一般的
  • 別パッケージのクラスを使用する場合、パッケージ名から指定するか、importを使用する
  • importでは「*」を使用することで一括でインポートすることができるが、あまり使用されない
  • Javaでは「java.lang」というパッケージがデフォルトでインポートされている

アクセス修飾子

アクセス修飾子とはフィールドやメソッドの公開範囲を決めるものです。
4種類あります。

  • public:どこからでもアクセス可能
  • protected:同じパッケージと継承先のクラスからアクセス可能
  • なし:同じパッケージ内からアクセス可能
  • private:同じクラス内からアクセス可能

の4つです。
publicが最も公開範囲が広く、下に行くにつれて公開範囲が狭くなります。
4つありますが、実際にはpublicかprivateの場合がほとんどです。
protectedとアクセス修飾子なしはほとんど使用しないと思ってよいでしょう。

また、フィールド、メソッド、コンストラクタについては、4つのアクセス修飾子のいずれかを指定することができますが、クラスに対してはpublicかアクセス修飾子を付けないかの2択となります。

Access.java

package app;

public class Access {
    public int numPublic;
    protected int numProtected;
    int numDefault;
    private int numPrivate;

    public Access(int numPublic, int numProtected, int numDefault, int numPrivate) {
        this.numPublic = numPublic;
        this.numProtected = numProtected;
        this.numDefault = numDefault;
        this.numPrivate = numPrivate;
    }

    // 平均取得
    public int getAvg() {
        return getSum() / 4;
    }

    // 合計取得
    private int getSum() {
        return numPublic + numProtected + numDefault + numPrivate;
    }
}

Main.java

package app;

// 動作確認
public class Main {
    public static void main(String[] args) {
        Access a = new Access(10, 15, 23, 28);

        System.out.println(a.numPublic); // アクセス可能
        System.out.println(a.numProtected); // 同一パッケージの場合アクセス可能
        System.out.println(a.numDefault); // 同一パッケージの場合アクセス可能
        // System.out.println(a.numPrivate); // アクセス不可

        // System.out.println(a.getSum()); // アクセス不可
        System.out.println(a.getAvg()); // アクセス可能
    }
}

protectedについては少しややこしいので注意が必要です。
protectedは、継承したクラスからアクセス可能となるアクセス修飾子ですが、アクセスする場合には継承したクラスからsuperを使ってアクセスします。
つまり、インスタンスメソッド内やコンストラクタ内からのアクセスが可能ということです。
たとえ継承したクラスだとしても、パッケージが異なる場合はスーパークラスインスタンスを新しく作成し、そのフィールドやメソッドに直接アクセスすることはできません。

AccessSub.java

package app2;

import app.Access;

public class AccessSub extends Access {

    public AccessSub(int numPublic, int numProtected, int numDefault, int numPrivate) {
        super(numPublic, numProtected, numDefault, numPrivate);
    }

    // superを使用することでアクセスすることが可能
    public void setNumProtected(int num){
        super.numProtected = num;
    }

    public static void main(String[] args) {
        Access a = new Access(1, 2, 3, 4);
        // 以下はコンパイルエラー
        // 継承したクラスでも、新しくインスタンスを作成してフィールドに直接アクセスすることはできない
        // System.out.println(a.numProtected);
    }
}

privateの使いどころ

詳しい説明はカプセル化の部分で行いますが、フィールドのアクセス修飾子はほとんどprivateになります。
publicとなるのはほぼ定数の場合だけだと思ってよいでしょう。
一方、メソッドについては目的に応じてpublicにする場合とprivateにする場合で分かれます。
プログラミングを学び始めの場合、メソッドを作成する際に何も考えずにアクセス修飾子をpublicにしてしまう人が多いです。
おそらくそれはアクセス修飾子を意識することに慣れていないからだと思われますが、自クラスでしか使用していないメソッドは意識してアクセス修飾子をprivateに設定するようにしましょう。
これは、後々修正が発生した場合に影響範囲を最小限に抑えるためです。
メソッドのアクセス修飾子がpublicになっていた場合、他のクラスからも使用されている可能性があります。
その場合、後からメソッドの中身を修正しようと思っても、影響範囲が分からないため、修正することが困難になります。
メソッドのアクセス修飾子がprivateになっていた場合、そのメソッドが使用されているのは自クラスだけであるのが明白なため、影響範囲は自クラスだけに抑えられます。
そのため修正が容易になります。
メソッドを作成する際は、どこから使用される可能性があるかを意識し、自クラスしか使用しないのであればprivateに設定するように意識してみましょう。

カプセル化

フィールドの値を誰でも簡単に変更できるようになっていた場合、プログラム上思わぬ不具合を起こす可能性があります。
そうならないために、アクセスを制限して、必要な情報だけアクセスできるようにする仕組みがカプセル化です。

Javaにおいてカプセル化を実現する仕組みはある程度ルール化されています。
フィールドを直接修正・参照せずに、メソッド経由でアクセスするようにします。
このようなメソッドのことをアクセッサメソッドと呼びます。

  • フィールドのアクセス修飾子をprivateにする
  • フィールドに対するアクセッサメソッドを用意する
    • アクセッサメソッドは、セッターとゲッターの2つがある。
    • セッター
      • メソッド名は「set + フィールド名(先頭大文字)」
      • 戻り値はなし(void)
      • 引数はフィールドの型
      • 処理は引数の値をフィールドにセット。必要に応じて処理を追加
    • ゲッター
      • メソッド名は「get + フィールドの名前(先頭大文字」
      • 戻り値はフィールドの型
      • 引数はなし
      • 処理はフィールドの値を返す。必要に応じて処理を追加
package app;

// カプセル化されていない
public class Ningen {
    public String name;
    public int age;

    public Ningen() {
    }

    public Ningen(STring name, int age) {
        this.name = name;
        this.age = age;
    }
}
package app;

// カプセル化されている
public class Ningen {
    private String name;
    private int age;

    public Ningen() {
    }

    public Ningen(STring name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

この2つのソースを見比べても、何が違うのか分からないかもしれません。
フィールドを直接参照したり値を変更したりするか、メソッドを使用して参照したり値を変更するかの差しかありません。
事実、このソースの場合だと実質差はありませんが、メソッド経由でアクセスすることで、柔軟性が高くなります。

例えば、age(年齢)はint型で設定していますが、マイナスの値や、200や300といった一般的に考えておかしな値が設定された場合、思わぬ不具合を引き起こす可能性があります。
フィールドをpublicにしていると、意図しない値が設定される可能性は十分にありますが、メソッドの場合、処理で制御することで意図しない値が入るのを防ぐことができます。

public void setAge(int age) {
    // 0より小さい、または100より大きい場合は強制的に0にする
    if (age < 0 && age > 100) {
        this.age = 0;    
    } else {
        this.age = age;
    }
}

セッターとゲッターの名前の意味

一般にアクセッサメソッドを作成する際、セッターのメソッド名は「set + フィールド名」、ゲッターのメソッド名は「get + フィールド名」となります。
メソッドを作成する際、この決まりを守らず、自分で別のメソッド名で作成することも可能です。
メソッド名を違う形式にしても、外部からメソッドを使用することに支障は内容に思います。
しかし、実はこの慣習に従ってメソッド名を付けることは非常に重要です。
フレームワークJSP/サーブレットというJavaでWebアプリケーションを作成する機能の中には、この命名規則に沿ってアクセッサメソッドが作られることを前提に、便利な機能を提供してくれているものも少なくありません。
ですので、アクセッサメソッドは自分で独自に定義するのではなく、慣習に従って作成することが望ましいです。

eclipse(JavaIDE)の機能では、フィールドを作成したら、そのフィールドからアクセッサメソッドを自動生成してくれる機能もあります。
メソッド名のスペルミスで思わぬ不具合を防ぐためにも、そういった機能を活用するとよいでしょう。

アクセス修飾子まとめ

  • アクセス修飾子には4つの種類(レベル)がある
  • ほとんどはprivateかpublicの2択
  • 自クラスでしか使用しないメソッドはprivateにする
  • フィールドは基本的にprivateにし、メソッド経由でアクセスする。これをカプセル化と呼ぶ
  • アクセッサメソッドにはセッターとゲッターがある
  • セッターとゲッターは命名規則に沿ったメソッド名にすることが大事

例外

コンパイルエラーと実行時エラー

Javaのエラーには2種類のエラーがあります。
1つはコンパイルエラーで、もう一つは実行時エラーです。

コンパイルエラー

これはプログラムの文法上のエラーです。
プログラムの書き方がそもそもJavaのルールに従っていない場合に起こるエラーです。
コンパイルエラーがある状態だと、ソースコードコンパイルするこができないため、当然プログラムの実行もできません。

実行時エラー

これは、プログラムの書き方に文法上のミスはないが、実行した時に起こるエラーです。
これを例外(Exception)といいます。

まずは実行時例外が起きるプログラムを見てみましょう。

Main.java

package app;

public class Main {
    public static void main(String[] args) {
        /*
        int型同士の割り算は問題ないため、コンパイルエラーにはならない
        しかし数学的に0による割り算はできないため、エラーになる
        */
        int n = 10 / 0;
        System.out.println(n);
    }
}

結果

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Main.main(Main.java:7)
package app;

public class Main {
    public static void main(String[] args) {
        /*
        配列の添え字にint型を使用することは文法上は正しい。
        しかし、この配列の要素数は3なので、使用できる添え字は0~2まで。
        ここでは3を使用しているのでエラーになる。
        */
        int[] nums = {1, 2, 3};
        System.out.println(nums[3]);
    }
}

結果

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
    at Main.main(Main.java:9)
package app;

public class Main {
    public static void main(String[] args) {
        /*
        Stringの変数名.メソッド名()
        によるメソッドの呼び出しは文法上問題ないが
        オブジェクトがnullだった場合はメソッドを呼び出せないため、エラーになる。
        */
        String str = null;
        System.out.println(str.length());
    }
}

結果

Exception in thread "main" java.lang.NullPointerException
    at Main.main(Main.java:9)
package app;

public class Main {
    public static void main(String[] args) {
        /*
        Integer.parseIntメソッドはString型を引数に取るので、文法としては正しい。
        しかし、"a"は数値に変換できないので、エラーになる。
        */
        String numStr = "a";
        int num = Integer.parseInt(numStr);
        System.out.println(num);
    }
}

結果

Exception in thread "main" java.lang.NumberFormatException: For input string: "a"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:580)
    at java.lang.Integer.parseInt(Integer.java:615)
    at Main.main(Main.java:8)
package app;

public class Main {
    public static void main(String[] args) {
        /* 
        部分文字列を作成しようとしているが、
        文字列の文字数が3文字しかないため、
        10番目の文字列を取得しようとしてもエラーになる。
        */
        String strAbc = "abc";
        String sub = strAbc.substring(10, 15);
        System.out.println(strAbc);
    }
}
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 15
    at java.lang.String.substring(String.java:1963)
    at Main.main(Main.java:9)

どのプログラムも正常に結果が出るものはなく、途中でエラーが発生してプログラムが異常終了してしまいます。
ここではエラーの1行目にある
java.lang.XXXXXExceptionE:
という部分に着目してください。
XXXXXの内容がプログラムによって違っているのが分かります。
これは、発生した例外を表すクラスになっています。

例外のクラスは数多くあるため、1つ1つのクラス名を覚える必要はありません。
プログラムをたくさん書いているうちに自然に覚える場合もありますし、クラス名を表す英語の意味が分かればある程度何のエラーかが理解できるようになってきます。
エラーが発生した場合は、慌てずにまずは何のエラーが発生したのかをきちんと見極めるようにしましょう。

例外の全体像

Object
|-Trowable
|-Error
|-Exception
|-RuntimeException
|-NullPointerExceptionなど
|-RuntimeException以外
(IOExceptionなど)

Javaでは例外は全てクラスになります。
ここでは例外のクラスの全体像を説明します。
まず、Trowableというクラスが例外の最上位のクラスになります。
TrowableのサブクラスとしてErrorとExceptionがあります。

Errorは、例えばJavaのプログラムを実行しようとしたが、メモリの容量が足りずに実行できなかった場合などに発生します。
つまり、プログラムでは対処しようのないものがErrorです。
プログラムの問題ではなく、根本的に別の対処が必要となるものなので、プログラムによる対処は必要ありません。

次にExceptionですが、これが実行時例外になります。
Exceptionは大きく2種類に分けることができます。
一つは、非チェック例外と呼ばれるもので、RuntimeExceptionとそのサブクラスが対象になります。
もう一つはチェック例外と呼ばれるもので、RuntimeException以外とそのサブクラス以外のExceptionです。

チェック例外と非チェック例外

チェック例外と非チェック例外について詳しく見ていきます。

Main.java

package app;

import java.io.File;

public class Main {
    public static void main(STring[] args) {
        // 非チェック例外
        int n = 10;
        int m = 0;
        System.out.println(n / m);

        // チェック例外
        File file = new File("C:\\test\\test.java");
        file.createNewFile();
    }
}

上の2つの処理は、どちらも例外が発生する可能性のある処理です。
非チェック例外が発生する可能性のある処理は、処理を書いても何の問題もありませんが、チェック例外が発生する可能性のある処理は、そのままではコンパイルエラーになります。
コンパイルエラーを解決するためには、エラーに対する対処をしてあげる必要があります。

チェック例外と非チェック例外の違い

さて、ではチェック例外と非チェック例外はどういった基準で分かれているのでしょうか。
非チェック例外は基本的にはプログラムのバグです。
例えば先ほどのプログラムでは、0除算のエラーが発生します。
しかし、このプログラム、割る数が0かどうかをif文で判断すればエラーの発生を防ぐことができます。
他にも、配列の要素数を超えたアクセスによるエラーであれば、lengthによってあらかじめ要素数のチェックを行えば防ぐことはできるはずです。
このように、if文などを使用して、プログラムをきちんと書けば例外の発生を防ぐことができるものが非チェック例外です。
プログラムを直せば発生しなくなるものなので、例外の対処をしなければいけない、ということはなく、
あえてコンパイラによってチェックをしない、というわけです。

一方でチェック例外は、処理が正常に行われるか失敗するかが環境や状況によって左右されます。

File file = new File("C:\\test\\test.java");
file.createNewFile();

先の例で書いたプログラムは、C:\testのフォルダにtest.javaというファイルを作成するプログラムです。
実行したPCのCドライブ直下に「test」というフォルダが存在すれば処理は成功して例外は発生しません。
一方、「test」というフォルダが存在しなかった場合は、例外が発生します。
つまり、同じプログラムであっても、実行する環境やタイミングによって成功したり失敗したりする可能性がある。
そのときに発生する例外がチェック例外です。

ファイル操作以外でも、例えばデータベースに対する処理の場合、
ソースコードでエラーにならないように細かくチェックしていても、接続先のデータベースに異常があれば失敗する可能性があります。
ネットワーク経由でデータを取得するようなプログラムの場合、そもそもネットワークがつながっていなければ実行しようがありません。
このように、プログラムが正しくても環境や状況によって失敗の可能性がある処理はあらかじめ例外の発生に備えて対処をしておく必要がある、ということです。

try~catch

先の例で見たチェック例外が発生するプログラムの場合、例外の対処をしてあげる必要があります。
例外の対処方法は2つあります。
そのうちの1つ目の対処方法がtry~catchです。
例外が発生する可能性のある処理を、try句の中に書きます。
例外が発生した場合の処理をcatch句の中に書きます。
これにより、例外が発生した場合でも、プログラムが異常終了することなく、 引き続き処理が実行されます。

構文

try {
    例外が発生する可能性のある処理
} catch (発生するExceptionの型 変数名) {
    例外が発生した場合の処理
}

Main.java

package app;

import java.io.File;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        
        File file = new File("C:\\test\\test.java");
        try {
            file.createNewFile();
            System.out.println("ファイル作成完了");
        } catch (IOException e) {
            System.out.println("エラー発生!");
            e.printStackTrace(); // コンソールにエラー情報を出力する処理
        }
    }
}

結果 ※環境によって結果が変わります。 C:\testフォルダがあればエラーは発生しません。

エラー発生!
java.io.IOException: 指定されたパスが見つかりません。
    at java.io.WinNTFileSystem.createFileExclusively(Native Method)
    at java.io.File.createNewFile(File.java:1012)
    at Main.main(Main.java:9)

エラーが発生したらcatch句の中の処理が実行されます。
eには発生したExceptionクラスのインスタンスが入っています。
ExceptionクラスのprintStackTraceメソッドはコンソールにエラー情報が出力されます。

次の例を見てみましょう。
catch句は複数書くことができます。
※この例は非チェック例外なので例外対処は必要ありませんが、try~catchの中で学習のために使用しています。

Main.java

package app;

public class Main {
    public static void main(String[] args) {
        // catch句は複数書くことができます。
        try {
            String str = null;
            String sub;
            sub = str.substring(10);
            System.out.println(sub);
        } catch (NullPointerException e) {
            System.out.println("NullPointer");
        } catch (IndexOutOfBoundsException e) {
            System.out.println("IndexOutOfBounds");
        }
    }
}

結果

NullPointer

複数のExceptionをcatchする場合、|で区切って複数書くことができます。

Main.java

package app;

public class Main {
    public static void main(String[] args) {
        // 一つのキャッチ句にまとめて書くことも可能
        try {
            String str = null;
            String sub = str.substring(10);
            System.out.println(sub);
        } catch (NullPointerException | IndexOutOfBoundsException e) {
            e.printStackTrace();
        }
    }
}

結果

java.lang.NullPointerException
    at Main.main(Main.java:6)

catch句にはポリモーフィズムが適用されます。

Main.java

package app;

public class Main {
    public static void main(String[] args) {
        // 例外のcatchにもポリモーフィズムが働く
        // スーパークラスのExceptionでcatchすることもできる
        // 非チェック例外はRuntimeExceptionのサブクラスなので、
        // 非チェック例外ならRuntimeExceptionで対処できる
        try {
            System.out.println(10 / 0);
        } catch (RuntimeException e) {
            System.out.println("RuntimeException");
        }
    }
}

結果

RuntimeException
public class Main {
    public static void main(String[] args) {
        // こちらはコンパイルエラー
        // 継承関係で親に値するクラスが先に書かれている場合、
        // サブクラスのcatchは通らくなるため、コンパイルエラー
        try {
            System.out.println(10 / 0);
        } catch (Exception e) {
            System.out.println("Exception");
        } catch (RuntimeException e) {
            System.out.println("RuntimeException");
        }
    }
}

例外が発生した場合でも、発声しなかった場合でもどちらでも確実に実行したい処理がある場合があります。
そのような場合はfinaly句を使用します。

package app;

public class Main {
    public static void main(String[] args) {
        // finally句
        // 処理が正常の終了しようが、例外が発生しようが、どちらにせよ必ず実行したい処理がある場合もある
        // その場合、catch句の後にfinally句を使用することで実現可能
        // DB操作やファイル操作、ネットワーク関連の処理などでは、リソースを使用した後に開放する処理が必要で
        // そのような場合にfinallyが使用される
        try {
            int n = 10 / 0;
            System.out.println(n);
        } catch (Exception e) {
            System.out.println("例外発生");
        } finally {
            System.out.println("絶対に通る");
        }

    }
}

結果

例外発生
絶対に通る

throws宣言

try~catchを使用しない例外の対処方法があります。
それがthrows宣言です。
try~catchは、例外が発生する可能性のある処理に対して、自分自身で対応する方法でした。
一方でthrowsは、自分で例外の処理を行わず、呼び出し元に任せてしまう方法です。
メソッド宣言でメソッド名の後に、「throws 発生するException名」で定義します。

メソッドの宣言部に「throws 発生する可能性のあるException」 を付けることで、try~catchによる例外対処が不要になります。
その場合、メソッドの呼び出し元に例外対処を任せることになります。
呼び出し元のメソッドでtry~catchにより対処するか、
呼び出し元のメソッドでもthrows宣言をして、更に呼び出し元に任せて例外を対処します。

Main.java

import java.io.File;
import java.io.IOException;

public class Main {

    // throws宣言のあるメソッドを呼び出す場合は、例外の対処が必要
    // そのメソッドをtry~catchで囲って対処。またはthrows宣言をしてさらに呼び出し元に任せる
    // mainメソッドでthrows宣言をした場合、JVMが処理することになり、例外発生時点でプログラムは終了する
    public static void main(String[] args) {
        try {
            createJavaFile()
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 呼び出し元に例外処理を任せる
    // throws宣言を書くことで、try~catchが不要になる
    public static void createJavaFile() throws IOException {
        File file = new File("C:\\test\\test.java");
        file.createNewFile();
    }
}

結果

java.io.IOException: 指定されたパスが見つかりません。
    at java.io.WinNTFileSystem.createFileExclusively(Native Method)
    at java.io.File.createNewFile(File.java:1012)
    at Main.createJavaFile(Main.java:21)
    at Main.main(Main.java:11)

非チェック例外の場合は、例外の対処がいらないため、 throws宣言されていても対処の必要ありません。 この例では例外は対処されずプログラムが強制終了します。

Main.java

package app;

public class Main {

    public static void main(String[] args) {
        int n = calc();
        System.out.println(n);
    }

    public static int calc() throws RuntimeException {
        return 10 / 0;
    }
}

例外を作成・発生させる

Javaでは例外はクラスのため、自分で作成することもできます。
チェック例外を作成したい場合は、Exceptionクラスを継承して例外を作成します。
非チェック例外を作成したい場合は、RuntimeExceptionクラスを継承して例外を作成します。

また作成した例外は自分で発生させることもできます。
例外を発生させる場合はthrowを使用します。

SampleException.java

package app;

// チェック例外
public class SampleException extends Exception {

}

SampleRuntimeException.java

package app;

// 非チェック例外
public class SampleRuntimeException extends RuntimeException {

}

Main.java

package app;

// 動作検証
public class Main {
    public static void main(String[] args) {

        try {
            // 例外の対処が必要
            sampleExceptionTest();
        } catch (SampleException e) {
            e.printStackTrace();
        }

        // 例外対処不要
        sampleRuntimeExceptionTest();

    }

    public static void sampleExceptionTest() throws SampleException {
        throw new SampleException();
    }

    public static void sampleRuntimeExceptionTest() {
        throw new SampleRuntimeException();
    }
}

例外とオーバーライド

例外、その中でもthrowsについては、オーバーライドとも深い関わりがあります。
まずは以下のサンプルを確認してください。

Super.java

package app;

import java.io.File;
import java.io.IOException;

public class Super {

    // 例外は発生する可能性のあるメソッド
    public void method() throws IOException {
        final File file = new File("C:\\test\\Main.java");
        file.createNewFile();
    }

}

Sub.java

package app;

import java.io.IOException;

// Superを継承
public class Sub extends Super {

    @Override
    public void method() throws IOException {
       super.method();
    }

}
package app;

import java.io.IOException;

// 動作確認
public class Main {
    public static void main(String[] args) {
        Super s = new Sub();
        try {
            s.method();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

この例では例外が発生する可能性のあるメソッドをもつSuperクラスをSubクラスが継承しています。
Mainクラスではポリモーフィズムを利用し、Subのインスタンスでメソッドを呼び出します。
メソッドは例外が発生する可能性があるため、try~catchで囲っています。
この時、Subクラスのmethodのthrows宣言で、発生する可能性のある例外をExceptionに変更してみます。

@Override
    // throws宣言をExceptionに変更する
    public void method() throws Exception {
       super.method();
    }

そうすると、Subクラスでコンパイルエラーが発生します。
これはthrows宣言での例外のクラスを、継承元のメソッドでの例外よりも上位の例外に変更できないことを示しています。
オーバーライドする際になぜ発生する例外のクラスを上位のクラスにできないかというと、呼び出し元に影響が出てしまうからです。
この例で仮にオーバーライドしたメソッドのthrows宣言でExceptionを指定することができたとすると、それを使用するMainクラスのtry~catchの内容を変更する必要が出てきます。
これは、呼び出しの処理を変更せずに処理の中身を変更することができるというポリモーフィズムのメリットが失われてしまう事を示しています。
ポリモーフィズムの機能がうまく動作するように、オーバーライドしたメソッドで発生する例外のクラスを広げることはできない仕組みになっています。

例外対処の必要性

これまで、どんな場合に例外が発生するのか、
また、どうやって例外の対処をするかを学習しました。
しかし、そもそもなぜ例外の対処をする必要があるのでしょうか。

もし例外の対処をしないでそのままにしていると、
プログラムの途中で例外が発生した場合、その時点でプログラムが異常終了してしまいます。
Webサービスなど、不特定多数の人が利用するプログラムの場合、
例外が発生してサービスが利用できなくなってしまっては困ります。

プログラムで想定外の事態が発生しても、 利用者がサービスの利用を続けることができるように、例外の対処をする必要があります。

例外のまとめ

  • エラーにはコンパイルエラーと実行時エラーの2種類がある
  • 実行時エラーのことを例外(Exception)と呼ぶ
  • 例外にはチェック例外と非チェック例外がある
  • 基本的には非チェック例外はプログラムのバグで、例外対処は必要ない
  • チェック例外は環境に依存するエラーで必ず対処が必要
  • 例外の対処の方法はtry~catchとthrowsの2種類がある
  • try~catchは自分で例外を処理し、throwsはメソッドの呼び出し元に任せる