JAVA

最終更新日: 2022.08.02 (公開: 2022.08.02)

Javaの「super」は親クラスのメソッド呼び出しに必須!使い方と応用法

Javaの「super」は親クラスのメソッド呼び出しに必須!使い方と応用法

Javaの「super」は、スーパークラス(親クラス)のフィールド(メンバ変数)やメソッドを、サブクラス(子クラス)から参照するときに使う修飾子です。オブジェクト指向プログラミングにおいて、クラスの「継承」は常に行われるため、サブクラスからスーパークラスのインスタンスを参照したいシーンはよくあります。

super修飾子は、親クラスの要素にアクセスできる便利な機能ではありますが、「使い方がよくわからない」ということもあるでしょう。また、superと同じく重要な「this」との使いわけも、分かりづらいかもしれませんね。そこで本記事では、Javaのsuperという修飾子の意味や使い方、thisとの違いについて解説します。

Javaの「super」とは?

Javaの「super」とは?

Javaの「super」は、サブクラス(子クラス)のインスタンスから、スーパークラス(親クラス)のインスタンスにアクセスするための修飾子です。superを使うことにより、スーパークラスのフィールドやメソッドを、サブクラスから参照できるようになります。

子クラスのメソッドから、親クラスの同じ名前のメソッドにアクセスしたいとき、もしくは同じ名前のフィールドにアクセスしたいとき、このsuperが役に立ちます。たとえば、スーパークラスを継承するときに、メソッドをオーバーライドすることはよくあります。

しかしメソッドをオーバーライドすると、通常は自身のクラスのものが呼び出されることになり、親クラスのものにはアクセスできません。

また、「デフォルトコンストラクタ」とは異なり「引数付きコンストラクタ」は、スーパークラスのものが自動的に実行されません。そのため、クラスの性質をよく理解しておかないと、思わぬバグにつながってしまうこともあります。Javaのsuperは、このような課題を解決してくれる機能なのです。

これだけではイメージしづらいかもしれないので、まずはJavaのクラスの「継承」についておさらいしておきましょう。

Javaのクラスの「継承」に関する基本知識4つをおさらい

Javaのクラスの「継承」に関する基本知識4つをおさらい

Javaの「super」について学ぶ前に、まずは「継承」の基本知識をおさらいしておきましょう。継承を知ることによって、「superがなぜ必要か」「どんなときに使うのか」ということもわかるので、ぜひ改めてご確認ください。

  • 親クラスを継承した子クラスは「親クラスの要素」を引き継ぐ
  • インスタンス化するときは「デフォルトコンストラクタ」が実行される
  • 「引数付きコンストラクタ」は親クラスのものを自動的に呼び出さない
  • 「オーバーライド」した親クラスの要素にはアクセスできない

親クラスを継承した子クラスは「親クラスの要素」を引き継ぐ

あらゆるクラスは「継承」することで、機能を追加して発展していきます。継承とは、親クラス(スーパークラス)の要素を、子クラス(サブクラス)が引き継ぐことです。クラスの継承は、クラスの宣言時に「extends クラス名」と記述することで実現できます。

実際に、動物の基礎となる「Animal」クラスを、猫の「Cat」クラスが継承するというサンプルコードを見ていきましょう。

//サンプルプログラム
// 動物のオリジナルクラス

class Animal {

  // 動物の名前

  private String name;

  // 動物の名前を設定する

  public void setName(String name) {

    this.name = name;

  }

  // 動物の名前を取得する

  public String getName() {

    return name;

  }

}

// Animalから派生した猫クラス

class Cat extends Animal {

  // 鳴き声を表現するメソッド

  public void meow() {

    System.out.println(getName() + “が「にゃあ~♪」と鳴いた!”);

  }

}

public class Main {

  public static void main(String[] args) {

    // 猫クラスのインスタンスを生成する

    Cat cat = new Cat();

    

    // 猫の名前を設定する

    cat.setName(“みぃちゃん”);

    

    // 猫クラスの鳴き声メソッドを実行する

    cat.meow();

  }

}

 

//実行結果
みぃちゃんが「にゃあ~♪」と鳴いた!

上記のサンプルコードでは、まず最初に「Animalクラス」を作り、それを「Catクラス」が継承しています。Animalはスーパークラス(親クラス)、Catはサブクラス(子クラス)になるということです。

注目すべきは、子クラスの「catインスタンス」から、親クラスの「setName」メソッドにアクセスできることです。さらに、Catクラスの「meow」メソッドから、親クラスの「getName」メソッドにもアクセスできています。

つまり、親クラスを継承した子クラスから、親クラスのメソッドを問題なく参照できるということです。このように、親クラスを単に継承した場合は、子クラスでsuperを使う必要は基本的にありません。

