Hatena::ブログ(Diary)

達人プログラマーを目指して このページをアンテナに追加 RSSフィード

2011-11-25

Javaの型パラメーターに対してstaticメソッドを呼び出した場合の挙動

以前にJava配列関連で調べたことがあったのですが、Javaの総称型は型消去によって直感的でない挙動をする場合があります。

Java言語のClassクラスが持つちょっと不思議な性質について - 達人プログラマーを目指して

Java5の型システムを理解するにはリフレクションAPIを使ってみるのが最短の近道になる - 達人プログラマーを目指して

特に、総称型の型パラメーターTについては以下はコンパイルできないという制約があります。

  • new T()
  • new T[配列サイズ]
  • catch (T ...
  • extends T
  • T.class
  • instanceof T

また、staticメソッドやstatic初期化ブロック内でクラスの型パラメータを使えないという制約もあります。

AngelikaLanger.com - Java Generics FAQs - Type Parameters - Angelika Langer Training/Consulting

このような制約は型Tが実際にはコンパイル時に消去されるということを考えれば納得のできるものです。これらの制約は初心者だけでなく、C++C#Haskellといった言語を使いこなす上級者であっても勘違いしやすいところですね。

それで、最近ちょっと話題になって気づいたのですが、実は型パラメーターTに対してstaticメンバのアクセスはどうなるのかという点が実は盲点で、自分もちょっと勘違いしていました。当然new Tが認められていないのだから、T.staticメンバという形式の呼び出しはコンパイルエラーなのだと考えていたのですが、実際に以下のコードを実行してみるとわかるように、Tは単に消去型として処理されるのですね。だから、型の上限が設定されていた場合はその上限の型のstaticメンバを呼び出すことができてしまうようです。

class Parent {
    public static String field = "Parent";
    
    public static void method() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    public static String field = "Child";

    public static void method() {
        System.out.println("Child");
    }
}

public class Tester<T extends Parent> {

    public void test() {
        T.method(); 
        System.out.println(T.field);
    }

    public static void main(String[] args) {
        Tester<Child> child = new Tester<>();
        child.test();
        // Parent
        // Parent
    }
}

実際にはTの消去型は上限のParentなのでTesterクラスは型消去されると以下と等価になります。


public class Tester {

    public void test() {
        Parent.method(); 
        System.out.println(Parent.field);
    }

    public static void main(String[] args) {
        Tester child = new Tester();
        child.test();
        // Parent
        // Parent
    }
}

そのように考えれば、このように実際にバインドされたChildでなく「Parent」が表示されてしまうのも納得がいきますが、かなり直感に反する気もします。Tester<Child>のようにChild型がTにバインドされているのだから、Tのstaticメンバーの呼び出しはChild側にバインドされるのが自然に思えます。

どうして、Javaではこのように型パラメーターに対するstaticメンバーの呼び出しを認めているのでしょうか。newの場合と同じ理屈ならコンパイルエラーにもできたはずなのですけれどね。

(追記)

ちなみに、staticでない場合を試してみました。

class Parent {
    public String field = "Parent";
    
    public void method() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    public String field = "Child";

    public void method() {
        System.out.println("Child");
    }
}

public class Tester<T extends Parent> {

    public void test(T instance) {
        instance.method();
        System.out.println(instance.field);
    }

    public static void main(String[] args) {
        Tester<Child> child = new Tester<>();
        child.test(new Child());
        // Child
        // Parent
    }
}

new T()はコンパイルできないため、外部でChildをインスタンス化して渡すようにしています。普通のインスタンスメソッドの場合は、

いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について - 達人プログラマーを目指して

で説明したとおり、ポリモーフィズムがあるので、意図したとおりChildの方のメソッドが呼び出されていることが確認できます。しかし、publicフィールドの場合は、staticメソッドと同様にポリモーフィズムがないためParentの側が表示されました。

ブクマのコメントにもありましたが、教訓としては、インスタンスメソッド以外サブクラスで「オーバーライド」するのは避けた方が良いということですね。

2011-05-15

Clojure言語基本の「き」

ふと思い立ってClojureの勉強を始めることにしました。 - 達人プログラマーを目指してで書いたように、10日前にClojureの勉強を始めました。まだ勉強を始めたばかりのということもあり、他人に上手く正確に説明できる段階ではないかと思いますが、自分自身超初心者の立場で書くということも有意義であると思うので、間違いを恐れずに今まで理解したことのポイントについて書いてみようと思います。*1

スカラー値のリテラル

ClojureREPL(Read Eval Print Loop)という対話的実行環境を持つので、いきなり大きなプログラムを作成しなくても、一行一行結果を確認しながら勉強することができます。したがって、数値や文字列などのリテラルREPLのプロンプトに入力すると、値がそのまま結果として返ってくることが確認できます。

user=> 0xff
255
user=> 0377
255
user=> 1.1
1.1
user=> 1.5e10
1.5E10

以上のように数値のリテラルの書き方はだいたいJavaと似ています。ちょっと特徴的なのは整数の基数を示すrという記法が使えることで、36進数まで表現できます。

user=> 2r11111111
255

文字列リテラルJavaとよく似ているのですが、中に改行を埋め込むことができます。

user=> "Hello World"
"Hello World"
user=> "こんにちは 世界"
"こんにちは 世界"
user=> "Hello
World"
"Hello\nWorld"

文字リテラルの書き方はJavaと違っていてシングルクオーテーションで囲む代わりにバックスラッシュ(\記号)の後に文字を書きます。(Javaのように改行文字などを表現するにはユニコードで指定する必要があります。)

user=> \a
\a
user=> \n
\n
user=> \u0042
\B
user=> \u30DE
\マ

さらに、ClojureにはJavaのnullに相当するnilやtrue、falseといったリテラルが使えます。

関数の呼び出し

単に入力した値をそのまま表示するだけではつまらないので、簡単な計算をしてみたいと思います。

user=> 1 + 1
1
#<core$_PLUS_ clojure.core$_PLUS_@15353154>
1

意図に反して2が表示されずに、変な記号が表示されました。(これは各オペランドをそれぞれ独立して評価した結果となっています。)ここが、Lispに慣れていないと最初に敷居が高いところだと感じたのですが、足し算など四則演算も含めてClojureでは関数呼び出しの形式で記述する必要があります。つまり、「+」という名前の関数を呼び出すと考えるわけですね。算術演算を含めて常に前置記法で書く必要があるのです。

user=> (+ 1 1)
2
user=> (* 3 5)
15

一般に丸かっこはリストと呼ばれていますが、(関数名 param1 param2 ... paramN)の書き方で関数を呼び出すことができます。

もちろん、この呼び出しは入れ子にすることもできます。例えば、(1+2)*3を表現するには、以下のように記述します。

user=> (* (+ 1 2) 3)
9

慣れないと、相当奇妙な感じですが、この書き方にはちょっと便利なところもあって自然に可変長パラメータが実現できるのですね。だから、3つ以上の足し算も同様に記述できます。

user=> (+ 1 2 3 4 5)
15

しかも、四則演算だけでなく他の関数呼び出しもいつも同じ書き方になるという美しさもあります。他の言語のように演算子の結合規則や優先順位といったものを覚える必要がありません。たとえば、Javaで+演算子を使って行うような文字列結合をするにはstr関数を使えますが、この場合も同じ記法で呼び出すことができます。

user=> (str "Hello" " " "World")
"Hello World"

defを使った変数の定義

四則演算文字列結合のやり方がわかったら、プログラマーが普通次に考えることは変数の定義と値の代入でしょう。それで、厳密に言うと値の入れ物としての変数に直接相当するものはないようで、本には最初から名前空間とかvarの束縛といった難しい説明がされているのですが、ここでは詳細は考えないことにして、とにかくdef特殊形式*2を使って変数の定義と値の代入に相当することを行うことができます。

user=> (def a 100)
#'user/a
user=> (def b 200)
#'user/b
user=> (+ a b)
300

正確な数値演算

Clojureのちょっと面白い特徴として、JavaではBigIntegerやBigDecimalなどを使って行う計算を容易に実行できるということがあります。さらに、精度は必要に応じて自動的に変換されるため、メモリが許す限りどんなに大きな数でも計算できます。

user=> (class (* 1000 1000 1000))
java.lang.Integer
user=> (class (* 1000 1000 1000 1000))
java.lang.Long
user=> (class (* 1000 1000 1000 1000 1000))
java.lang.Long
user=> (class (* 1000 1000 1000 1000 1000 1000))
java.lang.Long
user=> (class (* 1000 1000 1000 1000 1000 1000 1000))
java.math.BigInteger

ここではclass関数を使って実際の数値の型を調べていますが、必要な桁数に応じて実際の数値のオブジェクト型が適切に変換されていることがわかります。(数値や文字列の型がClojure独自でなくてJavaのクラスになっているところも興味深いところです。)実数*3の計算については、何も指定しないとJavaのdoubleとして計算されます。だから、桁落ちが発生します。以下の例では、計算の順序によって計算結果が異なり、通常の足し算の結合法則が成り立たないことがわかります。なお、以下のようにセミコロン以降がコメント文として無視されます。

user=> (def a 1.0e50)
#'user/a
user=> (def b -1.0e50)
#'user/b
user=> (def c 17.0)
#'user/c
user=> (+ (+ a b) c) ; (a + b) + c 
17.0
user=> (+ a (+ b c)) ; a + (b + c)
0.0

この場合、数値の最後にMという記号を付けることで値を正確に計算できるようになります。

user=> (def a 1.0e50M)
#'user/a
user=> (def b -1.0e50M)
#'user/b
user=> (def c 17.0M)
#'user/c
user=> (+ (+ a b) c)
17.0M
user=> (+ a (+ b c))
17.0M

正確な演算は実数だけではありません。割り算について、割り切れない場合は実数として処理されずに、分数として扱われるため、有理数を正確に表現することができます。以下のように通分などの計算もできます。

user=> (def a (/ 1 2))
#'user/a
user=> (def b (/ 1 3))
#'user/b
user=> (+ a b)
5/6

分数の計算は変数を使わなくてもできます。

user=> (+ 1/2 1/3)
5/6

なお、これらの結果得られた分数を実数に変換するためには実数と計算するかdoubleにキャストします。

user=> (+ 5/6 0.0)
0.8333333333333333
user=> (double 5/6)
0.8333333333333333

コレクションのリテラル

今まで使用してきた関数呼び出しは括弧に複数の要素が記述された形になっており、リストと呼ばれるもっとも重要なコレクションです。Clojureにはリストの他にもいくつかの種類のコレクションを利用することができます。*4

ベクタ

普通の丸括弧の代わりに角括弧で囲むことでベクタを記述することができます。ベクタJava配列やListに近い概念で、要素が順序づけされたデータ構造を表します。

user=> [1 2 3]
[1 2 3]
user=> [1 2 "Hello"]
[1 2 "Hello"]

ベクタには様々な型の要素を入れられます。なお、Clojureではカンマ「,」は空白やスペースと同じように無視される文字なので、お好みで以下のように記述することも可能です。こちらの書き方のほうがGroovyJava Scriptのようですね。

user=> [1, 2, "Hello"]
[1 2 "Hello"]

ベクタの要素をインデックスで一つ一つアクセスするのは、あまりClojure的ではないと思うのですが、以下のようにしてget関数かnth関数を使って行えます。*5

user=> (def a [1, 2, "Hello"])
#'user/a
user=> (get a 2)
"Hello"
user=> (nth a 2)
"Hello"

さらに、実はベクタそのものを関数のように記述することもできます。

user=> (a 2)
"Hello"
マップ*6

マップは中括弧を利用して{キー 値 ...}の形で記述できます。ここでも適当にカンマを利用して要素を区切ることができます。

{1 "one", 2 "two", 3 "three"}

なお、マップのキーとしてはキーワードと呼ばれる特殊な値を利用することが一般的です。これはややこしいのですが、「:」が先頭に来る書き方も含めてRubyのシンボルに近いものです。(Clojureのシンボルはdefなどで定義した変数名や関数名のこと)

{:key1 "one", :key2 "two", :key3 "three"}

マップの要素もget関数でアクセスできます。

user=> (def a {:key1 "one", :key2 "two", :key3 "three"})
#'user/a
user=> (get a :key1)
"one"
user=> (get a :key2)
"two"

ちょっと面白いのは、シンボルそのものを関数のようにして呼び出すことが可能ということです。以下の書き方でもシンボルをキーとする値を取得できます。

user=> (:key3 a)
"three"
セット(集合)

以下の記述方法でセットを記述できます。中括弧の前にシャープ記号*7を書きます。

#{1 2 "Hello"}

関数の定義方法

最後に独自の関数の定義方法について説明します。以上説明してきた基本が理解できていれば、関数の定義方法を理解することは容易です。実際、関数の定義は以下の形式でdefnマクロを使って行うことができます。

(defn 関数名
"ドキュメントコメント" ; 省略可能
[p1 p2 ...] ;パラメーター群を表すベクタ
関数本体)

これは同図像性と呼ばれるそうですが、こうした関数定義そのものがデータ構造と同じ構文で表現できる、つまりコードがデータで組み立てられているというのがClojureの特徴の一つのようです。実際、名前を受けとってメッセージ文字列を作成して返す関数は以下のように定義できます。

user=> (defn greeting
"名前を受け取ってメッセージを返す。"
[name]
(str "Hello, " name))
#'user/greeting

そうすると、呼び出しは標準の関数と同様に以下のようにして行えます。

user=> (greeting "Ryo")
"Hello, Ryo"

なお、省略可能なドキュメントコメントを定義している場合はdoc関数を使ってドキュメントを表示できます。

user=> (doc greeting)
-------------------------
user/greeting
([name])
  名前を受け取ってメッセージを返す。
nil

もちろん、Java Scriptと同様にClojureでは関数は第一級市民の値なので、関数パラメータに簡単に関数自身を引き渡して呼び出すことができます。

user=> (defn calc-print 
  [a b f]
  (str "Answer = " (f a b)))

#'user/calc-print
user=> user=> (calc-print 1 2 +)
"Answer = 3"
user=> (calc-print 1 2 -)
"Answer = -1"

まとめ

今回は、超初心者の立場で、厳密性は犠牲にしつつもClojureの基本的な使い方について説明しました。構文が一般的な*8プログラミング言語と大きく異なるため、最初は見た目がすごく難しそうなのですが、実は言語の文法自体は驚くほどシンプルなのであり、基本を理解すれば意外にわかりやすいと感じました。

とは言え、本の先のほうを見てみると以下のような話題が書かれていて、まだまだ自分で有用なプログラムを書けるようになるにはしばらく時間がかかると思いますが、少しずつ勉強していきたいと考えています。

これらについては、また続編で説明できればと思います。

*1:そういう事情なので、内容の正確さについては全く保障の限りではありませんが、間違っている点があればご指摘いただければ幸いです。

*2:見かけ上defも関数呼び出しの形に見えるので、最初はそんな理解でもよいのかと思いますが、厳密には処理されるタイミングがコンパイル時となるため、関数ではありません。

*3:ここで実数という用語は数学の用語というよりプログラミングの分野での方言ですね。

*4:さまざまなコレクションがありますが、これらはシーケンスとして抽象的に考えることで、多くの場合同じように使用することができます。

*5:getとnthは存在しないインデックスの時に異なります。getはnilが返るのに対して、nthの場合は例外となります。

*6Clojureにはmap関数というのがあってややこしいのですが、こちらは普通の連想配列としてのマップです。

*7:このシャープ記号はリーダーマクロを起動する文字の一つのようです。

*8:ALGOL系言語の

2011-05-05

ふと思い立ってClojureの勉強を始めることにしました。

最近は並行プログラミングの必要性から、関数型言語の人気が以前にもまして高まってきているという話はいろいろなところで耳にするようになったのですが、普段はJavaしか書かない私のような普通の業務プログラマーにとっては、正直なところHaskellやScalaといった言語はなかなかに敷居が高いところがあると思います。これらは型安全性を重視している静的な型付けの言語であり、プログラムを正しく実行するには型推論などの仕組みを理解する必要があります。また、純粋な関数型としての特性を理解するにはモナドなる難解な概念を理解しないといけないようですし、特に、Scalaの場合はオブジェクト指向言語の要素がミックスされているため、クラス、トレイト、抽象型などの概念や、関数型と命令型の正しい使い分けなどを理解しなくてはなりません。*1とにかく、これらの言語では関数型を学ぶ以前にいろいろ新しく学ぶことがたくさんあるため、自力で最初に学ぶ関数型言語としてはなかなか難しいところがあるように思います。実際、フレームワーク、ツール、ドメインの業務知識などいろいろなことを勉強しないといけない業務プログラマーの立場としては、プログラミング言語の勉強だけに長い時間を割くということが難しいという実情もあります。

そんな中で、

のようなことを何となく考えていたのですが、残念ながらGroovyだとどうしても関数型のメリットをはっきりと活かすようなプログラミングが難しいのも確かです。何か関数型の考え方を勉強するのによい言語はないかなと考えていた折、ちょうどよいタイミングでスターフレンドのid:fumokmmさんの

というつぶやきが目にとまりました。GroovyだけでなくClojureまで知っているとはすごいなと思ったのですが、ご謙遜かもしれませんが、

というお言葉を信じて、私も翻訳協力者に混ぜていただく運びとなりました。その翻訳サイトは以下にあります。

Clojure Japanese Documentation

ドキュメントを翻訳しながら新しい言語も学ぶというのは、最終的な成果物もみんなの役に立って、自分の勉強にもなるのでなかなかに良いアイデアですね。ただ、さすがにまったく基礎知識なしで翻訳を進めるのは無謀なところがあるので、本日書店で早速以下を購入して勉強することにしました。*2

プログラミングClojure

プログラミングClojure

ちょっとかじったところでは、

  • 文法はほとんどLispLispの方言と言える)
  • クラスなどは定義できないから関数型のパラダイムに集中できる
  • Javaメソッドを簡単に呼び出せる(静的な型も使えるということ?)
  • 純粋関数型以外に状態共有のメカニズムがある

