Enumeration を for-each ループする方法

Twitterで @RayStark77 氏に教えてもらった方法

var v = new java.util.Vector<String>();
java.util.Enumeration<String> e = v.elements();
for (String s : (Iterable<String>)e::asIterator) {
    // ...
}

Enumeration#asIterator() がJava9からのメソッドなので Java9 以降で利用可能。


構文の解説

for (String s : (Iterable<String>)e::asIterator) 

この部分、構文的には少々難しい。

まず拡張for構文。for-eachとも呼ばれる。Java言語仕様としては
14.14.2. The enhanced for statement
を参照されたし。コロン(:)の左側にループ内で用いる変数宣言、右側はjava.lang.Iterableか、配列である必要がある。
ここで

(Iterable<String>)e::asIterator

がどう解釈されるかだが、メソッド参照 e::asIterator が関数型インターフェース Iterable である、というのが概要だ。

まず関数型インターフェースとは何か?について具体例はひしだまさんのサイトに任せるとして、大雑把には未実装の抽象メソッドが1つだけのinterfaceと思えばよい。Iterableはこの条件を満たしていて

Iterator iterator()

Iterable (Java SE 11 & JDK 11 )

というメソッドがその抽象メソッドだ。紛らわしいがIterableのiteratorメソッドがIterator型を返す。

ここで、メソッド参照 e::asIterator はIterator型を返すメソッドだから、関数型インターフェースIterableとなることができて、拡張for文のループ対象となることができる、という仕組み。

メソッド参照が Iterator<String> iterator() と合致するかのコンパイル時動作は15.13.1. Compile-Time Declaration of a Method Referenceに詳細が記載されている。今回のケースだと e::asIterator は Enumeration のインスタンスメソッドから asIterator を探し、 Iterator<String> iterator() と合致する引数・戻り値が適合するものがあるかを調べられる。asIterator() は e、ここでは Enumeration<String> のメソッドだから

  • 引数はなし : 合致
  • 戻り値は Iterator<String> : 合致

ということで、無事合致することができる。

歴史的経緯

java.util.Enumerationは最初期のJava1.0 (1995年)から存在するinterfaceなのだが、Java1.2 (1997年)にコレクションフレームワークが導入された際にその役割を
java.util.Iterator に譲った。deprecated がつく非推奨ではないものの、

新しい実装では、Enumerationに優先してIteratorを使用することを検討する必要があります。

https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/Enumeration.html

と記されている通り、新規利用は推奨されていない。なので、バージョンの移行のタイミングなどもあるかもしれないが2000年以降では新規にEnumerationが用いられたコードが書かれることは稀で次第に滅びていくハズ……であったがシェアの高いライブラリに残存しているがために現代でも遭遇してしまう。代表例としてはServletHttpServletRequest.html#getHeaderNames() などで、これはServletの設計がJava1.1時代に行われたことに依る。

Java ServletAPIというのは、当時のSun Microsystemsが定めているが、これはServletを動かす際のAPIであって、サーバーとしての実装は別に各サードパーティーが作って売っていた。Servlet APIに則って作られたプログラムは、どのServletコンテナでも動く。(もっとも各社それぞれ独自機能を提供していたため、それらに依存すれば単純に載せ替えができないが)

この状況はつまり、Servlet API の変更が極めて行いにくい状況を産み出し、20年前の遺恨が現代でも残される状況となった。

なぜキャストを書かないといけないのか?

for (String s : (Iterable<String>)e::asIterator) 

for (String s : e::asIterator) 

と書けないのはなぜなのか?

これは言語仕様には明示されていて

It is a compile-time error if a method reference expression occurs in a program in someplace other than an assignment context (§5.2), an invocation context (§5.3), or a casting context (§5.5).

15.13. Method Reference Expressions

代入、メソッドやコンストラクタの呼び出し、キャスト以外に現れたメソッド参照はコンパイルエラーとなります

このように明示的に規定されている以上、拡張for文には直接書くことができない。

拡張for文

拡張for文が導入されたのはJava5 (2004年)で、同時に導入された機能としてはジェネリクスがある。

再掲になるが拡張for文 14.14.2. The enhanced for statement はコロン(:)の右側はjava.lang.Iterableか、配列である必要がある。

配列をjava.lang.Iterableとしてしまえば良かったのだが、プリミティブ型の配列の問題があって int[] を Iterable<int> とできなかったのだろう。とはいえ、拡張for文はIterableか配列であることが決まっているのだから、型の推論はできるのではないか?と考えられるところだが、OpenJDKコミッタ筋からの情報によると、「Brianが昔できるけどやらないって言ってましたね」とのこと。このBrianというのはJava言語の開発責任者の Brian Goetz氏のことで、この推論をやることになんらかの躊躇があったのだろう。