ただし、親クラスのメソッドのアクセス修飾子が「private」に指定されている場合は、子クラスからそのメソッドを参照することはできません。この場合は、後述するsuperを用いても、メソッドは参照できないのでご注意ください。

インスタンス化するときは「デフォルトコンストラクタ」が実行される

クラスをインスタンス化するときは、まず「コンストラクタ」が実行されます。コンストラクタの主な役割は、クラスのフィード(メンバ変数)の生成と初期化を行うことです。コンストラクタは、メソッド名をクラス名と同じものにして、戻り値を記載せずに宣言します。

コンストラクタには、大きく分けて2種類のものがあります。「デフォルトコンストラクタ」と「引数付きコンストラクタ」です。デフォルトコンストラクタは、基本的には記載しなくても自動的に生成されるため、「デフォルト」という名称がついているのです。

先ほどのサンプルコードでは、コンストラクタを記載していませんが、これは上記の理由から記載する必要がなかったからです。またコンストラクタには、「親クラスのコンストラクタを自動的に呼び出す」という性質があります。Javaのコンストラクタの性質について、下記のサンプルコードで確認しましょう。

//サンプルプログラム
class Animal {

  // 動物の名称

  private String name;

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

  Animal() {

    System.out.println(“Animalクラスのデフォルトコンストラクタ”);

  }

  // 動物の名前を設定する

  public void setName(String name) {

    this.name = name;

  }

  // 動物の名前を取得する

  public String getName() {

    return name;

  }

}

class Cat extends Animal {

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

  Cat() {

    System.out.println(“Catクラスのデフォルトコンストラクタ”);

  }

  // 鳴き声を表現するメソッド

  public void meow() {

    System.out.println(getName() + “が「にゃあ~♪」と鳴いた!”);

  }

}

public class Main {

  public static void main(String[] args) {

    // 猫クラスのインスタンスを生成する(デフォルトコンストラクタを呼び出す)

    Cat cat = new Cat();

    // 猫の名前を設定する

    cat.setName(“みぃちゃん”);

    // 猫クラスの鳴き声メソッドを実行する

    cat.meow();

  }

}

 

//実行結果
Animalクラスのデフォルトコンストラクタ

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

みぃちゃんが「にゃあ~♪」と鳴いた!

処理の流れがわかりやすくなるように、各クラスのコンストラクタで「printlnメソッド」でメッセージを出力しています。上記の実行結果から明らかなように、サブクラスのコンストラクタを呼び出すと、自動的にスーパークラスのコンストラクタが呼び出されるのです。

さらに注目すべきなのが、まず親クラスのコンストラクタを実行し、それから子クラスのコンストラクタが実行されているということ。いわば「年功序列」のように、年上のクラス→年下のクラスの順番で、インスタンスが生成されるとイメージするとわかりやすいです。

ここでも、super修飾子の必要性はとくに感じませんよね。しかし、ここから話が大きく変わってくるので、ここまでのポイントを改めて理解しておくことが重要です。

「引数付きコンストラクタ」は親クラスのものを自動的に呼び出さない

「引数付きコンストラクタ」は、1つ以上の引数を取るコンストラクタのことです。引数付きコンストラクタは、下記5つの点でデフォルトコンストラクタとは異なります。

  • 引数付きコンストラクタを書くと、デフォルトコンストラクタは自動生成されない
  • 引数付きコンストラクタは、明示的に記述する必要がある
  • 引数付きコンストラクタは、サブクラスに継承されない
  • 引数付きコンストラクタは、デフォルトコンストラクタも実行する
  • 引数付きコンストラクタは、親クラスの引数付きコンストラクタを実行しない

これだけではよく理解できないので、ひとまず下記のサンプルプログラムで大まかなポイントを掴んでみましょう。クラス名と同じで戻り値がない「コンストラクタ」が、各クラス2つずつになっていることがポイントです。

//サンプルプログラム
class Animal {

  // 動物の名称

  private String name;

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

  Animal() {

    System.out.println(“Animalクラスのデフォルトコンストラクタ”);

  }

  // 引数付きコンストラクタ(動物の名前を設定する)

  Animal(String name) {

    System.out.println(“Animalクラスの引数付きコンストラクタ”);

    setName(name);

  }

  // 動物の名前を設定する

  public void setName(String name) {

    this.name = name;

  }

  // 動物の名前を取得する

  public String getName() {

    return name;

  }

}

class Cat extends Animal {

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

  Cat() {

    System.out.println(“Catクラスのデフォルトコンストラクタ”);

  }

  // 引数付きコンストラクタ(動物の名前を設定する)

  Cat(String name) {

    // ここでスーパークラスの「引数付きコンストラクタ」が呼び出されることを期待するが…

    System.out.println(“Catクラスの引数付きコンストラクタ”);

  }