といったところが特徴のようです。実際、hello worldは以下のように書けます。

(println "Hello World") ; Hello Worldと表示される。結果はnil。

クラスやメソッドの定義といったものを考える必要がなく、単純な関数の評価なのでわかりやすいですね。一方で、Javaメソッド呼び出しは、以下のように書けます。

(println (.toUpperCase "Hello World")) ; HELLO WORLDと表示される。結果はnil。

括弧の先頭で与えるシンボルが関数名だと関数呼び出しとなるのですが、この関数名がピリオドで始まっていると次の要素に対するメソッド呼び出しとなるようです。Javaの呼び出しなどで関数型としての厳密性はなくなっているのですが、既存のJavaのライブラリーと相互運用できるというところは実用面では非常にメリットが大きいと思います。Lispの独特の構文に慣れる必要はありますが、直感的にはなんとくなく私が求めていた言語に出会えた気がします。ちなみに、日本ではまだまだ知名度が低いようですが、既に洋書は多数出版されているみたいですし、あのDDDのEvans氏もお好きとのこと。これからあせらず、じっくり勉強していこうかと思います。なお、以上に紹介したClojureの本はJava EE勉強会で読書会が行われているみたいですので、東京近郊の方は参加してみてはいかがでしょうか。

FrontPage - Java EE勉強会

