Javaを利用したシステム開発において、オブジェクト指向の概念を理解することは非常に重要です。

その中でも、名前が似ていて混同されやすいキーワードに「オーバーロード」と「オーバーライド」があります。

これらはどちらもメソッドの定義に関する仕組みですが、その目的や動作するタイミング、記述ルールは全く異なります。

本記事では、Javaエンジニアが必ず押さえておくべきこれら2つの違いについて、ソースコードの例を交えながら詳細に解説します。

オーバーロード(Overload)とは

オーバーロードとは、同一クラス内に、名前が同じで引数の構成(型、数、並び順)が異なるメソッドを複数定義することを指します。

日本語では「多重定義」とも呼ばれます。

通常、一つのクラス内で同じ名前のメソッドを定義することはできません。

しかし、引数のリストが異なれば、コンパイラはそれらを「別のメソッド」として識別できるため、同じ名前を使い回すことが可能になります。

オーバーロードの目的とメリット

オーバーロードの主な目的は、プログラムの可読性と利便性の向上です。

例えば、数値を加算するメソッドを作成する場合、整数(int型)を足すケースもあれば、小数(double型)を足すケースもあります。

もしオーバーロードがなければ、addInt(int a, int b)addDouble(double a, double b) のように、処理内容は似ているのに引数ごとに異なる名前を付けなければなりません。

オーバーロードを利用することで、呼び出し側は引数の型を気にせず、一貫した名前でメソッドを呼び出せるようになります。

オーバーロードの成立条件

オーバーロードを成立させるためには、以下のいずれかが異なっている必要があります。

これを「シグネチャが異なる」と表現します。

  1. 引数の数
  2. 引数のデータ型
  3. 引数の並び順

注意点として、「戻り値の型」や「アクセス修飾子」だけが異なる場合は、オーバーロードとして認められません。

コンパイラは呼び出し時の引数情報だけでメソッドを特定するため、戻り値が違うだけではどのメソッドを呼ぶべきか判断できないからです。

オーバーロードの実装例

具体的なコード例を見てみましょう。

以下のプログラムでは、計算を行う Calculator クラス内で add メソッドをオーバーロードしています。

Java
public class Calculator {

    // 2つの整数の和を求める
    public int add(int a, int b) {
        System.out.println("int型の引数2つが呼ばれました");
        return a + b;
    }

    // 3つの整数の和を求める(引数の数が異なる)
    public int add(int a, int b, int c) {
        System.out.println("int型の引数3つが呼ばれました");
        return a + b + c;
    }

    // 2つの小数の和を求める(引数の型が異なる)
    public double add(double a, double b) {
        System.out.println("double型の引数2つが呼ばれました");
        return a + b;
    }

    public static void main(String[] args) {
        Calculator calc = new Calculator();

        // それぞれ異なるシグネチャのメソッドが呼び出される
        System.out.println("結果1: " + calc.add(10, 20));
        System.out.println("結果2: " + calc.add(10, 20, 30));
        System.out.println("結果3: " + calc.add(1.5, 2.5));
    }
}
実行結果
int型の引数2つが呼ばれました
結果1: 30
int型の引数3つが呼ばれました
結果2: 60
double型の引数2つが呼ばれました
結果3: 4.0

このように、同じ add という名前であっても、渡す引数によって自動的に適切なメソッドが選択されます。

オーバーライド(Override)とは

オーバーライドとは、親クラス(スーパークラス)で定義されているメソッドを、子クラス(サブクラス)で再定義することを指します。

日本語では「上書き」を意味します。

継承関係にあるクラスにおいて、親クラスが持つ汎用的な機能を、子クラスで特定の用途に合わせてカスタマイズしたい場合に使用されます。

オーバーライドの目的とメリット

オーバーライドの最大のメリットは、ポリモーフィズム(多態性)の実現にあります。

ポリモーフィズムとは、同じメソッド呼び出しでも、操作対象のオブジェクトによって異なる振る舞いをさせる仕組みのことです。

例えば、「動物」という親クラスに「鳴く(makeSound)」というメソッドがあるとき、「犬」クラスでは「ワンワン」、「猫」クラスでは「ニャー」と鳴くように、子クラスごとに具体的な処理を上書きできます。

これにより、呼び出し側は相手が「何の動物か」を詳細に意識することなく、共通のインターフェースで操作が可能になります。

オーバーライドの成立条件

オーバーライドを正しく行うには、厳格なルールがあります。

  1. メソッド名が親クラスと同じであること。
  2. 引数の数、型、順序が親クラスと完全に一致すること。
  3. 戻り値の型が親クラスと同じ、またはその子クラスの型であること(共変戻り値)。
  4. アクセス修飾子は、親クラスよりも厳しく制限してはならない(例:親が protected なら、子は protectedpublic)。
  5. 親クラスのメソッドに finalstatic が付いている場合はオーバーライドできない。

@Override アノテーションの重要性

Javaでは、オーバーライドを行う際に @Override というアノテーションを記述することが推奨されます。

これはコンパイラに対して「このメソッドはオーバーライドする意図である」と伝えるためのものです。

もしスペルミスなどで引数の型を間違えてしまった場合、アノテーションがないと「新しいメソッド(オーバーロード)」として扱われてしまいます。

しかし、@Override を付けておけば、正しくオーバーライドできていない場合にコンパイルエラーとして検知できるため、バグを未然に防ぐことができます。

オーバーライドの実装例