  // 鳴き声を表現するメソッド

  public void meow() {

    System.out.println(getName() + “が「にゃあ~♪」と鳴いた!”);

  }

}

public class Main {

  public static void main(String[] args) {

    // 猫クラスのインスタンスを生成する(デフォルトコンストラクタを呼び出す)

    Cat catA = new Cat();

    // 猫の名前を設定する

    catA.setName(“みぃちゃん”);

    // 猫クラスの鳴き声メソッドを実行する

    catA.meow();

    

    // 改行する

    System.out.println();

    // 猫クラスのインスタンスを生成する(デフォルトコンストラクタを呼び出す)

    Cat catB = new Cat(“たまちゃん”);

    // 猫クラスの鳴き声メソッドを実行する

    catB.meow();

  }

}

 

//実行結果
Animalクラスのデフォルトコンストラクタ

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

みぃちゃんが「にゃあ~♪」と鳴いた!

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

Catクラスの引数付きコンストラクタ

nullが「にゃあ~♪」と鳴いた!

catBのインスタンス生成時、Animalクラスのデフォルトコンストラクタ→Catクラスの引数付きコンストラクタの順番で実行されています。Animalクラスの引数付きコンストラクタが実行されないので、catBの名前が設定されていません。

その結果として、設定したはずの「たまちゃん」ではなく、「null」という名前の猫が鳴いたことになってしまいました。名前を格納しているname変数の中身が空だからです。つまり、親クラスの引数付きコンストラクタを子クラスから呼び出したい場合、「super」が必要だということです。

以降のことはsuperとは直接関係ありませんが、継承を知るうえで重要なので認識しておくと良いでしょう。まず上記のサンプルコードから、AnimalクラスとCatクラスのデフォルトコンストラクタを削除してみてください。コンパイルエラーが発生しますよね。

引数付きコンストラクタを書いている場合は、デフォルトコンストラクタは自動生成されないので、明示的に書く必要があります。

さらに、Catクラスの引数付きコンストラクタを削除すると、コンパイルエラーが発生します。これは、Animalクラスの引数付きコンストラクタが、Catに継承されていないからです。setNameメソッドは、Catクラスには書いていませんが、Animalクラスから継承されているので問題なくアクセスできます。引数付きコンストラクタは、子クラスにも記載する必要があるのです。

「オーバーライド」した親クラスの要素にはアクセスできない

もうひとつ、「super」が必要になる代表的なケースをご紹介します。それがメソッドを「オーバーライド」したときです。オーバーライドとは、クラスを継承するときに「スーパークラスのメソッドと同名のメソッドを改めて定義すること」を指します。

オーバーライドの目的は、メソッドの処理内容をスーパークラスから変化させて、そのクラス独自の振る舞いを表現することです。メソッドをオーバーライドしたとき、つまりスーパークラスと同名のメソッドがサブクラスにあるときは、サブクラスのメソッドのほうが実行されます。つまり、スーパークラスのメソッドが「上書き」もしくは「隠蔽」されたような結果になるのです。

ちなみに、メソッド以外にフィールド(メンバ変数)をオーバーライドすることもありますが、フィールドのオーバーライドが必要なケースはあまり一般的ではありません。フィールドをオーバーライドしたときも、基本的な振る舞いはメソッドの場合と同じなので、今回はメソッドに絞って下記のサンプルコードで解説します。

//サンプルプログラム
// キャラクターのオリジナルクラス

class Character {

  // キャラクターの名前

  private String name;

  // キャラに名前を設定するメソッド

  public void setName(String name) {

    this.name = name;

  }

  // キャラの名前を取得するメソッド

  public String getName() {

    return name;

  }

}

// キャラクターから派生した人間クラス

class Human extends Character {

  // 人間が持てる武器の名前

  private String weapon = “”;

  // 武器を設定するメソッド

  public void setWeapon(String weapon) {

    this.weapon = weapon;

  }

  // 攻撃メソッド

  public void attack() {

    // 武器の有無で処理を分岐する

    if (weapon.isEmpty()) {

      System.out.println(getName() + “は武器を持っていないので攻撃できない!”);

    } else {

      System.out.println(getName() + “は「” + weapon + “」で攻撃した!”);

    }

  }

}

class Player extends Human {

  // HumanクラスのAttackメソッドをオーバーライド

  @Override

  public void attack() {

    // ここでスーパークラスの「attack」メソッドが実行されることを期待するが…

    System.out.println(getName() + “の体力が「1」減った!”);

    System.out.println(getName() + “の剣術が「1」増えた!”);

  }

}

public class Main {

