Lesson 5

クラスとインスタンス

Lesson 5 Chapter 1
オブジェクト指向とは

皆さんは、これまでにJavaの基本的な文法について学び、変数宣言、条件分岐、繰り返しなど様々な処理を行えるようになりました。このレッスン5では、今まで学んだ文法を使用して書かれたプログラムを、より引いた視点から見て、全体としてどのように整理するかについて学んでいきます。

プログラムをどう整理するかというのは、一見重要ではないように思えます。なぜなら、文法が正しければ、どのように整理しても、プログラムは同じ出力を返すからです。

ではなぜ、「整理」について考える必要があるのでしょうか。日常の例から考えてみましょう。

皆さんは旅行先で「あの荷物どこにいれたっけ?」ということはありませんか?もし荷物が少なければ、特に整理することなくすぐに取り出すことができますが、旅行などで荷物がたくさんある場合は、しっかりと整理しなければ、どこに何を入れたのか、分からなくなってしまいます。

これはプログラミングにも同じことがいえます。開発するシステムが大きくなるにつれて、プログラムは複雑になり、整理の必要がでてきます。

整理の方法にも様々なものがあります。例えば、キャンプに行くことを想定してみましょう。以下の図のように、荷物を「食品」「服」「テント類」などの「モノのカテゴリー」や「川遊び用」「バーベキュー用」など「用途のカテゴリー」で分類することができます。それ以外にも「1日目」「2日目」「3日目」のように「時間的なカテゴリー」で分類することもできます。

Java-5_1

プログラミングにおいても、整理の方法は多岐にわたります。プログラミングの整理の考え方、記述ルールなどの枠組みを「プログラミングパラダイム」と呼ぶことがあります。

レッスン5では、Javaの基本にある「オブジェクト指向」というプログラミングパラダイムについて触れ、オブジェクト指向でプログラミングしていく上で必要な概念を学習していきます。

プログラミングパラダイムとはなにか

冒頭でも少し触れたようにプログラミングパラダイムとは、プログラミングの整理の考え方、記述ルールなどの枠組みのことです。プログラミング言語によっても異なりますが、一般的によく使われるプログラミングパラダイムには、以下のようなものがあります。

  • 命令型プログラミング (Imperative Programming)

  • 関数型プログラミング (Functional Programming)

  • オブジェクト指向プログラミング (Object-Oriented Programming)

ひとつずつ簡単に見ていきましょう。命令型プログラミングではプログラムを構成する基本的な単位が「命令」であり、プログラマはプログラムを実行するために「命令」を組み合わせることで、プログラムを実行します。一方、関数型プログラミングでは、プログラムを構成する基本的な単位が「関数」であり、プログラマは「関数」を組み合わせることでプログラムを実行します。オブジェクト指向プログラミングは、プログラムを構成する要素を「オブジェクト」として抽象化し、それらのオブジェクト操作によってプログラムを実行します。このように、プログラミングパラダイムには、さまざまな種類があります。

プログラミングパラダイム 記述方法 整理の基準
命令型 実行するべき命令を順番記述する 実行順序
関数型 関数(命令のまとまり)を定義し、その組み合わせで処理を記述する 関数
オブジェクト指向 オブジェクトとその関係性を定義し、オブジェクトの操作を記述する オブジェクト

オブジェクト指向とはなにか

さて、プログラミングパラダイムについて知ったところで、Javaに目を向けてみましょう。Javaは「オブジェクト指向」というプログラミングパラダイムを主眼においたプログラミング言語です。本節では、この「オブジェクト指向」とそれに関わる概念を理解していきましょう。

オブジェクト指向プログラミング(Object-Oriented Programming、OOP)では、プログラムを構成する基本的な単位として、オブジェクト(object)を使います。オブジェクトは、データを保持し、それを操作するための「メソッド」を持った小さなプログラム単位です。オブジェクトは、それぞれが独自のデータを持ち、そのデータを外部から隠蔽して保護することができます。これにより、プログラムが複雑になっても、個々のオブジェクトを理解しやすくなります。また、オブジェクトを再利用することができるため、プログラムを効率的に構築することができます。

難しい内容であるため、「オブジェクト」という単位でデータとメソッドを持ち、それを整理するという理解を得ることができれば、大丈夫です。次に、Javaでオブジェクトがどのように作成されるかを見て、理解を深めていきましょう。

オブジェクトを実現するクラスとインスタンス

Javaでは、「クラス」と「インスタンス」という概念を使ってオブジェクトを実現します。クラスは、オブジェクトを作るための設計図のようなものです。クラスには、そのオブジェクトが持つデータやそれを操作するためのメソッドが定義されています。インスタンスは、クラスから実際に作られたオブジェクトのことです。クラスを使って複数のインスタンスを作ることができます。

例えば、次のようなDogクラスがあるとします。

Dog.java
class Dog {
   public int age;
   public void bark() {
      System.out.println("ワンワン");
   }
}