あと、以下も読書記録として非常に参考になると思います。

clojureプログラミング入門-1 clojureをはじめてみる - すにぺっと

*1:Scalaは名前のとおりスケーラブルな言語であり、入門レベルではとっつきやすいプログラムを書けるのですが、言語を究めてきちんと使いこなそうと思えば多くの概念を学ぶ必要があるようです。

*2プログラマーの定年を過ぎてまったく新しい言語を学ぶというのは何とも無謀な試みのようなのですが、気持ちだけは若いつもりです。

2011-04-26

普通の業務系Java PGでなくても一度はハマる?JavaScriptのthisの奇妙な振る舞い

先日書いた普通の業務系PGには意外と知られていないJavaとJavaScriptの相違点10選 - 達人プログラマーを目指してでは、これからJavaScriptを本格的に勉強する層のプログラマーの人を対象に、JavaとJavaScriptの違いを理解する上で重要な10個のポイントについて説明しました。いただいたコメントの中には、JavaScriptとJavaは当然まったく別の言語で、比較すること自体問題であるという趣旨のご指摘もいただきました。確かにその通りなのですが、実際、業務で本格的なプログラムの開発はJavaでしかしたことがないという開発者は結構自分のまわりにはたくさんいますし、時代の流れから言って、これから初めて本格的にJavaScriptを書くという人も今後たくさん出てくるのではないかと思います。そういう人にとっては、やはり、違いを意識するところから入っていくというのは学習のアプローチの一つとしてはあるのではないかと思います。