  public static void main(String[] args) {

    // 村人のインスタンスを生成する

    Human villager = new Human();

    // 村人に名前を付ける

    villager.setName(“村人A”);

    // 村人が攻撃する

    villager.attack();

    

    // 改行する

    System.out.println();

    // プレイヤーのインスタンスを生成する

    Player player = new Player();

    // プレイヤーに名前を付ける

    player.setName(“プレイヤー”);

    // プレイヤーに「伝説の剣」を付ける

    player.setWeapon(“伝説の剣”);

    // プレイヤーが攻撃する

    player.attack();

  }

}

 

//実行結果
村人Aは武器を持っていないので攻撃できない!

プレイヤーの体力が「1」減った!

プレイヤーの剣術が「1」増えた!

上記のサンプルコードは、ゲームプログラムを題材にしたものです。まずキャラクターの基礎となる「Character」クラスを作り、人間の「Human」クラス、さらにプレイヤー専用の「Player」クラスと派生します。重要なポイントは、Humanの「attack」メソッドをPlayerでオーバーライドしている(「@Override」アノテーションが付いている)部分です。

このサンプルプログラムは、デフォルトコンストラクタと同じように、Playerのattackメソッドを呼び出したとき、自動的にHumanのattackメソッドも呼び出されることを期待しています。しかし実際には、Humanのattackメソッドはオーバーライドで上書きされた形になるでしょう。

そのため、プレイヤーは「伝説の剣」を装備しているにも関わらず、Playerクラスのattackメソッドしか実行されません。結果的に、Humanのattackメソッドは実行されないため、伝説の剣による攻撃が行われず、よくわからない結果が表示されたのです。

つまり、オーバーライドした親クラスのメソッドを呼び出したいときも、「super」修飾子で明示的に呼び出す必要があるということです。以上でクラスの継承のおさらいが完了し、どのようなケースでsuperが必要になるのかがわかりました。

Javaの「super」の基本的な使い方

Javaの「super」の基本的な使い方

Javaの「super」の必要性がわかったところで、下記3つの観点からsuperの基本的な使い方をご紹介します。

  • 親クラスの「引数付きコンストラクタ」を呼び出す場合
  • 「オーバーライドした親クラスのメソッド」を呼び出す場合
  • 【番外編】親クラスのフィールドにアクセスしたい場合

親クラスの「引数付きコンストラクタ」を呼び出す場合

デフォルトコンストラクタとは異なり、引数付きコンストラクタでインスタンスを生成すると、親クラスの引数付きコンストラクタは実行されません。そのため、親クラスの引数付きコンストラクタでフィード(変数)を初期化する処理を記載している場合は、意図しない結果となります。

そこでsuperを使えば、親クラスの引数付きコンストラクタを呼び出すことができ、こうしたトラブルを解決することができます。下記のサンプルコードで詳細を確認しましょう。

//サンプルプログラム
class Animal {

  // 動物の名称

  private String name;

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

  Animal() {

    System.out.println(“Animalクラスのデフォルトコンストラクタ”);

  }

  // 引数付きコンストラクタ(動物の名前を設定する)

  Animal(String name) {

    System.out.println(“Animalクラスの引数付きコンストラクタ”);

    setName(name);

  }

  // 動物の名前を設定する

  public void setName(String name) {

    this.name = name;

  }

  // 動物の名前を取得する

  public String getName() {

    return name;

  }

}

class Cat extends Animal {

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

  Cat() {

    System.out.println(“Catクラスのデフォルトコンストラクタ”);

  }

  // 引数付きコンストラクタ(動物の名前を設定する)

  Cat(String name) {

    // ここでスーパークラスの「引数付きコンストラクタ」を明示的に呼び出す!

    super(name);

    System.out.println(“Catクラスの引数付きコンストラクタ”);

  }

  // 鳴き声を表現するメソッド

  public void meow() {

    System.out.println(getName() + “が「にゃあ~♪」と鳴いた!”);

  }

}

public class Main {

  public static void main(String[] args) {

    // 猫クラスのインスタンスを生成する(デフォルトコンストラクタを呼び出す)

    Cat catA = new Cat();

    // 猫の名前を設定する

    catA.setName(“みぃちゃん”);

    // 猫クラスの鳴き声メソッドを実行する

    catA.meow();

    

    // 改行する

    System.out.println();

    // 猫クラスのインスタンスを生成する(デフォルトコンストラクタを呼び出す)

    Cat catB = new Cat(“たまちゃん”);

    // 猫クラスの鳴き声メソッドを実行する

    catB.meow();

  }

}

 

//実行結果
Animalクラスのデフォルトコンストラクタ

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

みぃちゃんが「にゃあ~♪」と鳴いた!

Animalクラスの引数付きコンストラクタ

Catクラスの引数付きコンストラクタ

たまちゃんが「にゃあ~♪」と鳴いた!

