プロジェクト

全般

プロフィール

Javaプログラミング Lambda道場

このページは、JJUG CCC 2013 Fall のハンズオンセミナー「R5-3 Project Lambdaハンズオン」(講師:櫻庭祐一氏)を受講した内容の復習です。セミナー資料は上述リンク先にあります。

Lambda式

Lambda式は、

(引数) -> {処理}

の形式で表します。引数は、関数型インタフェースの実装すべき(唯一の)メソッドの引数を、処理は、そのメソッドの実体を表します。

匿名クラスをLamda式に書き換え

問題2-1 次の匿名クラス生成式をLambda式に書き換えましょう

Comparator<Integer> comparator1 = new Comparator<Integer>() {
    @Override
    public int compare(Integer x, Integer y) {
        return x - y;
    }
};
解答2-1ーⅰ 省略表現のない素直な?解
Comparator<Integer> comparator1 = (Integer x, Integer y) -> {return x - y;};
解答2-1-ⅱ 引数の型を省略

引数の型はコンパイラが推論できるので省略することができます。

Comparator<Integer> comparator1 = (x, y) -> {return x - y;};
解答2-1-ⅲ returnキーワードを省略

処理がreturn式だけの場合、returnキーワードを省略することができます。

Comparator<Integer> comparator1 = (x, y) -> {x - y;};
解答2-1-ⅳ 処理の波括弧を省略

処理が1つだけの文の場合、処理の波括弧およびセミコロンを省略することができます。

Comparator<Integer> comparator1 = (x, y) -> x - y;

問題2-2 次の匿名クラス生成式をLambda式に書き換えましょう

Callable<Date> callable1 = new Callable<Date>() {
    @Override
    public Date call() throws Exception {
        return new Date();
    }
};
解答2-2-ⅰ returnキーワードおよび波括弧を省略
Callable<Date> callable1 = () -> new Date();
解答2-2-ⅱ メソッドリファレンスを使用

処理がstaticなメソッドを呼び出す場合、メソッドリファレンスを用いて記述することができます。

Callable<Date> callable1 = () -> Date::new;

コンストラクタはstaticなメソッドなので、このような呼び出しが可能です。

問題2-3 次の匿名クラス生成式をLambda式に書き換えましょう

Runnable runnable1 = new Runnable() {
    @Override
    public void run() {
        doSomething();
    }
};
解答2-3 returnキーワードおよび波括弧を省略
Runnable runnable1 = () -> doSomething();

問題2-4 次の匿名クラス生成式をLambda式に書き換えましょう

@FunctionalInterface
public interface Doubler<T extends Number> {
    T doDouble(T x);
}
------
Doubler<Double> doubler = new Doubler<Double>() {
    @Override
    public Double doDouble(Double x) {
        return 2.0 * x;
    }
};
解答2-4-ⅰ 省略表現のない素直な解
Doubler doubler = (Double x) -> {return 2.0 * x;};
解答2-4-ⅱ 引数の型を省略
Doubler doubler = (x) -> {return 2.0 * x;};
解答2-4-ⅲ returnキーワードを省略
Doubler doubler = (x) -> {2.0 * x;};
解答2-4-ⅳ 波括弧およびセミコロンを省略
Doubler doubler = (x) -> 2.0 * x;
解答2-4-ⅴ 丸括弧を省略
Doubler doubler = x -> 2.0 * x;

問題2-5 Swingプログラムで使われる匿名クラス生成式をLambda式に書き換えましょう

public class LambdaForSwing {
    private int count;

