こまぶろ

技術のこととか仕事のこととか。

JavaにおけるOptionalによるnull安全のための覚書

Java8から導入されたOptionalを利用することで、よりnull安全なコードを書くことができる。ただし、使い方を間違えるとかえってバグを生むことにも繋がる。適切に利用するための覚書を書く。

nullを返す可能性のあるメソッドは、Optional型を返すメソッドに変更する

これがベースになる。Optionalが導入されるまでは、nullを返す可能性のあるメソッドと返す可能性のないメソッドは言語の上では区別できなかった。そのため、nullを返すかもしれないメソッドを呼び出す際には、プログラマがそれを意識した上で、呼び出し元の方でnullチェックを書く必要があった*1

Optionalを返すように変更することで何が起きるか。Optional型のオブジェクトが戻り値となることによって、そのまま(Optionalから中身を取り出さずに)利用することが禁止されるのだ。中身を利用するためには、Optional型に定義された各種のメソッドを利用する必要がある。同時に、空のOptional(メソッド変更前のnullに対応)が返ってくることを考慮して呼び出すことも強制されるので、「うっかり」nullが画面に表示されてしまったり、NullPointerExceptionをスローしてしまったりということはなくなる。

nullになる可能性のあるフィールドに対するgetterは、Optional型を返すメソッドとする

これは上の原則に含まれているが、意識しにくいものだと思う。通常、getterというと、以下のようなものを想像する。

public class User {
  private String name;
  public String getName(){
    return this.name;
  }
}

このような(しばしばsetterを一緒に書かれる)getterは、Javaプログラマにとっては見慣れたもの(好き嫌いはともかく)だろう。フレームワークがこの種のメソッドの定義を要求している場合もあり、全部を否定するわけではないのだが、Optionalを使ってnull安全なコードを書くという観点からは、この種のgetterは再考に値する。

getterもまた、ひとつのメソッドだ。nullを返す可能性があるのであれば、戻り値の型はOptional型にするべきだ。もしUserのname属性が未設定を許すのであれば、以下のように修正する*2

public class User {
  private String name;
  public Optional<String> getName(){
    return Optional.ofNullable(this.name);
  }
}

ちなみに、上のようなメソッドを用意する場合、同一クラス内からもフィールドに直接アクセスするのではなく上記のメソッドを利用するようにすべきだ。そうすることで、nullチェックを実装し忘れることを防ぐことができる。このように、外部に対するカプセル化のためのメソッドを自分自身もまた利用することを、自己カプセル化という。Optional型を戻すgetter以外についても使えるテクニックだ。

引数やフィールドにはOptional型を使わない

以上では、メソッドの(nullになる可能性のある)戻り値をOptional型に変更すべきだと書いてきたが、メソッドの(未設定を許容する)引数やフィールドに対しては、Optional型は使わない。

それは、Optional型の引数やフィールドが実行時にnullである可能性を排除できないためだ。下記のようなコードを書いても、思った通りにメソッドが使ってもらえる保証はない。メソッドの利用者は、空のOptionalを渡す代わりに、Optional型のnullを渡すことができてしまう。

public int calculatePrice(Optional<DiscountRate> discountRate){
 // 省略
}

設定を任意としたい引数がある場合には、従来通りnullチェックをメソッド内部で行い、処理を分岐させればいい。一方、nullを許容しない引数については、Javadocコメントにその旨を明記し、実行時にnullが渡された場合に備え、Objects.requireNonNull()等を用いて「防御的プログラミング」を行う。nullを渡さないでくれと言っているメソッドにnullを渡したのであれば、それは利用者側のミスであり、供給者の責任ではない。容赦無くNullPointerExceptionなどをスローして落としてしまえばいい。

参考

*1:nullチェックを怠った結果としてよく見られるのが、ユーザ向けの画面上に表示される"null"の文字列だ。

*2:メソッド名はgetterらしいgetName()よりも単にname()などとした方が良いかもしれない。

Web API の仕様をOpenAPI Specで書いてみて

