まえがき
Java 1.0が発表されたのが1996年1月だそうで、かれこれ20年近く経っています。その間に次のようなバージョンの遷移を経て今に至ります。
- Java 1.0(1996年1月23日)
- Java 1.1(1997年2月19日)
- Java 1.2(1998年12月8日)
- Java 1.3(2000年5月8日)
- Java 1.4(2002年2月6日)
- Java 5.0(2004年9月30日)
- Java 6(2006年12月11日)
- Java 7(2011年7月28日)
- Java 8(2014年3月18日)
今回、とある事情でJavaの言語仕様の変化を時系列で把握する必要があったので、ついでに記事のネタとしてまとめる事にしました。
どのようにすれば分かりやすいかを考えた結果、ひとつのソースコードをJavaのバージョンアップ毎にリファクタリングしていくといったストーリーでまとめる事にしました。言語仕様に着目して書いていますので、新たに導入されたクラスやAPIについては記載しません(一部、書いていますが)。ひとつのソースコードで言語仕様の変化を全て説明しようとしているため、ちょっと元のソースコードに無理がありますが、ご容赦ください。
Java 1.2以前については良く分からないのとJDKが入手できなかったので、Java 1.3からスタートです。
それでは、Java言語仕様の15年の進化をお楽しみください。
Java 1.3
まず、ベースとなるJava 1.3のソースコードです。コレクションに格納される型の指定が無く、今見ると不安になります。
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; public class Test { private static final int CNT = 1000000; private static final String ODD = "ODD"; private static final String EVEN = "EVEN"; public static void main(String[] args) { String param = args[0]; List list; Map result; if ("gen".equals(param)) { // あまり意味のないif文。後の説明用。 list = generate(); } else if ("genAndCalc".equals(param)) { list = generate(); result = sum(list); } else if ("genAndCalcAndOutput".equals(param)) { list = generate(); result = sum(list); writeResult(result, "C:\\result\\result.txt"); } } private static List generate() { List ret = new ArrayList(); Random random = new Random(1); for (int i = 0; i < CNT; i++) { int val = random.nextInt(1000); if (val > Integer.MAX_VALUE || val < Integer.MIN_VALUE) { throw new UnsupportedOperationException("value range error"); } ret.add(new Integer(val)); } return ret; } private static Map sum(List list) { Map map = new HashMap(); map.put(ODD , new Integer(0)); map.put(EVEN, new Integer(0)); for (int i = 0; i < list.size(); i++) { Integer val = (Integer)list.get(i); int intVal = (val).intValue(); String procType; if (intVal % 2 != 0) { procType = ODD; } else { procType = EVEN; } int tmp = ( (Integer)map.get(procType)).intValue(); tmp += intVal; map.put(procType, new Integer(tmp)); } return map; } private static void writeResult(Map result, String path) { StringBuffer sb = new StringBuffer(); sb.append("["); sb.append(result.get(ODD)); sb.append(","); sb.append(result.get(EVEN)); sb.append("]"); String resultStr = sb.toString(); File File = new File(path); OutputStream os = null; try { os = new FileOutputStream(File); os.write(resultStr.getBytes()); } catch (FileNotFoundException e) { System.err.println("file not found"); e.printStackTrace(); } catch (UnsupportedEncodingException e) { // 本当は発生しない。後の説明用。 System.err.println("encoding error"); e.printStackTrace(); } catch (IOException e) { System.err.println("io error"); e.printStackTrace(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { System.err.println("finally error"); e.printStackTrace(); } } } } }
Java 1.3からJava 1.4へリファクタリング
次に、Java 1.4のソースコードです。assertの追加だけの、地味な変更です。
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; public class Test { private static final int CNT = 1000000; private static final String ODD = "ODD"; private static final String EVEN = "EVEN"; public static void main(String[] args) { String param = args[0]; List list; Map result; if ("gen".equals(param)) { // あまり意味のないif文。後の説明用。 list = generate(); } else if ("genAndCalc".equals(param)) { list = generate(); result = sum(list); } else if ("genAndCalcAndOutput".equals(param)) { list = generate(); result = sum(list); writeResult(result, "C:\\result\\result.txt"); } } private static List generate() { List ret = new ArrayList(); Random random = new Random(1); for (int i = 0; i < CNT; i++) { int val = random.nextInt(1000); // Java 1.4からassert機能が追加される assert val > Integer.MAX_VALUE || val < Integer.MIN_VALUE : "value range error"; ret.add(new Integer(val)); } return ret; } private static Map sum(List list) { Map map = new HashMap(); map.put(ODD , new Integer(0)); map.put(EVEN, new Integer(0)); for (int i = 0; i < list.size(); i++) { Integer val = (Integer)list.get(i); // Java 1.4からassert機能が追加される assert val != null : "value is null"; int intVal = (val).intValue(); String procType; if (intVal % 2 != 0) { procType = ODD; } else { procType = EVEN; } int tmp = ( (Integer)map.get(procType)).intValue(); tmp += intVal; map.put(procType, new Integer(tmp)); } return map; } private static void writeResult(Map result, String path) { StringBuffer sb = new StringBuffer(); sb.append("["); sb.append(result.get(ODD)); sb.append(","); sb.append(result.get(EVEN)); sb.append("]"); String resultStr = sb.toString(); File File = new File(path); OutputStream os = null; try { os = new FileOutputStream(File); os.write(resultStr.getBytes()); } catch (FileNotFoundException e) { System.err.println("file not found"); e.printStackTrace(); } catch (UnsupportedEncodingException e) { // 本当は発生しない。後の説明用。 System.err.println("encoding error"); e.printStackTrace(); } catch (IOException e) { System.err.println("io error"); e.printStackTrace(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { System.err.println("finally error"); e.printStackTrace(); } } } } }
Java 1.4からJava 5.0へリファクタリング(警告を消すだけ)
次に、Java 5.0のソースコードです。総称型(ジェネリックス)の導入に伴い、Java 1.4時代のソースコードは警告だらけになります。まず、警告を消すだけの方法を紹介します。警告を消すにはJava 5.0から導入されたアノテーションを使用します。今回は触れませんが、アノテーションは総称型のエラーを消す以外にも様々な用途があります。例えば自分で作ったり、APIを通してどんなアノテーションが付与されているか調べることもできます。
(余談ですが、私がJavaに携ったのがこのバージョンからです)
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; public class Test { private static final int CNT = 1000000; private static final String ODD = "ODD"; private static final String EVEN = "EVEN"; // Java 5.0からアノテーションが追加される。メタデータとして属性値を設定可能。 // これはメソッドにつけているが、クラスや変数にも付けられる。 @SuppressWarnings("rawtypes") public static void main(String[] args) { String param = args[0]; List list; Map result; if ("gen".equals(param)) { // あまり意味のないif文。後の説明用。 list = generate(); } else if ("genAndCalc".equals(param)) { list = generate(); result = sum(list); } else if ("genAndCalcAndOutput".equals(param)) { list = generate(); result = sum(list); writeResult(result, "C:\\result\\result.txt"); } } // ひとつのアノテーションに複数の属性値を複数指定する場合はこんな感じ。 @SuppressWarnings({"rawtypes", "unchecked"}) private static List generate() { List ret = new ArrayList(); Random random = new Random(1); for (int i = 0; i < CNT; i++) { int val = random.nextInt(1000); assert val > Integer.MAX_VALUE || val < Integer.MIN_VALUE : "value range error"; ret.add(new Integer(val)); } return ret; } // 属性がvalueひとつのアノテーションは「単一値アノテーション」といって、属性名を省略可能。 // 今回は「value =」を省略せずに書いた例。 @SuppressWarnings(value = { "rawtypes", "unchecked" }) private static Map sum(List list) { Map map = new HashMap(); map.put(ODD , new Integer(0)); map.put(EVEN, new Integer(0)); for (int i = 0; i < list.size(); i++) { Integer val = (Integer)list.get(i); assert val != null : "value is null"; int intVal = (val).intValue(); String procType; if (intVal % 2 != 0) { procType = ODD; } else { procType = EVEN; } int tmp = ( (Integer)map.get(procType)).intValue(); tmp += intVal; map.put(procType, new Integer(tmp)); } return map; } private static void writeResult( // アノテーションは仮引数や変数にも付けられる。 @SuppressWarnings("rawtypes") Map result, String path) { StringBuffer sb = new StringBuffer(); sb.append("["); sb.append(result.get(ODD)); sb.append(","); sb.append(result.get(EVEN)); sb.append("]"); String resultStr = sb.toString(); File File = new File(path); OutputStream os = null; try { os = new FileOutputStream(File); os.write(resultStr.getBytes()); } catch (FileNotFoundException e) { System.err.println("file not found"); e.printStackTrace(); } catch (UnsupportedEncodingException e) { // 本当は発生しない。後の説明用。 System.err.println("encoding error"); e.printStackTrace(); } catch (IOException e) { System.err.println("io error"); e.printStackTrace(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { System.err.println("finally error"); e.printStackTrace(); } } } } }
Java 1.4からJava 5.0へリファクタリング(正しい修正)
次に、Java 5.0の正しい修正方法です。総称型(ジェネリックス)の導入に伴い、ListやMapの宣言で格納される型を限定でき、その結果としてキャストも省略できます。また、オートボクシング・アンボクシングでプリミティブラッパークラスを使った明示的な型変換も不要となります。staticインポート、列挙型(enum)、拡張for文(for-each構文)、可変長引数が導入されたのもJava 5.0からです。
// Java 5.0からstaticインポートが導入される import static java.lang.Integer.MAX_VALUE; import static java.lang.Integer.MIN_VALUE; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; public class Test { private static final int CNT = 1000000; // Java 5.0から列挙型(enum)が導入される private enum NumType { ODD, EVEN } public static void main(String[] args) { String param = args[0]; // Java 5.0から総称型(ジェネリックス)が導入される // 変数宣言の場合 List<Integer> list; Map<NumType, Integer> result; if ("gen".equals(param)) { // あまり意味のないif文。後の説明用。 list = generate(); } else if ("genAndCalc".equals(param)) { list = generate(); result = sum(list); } else if ("genAndCalcAndOutput".equals(param)) { list = generate(); result = sum(list); writeResult(result, "C:\\result\\result.txt"); } } // 総称型(ジェネリックス) // (戻り値宣言の場合) private static List<Integer> generate() { // インスタンスの生成時にも総称型を指定 List<Integer> ret = new ArrayList<Integer>(); Random random = new Random(1); for (int i = 0; i < CNT; i++) { int val = random.nextInt(1000); // Java 5.0からstaticインポートが導入され、クラス名の修飾が不要に assert val > MAX_VALUE || val < MIN_VALUE : "value range error"; // Java 5.0からオートボクシング・アンボクシングが導入される // Listにint型をaddしている ret.add(val); } return ret; } private static Map<NumType, Integer> sum(List<Integer> list) { // 総称型(ジェネリックス) Map<NumType, Integer> map = new HashMap<NumType, Integer>(); // 総称型(ジェネリックス) // Mapにintをput(オートボクシング) map.put(NumType.ODD , 0); // Mapにintをput(オートボクシング) map.put(NumType.EVEN, 0); // Java 5.0から拡張for文(for-each構文)が導入される for (Integer val : list) { assert val != null : "value is null"; NumType numType; // Integerの余りを計算(オートアンボクシング) if (val % 2 != 0) { numType = NumType.ODD; } else { numType = NumType.EVEN; } int tmp = map.get(numType); // MapからIntegerを取り出してintへ格納(オートアンボクシング) tmp += val; // intにIntegerを加算(オートアンボクシング) map.put(numType, tmp); // Mapにintをput(オートボクシング) } return map; } // 総称型(ジェネリックス)が導入される private static void writeResult(Map<NumType, Integer> result, String path) { String resultStr = join("[", "]", ",", result.get(NumType.ODD), result.get(NumType.EVEN)); File File = new File(path); OutputStream os = null; try { os = new FileOutputStream(File); os.write(resultStr.getBytes()); } catch (FileNotFoundException e) { System.err.println("file not found"); e.printStackTrace(); } catch (UnsupportedEncodingException e) { // 本当は発生しない。後の説明用。 System.err.println("encoding error"); e.printStackTrace(); } catch (IOException e) { System.err.println("io error"); e.printStackTrace(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { System.err.println("finally error"); e.printStackTrace(); } } } } /** * 説明のために作ったメソッドなので少し無理がある。。。 */ // Java 5.0から可変長引数が導入される // コード上は配列として扱う private static String join(String prefix, String suffix, String delimiter, int... words) { // ※ 言語仕様の拡張ではないが、Java 5.0からStringBuilderが導入された // シングルスレッドであればStringBuilder、マルチスレッドではStringBufferと使い分ける StringBuilder sb = new StringBuilder(); sb.append(prefix); for (int i = 0; i < words.length - 1; i++) { sb.append(words[i]); sb.append(delimiter); } sb.append(words[words.length - 1]); sb.append(suffix); return sb.toString(); } }
Java 5.0からJava 6へリファクタリング
コーディング上の変更点はありません。コードも省略します。
Java 6からJava 7へリファクタリング
大きな変更点としてはリソース付きtry文(try-with-resources statement)と例外のマルチキャッチでしょうか。Javaで煩わしかった例外処理とcloseする必要があるリソースの扱いがずっと楽になります。
あとはリテラルが拡張されたり、ダイアモンド演算子による総称型(ジェネリックス)の型宣言を省略できたり、switch文でString型を使えるようになったりと、コーディングが楽になる小規模拡張です。
import static java.lang.Integer.MAX_VALUE; import static java.lang.Integer.MIN_VALUE; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; public class Test { // Java 7からリテラルが拡張され、数値リテラルにアンダースコアを入れられるようになった。 // ここには登場しないが、バイナリリテラル(0bで始まる0と1の文字列)も導入された。 private static final int CNT = 1_000_000; private enum NumType { ODD, EVEN } public static void main(String[] args) { String param = args[0]; List<Integer> list; Map<NumType, Integer> result; // Java 7からswitch文で文字列が使用可能に switch (param) { case "gen": list = generate(); break; case "genAndCalc": list = generate(); result = sum(list); break; case "genAndCalcAndOutput": list = generate(); result = sum(list); writeResult(result, "C:\\result\\result.txt"); break; } } private static List<Integer> generate() { // Java 7からダイアモンド演算子が導入され、右辺の型指定を省略可能に // (左辺で型が明確になる場合のみ) List<Integer> ret = new ArrayList<>(); Random random = new Random(1); for (int i = 0; i < CNT; i++) { int val = random.nextInt(1000); assert val > MAX_VALUE || val < MIN_VALUE : "value range error"; ret.add(val); } return ret; } private static Map<NumType, Integer> sum(List<Integer> list) { // ダイアモンド演算子による右辺の型指定の省略 Map<NumType, Integer> map = new HashMap<>(); map.put(NumType.ODD , 0); map.put(NumType.EVEN, 0); for (Integer val : list) { assert val != null : "value is null"; NumType numType; if (val % 2 != 0) { numType = NumType.ODD; } else { numType = NumType.EVEN; } int tmp = map.get(numType); tmp += val; map.put(numType, tmp); } return map; } private static void writeResult(Map<NumType, Integer> result, String path) { String resultStr = join("[", "]", ",", result.get(NumType.ODD), result.get(NumType.EVEN)); File File = new File(path); // Java 7からリソース付きtry文(try-with-resources statement)が導入される // AutoCloseableインターフェースを実装している場合はリソースの開放を実装する必要がない! try (OutputStream os = new FileOutputStream(File)) { os.write(resultStr.getBytes()); } catch (FileNotFoundException | UnsupportedEncodingException e) { // ↑Java 7から例外のマルチキャッチが導入される System.err.println("file not found or encoding error"); e.printStackTrace(); } catch (IOException e) { System.err.println("io error"); e.printStackTrace(); } } /** * 説明のために作ったメソッドなので少し無理がある。。。 */ private static String join(String prefix, String suffix, String delimiter, int... words) { StringBuilder sb = new StringBuilder(); sb.append(prefix); for (int i = 0; i < words.length - 1; i++) { sb.append(words[i]); sb.append(delimiter); } sb.append(words[words.length - 1]); sb.append(suffix); return sb.toString(); } }
Java 8
はい、革命来ました。ラムダ式の導入です。
ただ、言語仕様としてはラムダ式とメソッド参照の導入位の変更です。あと、ここには登場しませんがインターフェースにデフォルトメソッドが定義できるように拡張されています。
言語仕様の拡張自体は最低限に抑えられているけど、それを活用するためのStream系のクラス/APIが大量に新設されており、それによってコーディングスタイルがガラッと変わっているといった印象です。
ただ、ラムダ式を使わないといけない訳ではなく、当然ですがJava 7までの書き方でもコンパイルが通ります。とは言っても、Javaエンジニアは今後はラムダ式やストリームと付き合わざるを得ないでしょうね。自分が書かなくてもサンプルコードとしてラムダやストリームを使ったものが出回るでしょうし、ライブラリについても同様です。
言語仕様の拡張ではないですが、新たに追加されたStringJoinerクラスが自分の好みです。
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.StringJoiner; import java.util.stream.Collectors; import java.util.stream.IntStream; import annotation.Sample; public class Test { private static final int CNT = 1_000_000; private enum NumType { ODD, EVEN } public static void main(String[] args) { String param = args[0]; List<Integer> list; Map<NumType, Integer> result; switch (param) { case "gen": list = generate(); break; case "genAndCalc": list = generate(); result = sum(list); break; case "genAndCalcAndOutput": list = generate(); result = sum(list); writeResult(result, "C:\\result\\result.txt"); break; } } private static List<Integer> generate() { List<Integer> ret = new ArrayList<>(); Random random = new Random(1); // 元のfor文をJava 8のStreamとラムダ式で書き換えたが、 // これだと元のfor文と等価じゃない気がするのでこれで置き換えるのは違う気もする // じゃ、どうすべきかというと、良く分からない IntStream.range(0, CNT).forEach(s -> ret.add(random.nextInt(1000))); return ret; } // Java 8から総称型の型パラメタにもアノテーションが付与可能に private static Map<@Sample NumType, Integer> sum(List<Integer> list) { // Java 8から導入されたStreamを使用して奇数と偶数をグループ化して集計 // SQLをイメージすると分かりやすい、のか? // インデントがこれでいいのかは良く分からん Map<NumType, Integer> map = list.stream() .collect( Collectors.groupingBy( value -> value % 2 != 0 ? NumType.ODD : NumType.EVEN, // ラムダ式 Collectors.summingInt(Integer::intValue) // メソッド参照 ) ); return map; } private static void writeResult(Map<NumType, Integer> result, String path) { String resultStr = join("[", "]", ",", result.get(NumType.ODD), result.get(NumType.EVEN)); File File = new File(path); try (OutputStream os = new FileOutputStream(File)) { os.write(resultStr.getBytes()); } catch (FileNotFoundException | UnsupportedEncodingException e) { System.err.println("file not found or encoding error"); e.printStackTrace(); } catch (IOException e) { System.err.println("io error"); e.printStackTrace(); } } /** * 説明のために作ったメソッドなので少し無理がある。。。 */ private static String join(String prefix, String suffix, String delimiter, int... words) { // ※ 言語仕様の拡張ではないが、java 8からStringJoinerが導入された。 // 接頭語、接尾語、区切り文字、中の要素を指定したらつなげた文字列を生成してくれる。 // これ、良いですね。 StringJoiner stringJoiner = new StringJoiner(delimiter, prefix, suffix); IntStream intStream = IntStream.of(words); // プリミティブな変数には専用のStreamが用意されている intStream.forEach(word -> stringJoiner.add(Integer.toString(word))); // ラムダ式 return stringJoiner.toString(); } }
あとがき
Javaの言語仕様は、Java 5.0で大きく進化しJava 7は正統進化、Java 8でようやく時代をキャッチアップって感じでしょうか。
こうやって歴史を振り返ると、もっとも多くのユーザを抱えるJava言語が、後方互換性に苦慮しながらもしっかり進化しているのが分かって面白いです。ラムダ式の導入に関しては、たくさんの賢い人たちが議論を重ねてようやく導入された経緯を見てきているので、よくぞここまで辿り着いたといった印象です。
Java 8の進化の方向性は、とても多くの意味を含んでいるように思います。おそらく今までとは全然違った考え方のWEBフレームワークが登場したり、並列処理のライブラリが生まれたりする気がします(Javaに対する世の中の情熱が残っていればですが)。あと、Javaの弟言語であるScalaの将来や、積極的にオープンな方向に舵を切り始めたMicrosoftのC#とも無関係では無い気もします。
…というのは大げさかも知れませんが、こういった事を考えるネタを提供してくれるJava 8は、Javaの20年の歴史の中でもっとも壮大で、面白くて、挑戦的で、やっかいなアップデートだったと私は思うのです。
語り過ぎましたね。では続きは20年後に。
JAVAのアップデートの歴史、とても興味深かったです!
師子乃さん
コメントありがとうございます。
興味深く読んでいただいたようで良かったです。