上記のように、Catクラスの引数付きコンストラクタ内部に、「super(name);」を追加するだけでOKです。結果的に、Animaiクラスの引数付きコンストラクタが実行され、catBに「たまちゃん」という名前が付きました。もう「nullちゃん」になることはありませんね。

ちなみに、superを書かない場合は「Animaiのデフォルトコンストラクタ→Catの引数付きコンストラクタ」の順番で実行されましたが、superを書くと「Animaiの引数付きコンストラクタ→Catの引数付きコンストラクタ」となっています。

つまり、superを書かなければ、子クラスでどのコンストラクタを呼んでも、親クラスのデフォルトコンストラクタが最初に実行されるということです。

「オーバーライドした親クラスのメソッド」を呼び出す場合

親クラスのメソッドを子クラスでオーバーライド(上書き)すると、子クラスのメソッドのみが呼び出されるようになります。しかし、子クラスのメソッドが呼び出されたとき、親クラスのメソッドを実行したいケースもあるでしょう。先ほどのゲームプログラムにはバグがありましたが、下記のサンプルコードのようにsuperを使えば解決できます。

//サンプルプログラム
// キャラクターのオリジナルクラス

class Character {

  // キャラクターの名前

  private String name;

  // キャラに名前を設定するメソッド

  public void setName(String name) {

    this.name = name;

  }

  // キャラの名前を取得するメソッド

  public String getName() {

    return name;

  }

}

// キャラクターから派生した人間クラス

class Human extends Character {

  // 人間が持てる武器の名前

  private String weapon = “”;

  // 武器を設定するメソッド

  public void setWeapon(String weapon) {

    this.weapon = weapon;

  }

  // 攻撃メソッド

  public void attack() {

    // 武器の有無で処理を分岐する

    if (weapon.isEmpty()) {

      System.out.println(getName() + “は武器を持っていないので攻撃できない!”);

    } else {

      System.out.println(getName() + “は「” + weapon + “」で攻撃した!”);

    }

  }

}

class Player extends Human {

  // HumanクラスのAttackメソッドをオーバーライド

  @Override

  public void attack() {

    // ここでスーパークラスの「attack」メソッドを明示的に呼び出す!

    super.attack();

    System.out.println(getName() + “の体力が「1」減った!”);

    System.out.println(getName() + “の剣術が「1」増えた!”);

  }

}

public class Main {

  public static void main(String[] args) {

    // 村人のインスタンスを生成する

    Human villager = new Human();

    // 村人に名前を付ける

    villager.setName(“村人A”);

    // 村人が攻撃する

    villager.attack();

    

    // 改行する

    System.out.println();

    // プレイヤーのインスタンスを生成する

    Player player = new Player();

    // プレイヤーに名前を付ける

    player.setName(“プレイヤー”);

    // プレイヤーに「伝説の剣」を付ける

    player.setWeapon(“伝説の剣”);

    // プレイヤーが攻撃する

    player.attack();

  }

}

 

//実行結果
村人Aは武器を持っていないので攻撃できない!

プレイヤーは「伝説の剣」で攻撃した!

プレイヤーの体力が「1」減った!

プレイヤーの剣術が「1」増えた!

Playerクラスのattackメソッドに、「super.attack();」という文を追加するだけで、ゲームが正常に動作するようになりました。スーパークラスであるHumanクラスのattackメソッドが実行されて、Playerクラスのattackメソッドが実行されるからです。ただし、superを書く場所を誤ると、やはり意図しない結果になるので注意してください。

【番外編】親クラスのフィールドにアクセスしたい場合

あまり一般的ではありませんが、親クラスのフィード(メンバ変数)を子クラスでオーバーライドすることもあります。フィードをオーバーライドした場合も、基本的な振る舞いはメソッドの場合と同じです。下記のサンプルコードで詳細を確認しておきましょう。

//サンプルプログラム
class SuperClass {

  String name = “スーパークラス”;

  public String getName() {

    return name;

  }

}

class SubClass extends SuperClass {

  String name = “サブクラス”;

  public String getName() {

    return name;

  }

  public void print() {

    // フィード(メンバ変数)の比較

    System.out.println(“「name」と書いた場合:” + name);

    System.out.println(“「super.name」と書いた場合:” + super.name);

    // 改行する

    System.out.println();

    // メソッドの比較

    System.out.println(“「getName()」と書いた場合:” + getName());

    System.out.println(“「super.getName()」と書いた場合:” + super.getName());

  }

}

public class Main {

  public static void main(String[] args) {

    // テスト用クラスの生成とメソッド実行を行う

    SubClass sub = new SubClass();

    sub.print();

  }

}

 

//実行結果
「name」と書いた場合:サブクラス

「super.name」と書いた場合:スーパークラス

「getName()」と書いた場合:サブクラス