以前、OpenAPI Generatorの紹介記事を書いた。

ky-yk-d.hatenablog.com

上の記事を書いてから、実際のプロジェクトでOpenAPI SpecでAPIの仕様を書くとともに、その定義ファイルからOpenAPI GeneratorでAngularのクライアントコードを自動生成するということを始めた。まだ課題もあるが、中間報告を書く。

前提条件

今回開発対象としているAPIサードパーティによる利用を想定したものではなく、もっぱら自社で開発するクライアント(Web/モバイル)から利用されるもの。サーバとクライアントを並行で開発しており、APIの仕様を決めるに当たってはクライアントでの利用のしやすさを重視している。

開発チームとしては、サーバでHTMLを生成する種類のWebアプリケーション開発の経験が長いメンバーが多く、Web API開発の知見はチーム内に乏しい。また、これまで仕様書はExcelがメインであったとともに、構成管理ツールとしてもVSS/TFSに慣れ親しんでいるメンバーがほとんどである。

やっていること

現在は、以下の事柄を実施している。

  • OpenAPI SpecによるAPI仕様の記述
  • 上記ファイル(YAML)のGitでの管理
  • ReDocによるHTMLドキュメントの生成
  • OpenAPI GeneratorによるAPIクライアントコードの生成

補足すると、YAMLファイル(およびそこから生成されたHTMLドキュメント)がAPI仕様の正式版ではあるものの、いきなりYAMLファイルを記述するのは辛いので、仕様を検討する過程では一度Excelに記述するようにしている。Excelのフォーマットは、自動生成したHTMLドキュメントの記載項目を参考に作成した。

OpenAPI Generatorのスタブサーバのコード自動生成の機能は、開発の当初の段階では試してみたが、現在は使っていない。クライアントの開発がサーバの開発に先行している場面では、クライアントコードの中にダミーデータを直書きすることで済ませている。また、HTMLドキュメントの生成は、OpenAPI Generatorでも可能だが、見やすいものではなかったためReDocという別のツールを利用している。

github.com

iktakahiro.hatenablog.com

よかったこと:情報の集約

スキーマ駆動開発」というテーマの特集を読んで導入したOpenAPIだったが、ここまでのところで実感している効用はむしろ、「情報の集約」という側面に関するものである。

具体的には、チーム内で(あまりよくないことだが)よく交わされる「〇〇ってどの資料に書いてありましたっけ」という会話が、Web APIの仕様については全く発生しない。

自社はドキュメントをそれなりに作る会社なので、各種の設計書を作成する際に利用できる標準フォーマットがある程度整備されている。しかし、標準的なものがないケースにおいては、まず大雑把な資料が作成され、プロジェクトが進んで情報が増えていく中でより詳細な、あるいは別の側面からの資料が作成される。その結果、フォーマットの異なる複数のファイルに情報が散逸するということになりやすい。

Web APIの仕様についても、社内には標準がないので、当初は上のような管理が困難な状況に陥りそうになっていた。しかし、OpenAPI Specの導入により、YAMLファイルが単一の情報集約点となった。OpenAPI Specでは様々な情報を記述することができるようになっているから、原則としてYAMLファイルに情報が追加されるようになる。

YAMLファイルには記述しづらい情報のために、補足的な資料を作成することがあっても、YAMLファイルが主たる地位を占めることはチーム内で了解されているので、情報が散逸しづらいのだ。また、OpenAPI Specの仕様で「どのような情報を記述できるか」を知ることは、開発の経験が乏しいWeb APIについて「どのような事柄を考慮すべきか」を考える助けにもなっている。

展望と課題:その他の情報の集約と「スキーマ駆動」

「情報の集約」は、OpenAPIの導入に拠らずとも達成されうるものであるし、本来は達成されているべきものだ。抱えている問題のレベルの低いというのは一方で真理だろう。しかし、自分の現場にとっては、OpenAPIという仕様とその周辺ツール群が、その問題を浮き彫りにすると同時に、解決できるものであることを気づかせてくれた。この気づきを承けて、Web APIの定義以外のドキュメントも、なるべく一箇所に集めるように動き始めている。