ところで、前回の記事は初心者向けということもあり、書き漏らしたのですが、主にJavaで開発してきた自分の経験から言って、JavaScriptを本格的に使う上で最も注意すべき相違点として、JavaSciptにおけるthisの振る舞いがJavaとは大きく異なっているという点があります。私もこのポイントを理解するまで、実際に何回か同じような問題にハマりました。今回は、この注意点に関して補足します。

JavaScriptのthisは省略できない

Java言語でメソッドを定義する際に、thisはそのメソッドが定義されているクラス自身のインスタンスを指します。そして、通常はこのthisは省略しても意味が変わりません。(同一名のパラメーターなどで名前がかぶる場合を除く)

public class Person {
    private String firstName;	
    private String lastName;
	
    public Person(String firstName, String lastName) {
        // 名前が重なるときのみthisを付ける
        this.firstName = firstName;
        this.lastName = lastName;
    }
	
    public String getName() {
     // return this.lastName + " " + this.firstName; 同じ意味
        return lastName + " " + firstName;
    }
}

同様のPersonオブジェクトをJavaScriptでも定義できます。JavaScriptでクラスのようなオブジェクトを定義する方法はいろいろありますが、たとえば、以下のように記述できます。*1

// コンストラクタ関数
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

// プロトタイプに対してメソッドを追加
Person.prototype.getName = function() {
    return this.lastName + " " + this.firstName;
};