細かい構文は後から見ていきますが、ここまでの勉強から Dogクラスが ageという変数と bark() というメソッドを持っていることが分かると思います。クラスにおける変数のことを「フィールド」といいます。クラスには、「情報」としての「フィールド」や、「処理」としての「メソッド」を定義することができます。

しかし、このクラスだけでは 、Dogを動かすことはできません。すでに説明した通り、クラスは、オブジェクトを作るための「設計図」だからです。では以下のコードを見てみましょう。

Main.java
Dog jack = new Dog();
jack.age = 7;
jack.bark(); //ワンワン

まず一行目を見てください。この Dog 型で宣言された jack のことを「インスタンス」と呼びます。すでに定義した、Dogクラスという設計図をもとに実際に作られたオブジェクトです。2行目では、jack.age という形でこのjackインスタンスのageフィールドに7を代入しています。そして3行目では jack.bark()という形でDogクラスで定義したbark()メソッドを呼び出しています。

このようにJavaにおいては、クラスとインスタンスを用いて、オブジェクトを作っていきます。

オブジェクト指向の3原則

ここまで、オブジェクト指向というプログラミングパラダイムの概要を辿ってきました。オブジェクト指向ではプログラムを「オブジェクト」の単位に分け、Javaにおいてはそれをクラスとインスタンスを使って実現していました。ここからは、実際にクラスとインスタンスを定義しながらオブジェクト指向の理解を深めていくのですが、その前にオブジェクト指向の3原則というものを紹介します。クラスとインスタンスを使えば「オブジェクト」は実現できますが、「オブジェクト指向」を実現できているとはまだ言えません。オブジェクト指向を実現するには、以下の3つの原則を抑える必要があります。これらの原則の詳細な実現方法については、今後各チャプターで扱っていきます。

  • カプセル化

  • 継承

  • ポリモーフィズム

カプセル化とは、オブジェクトに対する外部からのアクセスを制限することを意味します。適度にオブジェクトを閉じることで、プログラムの挙動が予測可能で安定するようになります。(Chapter 5で詳しく解説)

継承とは、あるクラスをもとに新しいクラスを定義することです。継承されたクラスは、元のクラスからメソッドやフィールドを継承し、新しく独自のフィールドやメソッドを定義することができます。(Chapter 3で詳しく解説)

ポリモーフィズムとは、異なるクラスで同じメソッド名を使用できる仕組みを指します。実際に実行されるメソッドは、呼び出し元のオブジェクトのクラスによって決まります。これにより、同じメソッド名を使用しながらも、異なる振る舞いをすることができます。(Chapter 3で詳しく解説)

これらの3つの原則は、オブジェクト指向プログラミングにおいて重要な概念です。少し難しいかもしれませんが、心配する必要はありません。実際にコードを書きながら、Javaでこれらの原則を実現する方法を学ぶことで、少しずつ理解することができます。

Lesson 5 Chapter 2
クラスの定義

レッスン5-1 では、オブジェクト指向について学んできました。オブジェクト指向とは、プログラミングパラダイムのひとつで、Javaにおいてはクラスとインスタンスを用いてオブジェクトを実現させていました。ここからは、実際に手を動かしながら、クラスとインスタンスについて学んでいきます。

クラスファイルの作成

はじめに、クラスを定義するには、ファイルを作成する必要があります。ただし、いくつかの注意点があります。

  • ひとつのファイルにひとつのクラスを定義する

  • ファイル名とクラス名を同一にする

  • ファイルの拡張子を「.java」にする

以上の点を踏まえて、Personクラスを定義するためのファイルを作成してみましょう。Person.javaというファイルを作成できれば完了です。

クラスの命名

クラス名には、任意の文字列を使用することができます。ただし、慣習的に大文字で始めるようにします。また、同じパッケージ内で重複するクラス名を使用することはできません。

では次に作成したPerson.javaファイルにクラス定義を書いていきます。Javaでクラスを定義するには、以下のようにclassキーワードを使用します。

Person.java
public class Person {
    // クラスの中身を書く
}

上の例では、publicというアクセス修飾子が指定されています。これは、どのクラスやメソッドからもアクセスできることを意味します。他にも、privateprotectedなどのアクセス修飾子があります。詳しくは後述します。

クラスの定義

クラスファイルを作成して、クラスを定義できました。次は Personクラスの中身を書いていきます。以下のように書いてみましょう。

Person.java
public class Person {
    // フィールド
    String name;
    int age;
    int height;
    
    // メソッド
    public void introduce() {
        System.out.println(name + "です。" + age + "歳です。");
    }
    
    public void showAge() {
        System.out.println("私は" + age + "歳です。");
    }
}

このクラスは、次のような内容を含んでいます。

  • フィールド:name age height

  • メソッド:introduce showAge

それぞれのメソッドについて説明します。introduce()メソッドは、インスタンスの「名前」「年齢」を表示するメソッドです。showAge()メソッドは、インスタンスの「年齢」を表示するメソッドです。

