Java8

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

1. MapReduce

Java8 ではマルチコア環境での高速化をするために、 Map Reduce という手法 を取り込みました。 したがって、この手法を取り入れることにより、マルチコアを活用して従来よ り高速に計算することができるようになります。

Map Reduce とは大きなデータの集まりに対して、並列的に処理する方法です。

Map

データの集まりのすべての要素に対して、メソッド f により値の集まりを求 めることを考えます。 数学の記号を使うと次のようになります。

x= x0 x1 ... xn-1 f x = f x0 f x1 ... f xn-1

これは関数を変数に入れられるようなプログラミング言語だ と 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);
		
	}
}

ポイントは次の通りです。

  1. ストラテジデザインパターンを使います。これは実施するメソッドを取り 替えるためのデザインパターンです。 一つだけメソッドを持つ interface として Function 型を定義します (Function や apply などの名前に特に意味は無い)。
  2. map メソッドは、データの集まりと Function 型のオブジェクトを引数に取り ます。
  3. 拡張 for 文により、与えられた Function 型のオブジェ クトのメソッドに各データを与え、得られた値を新たなデータの集まりとする。
  4. 無名クラスという手法により、 Function 型のオブジェクトを作ります。 これは、 new クラスまたはインタフェイス名(){ メソッド定義 } という構文でクラスまたはインタフェイスを指定のメソッド定義により実装し た無名のクラスのオブジェクトを作るものです。
map
Map

データの集まりに対して、各要素に同じ処理を行うことは自然なデータ処理で すが、この様に従来の Java では冗長な表現にな り map(xarray,f) のような簡素な表現になってません。

また、処理において、データの順番さえ狂わなければ、 データをどの順番に処理しても、さらには同時に処理しても結果は同じになり ます。 そのため、これをマルチコアプロセッサを使って手分けをすれば高速に処理で きます。

このようなことから、Java8 では、この map 処理をできるように、次のよう な仕様が追加されました。

  1. ストラテ ジデザインパターンを簡便に使える記法を与える
  2. map処理ができるようなデー タを並列に処理できるようなデータ型

reduce

次に、データの集まりを集計することを考えます。 例えば合計を考えます。 これは map とは違い、ここのデータが互いに関係し合いますので、単純にバラバラ に手分けすることはできません。 しかし、合計であれば足す順番を変えると同時にできるようになります。 足し算は次の結合法則が成り立ちます。

x + y + z = x + y + z

このような条件の場合、隣同士の足し算をそれぞれ手分けして行うことができ ます。 もし、すべての隣同士の足し算を手分けできて 1step で完了するなら、 1step で2個ずつのデータの和が求まりますから、集計対象のデータ数は 1/2 になります。 そのため、この隣同士の足し算を繰り返すことで log n step で集計が完了し ます。 実際はマルチコア数よりデータ数の方が多いことがほとんどですから、ここま で高速にはなりませんが、逐次加算するより高速化できることは明かです。 Java8 では、この reduce の機能も追加されました。

reduce
Reduce

Java8 で追加された仕様の概要

このように MapReduce の機能を追加するため、次のような機能が追加されま した。

  1. 並列で map や reduce の処理可能なデータ型 java.util.stream.Stream の追 加
  2. ストラテジデザインパターンを簡便に記述できるよう、一つだけしか abstract なメソッドない interface を FunctionalInterface と名付け、特別扱いする。
  3. java.util.function に様々な FunctionalInterface を定義
  4. 代入する変数の型や、メソッドの引数の型から生成するオブジェクトの型を推 論で決定する型推論の機能
  5. ラムダ式という、簡便な FunctionalInterface オブジェクトのリ テラルの記法。
  6. 従来より使用されていたストラテジデザインパターン用の interface である Comparator, Runnable, Iterable などを互換性を保たせたまま MapReduce に 対応させるため、 default というキーワードで実装されたメソッドを記述できるようにした。
  7. Comparator, Runnable, Iterable などの interface の機能拡張。