以下のコードでは、動物を扱うクラス構成でオーバーライドの挙動を確認します。

Java
// 親クラス
class Animal {
    public void makeSound() {
        System.out.println("動物が何か音を出しています");
    }
}

// 子クラス1
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("ワンワン!");
    }
}

// 子クラス2
class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("ニャーニャー");
    }
}

public class Main {
    public static void main(String[] args) {
        // 親クラスの型で変数を宣言
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        // 実行時にインスタンスの型に応じたメソッドが呼ばれる
        myDog.makeSound();
        myCat.makeSound();
    }
}
実行結果
ワンワン!
ニャーニャー

変数の型が Animal であっても、実際に生成されたインスタンスが DogCat であれば、オーバーライドされた側のメソッドが優先して呼び出されます。

オーバーロードとオーバーライドの違いを比較

ここで、両者の違いを表にまとめて整理しましょう。

これらは混同しやすいですが、「同じクラス内での拡張」か「継承先での変更」かという点が根本的な違いです。

比較項目オーバーロード (Overload)オーバーライド (Override)
意味メソッドの多重定義メソッドの上書き
関係性同一クラス内(または継承先)親子クラス間の継承関係
メソッド名同じ同じ
引数リスト必ず変える必要がある必ず同じにする必要がある
戻り値の型任意(変えても変えなくても良い)同じ(またはサブタイプ)
アクセス修飾子制限なし親より制限を緩くする必要がある
決定タイミングコンパイル時(静的)実行時(動的)

決定タイミングの違い(静的結合と動的結合)

非常に重要な違いの一つに、どのメソッドを実行するかを「いつ決めるか」という点があります。

オーバーロードは、コンパイルの時点でどの引数のメソッドを呼ぶかが確定します。

これを「静的結合」と呼びます。

一方、オーバーライドはプログラムの実行中、その変数が指し示している実際のインスタンスが何であるかに基づいて決定されます。

これを「動的結合」と呼びます。

この動的結合の仕組みがあるからこそ、Javaなどのオブジェクト指向言語では、柔軟なプログラム設計が可能になっています。

実践的な使い分けのポイント

開発現場でどちらを使うべきか迷った際は、以下の観点で判断するとスムーズです。

オーバーロードを使うべき場面

オーバーロードは、「同じ種類の処理だが、入力データの形式が複数ある場合」に活用します。

コンストラクタの定義

初期化時に渡す情報のパターンが複数ある場合(名前だけ渡す、名前と年齢を渡すなど)。

ユーティリティメソッド

数値を文字列に変換する処理で、intlongfloatなど複数の型に対応させたい場合。

オプション引数の模倣

Javaにはデフォルト引数の機能がないため、引数が少ないメソッドから引数が多いメソッドを呼び出すことで、オプション設定を実現します。

コンストラクタオーバーロードの例

Java
public class User {
    private String name;
    private int age;

    // 名前のみで初期化
    public User(String name) {
        this(name, 0); // もう一方のコンストラクタを呼び出す
    }

    // 名前と年齢で初期化
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

オーバーライドを使うべき場面

オーバーライドは、「共通の操作インターフェースを持ちつつ、具体的な振る舞いを個別に変えたい場合」に活用します。

フレームワークのカスタマイズ

既存のクラスを継承し、特定の処理(保存処理や表示処理など)だけを自作のロジックに置き換える場合。

デザインパターンの適用

Template Methodパターンなどのように、親クラスで処理の骨組みを決め、子クラスで具体的なステップを実装する場合。

toString()などの標準メソッド変更

全てのクラスの親である Object クラスのメソッドを書き換えて、デバッグしやすくする場合。

注意すべき落とし穴

実装時に間違えやすいポイントについても触れておきます。

1. staticメソッドはオーバーライドできない

static メソッドはクラスに紐付くものであり、インスタンスに紐付くものではありません。

そのため、子クラスで同じシグネチャの static メソッドを定義しても、それはオーバーライドではなく「隠蔽(Hiding)」と呼ばれます。

ポリモーフィズムは効かないため注意が必要です。

2. アクセス修飾子の制限

親クラスで public と定義されているメソッドを、子クラスで private にオーバーライドすることはできません。

これは「親クラスとして扱えるものは、子クラスでも同様に扱えなければならない」というリスコフの置換原則に基づいています。

3. 例外の送出範囲

オーバーライドしたメソッドは、親クラスのメソッドが投げる例外よりも「広い(上位の)」チェック例外を投げることはできません。

ただし、より制限された例外や、実行時例外(RuntimeException)であれば自由に投げることが可能です。

まとめ

Javaにおけるオーバーロードとオーバーライドは、名前こそ似ていますが、その役割は対照的です。

オーバーロードは「一つの名前に複数の役割を持たせる」ことで、同じクラス内での「利便性」を高めます。

一方で、オーバーライドは「親の役割を子の役割で塗り替える」ことで、クラス間の継承関係における「柔軟な振る舞い(ポリモーフィズム)」を実現します。

これらの違いを正しく理解し、適切に使い分けることで、コードの重複を減らし、拡張性の高いプログラムを設計できるようになります。

特に、意図しないバグを防ぐために @Override アノテーションを活用する習慣を身につけておきましょう。

Javaの基本概念であるこれらをマスターすることは、中級以上のエンジニアへの第一歩となります。

今後の開発において、メソッドを定義する際には「これはオーバーロードなのか、それともオーバーライドなのか」を常に意識して設計に取り組んでみてください。