一方で、先に述べた通り、スキーマという部分でのメリットはあまり享受できていない。また、各種ツールを利用することによるメリットも最大限享受できているとはいえない。あまり色々やろうとしすぎても大変&混乱を招くのでそこは注意したいが、ツールの導入を少しずつ進めていきたいなとは思っている。

WEB+DB PRESS Vol.108

WEB+DB PRESS Vol.108

  • 作者: 中野暁人,山本浩平,大和田純,曽根壮大,ZOZOTOWNリプレースチーム,権守健嗣,茨木暢仁,松井菜穂子,新多真琴,laiso,豊田啓介,藤原俊一郎,牧大輔,向井咲人,大島一将,上川慶,末永恭正,久保田祐史,星北斗,池田拓司,竹馬光太郎,粕谷大輔,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/12/22
  • メディア: 単行本
  • この商品を含むブログを見る

6月22, 23日に、豪華スピーカー陣によるカンファレンス「DevLOVE X」が開催されます

DevLOVE10周年イベント「DevLOVE X」

きたる6月22日(土)・23日(日)の2日間の日程で、株式会社ナビタイムジャパンさまにて、ソフトウェア開発者向けイベント「DevLOVE X」が開催されます。

devlove.wixsite.com

こちらは、ソフトウェア開発者コミュニティ「DevLOVE」の10周年記念イベントです。DevLOVEは、『カイゼン・ジャーニー』の著者のお一人である市谷聡啓(@papanda)さんが2008年に立ち上げられたコミュニティです。僕は、DevLOVEの運営にここ一年弱ほど関わらせていただいていまして、今回の10周年イベントにもスタッフとして参加します。

すでに市谷さんを中心としたスタッフ陣、それに登壇者の方々からTwitter等で告知をしていただいていますが、こちらのブログでも僕の個人的な思いを添えて宣伝させていただきたいと思います。

豪華スピーカー陣のみなさま

今回の「DevLOVE X」は、総勢60名以上のスピーカーをお招きするカンファレンスという形で行われます。すでにタイムテーブルも公開されています*1

f:id:ky_yk_d:20190528212935p:plain f:id:ky_yk_d:20190528212939p:plain

ご覧のような、「何回分の基調講演ができるんだ」という豪華なスピーカーの方々にご登壇いただきます。

togetter.com

完全に個人的なスピーカーのご紹介(一部)

今回のスピーカーの方々には、僕自身、書籍などを通じて学ばせてもらっている方、あるいは直接的に勉強会等でお世話になっている方が多く含まれています。その中でも、僕がこの業界に入ってからの2年間を振り返る上で欠かせない方々を特に取り上げてご紹介したいと思います。

和田卓人(@t_wada)さん

言わずと知れたt-wadaさん。新卒1年目だった2017年11月に『テスト駆動開発』の新訳が出版され、12月に開催されたイベントで初めて生で接しました。テスト駆動開発というプラクティスを通じて、ドライだと思っていたプログラミングが実は人間の心理と深く結びついた営みであることを教えてもらいました。

テスト駆動開発

テスト駆動開発

広木大地(@hiroki_daichi)さん

『エンジニアリング組織論への招待』の著者。2018年の2月に出版された書籍は、「ソフトウェア開発をこういう風に語れるのか、語っていいのか」という気づきを与えてくれました。僕にこの2年間の中で最も大きな影響を与えた人々の一人と言えます。何とか広木さんの影響圏から脱したいともがいている最中です。

横道稔(@ykmc09)さん

omoiyari.fm」のパーソナリティの一人。omoiyari.fmは、僕がアジャイルやマネジメントというものに関心を持つようになる一因となったとともに、カカカカックさんとの出会いを与えてくれた大切な番組です。伝説の第22回に限らず何度も聴き直しているので、2年間で家族と同僚以外で最も長く声を聴いていた人かも知れません。

