主に言語とシステム開発に関して RSSフィード



2012-05-02

Javaのジェネリクスで,T.class や new T() ができず悩んだ話 (型パラメータのインスタンス化に関し、フレームワーク設計からケーススタディ)

| Javaのジェネリクスで,T.class や new T() ができず悩んだ話 (型パラメータのインスタンス化に関し、フレームワーク設計からケーススタディ)のブックマークコメント


Javaのジェネリクスで,型パラメータ T のインスタンスが欲しくなったことはあるだろうか?



昨今のオブジェクト指向プログラミングにおいて,ジェネリクスは必須の基本文法だ。


扱う対象のクラスが抽象化されて汎用的になりつつ,なおかつ型安全性が確保される。

そのおかげで,処理の重複や分岐をコーディングする必要が無くなり,コード量が驚異的に削減される。



そういう基本的な原則を踏まえると,

  • 「型パラメータのインスタンスが欲しい」

というシチュエーションは,Javaのジェネリクスの本来の導入目的に真っ向から逆らう。


なぜなら,ジェネリクスは型を抽象化して透過的に扱えるようにするための機構なのだから,

せっかく抽象化した物をわざわざ具体化してどうするというお怒りを生む事になるのだ。



頑張って詳細なクラス情報を「T」でパラメータ化して具体性を隠ぺいしたにも関らず,

その T に対して .class で具体的な Class を取得しようと試みたり,new で現物のオブジェクトを生成してしまう。というのは,おかしい。


また,総称型 T は,newしちゃったが最後だ。

そこまで単なるパラメータで済んでいたものが,new 以降は具体的なオブジェクトとしてインスタンス化されてしまう。

そして,それ以降は「個別の具体的なオブジェクトの性質と付き合う」必要が生じてしまう。要は,抽象性が損なわれる。

だから new T() があってはならない。


こう考えるのは極めて自然であり,Java使いとして正統で,まっとうな反応でもある。



だから一般的に考えて,「T.class とか new T() が欲しい」などという発想は,

例えるならJavaで手続き型プログラミングをするかのような,あってはならない禁じ手のアンチパターンということになる。



したがって,そういう発言をすると「いったい何を考えてるんだ」という旨のツッコミを頂く事になる。




この基本的かつ貴重なツッコミを下さった @nagise 氏は,Hatena上では id:Nagise 氏であり,Web上でジェネリクスについて非常に多様な情報発信をして下さっている。


私自身,プログラミングで調べ物をする際に,普段からnagise氏の執筆した情報にたどりつく事が日常的によくある。

そしてとても勉強になる。


このお方は,なんとtwitterの自己紹介欄で・・・

と名乗っている。

(プログラミングの一分野について「○○を愛する」と公言できるというのは,敬服すべき事ではなかろうか?)



さらにすごいのが,保有しておられるブログ(複数)に「ジェネリクス」という独立したカテゴリを設け,そのトピックを詳細に論じて下さっている点。

プログラマーの脳みそ / Generics

http://d.hatena.ne.jp/Nagise/archive?word=*[Generics]


凪瀬 Blog / ジェネリクス

http://blogs.wankuma.com/nagise/category/1924.aspx

おかげで,ネット越しに情報収集する技術者にとって大助かりだ。

上記のWebページの情報は,Effective Java(第2版)の第5章の補遺として利用させて頂くのがよいと思う。



ここまでの情報を前提とした上で,タイトルの質問を考えてみよう。


ここから先が本論となる。

Javaのジェネリクスで,T.class とか new T() のようなコードが必要になる妥当なケースは存在するのか。

また,Java言語を使いつつもそういったニーズに応えることは,果たして技術的に可能なのか。



氏のツイートを再度引用:

「ぶっちゃけるとT.class やnew Tしたいケースは設計が悪いのだと思う。

それを使う妥当なシチュエーションが候補があれば是非教えて欲しい。検討したい

QT "@lang_and_engine: ジェネリクスでT.classやnew T()できないとさんざん嘆き"」