クラスは、ある種類のオブジェクトを作成するための設計図やテンプレートのようなものです。クラスからインスタンスを作成することを「インスタンス化」と呼びます。

Java-5_2

インスタンスは、クラスから作成された具体的なオブジェクトであり、クラスで定義されたフィールドやメソッドを持っています。

インスタンスの定義

では、作成した Person クラスを元に、インスタンスを作成してみましょう。Javaでは、インスタンスを作成するには、new演算子を使用します。次のようにして、Personクラスから田中さんのインスタンスを作成します。

Main.java
Person tanaka = new Person();

インスタンス化に成功しました。作成されたインスタンスは、tanakaに代入されています。作成されたインスタンスに対して、フィールドやメソッドを呼び出すには、.を使用します。

Main.java
// インスタンスのフィールドに値を設定する
tanaka.name = "田中 太郎";
tanaka.age = 30;
tanaka.height = 175;

// インスタンスのメソッドを呼び出す
tanaka.introduce();  // 田中 太郎です。30歳です。
tanaka.showAge();   // 私は30歳です。

同じようにして、「鈴木さん」「佐藤さん」などのインスタンスも作成することができます。

Main.java
// Personクラスから鈴木さんのインスタンスを作成する
Person suzuki = new Person();

// インスタンスのフィールドに値を設定する
suzuki.name = "鈴木 太郎";
suzuki.age = 25;
suzuki.height = 180;

// インスタンスのメソッドを呼び出す
suzuki.introduce(); // 鈴木 太郎です。25歳です。
suzuki.showAge(); // 私は25歳です。


//Personクラスから佐藤さんのインスタンスを作成する
Person sato = new Person();

// インスタンスのフィールドに値を設定する
sato.name = "佐藤 太郎"
sato.age = 35
sato.height = 170

// インスタンスのメソッドを呼び出す
sato.introduce(); // 佐藤 太郎です。35歳です。
sato.showAge(); // 私は35歳です。

ここまで Personクラスから3つのインスタンスを作成しました。それぞれのインスタンスは、異なる値を持っていますが、同じように「自己紹介する」「年齢を表示する」といったメソッドを呼び出すことができます。クラスとインスタンスの関係が分かってきたでしょうか。

静的フィールドと静的メソッド

ここまで、クラスの定義とそれをインスタンス化する方法を学んできました。インスタンスは、それぞれ異なる値を持つことができ、メソッドはそれぞれのインスタンスに対して呼び出されるものでした。しかし、特定のインスタンスではなく、クラスの全てのインスタンスで共有される情報を保持するフィールド、そしてそのフィールドを操作するメソッドが必要な場合もあります。例えば、先ほどPersonクラスに、インスタンス化された人数を保持するフィールドをつくるような場合です。そんな時に使われるのが、静的フィールドと静的メソッドです。これらは、クラスの全てのインスタンスで共有されるものです。そのため、クラスのインスタンスを作成することなく、クラス名を使用してアクセスすることができます。

例えば、次のようなクラスがあるとします。

Counter.java
public class Counter {
    public static int count = 0;

    public static void increment() {
        count++;
    }
}

このクラスでは、静的フィールド countと静的メソッド incrementを定義しています。静的フィールドおよびメソッドを定義する際は、static修飾子を使います。これらを使用するには、次のようにクラス名を使用します。

Main.java
int value = Counter.count; // 静的フィールドの値を読み取る
Counter.increment(); // 静的メソッドを呼び出す

このように、静的フィールドや静的メソッドは、インスタンスを必要とせず、クラス名を使用してアクセスすることができます。

コンストラクタ

コンストラクタとは、インスタンス化する際に、自動的に呼び出されるメソッドのことを指します。コンストラクタは、クラスのインスタンスを作成するときに、必要な初期化処理を行うために使用されます。

Java では、次のようにコンストラクタを定義します。

MyClass.java
public class MyClass {
    // コンストラクタ
    public MyClass() {
        // コンストラクタの処理
    }
}

コンストラクタは、クラス名と同じ名前で定義します。また、コンストラクタは、戻り値を持たないので、型を指定することはありません。

コンストラクタを使用するには、次のようにクラスのインスタンスを作成します。

Main.java
MyClass instance = new MyClass();

クラスのインスタンスを作成するときに、コンストラクタが自動的に呼び出されます。しかし、今回の例では、コンストラクタ内に処理を定義していないので、何も起きません。では、実際に処理を加えてコンストラクタを呼び出してみましょう。

コンストラクタには、引数を指定することができます。引数を指定することで、コンストラクタを呼び出すときに、必要な値を渡すことができます。

例えば、次のように、引数を持つコンストラクタを定義することができます。

MyClass.java
public class MyClass {
    int value;
    
    public MyClass(int value) {
        this.value = value; //valueフィールドに代入
    }
}