lean-agile.fm

カカカカック(@kakakakakku)さん

この人がいなかったら僕はいまこの記事を書いていないでしょう。2018年5月から2ヶ月間、ブログのメンタリングをしてもらいました。影響を受けたというよりも、「生活を規定されている」存在です。僕がブログを書くことを止めるか、僕なりのブログに対する向き合い方を見つけるその時まで、それは続くのだと思います。

kakakakakku.hatenablog.com

参加申し込みはDoorkeeperから

以上、甚だ個人的なご紹介を4名のスピーカーの方について書きました。上に挙げた方々以外にも、様々な分野でご活躍のスピーカーのみなさまが参加してくださり、スタッフという贔屓目を差し引いても「豪華だ!」と言ってよいと確信しています。僕もとても楽しみです。

そんな「DevLOVE X」、まだまだ参加申し込みを受け付けていますので、ぜひ下記からチケット(2days + 懇親会チケット付きで10000円です)を入手の上、ご参加ください!

devlove.doorkeeper.jp

*1:画像は2019年5月28日現在。

リレーショナルモデルと『Clean Architecture』のエンティティ

(エリック・エヴァンスの)ドメイン駆動設計を入り口にして、オブジェクトモデルとリレーショナルモデルについて考えているなかで、「ドメインモデルって必ずしもオブジェクトじゃなくていいんじゃないの」という思いを強めている。

ky-yk-d.hatenablog.com

そのような観点で、改めてアンクル・ボブの『Clean Architecture』を眺めていたら、一読したときは読み飛ばしていた記述に引っかかったので、覚書を残す。

Clean Architecture 達人に学ぶソフトウェアの構造と設計

Clean Architecture 達人に学ぶソフトウェアの構造と設計

『Clean Architecture』におけるエンティティ

アンクル・ボブの『Clean Architecture』において、エンティティ*1は以下のように説明されている。

エンティティは、企業全体の最重要ビジネスルールをカプセル化したものだ。エンティティは、メソッドを持ったオブジェクトでも、データ構造と関数でも構わない。企業にあるさまざまなアプリケーションから使用できるなら、エンティティは何であっても問題はない。*2

ビジネスルールをカプセル化したものがエンティティである。そして、それは必ずしもオブジェクト指向モデルによって表現されるものではない。ビジネスルールをカプセル化したものが、複数のアプリケーションから利用されるというのが、アンクル・ボブの描く絵だ。

リレーショナルデータベースはエンティティたり得ないのか

データ構造とその操作によってビジネスルールを表現するということであれば、【リレーショナルモデルにおけるデータ構造+SQLによる操作】を提供するリレーショナルデータベースもまた、エンティティたりうるのではないか、という疑問が生じる。

そもそも、データベースというものは、複数の箇所からアクセスされることを想定したものである点で、単なる永続化手段であるファイルとは異なる。

ファイルはプログラムに「隷属したデータ群」である。一方、データベースはデータをプログラム群から切り離し(=独立させ)、データベース管理システムにより統合して管理・運用しようとするもので、「多数のユーザから同時にアクセス可能な組織体の唯一無二の共有資源」となる。*3

このような見方をすると、データベースというものが「企業にあるさまざまなアプリケーションから使用できる」というエンティティの一つの特質を満たそうとするものであるということがわかる。

「リレーショナルデータベース」はエンティティではない

しかし、別の箇所で、アンクル・ボブはまず、データベースをエンティティと捉えることを明確に否定している。

 アーキテクチャの観点では、データベースはエンティティではない。データベースは詳細であり、アーキテクチャの構成要素として現れることはない。ソフトウェアシステムのアーキテクチャにおけるデータベースの立ち位置は、あなたが住む家におけるドアノブのようなものだ。
 ケンカを売っているように聞こえるかもしれない。実際、論争になったこともある。念のために言っておくが、今話題にしているのはデータモデルのことではない。アプリケーションのデータをどのような構造で扱うかは、システムのアーキテクチャにおいて重要な問題だ。*4