システム開発で新しい事をやろうとすると,どうしても試行錯誤が伴う(特に開発のパラダイムを変えようとあがく場合はなおのことそうなる)ので,あくまでも現時点での状況に基づく回答となるが,ここでその答えを述べてみる事にする。


思考の過程を書き記しておけば,のちのちの別のクラス設計にフィードバックする点で後学のためにも有益だろうし・・・。



どのような思考回路で,T.class やnew T のニーズが生まれるのか

Javaで一般的なプログラムを書いている限り,

パラメータ化された型 T に対して T.class や new T() しようなどと考えるべきではない。

この点は,もう既に述べた。


では,作ろうとしているのが一般的なアプリケーションではなく,汎用的なクラスライブラリやフレームワークである場合は,どうなるか。



つまり,一般的なアプリケーションの開発作業を裏方で支える,ユーザからは意識されない存在の部分をコーディングしている場合をいう。


現実の例として,下記のようなケースを考慮する。

  • SQLiteにシンプルなORマッピングを施し,なおかつユーザが書くべきコード量はできるだけ減らしたい。

この課題に対処するために,次のような思考回路を辿ってみる:



  • ORMのユーザは,友人データを格納するためのfriendsテーブルをRDB上にCREATEする。
  • 次にORMのユーザは,RDB上のテーブルに対応するようなエンティティクラスとしてFriendクラスを作成する。また,FriendをCRUDするためのFriendDAOクラスを作成する。
  • その時にORMは,ユーザがFriendクラス内およびFriendDAOクラス内に記述するコード量を極限まで削減したい。できればActiveRecord並みに減らせたら理想だが,それに近づける事を目指してみる。
  • 例えばユーザは,条件にマッチするFriendレコードをSELECTするような検索メソッドが必要。ORMは,ユーザがその検索メソッドを実装する手間を省きたい。
  • ORMは当然ながら,ユーザによってFriendというクラスが作成される,などという事は,事前に知り得ない。だから,ユーザが作成したクラスをORM側で自由に取り扱うために,いろいろ工夫が必要になる。
  • ORMから見ると,どんなエンティティであっても共通の性質を持っていてほしい。したがって,全てのエンティティは共通の基底クラスBaseEntityを継承ないし実装するルールとする。
  • 次にユーザから見ると,FriendDAOの検索メソッドを呼んだ場合,返却値はBaseEntityではなく,Friendである事が保証されていてほしい。そうすれば,ユーザ側でBaseEntityからFriendにキャストする手間を省く事ができる。検索でFriendのインスタンスを取得した直後にすぐ,Friendクラスが固有に持つ性質を利用可能になる。
  • 上記の要求をORMの視点で言い直す。FriendDAOの基本的な機能は,基底となるBaseDAOで提供したい。しかしFriendクラスを扱う場合は,BaseDAOが検索メソッドの戻り値として返却するのは抽象化されたBaseEntityではなく,Friendである必要がある。
  • したがって,ユーザがコーディングする際には,FriendDAOはBaseDAOを継承するのみならず,親であるBaseDAOに対してFriendという型パラメータを渡す必要が生じる。ちょうど,下記のサンプルコードのように。

Android-MVC framework / FriendDAO.java

http://code.google.com/p/android-mvc-framework/source/browse/tags/20120323_ver0.2/src/com/android_mvc/sample_project/db/dao/FriendDAO.java

  • public class FriendDAO extends BaseDAO<Friend>
  • ユーザ側が上記のように対処することで,ORM側は,BaseDAO内でFriendクラスを取り扱えるようになる。とはいえ,ORM側がFriendを知っているわけではない。ORM内では,Friendは型パラメータ T であり,具体的なクラス名は知らない。ちょうど,下記のサンプルコードのように。

Android-MVC framework / BaseDAO.java

http://code.google.com/p/android-mvc-framework/source/browse/tags/20120323_ver0.2/src/com/android_mvc/framework/db/dao/BaseDAO.java

  • public class BaseDAO<T〜〜>
  • public T findById(〜〜)

ここまでは,異常はない。