このように、引数を持つコンストラクタを定義することで、コンストラクタを呼び出すときに、必要な値を渡すことができます。

引数を持つコンストラクタを使用するには、次のようにクラスのインスタンスを作成します。

Main.java
MyClass instance = new MyClass(123);
System.out.println(instance.value);
//実行結果: 123

このように、クラスのインスタンスを作成するときに、コンストラクタに指定した引数を渡すことができます。

先ほどのPersonクラスに応用すると以下のようにできます。

Person.java
public class Person {
    //フィールド
    String name;
    int age;
    int height;

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

このようにすることで、インスタンス化の時点で、nameageheightに対して値を持たせることができます。

Main.java
Person tanaka = new Person("田中 太郎", 30, 175);
Person suzuki = new Person("鈴木 太郎", 25, 180);
Person sato = new Person("佐藤 太郎", 35, 170);
          

以上のように、コンストラクタを使うことで、インスタンス化のタイミングで任意の処理を行うことができます。

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

ここまで、クラスにおけるコンストラクタの定義方法を学んできました。では、コンストラクタを定義しないとどうなるのでしょうか。Java では、クラスに明示的にコンストラクタが定義されていない場合に、自動的に引数なしのコンストラクタが用意されます。このコンストラクタは、デフォルトコンストラクタと呼ばれます。

一方で、クラスにコンストラクタが定義されている場合は、デフォルトコンストラクタは生成されません。例えば、次のように、引数を持つコンストラクタを定義している場合は、デフォルトコンストラクタは生成されません。

MyClass.java
public class MyClass {
    // コンストラクタ
    public MyClass(int value){
	// 引数を持つコンストラクタの処理
    }
}

そのため、クラスに引数を持つコンストラクタを定義し、引数なしのコンストラクタも定義したい場合は、両方とも明示的に定義する必要があります。

例えば、次のように、定義することができます。

MyClass.java
public class MyClass {
    // デフォルトコンストラクタ
    public MyClass() {
        // デフォルトコンストラクタの処理
    }

    // コンストラクタ
    public MyClass(int value) {
        // 引数を持つコンストラクタの処理
    }
}

このように、状況に応じて複数のコンストラクタを定義することができます。

スコープ

スコープとは、変数やメソッドなどが有効な範囲を指します。有効な範囲とは、使える範囲のことです。例えば、写真を一枚撮ったとします。その写真を自分だけで留めておくのか、友人にだけ共有するのか、誰でも見れる形でネットにあげて、全世界に共有するのか。このように、アクセスできる範囲がスコープです。

Java では、アクセス修飾子を使用することで、スコープを実現することができます。

次のようなアクセス修飾子があります。

  • public: どのクラスからでもアクセスできる。

  • private: 同じクラス内からのみアクセスできる。

  • protected: 同じパッケージ内のクラスからのみアクセスできる。また、サブクラスからもアクセスできる。(サブクラスについてはチャプター3で扱います)

  • (無記述): 同じパッケージ内のクラスからのみアクセスできる。

例えば、次のように、アクセス修飾子を使用することで、変数のスコープを指定することができます。

MyClass.java
public class MyClass {
    public int value1; // どのクラスからでもアクセスできる
    private int value2; // 同じクラス内からのみアクセスできる
    protected int value3; // 同じパッケージ内のクラスからのみアクセスできる。また、サブクラスからもアクセスできる。
    int value4; // 同じパッケージ内のクラスからのみアクセスできる
}

また、アクセス修飾子を使用することで、メソッドのスコープを指定することができます。

MyClass.java
public class MyClass {
    public void method1() { // どのクラスからでもアクセスできる
        // メソッドの処理
    }
    private void method2() { // 同じクラス内からのみアクセスできる
        // メソッドの処理
    }
    protected void method3() { // 同じパッケージ内のクラスからのみアクセスできる。また、サブクラスからもアクセスできる。
        // メソッドの処理
    }
    void method4() { // 同じパッケージ内のクラスからのみアクセスできる
        // メソッドの処理
    }
}

Lesson 5 Chapter 3
クラスの継承

レッスン5-2では、クラスとインスタンスを Java でどのように定義するのか学んできました。クラスには、インスタンスを作成することなくアクセスすることができる静的なフィールドや、メソッドがありました。また、インスタンスを作成する際に自動的に呼び出されるメソッドであるコンストラクタについても触れました。このレッスン5-3では、あるクラスが別のクラスの仕組みを引き継いで新しいクラスを作成する「継承」について触れていきます。継承はオブジェクト指向の3原則のひとつです。継承を使用することで、既存のクラスを拡張して新しいクラスを作成することができます。また、継承を使用することで、共通の機能を持つ複数のクラスをまとめて定義することができます。

継承の基本

Javaでは、extendsキーワードを使用して継承を行います。例えば、次のように継承を行うことができます。

Animal.java
public class Animal {
  protected String name;
  
  public Animal(String name) {
    this.name = name;
  }
  