    public LambdaForSwing() {
        JFrame frame = new JFrame("Swing Lambda");
        JButton button = new JButton("Count");
        frame.add(button, BorderLayout.NORTH);
        final JLabel counter = new JLabel(String.valueOf(count));
        frame.add(counter, BorderLayout.CENTER);

        button.addActionListener(new ActionListener() {    // (1) ActionListener匿名クラス生成式
            @Override
            public void actionPerformed(ActionEvent e) {
                count++;
                counter.setText(String.valueOf(count));
            }
        });
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String... args) {
        SwingUtilities.invokeLater(new Runnable() {    // (2) Runnable匿名クラス生成式
            @Override
            public void run() {
                new SwingLambda();
            }
        });
    }
}
解答2-5(1) 
button.addActionListener(e -> {count++; counter.setText(String.valueOf(count));};
  • 注記1 countはフィールドなのでLambda式内でアクセス可能
  • 注記2 counterはfinalなローカル変数なのでLambda式内でアクセス可能
解答2-5(2)
SwingUtilities.invokeLater(SwingLambda::new);
  • 注記1 関数型インタフェースRunnableの実装すべき唯一のメソッドrunの引数は0個なので本来省略できませんが、処理がメソッドリファレンスの場合は省略できます。
  • 注記2 インスタンスメソッドも、変数名::メソッド名として記述できます。

for文の変換(Iterable)

問題3-1 拡張for文をforEachメソッドに書き換えましょう

List<String> strings = Arrays.asList("a", "b", "c", "d", "e");
StringBuilder builder = new StringBuilder();
for (String s: strings) {
    builder.append(s);
}
System.out.println(builder.toString());
解答3-1-ⅰ 引数の丸括弧、処理の波括弧、セミコロンを省略
strings.forEach(s -> builder.append(s));
解答3-1-ⅱ メソッドリファレンス
strings.forEach(builder::append);
  • 注記1 インスタンス変数のメソッドリファレンスを指定しています。
  • 注記2 appendは引数1個(String)で一意に確定します。

拡張for文に対して性能はほとんど変わりません。invokeDynamicのブートストラップでクラスを生成しロードし、2回目以降はそれを使います。

問題3-2 拡張for文をforEachメソッドに書き換えましょう

List<Integer> numbers = Arrays.asList(10, 5, 2, 20, 12, 15);
int sum = 0;
for (Integer number: numbers) {
    sum += number;
}
System.out.println(sum);
解答3-2 引数の丸括弧、処理の波括弧、セミコロンを省略
int sum;   // フィールドに格上げ
...
numbers.forEach(number -> sum += number);

問題3-3 for文をforEachメソッドに書き換えましょう

for (int i = 0; i < 10; i++) {
    System.out.print(i);
}
System.out.println();
解答3-3 IntStreamを導入
IntStream.range(0, 10).forEach(i -> System.out.print(i));
  • IntStreamのrange(0, 10)は、0以上10未満の整数ストリームを生成します。
    上限の値を含める場合は、rangeClosedを使用します。

for文の変換(Stream)

問題4-1 拡張for文をStreamに書き換えましょう

List<Integer> numbers = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for (Integer x: numbers) {
    if (x % 2 == 0) {
        System.out.print(x);
    }
}
System.out.println();
解答4-1-ⅰ 単純なforEachへの書き換え
numbers.stream().forEach(x -> {
        if (x % 2 == 0) {
            System.out.print(x);
        }
});
解答4-1-ⅱ フィルタの導入
numbers.stream().filter(x -> x % 2 == 0).forEach(x -> System.out.print(x));
  • 2回ループが回るように見えます。filterで一回条件に合致するものを調べるループが回り、forEachでループが回るように見えます。しかし、実際にはループは1回しか回りません。それはforEachの中で動きます。
解答4-1-ⅲ フィルタとメソッドリファレンスの導入
numbers.stream()
       .filter(x -> x % 2 == 0)
       .forEach(System.out::print);
  • Stream APIを使うとメソッドチェーンが増えるので適宜折り返しを入れたくなりますが、その場合、メソッド呼び出しの.演算子の前で改行するのが習慣のようです。
解答4-1-ⅳ rangeの導入
IntStream.range(0, 11)
       .filter(x -> x % 2 == 0)
       .forEach(System.out::print);

問題4-2 拡張for文をStreamに書き換えましょう

Random random = new Random();
List<Double> numbers = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    numbers.add(random.nextDouble());
}
double ave = 0.0;
for (Double x: numbers) {
    ave += x;
}
ave /= numbers.size();
double div = 0.0;
for (Double x: numbers) {
    div += (x - ave) * (x - ave);
}
div /= numbers.size();
解答4-2-ⅰ
int ave = numbers.stream()
                 .reduce(0, (x, y) -> x + y) / numbers.size();
  • reduceの第1引数は初期値
  • reduceの第2引数のLamda式の引数部(x, y)において
    • xは初回は初期値が、2回目以降は前回の結果が入る
    • yはnumbersの要素が入る

この解は、ボクシングが多発しパフォーマンスが劣ります。

解答4-2-ⅱ
int ave = numbers.stream()
                 .mapToInt(x -> x)
                 .reduce(0, (x, y) -> x + y) / numbers.size();
  • 解答4-2-ⅰのボクシング多発を改善した解
  • mapToIntは、Integer型からint型へマッピングする
解答4-2-ⅲ
int ave = numbers.stream()
                 .mapToInt(x -> x)
                 .sum() / numbers.size();
  • 汎用な処理をするreduceではなく合計を算出するsumを使用
解答4-2ーⅳ
int ave = numbers.stream()
                 .mapToInt(x -> x)
                 .average().getAsDouble();
  • 平均を取るメソッドを使用、ただしaverage()はOptionalDouble型を返すのでgetAsDoubleでプリミティブのdoubleを取得

問題4-3 for文をStreamに書き換えましょう

try(BufferedReader reader = new BufferedReader(new FileReader(filename))) {
    int wordCount = 0;
    for (;;) {
        String line = reader.readLine();
        if (line == null) {
            break;
        }
        String[] words = line.split(" ");
        wordCount += words.length;
    }
    System.out.println(wordCount);
} catch (IOException ex) {
    ...
}
解答5-3-ⅰ 
int wordCount = reader.lines()
                      .flatMap(l -> Arrays.stream(l.split(" ")))
                      .count();

配列の場合、flatMapを使います。

オプション問題

for (int i = 0; i < 100; i++) {
    numbers.add(random.nextDouble());
}
オプション解答ーⅰ
IntStream.range(0, 100)
         .mapToObj(x -> random.nextDouble())
         .collect(Collectors.toList());
オプション解答ーⅱ
IntStream.range(0, 100)
         .mapToDouble(x -> random.nextDouble())
         .boxed()
         .collect(Collectors.toList());
オプション解答ーⅲ
DoubleStream.generate(random::nextDouble)
            .limit(100)
            .boxed()
            .collect(Collectors.toList());

クリップボードから画像を追加 (サイズの上限: 1 GB)