ジェネリクスの定石に反したところも別にない。


頭を抱えるのはここからなのだ。


  • 前述の通り,ORMから見て,全てのエンティティは共通の性質を持っている必要がある。その共通の性質は,BaseEntityに記述される。
  • ORMから見て,上記の「共通の性質」を呼び出すためには・・・?

・・・


なんと,そのためには,T.class やnew T するしかないのである。


本当なのか。

これを理解するためには,Java言語の持つ制約をいくつか復習する必要がある。


関係する制約は,3つある。



(制約1)Javaでは,staticメソッドの実装を強制する事ができない。

親である抽象クラスまたはインタフェースにおいて,

  • 共通処理は,abstractにしない。そしてフレームワーク側で親クラス内に共通処理を記述しておく。
  • 個別処理は,abstractにしておく。そして実装クラスにユーザ側で個別に詳細を記述させる。

要するに,abstract指定すれば,そのクラスの実装クラスに対して「実装を強制」する事ができる。

こうすればクラス名を書いただけで「次に実装すべきものが分かる(しかもIDEがスタブを自動生成してくれる)」ので,作業効率を向上・スピードアップさせ開発プロセスを標準化するためのフレームワークとしての要件を満たす。



ところが,staticメソッドはabstract指定できない。

Javaインタフェースの定義

http://www.syboos.jp/java/doc/interface.html

2、インタフェースのメンバー変数について

※1)すべての「変数」は「public static final」です。

3、インタフェースのメソッドについて

※1)すべてのメソッドはpublic abstract と見なされます。

※3)static修飾子をつけることはできません。


オーバーライド

http://msugai.fc2web.com/java/override.html

static 修飾されたクラスメソッドを abstract 修飾するとコンパイル・エラー


だから,フレームワーク側から見ると,ユーザに対して実装を強制するためには,staticメソッドをあてにすることはできない。


※staticメソッドの「共通化」は,もちろん可能。基底クラスで普通にクラスメソッドを記述すればよいだけ。また,インタフェースにstaticなネストクラスを持たせるという手もある。このように「共通化(静的コードの共有)」はできるのだが,でも「実装の強制」はできない。



(制約2)Javaでは,子クラスのstaticフィールドの値を,親クラス側で透過的に扱う事ができない。

前項の要件からすると,staticメソッドだけでなく,staticフィールドに頼る事もできないのである。


例えば下記のコードを実行すると,子クラスのインスタンスを生成しているにもかかわらず,「親」と表示される。


public class Main
{
    public static void method()
    {
        System.out.println("親");
    }

    public static void main(String[] args)
    {
        Main m = new SubMain();
        m.method();
    }
}

class SubMain extends Main
{
    public static void method()
    {
        System.out.println("子");
    }
}

これは最初は理解に苦しむが,staticメンバに関する,Javaの標準的で正常な仕様である。

Inheritance in Static Methods

http://stackoverflow.com/questions/4987127/inheritance-in-static-methods

Static methods are resolved on the compile-time type of the variable. m is of type Main, so the method in Main is called.

If you change it to SubMain m ..., then the method on SubMain will be called.

It is because static methods are not polymorphic. Moreover static method should be invoked not by object but using the class, i.e. Main.method() or SubMain.method(). When you are calling m.method() java actually calls Main.method() because m is of type Main.

If you want to enjoy polymorphism do not use static methods.



継承したstaticメソッドは親の元で実行され、そしてアクセスするstatic変数が親というのは何故なのでしょうか?

http://oshiete.goo.ne.jp/qa/2277327.html

staticでないフィールドはそれぞれのオブジェクトについてフィールドが生成されますが、staticなものはクラスに張り付いているので、そのフィールドはクラス間で共有されています。

サブクラスでフィールドの値を置き換えた場合、staticでないものに関してはそれぞれのオブジェクトに関してフィールドが生成されているのでスーパークラスとサブクラスで値を変えることができますが、staticなものに関してはフィールドを共有しているので、サブクラスで値を変えられたときはスーパークラスまでその変更が影響してきます。