var madoka = new Person("まどか", "鹿目");

console.log(madoka.getName()); // "鹿目 まどか"

JavaScriptの場合、以上のgetName()メソッドの定義で、正しくfirstNameとlastNameのプロパティの値を参照するためには、絶対にthisを省略できません。実際、省略すると、未定義変数エラーとなって実行できません。

// コンストラクタ関数
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

// プロトタイプに対してメソッドを追加
Person.prototype.getName = function() {
    return lastName + " " + firstName; // この行で未定義変数エラー
};

var madoka = new Person("まどか", "鹿目");

console.log(madoka.getName());

getName()はPersonオブジェクトのコンテキストで呼び出せるメソッドなのですが、定義する場所はthisなしで変数を参照すると、グローバル変数として解釈されてしまいます。この場合、当然firstNameなどのグローバル変数が宣言されていませんから、エラーとなるのです。以上の例ではあえて、プロトタイプを使い、コンストラクタ関数の外部でメソッドを追加したのですが、コンストラクタ関数内でメソッドを定義することもできます。この場合も、うっかりgetName()メソッドの定義中でthisを忘れると、関数がpersonオブジェクトのプロパティを参照せず、代わりにパラメーターの変数を参照するようになってしまいます。関数はクロージャーになるため、パラメーターなどのローカル変数の値を参照できることに注意してください。

// コンストラクタ関数
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.getName = function() {
        return lastName + " " + firstName; //thisの付け忘れ
    };
}

var madoka = new Person("まどか", "鹿目");

console.log(person.getName()); // "鹿目 まどか"
madoka.lastName = "魔法少女";  // プロパティを変更

console.log(madoka.getName()); // "鹿目 まどか" 変更が効かない!

この場合、正しくthisを付けることで意図した動作になります。

// コンストラクタ関数
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.getName = function() {
        return this.lastName + " " + this.firstName;
    };
}

var madoka = new Person("まどか", "鹿目");

console.log(madoka.getName()); // "鹿目 まどか"
madoka.lastName = "魔法少女";  // フィールドを変更

console.log(madoka.getName()); // "魔法少女 まどか"

