Generics で Signature 被って死んだらどうしよう?

久々に Java Generics 起因の Signature カブりで死んだので。


前置き

Java には Generics という機能がある。 List<​String> とかやるアレだ。
これは Java 5 から実装された機能であり、当時一つの革新であった。既に実装から軽く10年以上が経過しており、「え? 昔はなかったの?」という人間も珍しくはあるまい。
(一応、筆者自身ははじめての Java が2000年前後だったので、辛うじて Generics のない時代に触れたことがある――殆ど覚えていないのだが)

その昔、未だ Generics が存在しなかった頃。List や Map といえば Object を詰めるものであった。
何しろ Object はあらゆるオブジェクトの親である以上、事実上 (Primitive 以外の) あらゆる物を詰められる。当時、これと配列で十分だと考えられ……ていたかどうかはともかくとして、言語仕様上もそうする他なかったのだ。

しかし、 "一旦 Object にする" ということは、そのまま "適切に作ったとしても必ずダウンキャストが必要になる" ということを意味する。
いまさら言うまでもないが、ダウンキャストは安全ではない。
安全性の確保自体は簡単だが、いくら頑張って確認して「このダウンキャストは安全だ」としても、将来的に困ったことをしでかす人間は必ず出てくる。

かといって、詰めたい各種のオブジェクトごとにいちいちクラスを作るのは非常にめんどくさい。作るのもそうだが使うのもめんどくさい
例えば C++ のように、使われたクラスを使われたパターンごとにコンパイラ側で自動生成・自動切り替えしてくれるならよいのだが、この方式は最終出力物のサイズが大きくなる1上、コンパイル時の演算負荷も大きければコンパイラの実装自体もめんどくさく、何よりもどうあがいたところで後方互換性が保てない問題があった。
何しろ当時既に Java は広まっていたし、 "Write once, run anywhere" のスローガンはいまさら崩せるものではなかったのだ2

そこで採用されたのが、型消去 (Erasure) という方式である。
これは言ってみれば
「要するにダウンキャストの安全性を保証できないのが問題なんだから、ダウンキャスト自体をコンパイラにやらせればいいじゃないか
というある種の力技だったが、しかし、互換性を維持しつつ目的を達成するシンプルにして有力な手法だったのだ3。めでたしめでたし。

で、終わっていればこの記事はない

Java の Generics には弱点がいろいろとある。
しかしそれこそ Java 6 の頃は激しくやる気を削ぐ状況がホイホイ発生していたものだが4、言語仕様の変更により割と改善しつつある。
ダイヤモンド演算子は正直失笑ものだった5が使ってみればまあ実際相当マシにはなったし、Java 10 ではついに var が導入されて左辺側を省略できるようになる6ぐらいだ。

では実際どういう時に困るのか。もちろん表題のような時である。

つまりどういうことだってばよ

こんなクラスがあるとしよう。

class Alpha {
    private List<Integer> val = new ArrayList<>();
    public void val(Integer i) {
        this.val(Arrays.asList(i));
    }
    public void val(List<Integer> l) {
        this.val = l;
    }
}

何の変哲もない (?) 単一の List を保持するクラスである。
保持された List に対して別の List 、ないし単一引数を受け取って Arrays.asList で List 化したものをセットするメソッドを持つ。どうせ Arrays.asList なんだから単一引数側いらないだろうって? やかまし、こういうのを作りたいときもあるんだよ。
唯一何かしら特徴的な部分があるとすれば、Java のお約束、Java Beans に端を発する、例の setXXX になっていないことであろう7

さて、 Java 8 では関数型やモダンな言語仕様が大いに取り入れられた。
割とショボい部分があるためなんだかんだかゆいところが多いのだが、ともかく取り入れられている上に十分使えることは評価すべきだ8
重要なのは、これらの新要素が凄まじい勢いで Generics を使いまくることである。

例えば、何らかの理由でこれら引数を Supplier にしたくなったとする9

// コンパイル不能コード
class Alpha {
    private List<Integer> val = new ArrayList<>();
    public void val(Supplier<Integer> i) {
        this.val(Arrays.asList(i)); // もちろんここでも通らなくなる
    }
    public void val(Supplier<List<Integer>> l) {
        this.val = l;
    }
}