以上、大幅な仕様変更が行われたことにより、クラスライブラリも大幅に変更 されています。 従来は Eclipse の設定でコンパイラのバージョンとクラスライブラ リのバージョンが多少異なっても、エラーが出にくくて分かり辛かったです。 しかし、Java8 のクラスライブラリでは interface にメソッドが実装されて いるため、設定ミスによるコンパイルエラーが発生することが多々あります。 開発環境の設定は必ず確認するようにしてください。

2. お詫び

その1

今回、初めて Java の仕様書の原著を精読しましたが、これにより、従来教え ていた教材に誤りがあることがわかりました。 それは次の内容です。

  1. static メソッドはクラス名、インタフェイス名以外に、変数名.メソッド 名でも呼び出すことができる

従来のクラスライブラリではこの機能が多用されていなかったし、必要性も感 じ無かったので、気づかな かったのですが、 Java8 のクラスライブラリの拡張では多用されています。 java.util.Comparator は従来は compare メソッドと equals メソッドしか定 義されてませんでしたが、 Java8 では compare と equals メソッドの他に、 default メソッド7個、 static メソッド 9 個が新たに追加されています。

interface に static メソッドを定義する場合、 interface の static フィー ルドは必ず final になるという仕様があるため、 static フィールドを変数 にできないという制約があることに注意が必要です。

その2

Java の記述性の悪さから、簡便な記法を推奨してきませんでした。 しかし、 Java7 までの中途半端な簡便記法が伏線となって、 Java8 の簡便記 法は極めて重要な記法になりました。 今まで奨めていた下記の主張は撤回します。

  1. コンストラクタ利用時のジェネリックス指定は省略しない方が良い

    例1

    
    List<Integer> list = ArrayList<Integer>();
    
  2. 無名クラスによるインスタンス生成は行わず、インタークラス定義し、コ ンストラクタも定義すべきである

    例2

    
    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());
    

これらは次のように改めることにしました。

  1. コンストラクタ利用時のジェネリックス指定は可能な限り省略する。 変数宣言時以外の型指定はなるべくしない。

    例3

    
    List<Integer> list = ArrayList<>();
    

    <>ダイヤモンドと呼ぶようです。

  2. 無名クラスによるインスタンス生成は相変わらず行わないが、 FunctionalInterface に対するインスタンス生成はラムダ式を用いる。

    例4

    
    
    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);
    

3. interface

Java8 では interface に大幅な仕様変更がありました。

default

Java7 までの interface にはメソッドのシグネチャの宣言はできても、動作 の定義はできませんでした。 しかし、Java8 では default を指定することで、メソッドの定義 ができます。

例5


interface Function<T> {
    default T apply(T x){
        return x;
    }
}

default で指定されたメソッドは abstract になり得ません。

FunctionalInterface

abstract メソッドが一つだけの interface を FunctionalInterface と呼びます。 FunctionalInterface に対してその一つだけのメソッドを実装したインスタン スを生成するのに、次節で説明するラムダ式を使うことができます。

なお、 java.lang.Object のメソッドは、インスタンス化するときに自動的に オーバライドされるので、abstract とはみなしません。 従って、 compareTo と equals の二つが定義されていた java.util.Comparable は FunctionalInterface となります。

また、コンパイル時のチェックをするため に @FunctionalInterface というアノテーションが定義されて います。

例6


@FunctionalInterface
interface Function<T>{
    T apply(T x);
}

java.util.function パッケージのインターフェイス

java.util.function 内に定義されている FunctionalInterface の分類は下記 の通りです。

引数オブジェクト

引数の数戻り値 void戻り値 boolean 戻り値 オブジェクト 戻り値 double 戻り値 int戻り値 long
引数なしBooleanSupplierSupplier
1引数ConsumerPredicateFunction, UnaryOperator ToDoubleFunctionToIntFunction ToLongFunction
2引数BiConsumerBiPredicateBiFunction, BinaryOperator ToDoubleBiFunctionToIntBiFunction ToLongBiFunction
1引数+doubleObjDoubleConsumer
1引数+intObjIntConsumer
1引数+longObjLongConsumer

引数 double

引数の数戻り値 void戻り値 boolean 戻り値 double 戻り値 int戻り値 long 戻り値 オブジェクト
引数なしDoubleSupplier
1引数DoubleConsumerDoublePredicate DoubleUnaryOperator DoubleToIntFunctionDoubleToLongFunction DoubleFunction
2引数DoubleBinaryOperator