上の記述では、「データベース」を、したがって「リレーショナルデータベース」をエンティティとは認めないという明確な姿勢が示されている。しかし一方で、「データモデル」については別の問題だとし、判断を留保しているように見える。

「リレーショナルモデル」はエンティティのためのデータ構造ではない

しかし、さらに読み進めていくと、アンクル・ボブは、ビジネスルールを取り扱うためのデータ構造として、リレーショナルモデルを認めていないように読める記述がある。

さて、ここで考えてみよう。ディスクが絶滅し、すべてのデータがRAMに格納されるようになったとき、どのようにデータを扱うだろうか?表形式にしてSQLでアクセスする?それともファイルとして保存してディレクトリで管理する?もちろんそんなことはしないだろう。リンクリスト、ツリー、ハッシュテーブル、スタック、キューなどでデータ構造を表現し、ポインタや参照でデータにアクセスするだろう。だって、それがプログラマのやり方なのだから*5

「表形式にしてSQLでアクセスする」のは、リレーショナルモデルのことだろう。上の記述においては、ディスクが絶滅した世界を前提にしているから、インメモリのリレーショナルデータベースをイメージすればよい。SQLビジネスロジックを実装することもできるから、インメモリのリレーショナルデータベースをエンティティとしてビジネスロジックの担い手とすることもできるはずだ。しかし、アンクル・ボブはこれを否定するのである。

むすび:データ構造とデータモデル?

アンクル・ボブは、全てがメモリ上で行われる世界においては、「プログラマのやり方」が用いられるだろうと主張する。それは、「リンクリスト、ツリー、ハッシュテーブル、スタック、キューなど」のことである。これらのデータ構造は、オブジェクトモデルとの必然的結びつきを持たないと思う。

一方で、「リレーショナルモデル」は、上記のデータ構造とは異質なものとしてアンクル・ボブによって扱われている。上記で言われているようなデータ構造よりも、(リレーショナルあるいはオブジェクトといった)データモデルの方がより抽象的だと思うが、いまいち整理ができていない。Wikipediaの「データモデル」の項目には、「データ構造」という節があるが、関係性は明確ではない(単独の「データ構造」という項目も存在する。)。

データ構造とデータモデルについての明晰な整理を自分が欠いているということもあるだろうが、エンティティとしての資格をデータベース更にはリレーショナルモデルに認めないという論述に関しては、「プログラマのやり方」という語彙を含め、アンクル・ボブには誤魔化されているような感覚を覚えるというのが今のところの正直な感想である。

参考

a-suenami.hatenablog.com

*1:『エリック・エヴァンスのドメイン駆動設計』におけるエンティティとは区別されなければならない。

*2:『Clean Architecture』201ページ。

*3:増永良文『リレーショナルデータベース入門 第3版』4-5ページ。

*4:『Clean Architecture』259ページ。

*5:『Clean Architecture』262ページ。強調は原文。

ScalaとJavaにおける変位指定と型パラメータの境界

Scalaのジェネリクスの勉強で、簡単なリストを実装してみよう!というものが出てきた。

詳細は省くが、リストのadd()メソッドは以下のように宣言される。

/* Scala */
abstract class SimpleList[+A] {
  def add[B >: A](element: B): SimpleList[B]
}

ここで、クラスの型の変位の指定として+A、つまり共変(covariant)であることが指定されている。また、add()メソッドでは、境界型パラメータによって対象の型(A)およびそのスーパークラス(B)を引数に取り、また戻り値はその型のリストとなることが指定されている。

本記事では、変位の指定と境界型パラメータについてScalaとJavaを往復しながら見ていく。

ジェネリクスの変位

変位を指定できるのは、Javaのジェネリクスにはない機能だ。Javaのジェネリクスは不変(invariant)となっており、以下のような代入は許されない。いずれもコンパイルエラーとなる。