違う値を返したいなら,子クラス側でオーバーライドすること。

static なフィールドやメソッドというのは(正確な説明ではないかもしれませんが)他の言語で言うところのグローバル変数」「グローバル関数」みたいなものです。


要するに,Java言語ではstaticメソッドのオーバーライドが不可能である。


詳しく言うと,staticメソッドを子クラスで定義し直す事は可能なのだが,そのメソッドをポリモーフィズムの視点で透過的に扱うことはできない。


ポリモーフィズムを駆使している局面において,「X instanceY = new Y();」みたいな変数宣言をしたとしよう。

この場合,インスタンスメソッドを呼ぶ時には instanceY が参照され,ちゃんと子クラスのオーバーライドされたメソッドが呼ばれる。


しかしstaticメソッドを呼ぶ場合,インスタンスは関係なしとJVMが判断し,instanceY というインスタンスは全く参照されない。

そして,(Y クラスではなく,)変数の宣言型である X クラスのstaticメソッドが呼ばれる事になるのだ。


もしYのstaticメソッドを呼びたい場合は,宣言型を変えて「Y instanceY = new Y();」とする必要がある。

しかし,こんな宣言文をコーディングしてもJava言語ではほとんど付加価値を生まないということは周知の通りだ。

ポリモーフィズムを捨てたら,Javaを使う意味はあるのか。

Can I override a static method?

http://www.coderanch.com/how-to/java/OverridingVsHiding

  • Since instanceMethod() is (drum roll please...) an instance method, in which Bar overrides the method from Foo, at run time the JVM uses the actual class of the instance f to determine which method to run.
  • With classMethod() though. since it's a class method, the compiler and JVM don't expect to need an actual instance to invoke the method. And even if you provide one (which we did: the instance referred to by f) the JVM will never look at it. The compiler will only look at the declared type of the reference, and use that declared type to determine, at compile time, which method to call.

※こういった仕様は,SJC-P資格保持者等に尋ねると,とてもわかりやすく説明してくれるはずだ。

(プログラミング・センスを問うものではないが,Javaのインストラクターをするために要求されるような資格)


したがって,staticなフィールドやメソッドの利用は,あきらめる事になる。


本当は子クラス側で,staticフィールドに「テーブル名」等を格納したいのだが,それは不可能。

親クラス側で定義された共通便利メソッドからは,その値は見えない事になるので・・・。


だから,テーブル名などの情報も,子クラスの非staticメンバ(=インスタンスフィールドか,インスタンスメソッド)に格納する事になる。

何とも不自然。

(この時点で少し暗雲が見え隠れする)



(制約3)Javaでは,メソッドの実装は強制できるが,フィールドの実装を強制する事ができない。

staticをあきらめたところで,今度は非staticメンバつまりインスタンスのメンバに頼る事になる。

だが,インスタンスのフィールドには頼れないのである。

  • Abstractな親クラスを継承し,抽象メソッドの実装を強制することは可能。
  • インタフェースをimplementし,抽象メソッドの実装を強制する事も可能。
  • フィールドの実装を強制することは,不可能。

もちろん,子クラス側のコンストラクタで対処すれば,そのタイミングでインスタンスのフィールドの値をセットできるので,フィールドを「継承」したかのように見せることはできる。

ただ,実装というか値の代入を「強制」することはできない。

Why not abstract fields?

http://stackoverflow.com/questions/2211002/why-not-abstract-fields

Why can't Java classes have abstract fields like they can have abstract methods?

For example: I have two classes that extend the same abstract base class. These two classes each have a method that is identical except for a String constant, which happens to be an error message, within them. If fields could be abstract, I could make this constant abstract and pull the method up into the base class. Instead, I have to create an abstract method, called getErrMsg() in this case, that returns the String, override this method in the two derived classes, and then I can pull up the method (which now calls the abstract method).

Why couldn't I just make the field abstract to begin with? Could Java have been designed to allow this?


したがって,フィールドではなくメソッドを使う戦略になる。