このように、JavaScriptのメソッド定義中で、オブジェクト自身のプロパティやメソッドを参照する場合には常にthisが省略できないという点に注意する必要があります。Javaの癖でthisを省略してしまうと、以上のようなエラーとなり、場合によっては結構見つけにくいバグとなることがあるため、注意が必要です。

JavaScriptのthisは関数の呼び出し時のコンテキストで指す対象が動的に変化する

そして、次に注意が必要な点はこのthisの指す対象のオブジェクトについてです。Javaの場合は、thisはそのメソッドが定義されているクラスのオブジェクト以外を指し示すことは絶対にありません。しかし、JavaScriptの場合そのメソッドが呼び出される実行時の動的なコンテキストによって、thisの指すオブジェクトが変化します。実際、関数定義中のthisは、

というルールがあります。このルール自体は自然に理解できるものです。*2

function QB() {
    this.requestContract = function(person) {
        return person.getName() + " 僕と契約して魔法少女になってよ。";
    };
}

var madoka = new Person("まどか", "鹿目");
var qb = new QB();

console.log(qb.requestContract(madoka)); // "鹿目 まどか 僕と契約して魔法少女になってよ。"

ここで、少し設計を変更し、requestContractメソッドがPersonのオブジェクトではなく、関数そのものが渡されるように変更してみます。JavaScriptのメソッドは単に関数が特定のオブジェクトに保持されているだけであり、このメソッドを普通の関数オブジェクトとまったく同様に別のオブジェクトに代入したり、別の関数パラメーターとして渡したりすることができます。

実際、ちょっと不自然な例ですが、以下のコードを考えてみましょう。

function QB() {
    this.requestContract = function(nameFunc) {
       return nameFunc() + " 僕と契約して魔法少女になってよ。";
    };
}


var madoka = new Person("まどか", "鹿目");
var qb = new QB();

// ちょっと不自然だけど、メソッドをパラメーターとして直接渡してみる
console.log(qb.requestContract(madoka.getName)); // undefined undefined 僕と契約して魔法少女になってよ。

この場合、madokaオブジェクトのgetName()メソッドそのものをパラメーターとしてrequestContract()に渡して実行しようとしているのですが、requestContract()メソッド中で、パラメーターとして渡されたnameFuncは対象オブジェクトなしで呼び出されているため、getName()メソッド中のthisはmadokaオブジェクトではなく、グローバルオブジェクトを指すことになってしまうのです。したがって、firstNameもlastNameも未定義のため、以上の結果となります。*3この例だと、ちょっと人工的で、やらないと思われるかもしれませんが、コールバック関数をとるライブラリーの機能を呼び出す場合に、うっかりメソッドを渡してしまい、動作がおかしくなるという失敗はこの原理をしらないとよく起こしますね。

この場合、メソッドパラメーターとして渡しつつ、かつ、thisを正しいオブジェクトにバインドした状態で呼び出すには関数オブジェクトのcall(コンテキスト、パラメーター)を使うことができます。実際、以下のように記述することができます。

function QB() {
	
    this.requestContract = function(person, nameFunc) {
        return nameFunc.call(person) + " 僕と契約して魔法少女になってよ。"; // callで渡したpersonをthisが指す
    };
}


var madoka = new Person("まどか", "鹿目");
var qb = new QB();

// ちょっと不自然だけど、メソッドをパラメーターとして直接渡してみる
console.log(qb.requestContract(madoka, madoka.getName)); // "鹿目 まどか 僕と契約して魔法少女になってよ。"

Groovyでも.&演算子を使って、オブジェクトメソッドをクロージャーとして別メソッドパラメーターとして渡すことができますが、メソッド呼び出し時のコンテキストによって、thisの指す対象が動的に変化するということはありません。

class Person {
    String firstName
    String lastName
    
    def getName() {
        lastName + ' ' + firstName // this.lastName + ' ' + this.firstNameと同じ
    }
}

class QB {
    def requestContract(closure) {
        closure.call() + ' 僕と契約して魔法少女になってよ。'
    }
}

def sayaka = new Person(lastName:'美樹', firstName:'さやか')
def qb = new QB();

println sayaka.name

println qb.requestContract(sayaka.&getName) //'美樹 さやか 僕と契約して魔法少女になってよ。'

まとめ

JavaScriptのメソッド定義で、thisを使う場合

  • Javaと違ってthisを省略できない(省略するとオブジェクトのプロパティやメソッドを参照できない)
  • thisの指す内容は関数の定義時には決まらず、実行時のコンテキストで動的に変化する。

という点をしっかりと理解する必要があります。

*1:まどか☆マギカは今晩ようやく1話を観たところでドメインモデルが全くわかっていないため、そこはつっこまないでください。まどマギはjava-jaのデザインパターンの勉強会で例として頻繁に取り上げられていました。http://togetter.com/li/128336

*2:やはり、いくら言語の知識があっても、ドメイン知識がないうちは、まともな設計ができませんorz。

*3オブジェクトのプロパティはvar宣言なしでもエラーとならないという点も実は隠れたポイント

2011-03-29

