このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
Java8 ではマルチコア環境での高速化をするために、 Map Reduce という手法 を取り込みました。 したがって、この手法を取り入れることにより、マルチコアを活用して従来よ り高速に計算することができるようになります。
Map Reduce とは大きなデータの集まりに対して、並列的に処理する方法です。
データの集まりのすべての要素に対して、メソッド f により値の集まりを求 めることを考えます。 数学の記号を使うと次のようになります。
これは関数を変数に入れられるようなプログラミング言語だ
と map(xarray,f)
などと記述することができます。
ここで、ポイントなのはメソッド f は固定ではなく、パラメータとして扱う
必要があるということです。
これを従来の Java だと、次のように書くことができます。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Arrays;
interface Function<T>{ // *1
T apply(T x);
}
class Test {
static <T> Collection<T> map(Collection<T> xarray, Function<T> f){ // *2
Collection<T> result = new ArrayList<>();
for(T x : xarray){ // *3
result.add(f.apply(x));
}
return result;
}
public static void main(String[] arg){
Collection<Integer> xarray = Arrays.asList(1,2,3);
Collection<Integer> result = map(xarray, new Function<Integer>(){ // *4
public Integer apply(Integer x){ return x+1; }
});
System.out.println(result);
}
}
ポイントは次の通りです。
new クラスまたはインタフェイス名(){ メソッド定義 }
という構文でクラスまたはインタフェイスを指定のメソッド定義により実装し
た無名のクラスのオブジェクトを作るものです。
データの集まりに対して、各要素に同じ処理を行うことは自然なデータ処理で
すが、この様に従来の Java では冗長な表現にな
り map(xarray,f)
のような簡素な表現になってません。
また、処理において、データの順番さえ狂わなければ、 データをどの順番に処理しても、さらには同時に処理しても結果は同じになり ます。 そのため、これをマルチコアプロセッサを使って手分けをすれば高速に処理で きます。
このようなことから、Java8 では、この map 処理をできるように、次のよう な仕様が追加されました。
次に、データの集まりを集計することを考えます。 例えば合計を考えます。 これは map とは違い、ここのデータが互いに関係し合いますので、単純にバラバラ に手分けすることはできません。 しかし、合計であれば足す順番を変えると同時にできるようになります。 足し算は次の結合法則が成り立ちます。
このような条件の場合、隣同士の足し算をそれぞれ手分けして行うことができ ます。 もし、すべての隣同士の足し算を手分けできて 1step で完了するなら、 1step で2個ずつのデータの和が求まりますから、集計対象のデータ数は 1/2 になります。 そのため、この隣同士の足し算を繰り返すことで log n step で集計が完了し ます。 実際はマルチコア数よりデータ数の方が多いことがほとんどですから、ここま で高速にはなりませんが、逐次加算するより高速化できることは明かです。 Java8 では、この reduce の機能も追加されました。
このように MapReduce の機能を追加するため、次のような機能が追加されま した。
以上、大幅な仕様変更が行われたことにより、クラスライブラリも大幅に変更 されています。 従来は Eclipse の設定でコンパイラのバージョンとクラスライブラ リのバージョンが多少異なっても、エラーが出にくくて分かり辛かったです。 しかし、Java8 のクラスライブラリでは interface にメソッドが実装されて いるため、設定ミスによるコンパイルエラーが発生することが多々あります。 開発環境の設定は必ず確認するようにしてください。
今回、初めて Java の仕様書の原著を精読しましたが、これにより、従来教え ていた教材に誤りがあることがわかりました。 それは次の内容です。
従来のクラスライブラリではこの機能が多用されていなかったし、必要性も感 じ無かったので、気づかな かったのですが、 Java8 のクラスライブラリの拡張では多用されています。 java.util.Comparator は従来は compare メソッドと equals メソッドしか定 義されてませんでしたが、 Java8 では compare と equals メソッドの他に、 default メソッド7個、 static メソッド 9 個が新たに追加されています。
interface に static メソッドを定義する場合、 interface の static フィー ルドは必ず final になるという仕様があるため、 static フィールドを変数 にできないという制約があることに注意が必要です。
Java の記述性の悪さから、簡便な記法を推奨してきませんでした。 しかし、 Java7 までの中途半端な簡便記法が伏線となって、 Java8 の簡便記 法は極めて重要な記法になりました。 今まで奨めていた下記の主張は撤回します。
List<Integer> list = ArrayList<Integer>();
import java.util.Comparator;
class MyComparator<Integer> implements Comparator<Integer> {
public MyComparator(){}
@Override
public int compare(Integer o1, Integer o2){
return o1-o2;
}
}
Map<Integer, String> map = new TreeMap<Integer, String>(new MyComparator());
これらは次のように改めることにしました。
List<Integer> list = ArrayList<>();
<>
を
import java.util.Comparator;
class MyComparator<Integer> implements Comparator<Integer> {
public MyComparator(){}
@Override
public int compare(Integer o1, Integer o2){
return o1-o2;
}
}
Map<Integer, String> map = new TreeMap<>((o1,o2)->o1-o2);
Java8 では interface に大幅な仕様変更がありました。
Java7 までの interface にはメソッドのシグネチャの宣言はできても、動作
の定義はできませんでした。
しかし、Java8 では
interface Function<T> {
default T apply(T x){
return x;
}
}
default で指定されたメソッドは abstract になり得ません。
abstract メソッドが一つだけの interface
を
なお、 java.lang.Object のメソッドは、インスタンス化するときに自動的に オーバライドされるので、abstract とはみなしません。 従って、 compareTo と equals の二つが定義されていた java.util.Comparable は FunctionalInterface となります。
また、コンパイル時のチェックをするため
に @FunctionalInterface
というアノテーションが定義されて
います。
@FunctionalInterface
interface Function<T>{
T apply(T x);
}
java.util.function 内に定義されている FunctionalInterface の分類は下記 の通りです。
引数の数 | 戻り値 void | 戻り値 boolean | 戻り値 オブジェクト | 戻り値 double | 戻り値 int | 戻り値 long |
---|---|---|---|---|---|---|
引数なし | BooleanSupplier | Supplier | ||||
1引数 | Consumer | Predicate | Function, UnaryOperator | ToDoubleFunction | ToIntFunction | ToLongFunction |
2引数 | BiConsumer | BiPredicate | BiFunction, BinaryOperator | ToDoubleBiFunction | ToIntBiFunction | ToLongBiFunction |
1引数+double | ObjDoubleConsumer | |||||
1引数+int | ObjIntConsumer | |||||
1引数+long | ObjLongConsumer |
引数の数 | 戻り値 void | 戻り値 boolean | 戻り値 double | 戻り値 int | 戻り値 long | 戻り値 オブジェクト |
---|---|---|---|---|---|---|
引数なし | DoubleSupplier | |||||
1引数 | DoubleConsumer | DoublePredicate | DoubleUnaryOperator | DoubleToIntFunction | DoubleToLongFunction | DoubleFunction |
2引数 | DoubleBinaryOperator |
引数の数 | 戻り値 void | 戻り値 boolean | 戻り値 int | 戻り値 double | 戻り値 long | 戻り値 オブジェクト |
---|---|---|---|---|---|---|
引数なし | IntSupplier | |||||
1引数 | IntConsumer | IntPredicate | IntUnaryOperator | IntToDoubleFunction | IntToLongFunction | IntFunction |
2引数 | IntBinaryOperator |
引数の数 | 戻り値 void | 戻り値 boolean | 戻り値 long | 戻り値 double | 戻り値 int | 戻り値 オブジェクト |
---|---|---|---|---|---|---|
引数なし | LongSupplier | |||||
1引数 | LongConsumer | LongPredicate | LongUnaryOperator | LongToDoubleFunction | LongToIntFunction | LongFunction |
2引数 | LongBinaryOperator |
本来のラムダ式は無名関数の定義を言います。 ラムダ式の計算方法や、それに伴う性質などは「計算論」などの専門書を参照 して下さい。
Java8 のラムダ式には本来のラムダ式の性質をすべて持ち合わせているわけで はありません。 しかし、本来のラムダ式に似た記法ができます。 基本的な文法は次の通りです。
(型1 仮引数名1, 型2 仮引数名2, ..., 型n 仮引数名n)->{ 手続き }
但し、次のような省略記法があります。
次の FunctionalInterface を考えます。
@FunctionalInterface
interface Function<T>{
T apply(T x);
}
これに対するラムダ式として、例えば次を考えます。
Function<Integer> f = (Integer x)->{return x-1;};
これは次とほぼ等価です。
Function<Integer> f = new Function<>(){
@Override
public Integer apply(Integer x){
return x-1;
}
};
あるいはインナークラスを用いると次のように書けます。
class MyFunction implements Function<Integer>{
@Override
public Integer apply(Integer x){
return x-1;
}
}
Function<Integer> f = new MyFunction();
これに対して前述の省略構文は次のようになります。
Function<Integer> f = (x)->{return x-1;};
Function<Integer> f = (x)->return x-1;
Function<Integer> f = (x)->x-1;
Function<Integer> f = x->x-1;
なお、実際に int 型から int 型へのラムダ式を定義するには、既存のインター フェイスを利用して、次のようにします。
java.util.function.IntUnaryOperator f = x ->x-1;
様々なメソッドに対して、そのメソッドを FunctionalInterface のメソッド
として実装したオブジェクトとして見なす便利な表記として、
System.out::println
String::valueOf
Arrays::sort
Comparator は従来より並べ替えの順序を指定するのに java.util.TreeMap, java.util.TreeSet, java.util.Arrays.sort, java.util.Collections.sort などで使用されています。 さらに、マルチコア対応により追加された Arrays.parallelSort でも使用します。
例えば、文字列の長さを優先して並び替える TreeSet の初期化は次のように 行います。
Set<String> set = new TreeSet<>(x.length()==y.length()?x.compareTo(y):x.length()-y.length());
Thread による並列処理を行う場合、定石は java.lang.Thread クラスを継承 して void run メソッドをオーバーライドします。
class MyThread extends Thread {
public void run(){
System.out.println("Hello World");
}
}
Thread t = new MyThread();
t.start();
但し、何らかのサブクラスの処理を並列処理したい場合、Java では多重継承 が許されていないため、この定石では対応できません。 そのため、別の定石として、 Runnable インターフェイスを実装したクラスの オブジェクトを Thread クラスのコンストラクタに与えて並列処理を行うこと ができます。 Java8 ではラムダ式を使うことで、こちらの定石の方が簡潔に書けるようにな りました。
Runnable r = ()->System.out.println("Hello World");
Thread t = new Thread(r);
t.start();
あるいは
Thread t = new Thread(()->System.out.println("Hello World"));
t.start();
java.lang.Iterable インターフェイスには元々 iterator メ ソッドが abstract で定義されていました。 しかし、 Iterable は引数無しの java.util.Iterator を返すメソッドを与え なければならないので、ラムダ式で定義するには煩瑣です。
一方、 java.lang.Iterable には Java8 から forEach メソッドが追加されました (default なので互換性はあります)。 そのため、 Stream を生成しなくても 、java.util.function.Consumer 型のラムダ式により各要素への処理をするこ とができます。
import java.util.Iterator;
import java.util.stream.StreamSupport;
public class TestIterable {
public static void main(String[] args) {
int[] a = new int[]{1,2,3};
Iterable<Integer> i = ()->{
return new Iterator<Integer>(){
int index=0;
@Override
public boolean hasNext() {
return index<a.length;
}
@Override
public Integer next() {
return a[index++];
}
};
};
for(int x : i){
System.out.println(x);
}
StreamSupport.stream(i.spliterator(), false)
.forEach(System.out::println);
i.forEach(System.out::println);
}
}
ActionListener は ActionEvent e を引数とする actionPerformed メソッドをオーバーライドし、 GUI の操作により実行する処理を記述します。 基本的に、このアクションに記述する処理に複雑な処理は書きません (複雑な処理を書くのはマジックボタンアンチパターンと呼ばれ る、悪い定石と言われてます)。 そのため、ラムダ式により、簡便な記述をするのはプログラムの可読性を上げ るなどのメリットがあります。
import java.awt.Container;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class TestActionListener {
private static int counter=0;
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400,100);
Container contentPane = frame.getContentPane();
contentPane.setLayout(new FlowLayout());
JLabel label = new JLabel(String.valueOf(counter));
contentPane.add(label);
JButton plus = new JButton("+");
contentPane.add(plus);
plus.addActionListener(e->label.setText(String.valueOf(++counter)));
JButton minus = new JButton("-");
contentPane.add(minus);
minus.addActionListener(e->label.setText(String.valueOf(--counter)));
frame.setVisible(true);
}
}
さて、このようなラムダ式の導入は、それ自体、記述が容易になるという利点 がありますが、本来の目的はマルチコア環境における、粒度の小さい並列計算 が目的です。 つまり、 MapReduce の仕組みに与える関数として、ラムダ式を与えるのが目 的です。
そのため、データの集まりに対して、 map や reduce メソッドを持つ新たな データ構造が必要となります。 それがストリーム型です。
java.util.stream.Stream インタフェースはオブジェクトのデータの集まりに 対して操作をするものです。 他に、 double 型用の DoubleStream, int 型用の IntStream, long 型用の LongStream があります。
Collection 型に stream メソッドが追加され、 Stream オブジェクトを生成 できます。 また、配列に対しては Arrays クラスに stream メソッドが追加されて、 Stream オブジェクトを生成できます。
多くのメソッドはラムダ式を引数に取って、データの操作をします。 代表的なメソッドを紹介します。
戻り型 | メソッド | 機能 |
---|---|---|
<R> Stream<R> | map(Function<? super T,? extends R> mapper) | 各要素に与えた mapper を施し、新しい Stream を作り返す |
Optional<T> | reduce(BinaryOperator<T> accumulator) | accumulator という2引数の演算により、すべての要素を集計する |
void | forEach(Consumer<? super T> action) | 各要素に対して action を行う |
Stream<T> | filter(Predicate<? super T> predicate) | 条件 predicate が true を返す要素のみを取り出し、新しい Stream を 作る |
Stream<T> | sorted(Comparator<? super T> comparator) | comparator を比較条件として要素を並べる |
int[] a = {7, 3, 5, 6, 4};
に対しての処理
int[] b = Arrays.stream(a).map(x->x+1).toArray();
int c = Arrays.stream(a).reduce((x,y)->x+y).getAsInt();
Arrays.stream(a).forEach(System.out::println);
int[] d = Arrays.stream(a).filter(x->(x%2==0)).toArray();
int[] e = Arrays.stream(a).sorted().toArray();
プログラミング言語において、計算の対象はデータだけではなく、型やオブジェクトな ども対象になります。 しかし、例えば整数型に対して他の対象は下記のことができるほど自由ではあ りません。
Java においては、基本型はリテラルがありました。その他、String クラスに 対してはリテラルがあります。 Java5 からはオートボクシングにより Integer などのラッパークラスも擬似 的にリテラルが出来た一方、Generics の拡張により、Generics を指定した配 列のリテラルは作れなくなってしまいました。
Integer a = 1; //オートボクシングにより、1がIntegerクラスのリテラルに見える
B<C>[] d = new B<C>[]{ new B<C>() }; // ×
//Genericsの配列は作れない
B<C>[] d = (B<C>[]) new B[]{ new B<C>() }; // △
// 右辺はキャストをしているためコンパイル時にワーニングが出る
歴史的経緯や理論的な裏付けなどにより、代入操作においてしばしば異なる型 の代入がキャストなしで可能でした
byte, char, int, long, float, double などは歴史的な経緯により、暗黙の 丸め操作などにより、相互にキャストなしで代入可能です。
int i=1;
int j=2;
double a=3.4;
double b=5.6;
i=a;
b=j;
System.out.println(i+" "+j+" "+a+" "+b);
親クラス型の変数に子クラスのオブジェクトを参照させることが出来ます。
Number a = new Integer(1);
Number b = new Double(2.3);
System.out.println(a+" "+b);