前の段階で既にstaticメンバは除外されているので,必然的に,「インスタンス・メソッドを使う」という事になる。


テーブル名も,フィールドとして宣言するのではなく,Stringを返却するgetTableName()という非staticメソッド(=インスタンスメソッド)の実装を強制する事になる。

(エェー)

ちょうど下記のサンプルコードのように・・・。

Android-MVC framework / Friend.java

http://code.google.com/p/android-mvc-framework/source/browse/tags/20120323_ver0.2/src/com/android_mvc/sample_project/db/entity/Friend.java

  • public String tableName(){return "friends";}

ここまでをまとめると,どういう結論になるのか?


先に述べた通り,DAO利用時の「代入互換性」を排除するためには,DAOの親クラスに型パラメータFriendを渡す必要がある。


もし型パラメータを渡さない場合,親クラス側ではBaseEntityを透過的に扱うという事になる。

BaseEntityを継承したクラスのインスタンスなら,何でも許容されてしまう。


例えばユーザはFriendレコードを検索したのに,メソッドの返却値がAddressレコードだったら,困る。

FriendもAddressもエンティティなので,両方ともBaseEntityを継承しており,そういうミスキャストが紛れこんで,実行時例外を引き起こしかねない。

そんな型安全性のないORMは使いたくない。


そうではなく,特定のエンティティのインスタンスのみを返すような挙動を,親クラスのメソッドに対して「強制」したいのだ。

そういうわけで,親クラスに対してジェネリクスで子クラスをパラメータとして渡す事になる。



ここまでで,パズルのピースが出そろった。

  • 子クラス側では,インスタンスメソッドの実装を強制する形を取る。メソッド内には,エンティティやDAOの個別処理を記述。
  • 親クラス側では,子クラスの型はパラメータ化して扱われる。メソッド内には共通処理を記述。


そして,これらのピースはうまく合わないのである・・・。


新たな制約として,制約4を記載する。


(制約4)Javaでは,ジェネリクスの型パラメータTに対して,T.classやnew T()ができない。

子クラスのインスタンスメソッドを呼びたいのに。


Javaと同じJVM上で動作するプログラミング言語Scala」では可能なのに。


Scalaジェネリクス(パラメータ化された型)

http://www.ne.jp/asahi/hishidama/home/tech/scala/generics.html

T.class:

ScalaではClassManifestを使えば型パラメーターを受け取ることが出来るので、

そこからjava.lang.Classを取得できる。

new T():

ClassManifestを使えばjava.lang.Classが取得できるので、

後はリフレクション(newInstance())を用いればインスタンスを生成できる。


あるオブジェクトのインスタンスメソッドを呼ぶためには,

当然ながら,インスタンスが必要なのだ。


基底クラス内で,クラス型が「T」であることがわかっている場合,

そのインスタンスを得るために2つの方法を試みる:

  • 単純に,newする。→Tだと new は不可能。
  • クラス名.class でClassクラスのインスタンスを取得して,getInstance() から引数なしのコンストラクタ経由でインスタンスを生成する。 →Tだと .class は不可能。

そして両方とも,Java言語の制約上,駄目なのである。

(最終兵器のリフレクションすら拒否られるとは…。)


なぜなら,javacとJVMにおけるジェネリクスの扱いは

  • コンパイル時は全部Objectにしてしまう
  • 実行時に型パラメータに合わせてキャストする

というものだから。

コンパイル時には,具体的なクラス情報を取得するような操作は,基本的に許されていないのだ。



こうして,

「JavaではT.classやnew T()ができない,とさんざん嘆きつつ」,

Friend.classをメソッドの引数として渡すという選択肢をやむを得ず採用する事になったのだ。


ちょうど,下記のサンプルコードのように。

Android-MVC framework / FriendDAO.java

http://code.google.com/p/android-mvc-framework/source/browse/tags/20120323_ver0.2/src/com/android_mvc/sample_project/db/dao/FriendDAO.java

  • return findAll(helper, Friend.class);

ここに至るまでの道のりは,けっこう苦労した。