Java言語のClassクラスが持つちょっと不思議な性質について

前回のエントリーJava5の型システムを理解するにはリフレクションAPIを使ってみるのが最短の近道になる - 達人プログラマーを目指してで、Javaの型システムが総称型の導入によりJava5から大きく拡張されたということを説明しました。ポイントは

  • 総称型によりソースコード上は型システムが拡張されている。
  • JDK1.4までは型とClassクラスが一対一に対応していた*1がJava5では型消去により対応しなくなった。(対応するのはClassでなくてType)
  • Classクラスは具象化可能型と対応する。

ということでした。以前のバージョンの時の感覚でClassクラスと型が一対一に対応していると考えていると勘違いしてしまうところもあります。今回はJava5以降のClassクラスについて、型消去により生じるちょっと不思議な性質について説明させていただきたいと思います。

Java5以降ではClassクラスと型は一対多の関係にある

見かけ上うまいこと工夫されているため、ソースコード上は理解しにくいのですが、このブログで既に何度か説明してきたように、型パラメーターの情報はバイトコード上からは消去されてしまいます。したがって、List<String>もList<Integer>もすべて一つのListという消去型(erasure)に対応します。このことは実際に以下のコードの実行結果にも表れます。

// パラメーターの型によらずにすべて型の消去されたArrayListと同じ。
ArrayList<Integer> intList = new ArrayList<Integer>();
ArrayList<String> strList = new ArrayList<String>();

assert intList.getClass() == strList.getClass();
assert intList.getClass() == ArrayList.class;
assert strList.getClass() == ArrayList.class;

このようにコンパイル時の静的な型は違っていても、実行時の型はすべて消去型に等しくなります。

Class<T>の型変数Tにバインドしてよいのは具象化可能型だけ

ArrayList<E>など型変数で上限が指定されていなければ、一般的に任意の型を型変数に与えることができます。Classクラスの場合、型変数に上限は設定されていないため、文法上はTに任意の型を当てはめることができます。

しかし、Classは実行時の型を表現する特別な役割があるため、型変数には型消去の影響を受けない具象化可能型(か「? extends具象化可能型」の形のワイルドカード型)に限定する必要があります。つまり、Class<List<String>>などの宣言は文法上は可能なのですが、前節で見たようにList<String>のようなパラメーター化された型に一対一対応するクラスは存在しないため、混乱するだけです。実際、仮にClass<List<String>>のような型の変数を利用すると、以下のような矛盾が起こります。

ArrayList<Integer> intList = new ArrayList<Integer>();
ArrayList<String> strList = new ArrayList<String>();

// このキャストは型安全でないため警告が出るが、警告を意図的に無視
@SuppressWarnings("unchecked")
Class<ArrayList<Integer>> intListClass = (Class<ArrayList<Integer>>)intList.getClass();

strList.add("test");
intList = intListClass.cast(strList);
Integer value = intList.get(0); // ClassCastException

もともと、Classクラスは実行時の型と対応する前提で設計されているため、TにArrayList<Integer>など具象化可能でない型を与えてしまうと、cast()メソッドなどで型安全性に問題が生じてしまいます。

Java言語仕様上は、できるだけClass<T>の型変数Tに不適切な型がバインドされない工夫がされている

前節の例では警告を無視して無理やりClass<ArrayList<Integer>>型の変数に代入する値を作ったのですが、普通の使い方ではこういった不適切な型のClass変数が生成されないように言語仕様上工夫がされています。まず、クラスはコンストラクタが公開されていないため、普通にnewでインスタンスを生成することはできません。したがって、

Class<ArrayList<Integer>> intListClass = new Class<ArrayList<Integer>>(); //エラー

のような書き方はできません。また、クラスを生成する手段としてクラスリテラルがありますが、この場合も具象化可能型以外*2に対してクラスリテラルを指定できないようになっています。

Class<List> listClass = List.class; // OK
Class<List<String>> intListClass = List<String>.class; // エラー

一方、Classクラスのインスタンスを取得する手段としてもう一つ重要なものとして、ObjectクラスのgetClass()メソッドがあります。このメソッドの戻り値の型はObjectクラスのソースコード上はClass<?>と宣言されていますが、JavaDocや言語仕様の4.3.2節で書かれているとおり、コンパイラーの特別ルールでClass<? extends |T|>となることが規定されています。ここで|T|というのは宣言されている静的な型Tの消去型を表します。

実際に実験してみると、以下のコードがコンパイルできることがわかります。

public class Temp<T, S extends Serializable & Comparable<? super T>> {

	public void test(T t, S s) {
		String strValue = "test";
		List<String> strList = Arrays.asList("test");
		
		Class<? extends String> strClass = strValue.getClass();
		Class<? extends List> listClass = strList.getClass();// Class<? extends List<String>>ではない!
		Class<? extends Object> tClass = t.getClass(); // Class<? extends T>ではない!
		Class<? extends Serializable> sClass = s.getClass();// Class<? extends S>ではない!
	}
}