  public void speak() {
    System.out.println("動物は鳴きます");
  }
}

public class Cat extends Animal {
  public Cat(String name) {
    super(name);
  }
  
  @Override
  public void speak() {
    System.out.println(name + "がにゃーと鳴きます");
  }
}

public class Dog extends Animal {
  public Dog(String name) {
    super(name);
  }
  
  @Override
  public void speak() {
    System.out.println(name + "がわんわんと鳴きます");
  }
}

// 以下は、それぞれのクラスを使用する例です

Animal animal = new Animal("動物");
animal.speak();  // 動物は鳴きます

Cat cat = new Cat("ねこ");
cat.speak();  // ねこがにゃーと鳴きます

Dog dog = new Dog("いぬ");
dog.speak();  // いぬがわんわんと鳴きます

継承において、元となるクラスのことを「スーパークラス」、それを継承する新しいクラスのことを「サブクラス」と呼びます。

上記の例では、Animalクラスがスーパークラスであり、CatクラスとDogクラスがそれぞれAnimalクラスのサブクラスです。CatクラスとDogクラスは、Animalクラスで定義されたspeak()メソッドを上書き(オーバーライド)しています。(オーバーライドは、次のチャプターで詳しく扱います)

継承を使用することで、CatクラスとDogクラスは、Animalクラスで定義されたnameフィールドを利用することができます。また、CatクラスとDogクラスは、Animalクラスのコンストラクタを呼び出すことで、nameフィールドを初期化することができます。

このように、継承を使うことで、異なるクラスに対しても同じフィールド名、メソッド名を持たせることが出来ます。呼び出し側は、CatDogという異なるクラスに対して同じspeak()メソッドを呼び出せます。同じ操作を異なる型のデータに対して行うことが出来ることをポリモーフィズムといい、これはレッスン5-1の最後に紹介した、オブジェクト指向の3原則のひとつです。

親子のクラス?

継承元のクラスと継承されたクラスの呼び方は、スーパークラス/サブクラス以外にもあります。基底クラス/派生クラスと呼んだり、親クラス/子クラスと呼ぶこともあります。

インターフェース

インターフェースとは、異なるクラスに含まれる共通の機能を1つにまとめる機能です。インターフェースは、クラスが実装すべきメソッドの仕様を定義するだけで、実際の処理はクラス自身が実装します。

以下に、Javaのインターフェースを使う例を示します。

Measurable.java
public interface Measurable {
  double getArea();
  double getPerimeter();
}

public class Rectangle implements Measurable {
  private double length;
  private double width;

  public Rectangle(double length, double width) {
    this.length = length;
    this.width = width;
  }

  @Override
  public double getArea() {
    return length * width;
  }

  @Override
  public double getPerimeter() {
    return 2 * (length + width);
  }
}

public class Circle implements Measurable {
  private double radius;

  public Circle(double radius) {
    this.radius = radius;
  }

  @Override
  public double getArea() {
    return Math.PI * radius * radius;
  }