上記で説明した項目のほかにも,

  • クラスローダが静的安全性を失わせ,フレームワークのコンパクトさを損なうデメリットを抱え込んでいること

など,いろいろ粘って吟味した末で,このような設計に行きついたのだ。



解決編

しかし現時点では,Javaでも裏技を使えばT.classやnew T()ができるという事が発覚している。

よりによって,ツイートでツッコミを下さったNagise氏ご自身のブログに,その方法が書いてある。

Java変態文法最速マスター

http://d.hatena.ne.jp/Nagise/20100202/1265131791

4. 配列

コンストラクタに型変数の可変長引数をとることで、new Hoe<Fuga>()の呼び出しで大きさが0のFuga[]がコンストラクタ引数に渡され、そこから型変数の具体的な型であるFuga型を得る、という手法

最初に述べた通り,Nagise氏はジェネリクスのエキスパート(generixer)だ。なんとありがたい事か。


おかげで,今後はORMのメソッドの引数に Friend.class を渡す必要が無くなる。

そうすれば,ユーザのコード記述量が一層削減される。



そういうわけで私は,一時期 T.classやnew T() について,まあまあ妥当な理由に基づいてあれこれと悩んでいた時期があったのだが,今は解決しているのである。


Javaに支払う複雑税の金額は大きい。

だからこそ,ユーザがその税金を支払わなくても済むようなフレームワークがあれば,その意義は大きいのでは。



最後に念のために繰り返すが,一般的で具体的なアプリケーションを作っている限り,ジェネリクスで抽象化された型をわざわざ具象化すべきではない。

汎用的なライブラリや,下位レイヤを隠ぺいするためのフレームワークを作っている場合のみ,そういうニーズが生じる事もあるが,それは例外的な状況である事を忘れてはならない。


おまけ:今さら聞けないJavaのGenericsの歴史

ジェネリクスについて,おまけの情報を記載しておく。


Java言語のジェネリクス(総称型)はJDKのバージョン1.5から導入された。

JDKに正式な文法として組み込まれたのは,そのバージョンがリリースされた2004年9月という事になっているが,しかし元をたどると,1997年製のJava拡張プロジェクト「Pizza」に端を発するらしい事が分かる。

Generic Java / A Brief History (2002年の情報)

http://everything2.com/user/GlitterGum/writeups/Generic+Java

In 1997, a team of programmers from the University of Glasgow, including Martin Odersky and Phil Wadler designed an extension to Java for templates called Pizza (which itself was a spinoff of the EspressoGrinder project).

Pizza dabbled not only with the idea of paramorphism, but also with the idea of algebraic types, that is classes generated at compile time using case statements to further refine the parent class. Think of it as a class definition scattered about in different places of your source code, an implicit class definition. Pizza was also capable of producing plain Java code with all template references removed to be compiled later with regular Java classes.

Out of this grew the programming language GJ (Generic Java), which removed the concept of algebraic types and refined the concept of f-bounded paramorphism.

Sun Microsystems selected this language as the candidate for the new compiler that will hopefully become standard in JDK 1.5.


J2SE 5.0(2004年9月30日)

http://ja.wikipedia.org/wiki/Java#J2SE_5.0.EF.BC.882004.E5.B9.B49.E6.9C.8830.E6.97.A5.EF.BC.89

  • 総称型のサポート

そのようにアイデアレベルではけっこう前からJavaプラットフォーム上でも需要があったわけだが,

実際に正式採用に至るまでには,.net framework勢との間に「軍拡競争」があった。


.NET側では,Java5よりも1年遅れて,.Net Framework 2.0においてジェネリクスが導入されたという事になっている。

しかし,Javaと.NETでどちらが先にジェネリクスを取り入れようと「真剣に試み始めた」のか?という話題になると,エンジニア同士の間でケンカになる可能性がある。

炎上しかねない不毛な議論になるので,そのトピックはタブーとして避けた方がいいだろう。

C#と.NETのジェネリックプログラミング