「super.getName()」と書いた場合:スーパークラス

ちなみに、SubClassの「getName」メソッドを削除すると、「getName()」と書いた場合の結果が「スーパークラス」になります。これは、SuperClassのgetNameメソッドが実行されて、その際に参照されるのはSuperClass側の「name」だからです。SubClassの「name」フィールドは参照されません。

Javaの「super」の知っておきたい応用テクニック2選

Javaの「super」の知っておきたい応用テクニック2選

Javaのsuper修飾子を知っておくと、さまざまな応用テクニックにも活用できます。本章では、代表的なsuperの応用法を2つご紹介します。

  • 「Cloneable」なオリジナルクラスを実装する
  • ジェネリクスクラスで型名にスーパークラスを指定する

「Cloneable」なオリジナルクラスを実装する

自分自身のインスタンスをディープコピー、つまりデータそのものを複製できるクラスを作る場合は、「Cloneable」なクラスを作成する必要がある。Cloneableとは、ディープコピーを行うための「cloneメソッド」が使えるという意味です。

自作クラスをCloneableにするためには、「Cloneableインターフェース」を実装する必要があります。その際に「clone」メソッドを実装し、「super.clone()」を呼び出す必要があります。詳細を下記のサンプルコードで確認しましょう。

//サンプルプログラム
// Cloneableなクラス「Test」を実装する

//サンプルプログラム

// Cloneableなクラス「Test」を実装する

class Test implements Cloneable {

  // テスト用の変数を用意する

  private String string = “”;

  Test(String string) {

    this.string = string;

  }

  // cloneメソッドを実装する

  public Test clone() {

    try {

      // 自分自身のクラスのインスタンスを複製する

      // このときにsuper.cloneメソッドを呼び出す

      Test copy = (Test) super.clone();

      // ただし、string変数は初期化したいため、個別に初期化する

      copy.setString(“”);

      // クローンしたオブジェクトを返す

      return copy;

    } catch (Exception e) {

      // とりあえず例外を再度投げる

      throw new AssertionError(e);

    }

  }

  // 変数を変更する

  public void setString(String string) {

    this.string = string;

  }

  // 変数を取得する

  public String getString() {

    return string;

  }

}

public class Main {

  public static void main(String[] args) {

    // テストクラスのインスタンスを2つ生成する

    Test testA = new Test(“コードカキタイ”);

    Test testB = new Test(“コードカキタイ”);

    // testAは単なるコピー、testBはクローンを作成する

    Test copyA = testA;

    Test copyB = testB.clone();

    // それぞれの変数の値を表示する

    System.out.println(“testA : copyA = ” + testA.getString() + “:” + copyA.getString());

    System.out.println(“testB : copyB = ” + testB.getString() + “:” + copyB.getString());

    // copyAとcopyBの変数を書き換える

    copyA.setString(“変更”);

    copyB.setString(“変更”);

    // それぞれの変数の値を表示する

    System.out.println();

    System.out.println(“testA : copyA = ” + testA.getString() + “:” + copyA.getString());

    System.out.println(“testB : copyB = ” + testB.getString() + “:” + copyB.getString());

  }

}

 

//実行結果
testA : copyA = コードカキタイ:コードカキタイ

testB : copyB = コードカキタイ:

testA : copyA = 変更:変更

testB : copyB = コードカキタイ:変更

重要なポイントは、cloneableな「Test」クラスでcloneメソッドを実装し、スーパークラスのcloneを「super.clone();」で呼び出していることです。ただし、cloneメソッドはObject型を返すため、戻り値として返すときはキャストする必要があります。

また、すべてをコピーするのではなく、一部のフィールドだけは初期化しておきたいこともあるでしょう。たとえば、TestクラスにはString型変数がありますが、cloneしたあと個別にstring変数を初期化しています。それが、実行結果の2行目までの部分です。単に代入演算子でコピーしたcopyAは、元のtestAと内容が同じです。

cloneメソッドで重要なのが、それ以降の部分です。コピー後の変数testBに変更を加えたとき、代入演算子でコピーされたtestAの内容も変わっています。これは、代入演算子では「シャローコピー」、つまり変数に格納されているアドレスだけコピーされているからです。

一方でcloneメソッドを使ったほうは、元のtestB変数の内容はしっかり保持されています。これは、アドレスではなく変数の実体が、しっかりコピーされているためです。このように、super修飾子は重要な機能の実装にも役立つ機能なのです。

ジェネリクスクラスで型名にスーパークラスを指定する

「ジェネリクスクラス」は、さまざまなデータ型を受け付けるクラスです。型名を「<>」で指定してインスタンスを作成することが特徴で、たとえばListやMapなどのコレクションクラスが、ジェネリッククラスとして実装されています。