ここで、TやSなどの型変数については、上限が設定されていない場合はObjectが、上限が設定されている場合はもっとも左側で指定した上限型が消去型になることに注意してください。コンパイラーの特別ルールによって、宣言しているオブジェクト変数の型に対応したパラメーターがバインドされたClassインスタンスを得ることができます。しかし、この仕様によって型パラメーターが具象化可能型でない型になることはありません。

もしこの言語仕様がClass<? extends |T|>ではなくClass<? extends T>を返すとしたら、以下のサンプルが警告なしで実行できることになってしまうため、型安全性の考慮からTの消去型|T|を上限とするワイルドカード型をパラメーターとするClassが得られるという仕様になっているのだと思います。

public class FakeClassFactory<T> {
	T data;

	public FakeClassFactory(T data) {
		this.data = data;
	}

	@SuppressWarnings("unchecked") // really unsafe
	public Class<? extends T> getClassUnsafe() {
                // もし言語仕様がClass<? extends T>ということになっていたらこのキャストは不要になる。
		return (Class<? extends T>)data.getClass();
	}

	public static void main(String[] args) throws Exception {
		List<String> strList = new ArrayList<String>();
		List<Integer> intList = new ArrayList<Integer>();

		FakeClassFactory<List<String>> fakeStrListClassFactory = new FakeClassFactory<List<String>>(strList);
		Class<? extends List<String>> fakeStrListClass = fakeStrListClassFactory.getClassUnsafe();

		FakeClassFactory<List<Integer>> fakeIntListClassFactory = new FakeClassFactory<List<Integer>>(intList);
		Class<? extends List<Integer>> fakeIntListClass = fakeIntListClassFactory.getClassUnsafe();

		// 本来不正なキャストだがこの行で警告にならない。 
		strList = fakeStrListClass.cast(intList);
		strList.add("test");

		int value = intList.get(0); // ClassCastException 
	}
}

Class.cast()やClass.isInstance()メソッドの存在意義について

存在意義がなかなか理解しにくいものとしてClassクラスのcast()メソッドやisInstance()メソッドがあります。普通のキャスト演算子やinstanceof演算子があるのにどうしてわざわざこのようなメソッドが存在しているのでしょう?これは以下の例を考えるとよくわかります。この例では任意の型のコレクションから、特定の型のインスタンスの要素を抜き出してコピーしようとするものです。

public static void main(String[] args) throws Exception {
	List<String> strArray = Arrays.asList("test1", "test2");
	List<Integer> intArray = new ArrayList<Integer>();
	notSafe(strArray, intArray);
	
	Integer value = intArray.get(0); // ClassCastException
}

public static <T> void notSafe(Collection<?> src, Collection<? super T> dest) {
	for (Object o : src) {
		if (o instanceof T) { // コンパイルエラー
			T t = (T) o; // このキャストは実は見せかけでこの行ではClassCastExceptionにならない。
			dest.add(t); // コンパイル時に型がチェックされずに不正な型が格納されてしまう。
		}
	}
}

この例は2重の問題があります。まず、型Tに関してはinstanceof演算子の記述でコンパイルエラーとなります。instanceofは実行時の型を調べるための命令ですが、Tはこの例では見かけ上Integer型にバインドされますが、型消去により実際はObject型しか残らないため、命令が実行できないのです。仮にこのinstanceofのチェックをコメントアウトしたとしても、今度は型Tに対するキャストの行で警告が残ります。コンパイラーはこのキャストの行で本来意図している型へのキャストのコードを生成できないため、正しく型を判断することができません。結果として上記の例ではまったく別の行でClassCastExceptionが発生してしまいます。

この場合の問題は型Tに関する情報が実行時に残らないことにあります。この場合解決策としてはClass<T>を実行時に対する型情報としてパラメーターに渡すのが常套手段です。(Class<T>を型トークンとして使う)

public static <T> void safe(Collection<?> src, Collection<? super T> dest, Class<T> c) {
	for (Object o : src) {
		if (c.isInstance(o)) {
			T t = c.cast(o); // もしキャストが不正ならこの行で例外がでる。
			dest.add(t);
		}
	}
}

これだと、実行時に正しくインスタンスの型判定をしたり、キャストしたりすることができます。この場合、仮にif文をコメントアウトしても、今度はキャストの行でClassCastExceptionが発生しますので、広い意味で型安全と言えるわけです。

ただし、Classクラスのこれらのメソッドにももちろん限界があり、List<String>など型パラメーターを持った型へのキャストは先に述べた理由で安全ではありません。また、人によってはJava5以降では普通のキャスト演算子やinstanceofを使ってはいけないと勘違いしている人もいましたが、String.class.cast()のように静的に型が決まっているのにわざわざこれらのメソッドを使うような書き方は不可能ではないにせよ、あまり意味がないと思います。

*1:厳密に言うとクラスローダーの話がありますが、ここでは考えないことにします。

*2:厳密には具象化可能型ですが、List<?>のような型境界のないワイルドカード型のクラスリテラルも認められていません。