http://ja.wikipedia.org/wiki/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0#C.23.E3.81.A8.NET.E3.81.AE.E3.82.B8.E3.82.A7.E3.83.8D.E3.83.AA.E3.83.83.E3.82.AF.E3.83.97.E3.83.AD.E3.82.B0.E3.83.A9.E3.83.9F.E3.83.B3.E3.82.B0

C#(およびその他の.NET言語)のジェネリクスは.NET Framework 2.0の一部として2005年11月に追加された。Javaと似てはいるが、.NETのジェネリクスは、コンパイラによるジェネリクス型から非ジェネリクス型へのコンバートとしてではなく、実行時に実装される。このことにより、ジェネリクス型に関するあらゆる情報はメタデータとして保存される。


Generics

http://wisdom.sakura.ne.jp/programming/java/java5_1.html

Java 誕生後の数年で、プログラミング言語に求められる機能も変化しました。 とくに .NET と C# 言語の存在は Java 開発チームに少なからず影響を与えているでしょう。


Javaと.NETのどちらが先か?で揉めた結果,コメント欄が炎上した海外のブログ

http://www.facsim.org/node/77

  • Java 1.3 already had generics (surprise!) !!! Not the 'full-fledged' generics Java 5 "enhanced" (quotes add some sarcasm) but simpler - avoid the corner cases - generics. Ask Martin Oderski ... (※要出典)
  • Generics were part of the CLR from .net 1.1. The ECMA spec supported them in MSIL from the begining. (※要出典)

参考:

Javaには最近ジェネリックスとアノテーションが加わりましたが,これは.NETとの絶え間ない「軍拡競争」の結果です。


・・・Neal Ford著,「プロダクティブ・プログラマ」14章。


その上,Javaプラットフォーム上で動作するジェネリクスは,残念ながらあまり洗練されていない。

なぜかというと,ジェネリクスが存在しなかったころのJVMとの「後方互換性」を配慮せざるを得なかったからだ。

Java総称型のワイルドカードを上手に使いこなすための勘所

http://d.hatena.ne.jp/ryoasai/20110325/1301078699

  • Javaの配列をジェネリクスの視点で考えると「非変(invariant)」ではなく,「共変(covariant)」で,Hoge<Parent> は Hoge<Child> の親となる。
    • これは型安全性を崩し,実行時例外の元になる。配列を使うな。
  • Javaではおしなべて,他言語に比べ「反変(contravariant)」や変位の指定がしづらい。
  • 「Java言語の哲学ではWrite Once Run Anywhereという考え方が昔からあったため、以前のバージョンで作成されたプログラムが新しいバージョンでもそのまま利用できるということが求められました。そのため、型消去(type erasure)という方策がとられただけでなく、Listなどの既存のライブラリーのインターフェースの互換性の維持にも最大限の注意が払われました。・・・それゆえ、互換性という制約を満たすためにJava言語では常に総称型変数は非変として扱われるということになったのだと思われます」

Javaの未来についてのNeal Gafter氏とのディスカッション

http://www.infoq.com/jp/articles/neal-gafter-on-java

  • Javaのジェネリックは型消去という方式を採用するおかげで,JVMの後方互換性を得た。が,言語に対してクロージャなど新機能の追加が困難になってしまった。Javaには長期的ビジョンが欠如している。(元J2SE作者で,今はSunを見限ってMicrosoftに転職して.NETの仕事をしてるNeal Gafter氏の話)
  • 代わりにscalaかC#を使えと。

今のうちにGroovyかScalaでも勉強しておかないと…という気持ちになってこないだろうか。

GroovyならGRailsというRails系フレームワークが存在するし…



以上でJavaのジェネリクスの歴史を簡単に振り返った。

ジェネリクスの使い方等については,Javaの入門書を見れば済むので,この記事では触れない。



補足:その後

こちらこそ恐縮。



補足2:さらにその後

やはりフレームワーク全体が説明不足なので,さらに進んだ別のツッコミを頂戴する運びとなった。

id:happynow 氏より:

やはり T.class や new T() をしなくてもよい。

http://d.hatena.ne.jp/happynow/20120509/1336516068