書いてあるとおり、このコードは invalid である
何故か。このコードは (最終的には) 以下のコードと等しいからだ。

// コンパイル不能コード
class Alpha {
    private List val = new ArrayList();
    public void val(Supplier i) {
        this.val(Arrays.asList(i)); // もちろんここでも(ry
    }
    public void val(Supplier l) {
        this.val = l;
    }
}

こりゃ駄目だ。誰だってわかる。名前も引数のパターンも同じものを区別する方法はない
えらく前置きが長くなったがここからが本題である。短いんだが。

どうすればいい?

最も確実な解法、それは諦めることである。

先程

どうせ Arrays.asList なんだから単一引数側いらないだろうって? やかまし、こういうのを作りたいときもあるんだよ。

なんて書いたわけだが、現実問題として Arrays.asList を挟んでやればよいのだ。本当にどうしようもないならともかく、諦めうるなら諦めたほうが手っ取り早い。というわけで、こうしよう。

// できたもの
class Alpha {
    private List<Integer> val = new ArrayList<>();
    public void val(Supplier<List<Integer>> l) {
        this.val = l;
    }
}

結局の所、Java の Generics (というよりは、Erasure により実装されるありとあらゆるジェネリクス) は極めて貧弱である。特殊化の一つもできやしない。
そのくせ諸々の事情10で null-safety でさえも Generics に頼るぐらい11に Generics を限界までぶん回す方向に走り続けている以上、そんな言語を選択してしまった時点で、 "オーバーロードに頼る" という選択肢は常に放棄しうるものとすべきだろう。ある言語における最適解は、その言語の設計によって決定されるのだ

……とかっこよく言ってみたが、要するにこういうことだ。

  1. 実際問題例えば両方とも欲しいのであれば名前を変えるしかない
  2. 引数のパターンを増やすために名前を変えるというのはいかにも本末転倒だし、何よりも古の悪いハンガリアンのようで殺意が湧く
  3. そもそも多少妥協すればなんとかなるんじゃないか? メソッド一つ増える方がめんどくさくないか?
  4. わかった、もういいや

我々には、「もういいや」の精神が重要なのだ。妥協、大事。


  1. 今となっては大した問題でもない気がするが、記憶容量単価がひどいことになっている今だから言えることなのだろう。たぶん。実際その昔は "オブジェクト指向なんて出力物が非効率すぎてやってらんねーよバーカ!!" なんて声もあったらしいし。 

  2. 尤も、このスローガンが事実上の大嘘だというのは当時の時点から言われていたようだ。まあ誰だってちょっと考えればそう思うんじゃなかろうか。 

  3. ところで Java 以前に Erasure で総称型を実装した言語ってあったんだろうか。いかにも Java が初! みたいな話は聞くんだが、ちょっと気になる。 

  4. クソネーミングなクラスで List 作ったり Map 作ったりすれば誰だって嫌になる。しかも困ったことに、真っ当なネーミングでも DbC (契約による設計) 以来クラス名は伸びやすい…… 

  5. 主にネーミングが。ダイヤモンドて。 

  6. 可読性の事情から実用できるタイミングがものすごく悲しいことになるのはこのさい仕方ないというものだ。 

  7. なお、筆者はなんの意味もないアクセサを大量生産するバカに大義名分を与えるが如き所業である、という非常に感情的な理由でアレが死ぬほど大嫌いだ。 

  8. 評価すべき、どころかないとコードを書く気になれない。いや書くんだが。恥ずかしい話だが if や for, while あたりの条件を書くのがエラく苦手で、凄まじい勢いで間違えまくるのだ。でも Optional#or ぐらい最初っからつけてくれ。 

  9. 正直例が適当でなさすぎて困ったが、まあきっと実行を遅延したかったんだろう。 

  10. 主に後方互換性的な事情じゃないのかね。知らんけど…… 

  11. もちろん Optional のこと。……いやガード節をよこしてくれ。あるいはメソッドレベルで気軽に使える Bean Validation みたいなやつ。無理か。うん。 

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.