引数 int

引数の数戻り値 void戻り値 boolean 戻り値 int 戻り値 double戻り値 long 戻り値 オブジェクト
引数なしIntSupplier
1引数IntConsumerIntPredicate IntUnaryOperator IntToDoubleFunctionIntToLongFunction IntFunction
2引数IntBinaryOperator

引数 long

引数の数戻り値 void戻り値 boolean 戻り値 long 戻り値 double戻り値 int 戻り値 オブジェクト
引数なしLongSupplier
1引数LongConsumerLongPredicate LongUnaryOperator LongToDoubleFunctionLongToIntFunction LongFunction
2引数LongBinaryOperator

4. ラムダ式

本来のラムダ式は無名関数の定義を言います。 ラムダ式の計算方法や、それに伴う性質などは「計算論」などの専門書を参照 して下さい。

Java8 のラムダ式には本来のラムダ式の性質をすべて持ち合わせているわけで はありません。 しかし、本来のラムダ式に似た記法ができます。 基本的な文法は次の通りです。


(型1 仮引数名1, 型2 仮引数名2, ..., 型n 仮引数名n)->{ 手続き }

但し、次のような省略記法があります。

  1. 仮引数の型は推論可能であれば省略できる
  2. 手続きが一つの文のみの場合、中括弧{} を省略できる
  3. 手続きであるただ一つの文が return である時、return も省略でき る
  4. 引数が一つの場合は先頭の丸括弧を省略できる

例7

次の 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();

これに対して前述の省略構文は次のようになります。

  1. 型の省略
    
    Function<Integer> f = (x)->{return x-1;};
    
  2. 中括弧の省略
    
    Function<Integer> f = (x)->return x-1;
    
  3. return の省略
    
    Function<Integer> f = (x)->x-1;
    
  4. 丸括弧の省略
    
    Function<Integer> f = x->x-1;
    

なお、実際に int 型から int 型へのラムダ式を定義するには、既存のインター フェイスを利用して、次のようにします。


java.util.function.IntUnaryOperator f = x ->x-1;

メソッド参照

様々なメソッドに対して、そのメソッドを FunctionalInterface のメソッド として実装したオブジェクトとして見なす便利な表記として、 メソッド参照があります。 これは、メソッド呼び出し表現において、丸括弧と引数を省略する一方、最後 のピリオド(.)を二個のコロン(::)に置き換える記法になります。

例8


System.out::println
String::valueOf
Arrays::sort

既存の FunctionalInterfaceの運用

java.util.Comparator

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());

java.lang.Runnable

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

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);
	}
}

java.awt.ActionListener

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);
	}
}

5. ストリーム型

さて、このようなラムダ式の導入は、それ自体、記述が容易になるという利点 がありますが、本来の目的はマルチコア環境における、粒度の小さい並列計算 が目的です。 つまり、 MapReduce の仕組みに与える関数として、ラムダ式を与えるのが目 的です。

そのため、データの集まりに対して、 map や reduce メソッドを持つ新たな データ構造が必要となります。 それがストリーム型です。

java.util.stream.Stream

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 を比較条件として要素を並べる

例9

int[] a = {7, 3, 5, 6, 4}; に対しての処理

map
int[] b = Arrays.stream(a).map(x->x+1).toArray();
reduce
int c = Arrays.stream(a).reduce((x,y)->x+y).getAsInt();
forEach
Arrays.stream(a).forEach(System.out::println);
filter
int[] d = Arrays.stream(a).filter(x->(x%2==0)).toArray();
sorted
int[] e = Arrays.stream(a).sorted().toArray();

6. 参考

ファーストクラスオブジェクト(第一級オブジェクト)

プログラミング言語において、計算の対象はデータだけではなく、型やオブジェクトな ども対象になります。 しかし、例えば整数型に対して他の対象は下記のことができるほど自由ではあ りません。

  1. 変数に代入できる
  2. リテラル(定数)をプログラム中に記載できる
  3. 関数の引数に指定できる
  4. プログラム中で演算により生成できる

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);

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科