  @Override
  public double getPerimeter() {
    return 2 * Math.PI * radius;
  }
}

この例では、Measurableインターフェースは、図形の面積と周長を計算するための2つのメソッドを定義しています。RectangleクラスとCircleクラスは、それぞれ四角形と円を表すクラスで、それぞれMeasurableインターフェースの実装クラスになります。インターフェースはインスタンス化できないため、クラスを通じで実装しなければなりません。実装クラスの定義にはimplementsキーワードを使用します。インターフェースを実装するクラスは、インターフェースで定義されたメソッドに処理を追加する必要があるため、RectangleクラスとCircleクラスは、それぞれ面積と周長を計算するためのgetArea()およびgetPerimeter() メソッドの処理を定義しています。基本的なクラスの継承と同じように、インターフェースを使うことで、以下のように異なるクラスに対して、同じ名前のメソッドを呼び出すことが出来ます。

Main.java
Rectangle rect = new Rectangle(2.0, 3.0);
double rectArea = rect.getArea();
double rectPerimeter = rect.getPerimeter();

Circle circle = new Circle(2.0);
double circleArea = circle.getArea();
double circlePerimeter = circle.getPerimeter();

上記のコードでは、先ほど定義した[Rectangle]クラスと[Circle]クラスのインスタンスを生成し、それぞれに対して、メソッドを呼び出しています。異なるクラスに対して同じ名前のメソッドを呼び出せているのが分かります。

インターフェースの継承

インターフェースもクラスと同じように継承関係を持たせることができます。インターフェースを元にサブインターフェースを宣言する際は、extendsキーワードを使用します。

抽象クラス

抽象クラスは、継承されることを前提としたクラスです。抽象クラスは、自分自身をインスタンス化することができず、必ず別のクラスから拡張(継承)されることを想定しています。

例えば、Animalクラスを継承した、DogクラスとCatクラスがあるとします。スーパークラスであるAnimalクラスで、speak()メソッドを定義し、サブクラスである、Dogクラスと Catクラスでそれぞれのspeak()メソッドを上書きすると以下のようになります。

Main.java
public class Animal {
  public void speak() {
    System.out.println("動物は鳴きます");
  }
}

public class Cat extends Animal {
  @Override
  public void speak() {
    System.out.println("にゃー");
  }
}

public class Dog extends Animal {
  @Override
  public void speak() {
    System.out.println("わんわん");
  }
}

コードを見ると、Animalクラスのspeak()メソッドでは、「動物は鳴きます」の文字列を出力することになっています。なぜなら、動物は、犬や猫よりも抽象的で何と鳴くのか定義できないからです。このAnimalクラスは、継承されることを意図したクラスです。つまり、Animalクラスは抽象クラスを用いて定義することが出来ます。

抽象クラスでは、抽象メソッドを宣言することができます。抽象メソッドは、処理内容を定義しないことができるメソッドです。つまり、抽象クラス内では、必要なメソッドを宣言しつつも、そのメソッドの中身(処理内容)を定義しないことができます。拡張するクラス(サブクラス)は、そのクラスが抽象クラスで宣言されたメソッドを実装することで、そのクラスをインスタンス化することができます。

抽象クラスを宣言するには、abstractキーワードを使用します。また、抽象クラス内に宣言するメソッドは、必ず abstractキーワードを付与します。

Main.java
public abstract class Animal {
  // 抽象メソッド
  public abstract void speak();
}

public class Cat extends Animal {
  @Override
  public void speak() {
    System.out.println("にゃー");
  }
}

public class Dog extends Animal {
  @Override
  public void speak() {
    System.out.println("わんわん");
  }
}

上記のコードでは、実際にAnimalクラスを抽象クラスに変えて定義しています。Animalクラスの宣言に abstractキーワードが使われていることが分かります。これでクラスを抽象クラスとすることができます。

次に、Animalクラス内のspeak()メソッドを見てみましょう。通常のメソッド定義にabstractという指定を行っています。また、抽象メソッドは処理内容を書かないので、{}を省略して、セミコロンをつけます。

このように、抽象クラスを用いることで、抽象的なクラスを宣言し、処理内容を書かない抽象メソッドを定義することが可能になります。

アップキャストとダウンキャスト

アップキャストとは、サブクラスのインスタンスをスーパークラス型の変数に代入することを指します。アップキャストは、型変換を意図したものではありません。つまり、サブクラスのインスタンスをスーパークラス型に変換するわけではないということです。むしろ、サブクラスであることを保証した上で、スーパークラスと同じように扱うことができるようにすることを意図しています。

アップキャストを行う場合、代入する前に、明示的な型変換を行うことは不要です。ただし、代入する変数は、必ずスーパークラス型の変数である必要があります。

Main.java
class Animal {
  // 中略
}

class Cat extends Animal {
  // 中略
}

Cat cat = new Cat();
Animal animal = cat; // アップキャスト

一方、ダウンキャストは、スーパークラス型の変数をサブクラス型の変数に代入することを指します。ダウンキャストは、型変換を意図したものであり、サブクラスに特有の機能を使用するために必要になります。

ダウンキャストを行う場合、代入する前に、明示的な型変換を行う必要があります。そのため、代入する変数は、必ずサブクラス型の変数である必要があります。

Main.java
class Animal {
  // 中略
}

class Cat extends Animal {
  // 中略
}

Animal animal = new Animal();
// Cat cat = animal; // コンパイルエラー
Cat cat = (Cat) animal; // ダウンキャスト

また、ダウンキャストを行った場合、サブクラスに定義されているメソッドや属性にアクセスすることができるようになります。

Main.java
Animal animal = new Cat();
if (animal instanceof Cat) {
  Cat cat = (Cat) animal;
  cat.meow(); // Cat型に特有のメソッド
}

アップキャストとダウンキャストを使用することで、継承関係にあるクラス同士で、型を気にせずに処理を行うことができるようになります。

ポリモーフィズムの実現

本チャプターは様々な形の継承について見てきました。継承を行うことで、異なるクラスに対して同じ名前のメソッドを呼び出すことができることをポリモーフィズムといいました。しかしそれは、ポリモーフィズムの一部にすぎません。最後に学んだアップキャストを用いることで、異なるクラスをスーパークラスの型を使って扱うことができます。以下の例を見てみましょう。

Main.java
public abstract class Animal {
  public abstract void speak();
}

public class Cat extends Animal {
  @Override
  public void speak() {
    // 猫が にゃー と鳴く
  }
}

public class Dog extends Animal {
  @Override
  public void speak() {
    // 犬が わんわん と鳴く
  }
}

次に、これらのクラスを使用する例を示します。この例では、CatクラスとDogクラスのインスタンスを作成し、それらをAnimal型の変数に代入しています。そして、Animal型の変数を使用して、各動物が鳴くという処理を行っています。

Main.java
Animal cat = new Cat();
Animal dog = new Dog();

cat.speak(); // 猫が にゃー と鳴く
dog.speak(); // 犬が わんわん と鳴く

このように、スーパークラスであるAnimal型の変数を使用して、異なる動物を表すCatクラスとDogクラスのインスタンスを扱うことができます。また、Animal型の変数であっても、実際にはCatクラスやDogクラスで定義された「鳴く」メソッドが呼び出されるため、各動物が異なる鳴き声を出すことができます。このように、異なるクラスのインスタンスを同じように扱うことを、ポリモーフィズムと呼びます。

Lesson 5 Chapter 4
メソッド

前チャプターでは、クラスの継承について細かく見てきました。クラスの持てるメンバには、フィールドとメソッドがありました。本チャプターでは、これまで細かく触れてこなかった「メソッド」について扱っていきます。

メソッドとは何か

メソッドとは、プログラムの中で処理を行う単位のことを指します。特定の処理を行うために、処理をまとめて、名前をつけることができます。これをメソッドといいます。

メソッドの特徴は次の3つです。