/* Java */
List<Number> listOfSuper = new ArrayList<Integer>(); 
List<Integer> listOfSub = new ArrayList<Number>();

これに対して、Scalaで変位(上の例の場合は共変)を指定した場合には、以下のような代入が可能になる。CatAnimalのサブクラス)をパラメータにとったSimpleList[Cat]を、AnimalをパラメータにとったSimpleList[Animal]に代入できる。

/* Scala */
class Animal(val age: Int)
class Cat(age: Int) extends Animal(age)
val listOfSuper: SimpleList[Animal] = new SimpleList[Cat](/* 省略 */)

一方、反変(-A)を指定した場合は、型パラメータに代入された型の継承関係とは反対の関係になる。つまり、スーパークラスを型パラメータにとった値を、サブクラスを型パラメータにとった変数に代入することができる(=サブクラスの値をスーパークラスの変数に代入できるのと反対)。また、不変(A)を指定した場合には、Javaにおけるジェネリクスと同様、型パラメータ同士の継承関係にかかわらず、別の型として扱われる。

Use-site variance と Declaration-site variance、境界ワイルドカード型

Javaのジェネリクスは不変だが、境界ワイルドカード型を利用することで、部分的に共変や反変類似の効果を起こすことができる。以下の例では、メソッド定義時に下限境界<? super A>を利用している。

/* Java */
abstract class SimpleList<A> {
  abstract SimpleList<A> filter(Predicate<? super A> predicate);
}

上記のように定義されたメソッドを実際に呼び出すとき、引数predicateにはAまたはそのスーパークラスを型パラメータにとったPredicateのインスタンスを渡すことができる。再び、IntegerとNumberを例に示す。