ジェネリクスクラスは、格納できるデータ型を制限することができます。下記のサンプルコードのように「? super 型名」を指定すると、「下限境界ワイルドカード」となり、型名に指定したクラスと、そのスーパークラスのみを受け付けることが可能です。

//サンプルプログラム
// テスト用のジェネリッククラス

class Test<T> {

  private T t;

  Test(T t) {

    this.t = t;

  }

  public T get() {

    return t;

  }

}

public class Main {

  public static void main(String[] args) {

    // ジェネリッククラスをインスタンス化する

    // ここでは、プリミティブ型(byte・int・long・float・double)に変換可能な「Number」クラスを型名に指定する

    // なおNumberクラスは、Byte・Short・Integer・Long・Float・Double・BigInteger・BigDecimalのスーパークラスである

    Test<Number> generic = new Test<Number>(100);

    

    // 2つ目のジェネリッククラスをインスタンス化する

    // こちらは型名に「Integer」のスーパークラスを指定している

    // したがって、この変数にはNumber型のジェネリッククラスを代入できる

    Test<? super Integer> integer;

    

    // スーパークラスのインスタンスを代入する

    integer = generic;

    

    // 中身のデータを表示する

    System.out.println(integer.get());

  }

}

 

//実行結果
100

上記のサンプルプログラムでは、まず簡易的なジェネリクスクラス「Test」を作成し、mainメソッドで「Number」クラスを格納できる「generic」変数を生成しています。次に、「Test<? super Integer> integer;」と記載し、「Integerクラスとそのスーパークラス」のみ格納できる変数「integer」を生成しました。

ちなみに、IntegerやFloatなどは、プリミティブ型をクラス形式にしたラッパークラスです。ListやMapなどにプリミティブ型を格納することはできないので、代わりにラッパークラスを使用する必要があります。そのラッパークラスのスーパークラスが、今回使用したNumberクラスなのです。

なお、ジェネリクスクラスの「下限境界ワイルドカード」の概念は、Javaの中でもとくにわかりにくい分野なので、あくまで参考程度に覚えておけば問題ないでしょう。

「super」は親クラス・「this」は自分自身のクラスを意味する

「super」は親クラス・「this」は自分自身のクラスを意味する

「super」と混同しやすい修飾子が「this」です。superは親クラスのメソッドやフィードにアクセスできる一方、thisは自分自身のクラスの要素にアクセスするためのものです。基本的にはクラスでthisを使う必要はありませんが、下記2ついずれかの場合はthisが必要です。

  • 仮引数名とフィールド名が同じ場合
  • 自分自身のインスタンスをメソッドの戻り値にする場合

仮引数名とフィールド名が同じことは多く、両者を区別するためにthisを使うことが多いです。また、「メソッドチェーン」を活用する場合は、自分自身のインスタンスを返す必要があります。下記のサンプルプログラムで、superとthisの違いや使い方を確認しましょう。

//サンプルプログラム
// キャラクターのオリジナルクラス

class Character {

  // キャラクターの名前

  private String name = “”;

  // 引数付きコンストラクタ(キャラクター名を設定する)

  public Character(String name) {

    this.name = name;

  }

  // キャラの名前を取得するメソッド

  public String getName() {

    return name;

  }

}

// キャラクターから派生した人間クラス

class Human extends Character {

  // 人間が持てる武器の名前

  private String weapon = “”;

  //引数付きコンストラクタ(キャラクター名を設定する)

  public Human(String name) {

    super(name);

  }

  // 武器を設定するメソッド

  public Human setWeapon(String weapon) {

    this.weapon = weapon;

    return this;

  }

  // 攻撃メソッド

  public Human attack() {

    // 武器の有無で処理を分岐する

    if (weapon.isEmpty()) {

      System.out.println(getName() + “は武器を持っていないので攻撃できない!”);

    } else {

      System.out.println(getName() + “は「” + weapon + “」で攻撃した!”);

    }

    return this;

  }

}

class Player extends Human {

  //引数付きコンストラクタ(キャラクター名を設定する)

  public Player(String name) {

    super(name);

  }

  // HumanクラスのAttackメソッドをオーバーライド

  @Override

  public Player attack() {

    // ここでスーパークラスの「attack」メソッドを明示的に呼び出す!

    super.attack();

    System.out.println(getName() + “の体力が「1」減った!”);

    System.out.println(getName() + “の剣術が「1」増えた!”);

    return this;

  }

}

public class Main {

  public static void main(String[] args) {

    // 村人のインスタンス生成を名前の設定を行う

    Human villager = new Human(“村人A”);

    // 村人の武器設定と攻撃を行う

    villager.setWeapon(“錆びた斧”).attack();

    // 改行する

    System.out.println();

    // プレイヤーのインスタンスを生成する

    Player player = new Player(“プレイヤー”);

    // プレイヤーに「伝説の剣」を装備して攻撃する

    player.setWeapon(“伝説の剣”).attack();

  }

}

 