  • 実行されると何らかの処理を行うことができる

  • 実行する前に必要な情報(引数)受け取ることができる

  • 処理を行った結果を返すことができる(戻り値)

では実際に定義しながら見ていきましょう。

引数の読み方

引数の読みは「ひきすう」です。「いんすう」と間違える人が多いので気を付けましょう。

メソッドを定義する

Javaでは、以下のようにメソッドを定義します。

Main.java
[修飾子] 戻り値の型 メソッド名([仮引数1 型 引数名, 仮引数2 型 引数名, ...]) {
    // 処理を記述する
}

修飾子は、アクセス修飾子や、static修飾子などを指定できます。戻り値の型は、メソッドが返す値のデータ型を指定します。値を返さない場合は、voidを指定します。仮引数は、メソッドを実行するときに必要な情報を受け取るための引数です。仮引数は、型と引数名を指定します。

仮引数と実引数

仮引数とは、メソッドを定義するときの引数のことです。実際にメソッドを呼び出したときに渡される引数を実引数といいます。

例えば、次のように、整数を2つ受け取り、その和を返すaddというメソッドを定義することができます。

Main.java
int add(int x, int y) {
    return x + y;
}

このメソッドは、以下のように呼び出すことができます。

Main.java
int result = add(10, 20); // resultに30が代入される

オーバーライド

Javaでは、サブクラスでスーパークラスのメソッドを再定義することを「オーバーライド」といいます。

オーバーライドを行うことで、サブクラスではスーパークラスのメソッドをそのまま使用することもできますが、上書きしてサブクラス独自の処理を行うこともできます。

例えば、次のように、AnimalクラスとDogクラスを定義したとします。

Main.java
class Animal {
    public void bark() {
        System.out.println("わんわん");
    }
}

class Dog extends Animal {
    @Override
    public void bark() {
        System.out.println("ワンワン");
    }
}

この場合、Dogクラスでは、barkメソッドをオーバーライドし、上書きしています。Dogクラスでは、barkメソッドを呼び出すと、「ワンワン」と出力されます。一方、Animalクラスでは、barkメソッドを呼び出すと、「わんわん」と表示されます。

オーバーライドでは、スーパークラスのメソッドを上書きせずに、そのまま使うこともできます。

ではDogクラスで、スーパークラスのbarkメソッドを呼び出してみましょう。その場合は、superキーワードを使用します。

Main.java
class Dog extends Animal {
    @Override
    public void bark() {
        super.bark();
        System.out.println("ワンワン");
    }
}

また、オーバーライドを行う場合には、@Overrideアノテーションを使用することが推奨されます。このアノテーションを使用することで、親クラスに同じ名前のメソッドが存在しない場合には、コンパイルエラーが発生するようになります。

オーバーライドを行う際に注意することとして、次のようなものがあります。

  • スーパークラスのメソッドとサブクラスのメソッドで、名前、仮引数の型、数が一致している必要がある

  • スーパークラスのメソッドとサブクラスのメソッドで、戻り値の型が一致している必要がある

  • スーパークラスのメソッドが、privateやfinal修飾子を使用している場合は、サブクラスではオーバーライドできない

シグニチャ

メソッドにおいて、メソッド名、引数の型、引数の数の3つを合わせて「シグニチャ」ということがあります。英語の signature からきています。

オーバーロード

次に、オーバーロードについて説明します。オーバーライドとオーバーロード、とても命名が似ているので、間違えないように気を付けてください。Javaでは、同じ名前のメソッドを複数定義することをオーバーロードといいます。

メソッドのオーバーロードを行うことで、引数が異なる場合に、異なる処理を行うことができるようになります。

メソッドのオーバーロードを行うには、次のようにします。

  • 同じ名前のメソッドを複数定義する

  • 定義したメソッドで、仮引数の型、数が異なるようにする

例えば、次のように、同じ名前のaddメソッドを2つ定義したとします。

Calculator.java
class Calculator {
    public int add(int x, int y) {
        return x + y;
    }

