Groovyのオーバーロードメソッドの解決はユニークなところがある

JavaC#C++などを含めて現代のほとんどの静的な型付けのプログラミング言語では、異なる型を取る複数のメソッドを同じクラス中に多重定義することができます。これはメソッドのオーバーロード(overload)*1として知られています。たとえば、以下のように同じtestという名前のメソッドを複数定義することができます。

public class Sample {

    void test(String strParam) {
        System.out.println("String = " + strParam);
    }

    void test(Integer intParam) {
        System.out.println("Integer = " + intParam);
    }
}

この場合、どちらのメソッドが呼び出されるかは、呼び出し側がパラメーターで渡した変数のコンパイル時の型で静的に決まります。ですから、このSampleのインスタンスに対して、文字列リテラル"hello"を渡してtestメソッドを呼び出せば上のメソッドが呼び出されますし、int型の変数を渡してtestメソッドを呼び出せば下のメソッドが呼び出されることになります。ここでポイントとなるのは、オーバーロードメソッドのどちらを呼び出すのかの判断はあくまでもコンパイル時の静的な型に基づいて決定されなくてはならないということです。したがって、以下のコードは正しくコンパイルが通りません。

public class SampleTest {

    public static void main(String[] args) {
        Sample sample = new Sample();

        Object param1= "test";
        Object param2= 2;

        sample.test(param1);   // コンパイルエラー
        sample.test(param2);   // コンパイルエラー
    }
}

param1、param2ともに型がObject型になっているため、実際実行時には互換性のある型が代入されているにもかかわらずコンパイルエラーとなります。しかし、Groovyの場合以下のコードが正しくコンパイル、実行できてしまいます。

class Sample {

    def test(String strParam) {
        println "String = $strParam"
    }

    def test(Integer intParam) {
        println "int = $intParam"
    }
}

Object param1 = "test2"
Object param2 = 2

sample.test(param1)
sample.test(param2)

この場合、param1とparam2のコンパイル時の型はともにObject型にもかかわらず、パラメーターの実行時の型に基づいて正しくオーバーロードされたメソッドが呼び分けられます。パラメータに継承関係があっても同様のことが言えます。

class Parent {}

class Child extends Parent {}

class Sample2 {

    def test(Parent parent) {
        println "Parent" + parent
    }

    def test(Child child) {
        println "Child" + child
    }
}

def sample2 = new Sample2()

Parent parent = new Parent()
Parent child = new Child()

sample2.test(parent)  // parentのメソッドが呼ばれる
sample2.test(child)   // childのメソッドが呼ばれる

このようにGroovyではレシーバーのオブジェクトの型だけでなく、パラメーターオブジェクトの型を合わせて対象メソッドを実行時に判断してくれる仕組みがあるのです。このしくみは一般的にはマルチメソッド*2と呼ばれるようです。Javaではパラメーターの型を使って処理を呼び分けるためにダブルディスパッチというテクニックがありますが(Visitorパターンなどで利用される)、Groovyではメソッドがオーバーロードさえできればこのような動的な振る舞いは自然かつ簡単に実装することができます。*3
一見、Javaとそっくりな構文なのですが、振る舞いとして大きく異なるところもあるということですね。ちなみに、Groovyのこの機能はかなりユニークな機能かと思います。RubyJavaScriptのような完全に動的な言語ではパラメーターの型を定義することができないため、そもそもオーバーロードということ自体ができません。Groovyの場合動的な側面も持ちながら、パラメーターの型も定義可能という静的言語の性質も併せ持っているためマルチメソッドのようなユニークな機能が取り入れられたのではないかと思います。
ちなみに、

Programming Groovy: Dynamic Productivity for the Java Developer (The Pragmatic Programmers)

Programming Groovy: Dynamic Productivity for the Java Developer (The Pragmatic Programmers)

という本の78ページに、JavaとGroovyで動作が変わってくる面白い例が載っていたのでここで紹介させていただきます。

public class UsingCollection {
    public static void main(String[] args) {
        ArrayList<String> lst = new ArrayList<String>();
        Collection<String> col = list;

        lst.add("one");
        lst.add("two");
        lst.add("three");

        lst.remove(0);
        col.remove(0);

        System.out.println(lst.size());
        System.out.println(col.size());
    }
}

ちょっと人工的な例ですが、lstもcolも同一のArrayListインスタンスを参照しており、3つの要素を追加して、後から2つの要素を削除しているので、直感的には最終的に1個の要素が残っているはずと考えるのが自然です。しかし、上記をJavaコンパイルして実行すると答えは2になってしまいます。一方、Groovyでは直感の通り1になります。
Javaの場合どうして直感と異なる結果となってしまうのかというと、Collectionインターフェースにはremove(Object)*4があるのに、remove(int)は存在しないからです。よって、col.remove(0)の呼び出しはremove(Object)の呼び出しにバインドされ、0は自動的にIntegerのインスタンスにボックス化されてしまいます。当然そのようなオブジェクトはもともとの要素に含まれていないため、実際には要素は削除されず、結果として2個の要素が残ることになります。Groovyだと正しく実行時の型からremove(int)の方が呼び出されるため、意図した結果となります。

*1:初心者の人はよく混同しがちですが、もちろん継承で利用されるオーバーライド(override)とはまったく異なる概念です。

*2:レシーバーオブジェクトとパラメーターという複数のオブジェクトに基づいてバインドされるため

*3:残念ながら総称型は型消去されてしまうため、ListとListのようにパラメーターをとる同一名のメソッド(厳密にはパラメーターのイレイジャーが同じメソッド)をオーバーロードすることはGroovyでもできません。

*4:Collection.remove()メソッドのパラメータは総称型パラメーターにかかわらず常にObjectである点が隠れたポイント。これは過去の互換性のためそういう設計せざるを得ないということのようです。