返信としてお伝えしたい点のサマリは,コメント欄に記述させて頂いた。


非常に良い点のツッコミに感謝します。

実は,ご指摘頂いた点はまさに,いずれエントリに書いてまとめなければ・・・と思っていた点なのです。

時間が取れないので,伸ばし延ばしになっているのですが。。。
(例のジェネリクスの記事も,id:nagise氏からつっこみを頂いた時点で返信のネタは全て揃っていたのですが,記事にまとめ上げて公開するまでには1カ月かかってしまいました。)

なぜこのような設計になっているのか,簡潔にお伝えすると
「SQLiteに特化したORMを作るためには,『LP変換』という概念を導入する必要がある。」
という発想を持っているためです。

フレームワークのソースコード中でも「ここでLP変換」などのコメントが散りばめられております。

「LP変換」は「Logical-Physical変換」の略のつもりで,
・論理値:Javaの世界の値
・物理値:SQLiteの世界の値
の橋渡しをする操作です。

それぞれ,対応するエンティティが存在します。
・物理値を詰め込んだPhysicalEntityは,SQLiteに渡すためのオブジェクトです。
・論理値を詰め込んだLogicalEntityは,Javaの世界で違和感なく操作するためのオブジェクトです。

ユーザは論理エンティティに対して,下記のような命令を出す事になります。
・「あなたをDBに登録したいので,物理エンティティに変わってくれないか?」とお願いする。Javaの世界→DBの世界への変換。 =toPhysicalEntity()
・「SELECTしたてホヤホヤの物理エンティティが手元にあるんだけど,これを元手に,あなた自身を構築してくれないか?」とお願いする。DBの世界→Javaの世界への変換。 =logicalFromPhysical()

LP変換が必要になる理由は,SQLiteの取り扱える型が限定されているためです。
例えば,boolやdatetimeが存在しないので,integerやtextで代用せねばなりません。

現在のフレームワークのソースコードでは,その代用ロジックが「共通化」されておらず,場当たり的な実装に見える事でしょう。
そのため,違和感を感じられたのではないでしょうか。
現在の実装では,LP変換の責任の所在が各論理エンティティに存在することの理由が,明示的に伝わりにくいと思います。

いずれUMLを交えて解説記事を書く必要があると考えております。
また,変換ロジックを共通化し,LP変換そのものを楽にコーディングできるようにするための仕組みづくりも必要と思います。
(そのためには,主な型変換の需要をすべて洗い出す必要がありますが…。)

それまでご容赦ください。


※なお,

>この共通の性質というのは実はデータベースに関わる物理情報を管理するものである。
>通常の DAO(Data Access Object)パターンでは、物理情報はDAOオブジェクトが管理する。

とのご記載ですが,BaseEntity内にはLP変換以外にもCRUD操作など,DB操作の上で不可欠な処理が詰め込まれています。
http://code.google.com/p/android-mvc-framework/source/browse/tags/20120323_ver0.2/src/com/android_mvc/framework/db/entity/BaseLogicalEntity.java

また,本フレームワークは通常のDAOパターンを相当ひねって,ActiveRecordパターンに近付けたいという狙いもあります。(ソースコード中のコメントに記載)


設計に完全解はないので,本エントリで取り上げて下さったように,最終的にはDAOに処理を詰め込む形になってもおかしくない,と思います。
アイデアのご提供ありがとうございます。

「LP変換」は私の造語です。
SQLiteでORマッピングを実装するためのセオリーを体系化したいと思い,その過程で生まれた用語です。
ですから,いつか説明を加えなければ,ご理解を頂くことは難しい場合もあるだろうな,と懸念しておりました。
この場でお詫び申し上げます。

また,的を突いたご指摘に感謝します。


この件についての解説エントリも,早めに必要だろう。


このようなあまたのツッコミを経て,ブラッシュアップが繰り返され,より良い作品ができあがってゆくものと考える。

したがって,こういったツッコミを発信して下さる方々の存在は,極めて貴重だ。

この場を借りて,お礼を申し上げます。






 

リンク元