    public double add(double x, double y) {
        return x + y;
    }
}

この場合、Calculatorクラスでは、addメソッドが2つ定義されています。仮引数の型が異なるため、これらはオーバーロードされています。

このように、同じ名前のメソッドを複数定義することで、異なる引数を受け取ることができるようになります。

また、オーバーロードされたメソッドを呼び出す際には、仮引数の型や数によって、どのメソッドを呼び出すかが決まります。

例えば、次のように、addメソッドを呼び出すことができます。

Main.java
Calculator calc = new Calculator();

int result1 = calc.add(10, 20); // result1に30が代入される
double result2 = calc.add(1.5, 2.5); // result2に4.0が代入される

result1の行では、int型の仮引数が2つ指定されているため、Calculatorクラスで最初に定義されたaddメソッドが呼び出されます。また、result2の行では、double型の仮引数が2つ指定されているため、Calculatorクラスで2番目に定義されたaddメソッドが呼び出されます。

このように、Javaでは、同じ名前のメソッドを複数定義することで、異なる引数を受け取ることができるようになります。

ここまで学ぶと、オーバーライドとオーバーロードの違いが分かったかと思います。要約すると以下のようになります。

  • オーバーライド:サブクラスでスーパークラスのメソッドを再定義、上書きすること

  • オーバーロード:同じ名前のメソッドをシグニチャを変えて複数定義すること

抽象メソッド

抽象メソッドは、前のチャプターで扱った、抽象クラスやインターフェースで登場しました。おさらいしておきましょう。

抽象メソッドとは、処理内容が定義されていないメソッドのことを指します。抽象メソッドは、定義されるべき処理内容が異なるため、具体的な処理内容を定義できない場合に使用されます。

抽象メソッドを定義するには、次のようにします。

Animal.java
public abstract class Animal {
    public abstract void speak();
}

このように、抽象メソッドを定義する場合は、abstractキーワードを使用します。また、処理内容が定義されていないため、{}を使用せず、;で終わります。

抽象メソッドを実装するには、次のようにします。

Cat.java
class Cat extends Animal {
    @Override
    public void cry() {
        System.out.println("にゃー");
    }
}

このように、抽象クラスを継承したサブクラスで、抽象クラスで定義された抽象メソッドを実装することで、抽象メソッドをオーバーライドすることができます。

Lesson 5 Chapter 5
カプセル化・データ隠蔽

ここまで、レッスン5を通して、オブジェクト指向について学んできました。チャプター1では、オブジェクト指向は、オブジェクト単位でプログラムを整理する方法であると学びました。チャプター2では、Javaにおいてはクラスとインスタンスを用いてオブジェクトを作ることを学びました。チャプター3では、様々な形でクラスの継承を学びました。オブジェクト指向の3原則のうち、継承とポリモーフィズムを扱いました。そしてチャプター4では、メソッドを扱いました。オーバーライドとオーバーロードを使うことで、同じメソッド名でも様々な処理をさせることができました。チャプター5では、扱っていなかったオブジェクト指向の3原則のひとつ、「カプセル化」について学んでいきます。

カプセル化とはなにか

「カプセル化」とは、クラスのフィールドを隠蔽し、外部からのアクセスを制限することを指します。カプセル化を行うことで、クラス内部のデータを保護することができます。

以下がカプセル化の一例です。

Person.java
class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

このように、フィールドをprivateで修飾することで、外部からのアクセスを制限することができます。また、フィールドにアクセスするためのgetフィールド名メソッドと、フィールドに値を設定するためのsetフィールド名メソッドを定義します。これらのメソッドのことをそれぞれgetterメソッド、setterメソッドと呼びます。

今回の例では、privateを使いましたが、カプセル化は、アクセス修飾子を適切に使うことで、出来るだけ、外部からのアクセスを制限することを指します。つまり、アクセス修飾子は、適宜必要な範囲までのものを使うというのが、カプセル化・データ隠蔽にとって重要です。データ隠蔽とは、カプセル化を行うことで起きるデータが隠蔽されている状態を指します。それぞれ、明確な定義はなく、同じ意味で使われることがほとんどです。

カプセル化を行うことで、想定していない操作が起きにくくなります。

オブジェクト指向の3原則

オブジェクト指向を扱ったレッスン5も最後になりました。ここでオブジェクト指向の3原則を振り返りたいと思います。みなさん覚えているでしょうか。

  • 継承

  • ポリモーフィズム

  • カプセル化

以上の3つでした。継承とは、あるクラスを元に新しいクラスを定義することでした。ポリモーフィズムとは、異なるクラスで同じメソッド名を使用できる仕組みでした。カプセル化は、オブジェクトに対するアクセスを適度に制限することでした。

Javaは、オブジェクト指向を主眼においたプログラミング言語といえます。その点を踏まえて、次のレッスンに進んでいきましょう。