Java8とC#
このエントリーは「C# Advent Calendar 2013」の17日目のエントリーです。
前日は id:ksasao さんの「GDI+ で描画&保存あれこれ - まちみのな@はてな」でした。
Java 8は2014年3月にリリースされる予定です。
どういう変更が含まれているのかは、Java 8のすべてにまとめられています。
また、きしださん(id:nowokay)のように、Java8に関する記事をたくさん書いている人もちらほらいます。
この記事では、InfoQの記事(および参照元の英語ブログ)を参考に、Java8の新機能をC#と比べてみたいと思います。
インターフェースの改善
インターフェースでstaticメソッドが定義できるようになった
もともと、JavaのインターフェースはC#のインターフェースと違って、static final
なフィールドを持つことができました。
static final
なフィールドというのは、C#で言うと、const
で修飾された定数と、static readonly
で修飾された読み取り専用フィールドの両方に対応します。
Java 8では、static final
なフィールドに加えて、static
なメソッドの実装も含むことができるようになっています。
C#のインターフェースは、メソッド、プロパティ、イベント、インデクサの宣言しかできません。フィールドとかメソッド実装とかは、インターフェースとは別のクラス(たとえばstaticクラス)に定義するしか方法がありません。まあ、それでも用は足りるでしょうが、別のクラスを定義するのが面倒な人もいるでしょうね。
デフォルトメソッドの定義が可能になった
こっちは、static
なメソッドではなく、インスタンスメソッドです。宣言だけではなくてデフォルト実装を定義できるようになります。デフォルト実装があるメソッドは、実装クラス(implements
するクラス)に実装を定義しなくても、そのメソッドがインスタンスメンバーとして定義されているかのように動きます。
なぜデフォルトメソッドか?というと、interfaceに新しいメソッドを追加したいんだけれども、ただ追加すると既存の実装クラスが全部コンパイルできなくなってしまうからできない、そこでメソッドのデフォルト実装が提供できれば、既存の実装クラスに影響を与えずに新しいメソッドを追加できる、と、こういう理屈です。
C#の場合は、後からメソッドを追加したいという要求には、拡張メソッドという解決法が提供されています。LINQの標準クエリ演算子がその代表例ですね。拡張メソッドの場合は、「元の型の実装を全く変えずに、第3者が拡張メソッドを提供できる」という特徴があります。Java 8ではそのようなことはできません(デフォルトメソッドを定義できるのはインターフェース提供者のみ)が、そのかわり、「メソッド実装の多重継承ができる」「デフォルト実装をオーバーライドすることで、より適した実装に差し替えることができる」という特徴があります。
public interface Foo { // 定数 public const int INT_CONST = 10; // 読み取り専用静的フィールド public static List<String> LIST_FIELD = new ArrayList<String>(); // 【Java8】staticメソッドの実装 public static int getSize() { return LIST_FIELD.size(); } // 抽象メソッド public String bar(); // 【Java8】メソッドのデフォルト実装 public default int baz() { return 42; } }
C#だとこんな感じですが、定数、静的フィールド、staticメソッドがIFoo
と無関係になってしまいますね。
public interface IFoo { string Bar(); } public static class Foo { // 定数 public const int IntConst = 10; // 読み取り専用静的フィールド public static readonly List<string> ListField = new List<string>(); // staticメソッドの実装 public static int GetSize() { return ListField.Count; } // 拡張メソッド public static int Baz(this IFoo @this) { return 42; } }
関数型インターフェース
C# で言うところのデリゲート(delegate
)に相当する型は、Java 8では「関数型インターフェース」という名前のインターフェースです。
関数型インターフェースは、toString()
以外の抽象メソッドを1つだけ持つインターフェースのことです。
@FunctionalInterface
というアノテーションをインターフェースにつけておくと、そのインターフェースが関数型インターフェースでないとコンパイルエラーになります。
ただし、@FunctionalInterface
がなくてもコンパイルは通りますし、後述するラムダ式やメソッド参照も使うことができます。まあなんというか、設計意図をコードに埋め込むためだけのものですね。
C# で言うところのデリゲート相当とは言いましたが、C#みたいに+
演算子や-
演算子を適用して結合したり削除したりすることはできませんし、BeginInvoke
やEndInvoke
による別スレッド呼び出しなんかもできません。まあC#でもこのあたりの機能は黒歴史というか、なくてもいいので……
@FunctionalInterface public interface Foo { // さっきのFooインターフェースと同じように、 // 定数やフィールドや静的メソッドやデフォルトメソッドを // 定義していたとしても、 // 抽象メソッドが1つだけであればOK public String bar(); }
public delegate string Foo();
ラムダ
C# のラムダによく似た記法のラムダ式が入りました。
goes toの記号が違うだけで、後はほぼ同じです。
Javaとしては、ラムダがなくても匿名内部クラスがあればほぼ同等のことは実現できるのですが、やはり記述が面倒くさいということで、より簡潔に書ける記法としてラムダが導入されています。
ただし、ラムダで書いたときと匿名内部クラスで書いたときは、コンパイル結果が異なります(ただのシンタクティックシュガーではないです)。ここも掘ると面白いのですが、C#とは関係ない世界なのでやめておきましょうか。
(int x, int y) -> { return x + y; } (x, y) -> x + y x -> x * x () -> x x -> { System.out.println(x); }
C#の場合はdelegate
キーワードを使った匿名メソッドによる記法も有効です。でもまあラムダでいいでしょう。
(int x, int y) => { return x + y; } (x, y) => x + y x => x * x () => x x => { Console.WriteLine(x); } // 匿名メソッド delegate(int x, int y) { return x + y; } // 引数を使わない場合は省略できる謎仕様 Func<int, bool> pred = delegate { return true; };
メソッド参照
C#のデリゲートが名前付きメソッドに関連付けることができるのとほぼ同じです。
String::valueOf // x -> String.valueOf(x) Object::toString // x -> x.toString() x::toString // () -> x.toString() ArrayList::new // () -> new ArrayList<>()
C#の場合は::
じゃなくて.
ですね。
ただし、C#の場合はコンストラクタをデリゲートに入れる方法がありません。
Convert.ToString // x -> Convert.ToString(x) x.ToString // () -> x.ToString()
ラムダとキャプチャ
Javaの場合は、匿名内部クラスと同じように、final
または実質的なfinal
である変数しかキャプチャできません。
つまり、読み取り専用変数を読み取ることしかできないということなので、いわゆるクロージャとは若干異なります。
C#ではそういう制限はありません。メンバー変数やローカル変数を書き換えることが可能です。
java.util.function
Java 8には、Function<T, R>
、Predicate<T>
、Consumer<T>
、Supplier<T>
、BinaryOperator<T>
といった関数型インターフェースが標準ライブラリに入りました。
C#のデリゲートとの対応を書いてみましょうか。
Java 8 関数型インターフェース | C# デリゲート |
---|---|
Function<T, R> |
Func<T, TResult> |
Predicate<T> |
Func<T, bool> またはPredicate<T> |
Consumer<T> |
Action<T> |
Supplier<T> |
Func<T> |
BinaryOperator<T> |
Func<T, T, T> |
C#のLINQの場合は、Func<T>
およびAction<T>
の汎用デリゲートに寄せていますが、Java 8では使われ方を表すインターフェースが用意されているようです。
個人的には、C#の汎用デリゲートは「静的型付け言語では型がドキュメントになる」とは言えないので、そんなに好きじゃないです。
java.util.stream
要素のシーケンスで、その要素は必要になって初めて取り出されます。C# で言うところのIEnumerable<T>
と、それに対する拡張メソッド群、すなわちLINQに相当します。
APIドキュメントの非公式翻訳がjava.util.stream (java.util.stream API仕様 b128 非公式翻訳)にあります。
この、ネームスペース解説文が網羅的で良いと思います。
高階関数処理と列挙
C# では、IEnumerable<T>
に対して列挙もできるし、LINQ、つまり高階関数による処理もできるのですが、Javaの場合は、列挙用のインターフェース Iterable<T>
と 高階関数処理用のインターフェース Stream<T>
が分かれています。
これはどうしてでしょうか?
理由のひとつは、ジェネリクスの実装方式が違うためです。
C#では、すべてのジェネリックコレクションと配列がIEnumerable<T>
を実装しますが、Javaの場合は、配列はIterable<T>
インターフェースを実装しません*1。なので、もしIterable<T>
を高階関数処理の対象とすると、配列をIterable<T>
に変換する必要が出てきます。
ところがところが、Javaのジェネリクスはプリミティブ型(値型)に対応していないという制約があります。なので、たとえばint[]
を変換するとしたら、int
のラッパークラスであるInteger
を列挙するIterable<Integer>
に変換することはできるかもしれませんが、そうするとボックス化/ボックス化解除が発生するのでパフォーマンスは落ちます。
ボックス化/ボックス化解除を無くしたい場合は、Iterable<T>
とは別の型、たとえばIntIterable
みたいなプリミティブ型に特化した型*2を新たに作って、そこに変換するしかありません。でもこの型、列挙のためではなくって、高階関数処理のために作ったんだよね……
というわけで、どうせプリミティブ型向けの高階関数処理インターフェースは新たに作らなければならないのだから、いっそのこと全部新しいインターフェースでいいじゃないか、というわけで、オブジェクト用にはStream<T>
を、プリミティブ型にはIntStream
、LongStream
、DoubleStream
を用意したというわけです。
理由のもう一つは、こちらの方が重要なのですが、要素へのアクセス方法と高階関数処理を明確に分離することで、データ構造に適した形で高階関数処理の実装を提供するためです。
C#では、たとえ元のコレクションが木構造だったとしても、Select()
などのLINQを適用するとIEnumerable<T>
を経由することになってしまい、構造が壊れます。
Java 8では、高階関数処理メソッドは抽象メソッドで、デフォルト実装もありません。木構造用のStream<T>
実装を用意すれば、構造を壊さずにmap()
などのメソッドを呼び出すことができるでしょう。
reduceとcollect
進捗ダメです。後で書きます。
ジェネリック型インターフェースの改善
InfoQの記事によれば、推論が賢くなったみたいです。
C#はまだ貧弱ですね。
class Program { static void Main(string[] args) { // Java 8みたいに Foo(Utility.Bar()); とは書けない Foo(Utility.Bar<FooBar>()); // Java 8みたいに Utility.Foo().Bar(); とは書けない Utility.Foo<FooBar>().Bar(); } static void Foo(FooBar fb) { Console.WriteLine(fb.Baz); } } class FooBar { public string Baz = "Baz"; public void Bar() { Console.WriteLine("Bar"); } } static class Utility { public static T Foo<T>() where T : class, new() { return new T(); } public static T Bar<T>() where T : class, new() { return new T(); } }
java.time
きしださんによれば、「めんどくささが増しつつ非常に高機能になりました。」とのことです。
C#だと、ローカル時間とUTCの区別だけできるDateTime
構造体、時差を含められるDateTimeOffset
構造体、時刻または時間範囲を表すTimeSpan
構造体、
タイムゾーンを表現するTimeZone
クラスとTimeZoneInfo
クラス、夏時間の期間を表現するDaylightTime
クラス、1つ以上の時代(年号)をもつ暦を表現するCalendar
とその派生クラスで和暦を表現するJapaneseCalendar
、とこんなところですかね。
C#でもJavaでも、このあたりは非常にややこしいです。C#の場合はDateTime
構造体からDateTimeOffset
構造体へは暗黙の型変換がありますが、TimeZone
クラスとTimeZoneInfo
クラスには互換性はないですね。
Collections APIの拡張
Iterable
、Collection
、List
、Map
といったコレクションインターフェースにいくつかデフォルトメソッドが追加されました。重要なのはストリームに変換するstream()
メソッドや並列ストリームに変換するparallelStream()
メソッドですが、他にも関数型インターフェースを引数にとるforEach()
なんかも追加されています。
C#の場合は、すべてのジェネリックコレクションと配列はIEnumerable<T>
を実装しているので、「ストリームに変換」する必要はありません。ただし、PLINQで並列処理したい場合は、ParallelEnumerable
静的クラスに定義されたAsParallel()
拡張メソッドを呼び出す必要があります。
それと、IEnumerable<T>
にはForEach()
拡張メソッドは用意されていません(LINQに副作用を持ち込みたくないということらしいです)。ForEach()
したいときはList<T>
クラスを使うか、自分で拡張メソッドを書くか、どちらかですね。
Concurrency APIの拡張
並行プログラミングAPIが拡充されてます。上で書いた並列ストリームに関係する機能もいくつか入っているようです。
C# (というか .NET Framework) の場合はおおむねTPLに対応するという認識です。
Concurrent=並行 と Parallel=並列 の意味の違いは……やめましょうこの話は。
リフレクションとアノテーションの変更
アノテーションというのはC#で言うところのカスタム属性に相当します。
Java8では、型アノテーションと言って、型を利用するところにはどこでもアノテーションをつけられるそうです。
new @Interneed MyObject(); new @NonEmpty @ReadOnly List<String>(myNonEmptyStringSet); class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... } void monitorTemperature() throws @Critical TemperatureException { ... } myString = (@NonNull String) myObject; boolean isNonNull = myString instanceof @NonNull String; class Folder<F extends @Existing File>{ ... } Doc @Readonly [][] d2 = new Doc @Readonly [2][12]; // Doc の配列の "read-only な配列"
(http://www.slideshare.net/kimuchi583/r5-3-type-annotationより)
C#では今のところできませんが、Java 8でも「こう言うコードが書ける」だけらしいので、まああまり気にしなくてもいいかも。