/* Java */
SImpleList<Integer> list.= new Cons<>(1, new Cons<>(2, new Cons<>(3, new Empty<>()));
SimpleList<Integer> filtered = list.filter(
    new Predicate<Number> {
      @Override
      public boolean test(Number t) {
        return t.intValue() % 2 == 1;
      }
    });

上の例では、Integer型のリストのメソッドfilter(Predicate<? super A> predicate)に対して、Predicate<Number>*1のインスタンスを渡している。これは、Predicate<Number>Predicate<Integer>として扱われうる(=反変)ということではなく、あくまで引数で受け取る側がいずれの型も受け入れているに過ぎない。このように、使用する箇所において変位を指定する方式を、Use-site varianceという。

これに対して、Scalaが採用している方式は、Declaration-site varianceといい、宣言する箇所において変位を指定する。以下のサンプルコードにおけるPredicateでは、宣言時に変位を反変(-T)と指定している。宣言時に指定しているので、使用する箇所の方では単にdef filter(predicate: Predicate[A]): SimpleList[A]とすればよい。

/* Scala */
trait Predicate[-T] {
  def test(element: T): Boolean
}

abstract class SimpleList[+A] {
  def filter(predicate: Predicate[A]): SimpleList[A]
}

Predicateは反変に指定しているので、Predicate[Animal]Predicate[Cat]型の変数に代入できる。以下の例では、このPredicateの性質を利用して、SimpleList[Cat]filter(predicate: Predicate[Cat])メソッドに対して、Predicate[Animal]型のyoungerThan10を渡している。

/* Scala */
val listOfCat: SimpleList[Cat] = Cons(new Cat(4), Cons(new Cat(7), Cons(new Cat(12), Empty)))
val youngerThan10: Predicate[Animal] = new Predicate[Animal] {
  override def test(t: Animal): Boolean = t.age < 10
}
listOfCat.filter(youngerThan10) // Predicate[Animal]はPredicate[Cat]に代入できる(反変)

反変(あるいは下限境界ワイルドカード型)は、共変に比べて直感的にはわかりづらいが、具体的に考えるとわかりやすい。上の例で、filter()メソッドが引数であるPredicate型のオブジェクトに期待しているのは、thisの要素(Catまたはそのサブクラス*2のオブジェクト)についてBooleanの値を戻してくれることだ。個々の要素は、Predicate[MyClass]test(element: MyClass): Booleanに渡される引数になるから、このMyClassCatクラスを代入可能な型であれば、filter()メソッドに渡されるべきPredicateの型パラメータにとって適格だということになる。そのような性質を満たすMyClassは、Cat自身あるいはそのスーパークラスである。

関連して、『Effective Java』でも紹介されているJavaのジェネリクスについての原則で、「PECS(Producer Extends, Consumer Super)」というものがある。PECSは、コレクションを引数に取るメソッドの定義の際に、当該コレクションが要素を供給する側(Producer)であるときにはCollection(? extends MyClass)を、要素を受容する側(Consumer)であるときにはCollection(? super MyClass)を利用せよ、と指示する。要素を受容する側は、自らのメソッド(たとえばadd())の引数に要素を受け取るので、上の例におけるPredicateと同じ状況になる。

反対に、要素を供給する側は、自らのメソッド(たとえばget())の戻り値に要素を与える。この場合、戻り値を受け取る側の型がCatであるとすれば、Cat型の変数に代入できる型を返せればよく、それはつまりCat自身あるいはそのサブクラスだ。それを境界ワイルドカード型で表現すると、<? extends Cat>となる。

型パラメータの境界

境界ワイルドカード型と混同しやすいのが、境界型パラメータである。Javaにおいて、境界型パラメータは、以下のようなものである。

/* Java */
<E extends Number> // 上限境界型パラメータ
// <E super Integer>   // これは存在しない!(下限境界型パラメータ)

境界ワイルドカード型が、どのような型のインスタンスを受け取ることができるかを指定する(Predicate<? extends Animal>には、Predicate<Cat>を代入できる)のに対し、境界型パラメータは、その型パラメータに指定することのできる型を指定する。以下のチュートリアル記載の例では、型パラメータTComparable<T>の実装クラスに制限している。

/* Java */
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

境界ワイルドカード型との大きな違いは、Tを引数の型の指定に利用できるという点だ(だから「パラメータ」なのだ)。Javaの場合は、<? extends MyClass><T extends MyClass>と似た構文になっているため非常にわかりづらい(しかもいずれも使用するときに記述する)のに対し、Scalaでは明快に異なる構文になる。冒頭に掲げたadd()メソッドで利用されている[B >: A]がScalaにおける境界型パラメータだ。

/* 再掲 */
/* Scala */
abstract class SimpleList[+A] {
  def add[B >: A](element: B): SimpleList[B]
}

上のadd()メソッドでは、元々のリストの型パラメータであるA自身あるいはそのスーパークラスを型パラメータBとして利用できるようになっている。これにより、たとえばCatAとした場合のSimpleList[Cat]は、add[Animal](element: Animal): SimpleList[Animal]というメソッドを持っているのと同様の機能を持つ。このメソッドは、猫のリストに(必ずしも猫ではない)動物を追加することができ、追加後の新しいリストは動物のリストとなる、ということを意味している。

先述の通り、Javaにおいて境界型パラメータは上限(extends)のみ存在しており、下限(super)は存在しない。それゆえ、上のScalaのコードに対応する以下のようなコードをJavaで書くことはできない。

/* Java の擬似コード */
abstract class SimpleList<A> {
  // これはコンパイルエラーになる
  abstract SimpleList<B super A> add(B element);
}

下限境界型パラメータがJavaに存在しない理由は、調べてみたがよくわからなかった。

docs.oracle.com

おわりに

以上、ScalaとJavaにおける変位指定と型パラメータの境界について見てきた。今回、Scalaを勉強したことを通じて、曖昧な理解しかしていなかったJavaのジェネリクスについて学びなおすことができた。また、Scalaの下限境界型パラメータを利用したadd()は提示されてみると実に自然な設計で、言語仕様上の実装の制限が設計の考慮の幅を狭めてしまっているということを確認できたいい体験だった。

*1:正確にはそれをもとに作られた匿名クラス。

*2:上の例ではサブクラスは作成していないが。