//実行結果
村人Aは「錆びた斧」で攻撃した!

プレイヤーは「伝説の剣」で攻撃した!

プレイヤーの体力が「1」減った!

プレイヤーの剣術が「1」増えた!

先ほどのゲームプログラムを変化させたものです。Characterクラスのコンストラクタや、HumanクラスのsetWeaponメソッド内部では、仮引数とフィールド名(メンバ変数名)が同じになっていることに注目しましょう。

ここでそのまま「name = name;」と書くと、仮引数に仮引数を代入する結果になります。フィールドに代入したいので、「this.name = name;」と明示的に区別する必要があります。「this」と書くことで、自分自身のインスタンスのフィールドを参照できるのです。

また、HumanクラスのsetWeaponメソッドが、「this」を戻り値として返していることもポイントです。これは「自分自身のインスタンスを返す」ことを意味し、「player.setWeapon(“伝説の剣”).attack();」のようにメソッド呼び出しを連鎖できます。このように、メソッド呼び出しを連鎖的に続けることを「メソッドチェーン」と呼び、ソースコードの簡潔化に役立ちます。

Javaの「super」を理解してクラスを便利に活用しよう!

Javaの「super」を理解してクラスを便利に活用しよう!

Javaの「super」修飾子は、親クラスの引数付きコンストラクタやオーバーライドしたメソッドに、子クラスからアクセスするための機能です。引数付きコンストラクタでインスタンスを生成した場合、親クラスの引数付きコンストラクタは自動的には実行されません。また、オーバーライドしたメソッドは子クラスのものが優先されて、親クラスの同名メソッドは隠蔽されます。

以上の仕様を理解しておかないと、プログラムが意図しない結果になったり、厄介なバグが発生したりします。今回ご紹介したsuperを活用すると、必要に応じて親クラスのメソッドやフィードにアクセスして、今まで以上にクラスを便利かつ正確に使えるようになります。

cloneメソッドやジェネリッククラスなど、superには奥の深い応用テクニックもあるので、ぜひ少しずつ慣れていきましょう。

著者紹介

084tjsfdsf983asdlk03

084tjsfdsf983asdlk03

コードカキタイがオススメする記事!

  1. 子供におすすめのプログラミングスクール10選!学習メリットや教室選びのコツも紹介

    2022.01.06

    子供におすすめのプログラミングスクール10選!学習メリットや教室選びのコツも紹介

    #プログラミングスクール

  2. 【完全版】大学生におすすめのプログラミングスクール13選!選ぶコツも詳しく解説

    2022.01.06

    【完全版】大学生におすすめのプログラミングスクール13選!選ぶコツも詳しく解説

    #プログラミングスクール

  3. 【未経験でも転職可】30代におすすめプログラミングスクール8選!

    2022.01.06

    【未経験でも転職可】30代におすすめプログラミングスクール8選!

    #プログラミングスクール

  4. 初心者必見!独学のJava学習方法とおすすめ本、アプリを詳しく解説

    2022.01.06

    初心者必見!独学のJava学習方法とおすすめ本、アプリを詳しく解説

    #JAVA

  5. 忙しい社会人におすすめプログラミングスクール15選!失敗しない選び方も詳しく解説

    2022.01.06

    忙しい社会人におすすめプログラミングスクール15選!失敗しない選び方も詳しく解説

    #プログラミングスクール

  1. 【無料あり】大阪のおすすめプログラミングスクール14選!スクール選びのコツも紹介

    2022.01.06

    【無料あり】大阪のおすすめプログラミングスクール14選!スクール選びのコツも紹介

    #プログラミングスクール

  2. 【目的別】東京のおすすめプログラミングスクール20選!スクール選びのコツも徹底解説

    2022.01.06

    【目的別】東京のおすすめプログラミングスクール20選!スクール選びのコツも徹底解説

    #プログラミングスクール

  3. 【無料あり】福岡のおすすめプログラミングスクール13選!選び方も詳しく解説

    2022.01.06

    【無料あり】福岡のおすすめプログラミングスクール13選!選び方も詳しく解説

    #プログラミングスクール

  4. 【徹底比較】名古屋のおすすめプログラミングスクール13選!選び方も詳しく解説

    2022.01.06

    【徹底比較】名古屋のおすすめプログラミングスクール13選!選び方も詳しく解説

    #プログラミングスクール

  5. 【徹底比較】おすすめのプログラミングスクール18選!失敗しない選び方も徹底解説

    2022.01.06

    【徹底比較】おすすめのプログラミングスクール18選!失敗しない選び方も徹底解説

    #プログラミングスクール