
Java8からラムダ式による関数型プログラミングが記述できるようになりました。今回は、ムーブメントに乗り遅れた人向けに、改めてJavaで関数型プログラミングの基礎を紹介します。
はじめに
近年は関数型プログラミングが新しいパラダイムシフトとなっており、あらゆるプログラミング言語で関数型プログラミングをサポートしています。ここで言う関数型プログラミングとは、あくまでラムダ式による新しいコーディングのやり方の事です。モナド、カリー化、遅延評価、高階関数などのそもそもの関数型言語の概念を知りたければHaskellを学ぶのが一番だと言われていますが、今回はそういう話は扱いません。純粋にJavaでラムダ(Lambda)式、ストリーム(Stream)、関数型インターフェース(Functional Interfaces)を使ってプログラミングしたい人向けです。
それでは、Javaで関数型プログラミングに入門していきましょう。
ストリーム(Stream)を使ってみよう
java.util.streamのパッケージで基本的なものを使ってみましょう。
forEach
「forEach」でfor文を使わなくても流れるようにループ処理を記述できます。
var numbers = List.of(18, 4, 22, 7, 31, 1, 12, 25, 36, 3);
// 基本的な書き方
numbers.stream()
.forEach(number -> System.out.println(number));
// メソッド参照で簡素に書ける
numbers.stream()
.forEach(System.out::println);
filter
「filter」は条件に合うデータだけになるようにフィルターをかけます。
var numbers = List.of(18, 4, 22, 7, 31, 1, 12, 25, 36, 3);
// 偶数だけフィルターする
numbers.stream()
.filter(number -> number % 2 == 0)
.forEach(System.out::println);
map
「map」はそれぞれのデータに共通の処理を記述できます。
var numbers = List.of(18, 4, 22, 7, 31, 1, 12, 25, 36, 3);
// 全ての数字を2乗する
numbers.stream()
.map(number -> number * number)
.forEach(System.out::println);
reduce
「reduce」はデータをまとめます。
var numbers = List.of(18, 4, 22, 7, 31, 1, 12, 25, 36, 3);
// 合計を出す
var sum = numbers.stream()
.reduce(0, (x, y) -> x + y);
// => 159
// 奇数を3乗して足す
var result = numbers.stream()
.filter(number -> number % 2 != 0)
.map(number -> number * number * number)
.reduce(0, Integer::sum);
// => 45787
collect
「collect」はストリームを再利用できる型に変換します。
var numbers = List.of(18, 4, 22, 7, 31, 1, 12, 25, 36, 3);
// それぞれ2乗したデータをListで返す
var squaredNumbers = numbers.stream()
.map(number -> 2 * number)
.collect(Collectors.toList());
// => [36, 8, 44, 14, 62, 2, 24, 50, 72, 6]
takeWhile
「takeWhile」は、ストリームの最初から条件に合うものを抽出していき、条件が合わなかったところまでの範囲で取り出します。
var numbers = List.of(3, 6, 9, 12, 15, 16, 20, 24, 28, 36, 33, 36, 39, 42, 45);
// 3の倍数をリストの最初からそれ以外がでるまでの範囲で取り出す
var firstTripleNumbers = numbers.stream()
.takeWhile(number -> number % 3 == 0)
.collect(Collectors.toList());
// => [3, 6, 9, 12, 15]
// 3の倍数を全て取り出す
var tripleNumbers = numbers.stream()
.filter(number -> number % 3 == 0)
.collect(Collectors.toList());
// => [3, 6, 9, 12, 15, 24, 36, 33, 36, 39, 42, 45]
distinct
「distinct」は重複を省きます。
var numbers = List.of(10, 3, 22, 3, 22, 10, 22, 3, 3, 3);
var distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
// => [10, 3, 22]
sorted
「sorted」はソートします。「Comparator」を渡すことで、ソート方法を指定できます。
var numbers = List.of(18, 4, 22, 7, 31, 1, 12, 25, 36, 3);
// 昇順
var sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
// => [1, 3, 4, 7, 12, 18, 22, 25, 31, 36]
// 昇順
var sortedNumbers2 = numbers.stream()
.sorted(Comparator.naturalOrder())
.collect(Collectors.toList());
// => [1, 3, 4, 7, 12, 18, 22, 25, 31, 36]
// 降順
var reversedNumbers = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
// => [36, 31, 25, 22, 18, 12, 7, 4, 3, 1]
flatMap
「flatMap」はストリームのデータをフラットにします。
var names = List
.of("Keid", "Steve Jobs", "Jeff Bezos", "Larry Page", "Bill Gates", "Mark Zuckerberg");
var list = names.stream()
.map(str -> str.split(" ")) // 性と名を分けて
.flatMap(Arrays::stream) // フラットな1次元配列にして
.sorted() // アルファベット順にソートして
.collect(Collectors.toList()); // リストにして返す
// => [Bezos, Bill, Gates, Jeff, Jobs, Keid, Larry, Mark, Page, Steve, Zuckerberg]
range/rangeClosed
「range」と「rangeClosed」で指定した範囲の数字を扱えます。
var sum1 = IntStream.range(1, 10).sum(); // => 45
var sum2 = IntStream.rangeClosed(1, 10).sum(); // => 55
var bigSum = LongStream.range(1, 100000000).parallel().mapToObj(BigInteger::valueOf)
.reduce(BigInteger.ONE, BigInteger::add); // => 4999999950000063
「parallel」はマルチスレッドで並列処理させます。
iterate
「iterate」は繰り返し処理を記述できます。
var sum = IntStream.iterate(1, n -> n + 3).limit(5).peek(System.out::println).sum();
// => 1
// => 4
// => 7
// => 10
// => 13
System.out.println("total:" + sum);
// => total:35
「peek」はストリームの途中の値を抜き出せます。ストリームのデバッグに便利です。
allMatch/anyMatch/noneMatch
「allMatch」は、全てのデータが条件に合っているかどうか、「anyMatch」は、データのどれかが条件に合っているかどうか、「noneMatch」は、全てのデータが条件に合っていないかどうか、を判定します。
var names = List
.of("Keid", "Steve Jobs", "Jeff Bezos", "Larry Page", "Bill Gates", "Mark Zuckerberg");
System.out.println(names.stream().allMatch(name -> name.length() >= 4)); // => true
System.out.println(names.stream().allMatch(name -> name.length() >= 10)); // => false
System.out.println(names.stream().anyMatch(name -> name.length() == 4)); // => true
System.out.println(names.stream().anyMatch(name -> name.length() > 15)); // => false
System.out.println(names.stream().noneMatch(name -> name.length() > 15)); // => true
System.out.println(names.stream().noneMatch(name -> name.length() == 10)); // => false
関数型インターフェース(Functional Interfaces)を知ろう
java.util.functionのパッケージにあるLambda式やメソッド参照で使う型を見ていきましょう。
Predicate
「Predicate」は、1つの引数を与えて、booleanの値を返します。
var numbers = List.of(18, 4, 22, 7, 31, 1, 12, 25, 36, 3);
// 奇数を判定するPredicate
Predicate<Integer> isOdd = x -> x % 2 != 0;
var oddNumbers = numbers.stream()
.filter(isOdd)
.collect(Collectors.toList());
// => [7, 31, 1, 25, 3]
Function
「Function」は、1つの引数を与えて、1つの値を返します。
// 倍にするFunction
Function<Integer, Integer> doubleIt = x -> x + x;
var doubleNumbers = numbers.stream()
.map(doubleIt)
.collect(Collectors.toList());
// => [36, 8, 44, 14, 62, 2, 24, 50, 72, 6]
Consumer
「Consumer」は、1つの引数を与えて、voidで処理します。
// 標準出力をするConsumer
Consumer<Integer> print = x -> System.out.println(x);
numbers.stream()
.filter(isOdd)
.map(doubleIt)
.forEach(print);
// => 14
// => 62
// => 2
// => 50
// => 6
BinaryOperator
「BinaryOperator」は、2つの引数を与えて、1つの値を返します。
// 足し算をするBinaryOperator
BinaryOperator<Integer> sum = (x, y) -> x + y;
var oddSum = numbers.stream()
.filter(isOdd)
.reduce(sum);
// => Optional[67]
Supplier
「Supplier」は、引数を与えずに、1つの値を返します。
// 0〜99までの乱数を返すSupplier
Supplier<Integer> randomInteger = () -> {
Random random = new Random();
return random.nextInt(100);
};
var randomNumber = randomInteger.get();
UnaryOperator
「UnaryOperator」は、1つの引数を与えて、1つの値を返します。Functionとの違いは引数と戻り値の型が同じというところです。
// 10倍にするUnaryOperator
UnaryOperator<Integer> multiplyByTen = x -> 10 * x;
var tenfoldEvenNumbers = numbers.stream()
.filter(x -> x % 2 == 0)
.map(multiplyByTen)
.collect(Collectors.toList());
// => [180, 40, 220, 120, 360]
BiPredicate/BiFunction/BiConsumer
「BiPredicate」は、2つの引数を与えて、booleanの値を返します。「BiFunction」は、2つの引数を与えて、1つの値を返します。「BiConsumer」は、2つの引数を与えて、voidで処理します。
// Idが100以上かつNameにD.を含むかどうかを判定するBiPredicate
BiPredicate<Integer, String> isProtagonist = (id, name) -> {
return id >= 100 && name.contains("D.");
};
// IdとNameを結合するBiFunction
BiFunction<Integer, String, String> joinIdAndName = (id, name) -> {
return id + ":" + name;
};
// IdとNameを結合して標準出力するBiConsumer
BiConsumer<Integer, String> printIdAndName = (id, name) -> {
System.out.println(id + ":" + name);
};
class Hero {
private int id;
private String name;
public Hero(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
var protagonistName = List.of(
new Hero(200, "Edward Teach"),
new Hero(30, "Rocks D. Shanks"),
new Hero(100, "Monkey D. Luffy")
).stream()
.peek(hero -> printIdAndName.accept(hero.id, hero.name))
.filter(hero -> isProtagonist.test(hero.id, hero.name))
.map(hero -> joinIdAndName.apply(hero.id, hero.name))
.findFirst();
// => 200:Edward Teach
// => 30:Rocks D. Shanks
// => 100:Monkey D. Luffy
System.out
.println("The protagonist in One Piece is " + protagonistName.orElse("Nothing") + "!");
// => The protagonist in One Piece is 100:Monkey D. Luffy!
以上、ここまで分かれば、リファレンスだけで使いこなせるでしょう。
おまけ
ストリームによるファイル操作
「Files.lines」を使うことで、ファイルを読み込んでストリームで扱えます。
以下のCSVファイルを準備して読み込んでみます。
file.csv
Keid,Steve Jobs,Jeff Bezos
Larry Page,Bill Gates,Mark Zuckerberg
行ごとに読み込んで、カンマで分割し、それぞれを出力してみます。
Files.lines(Paths.get("file.csv"))
.map(str -> str.split((",")))
.flatMap(Arrays::stream)
.forEach(System.out::println);
// => Keid
// => Steve Jobs
// => Jeff Bezos
// => Larry Page
// => Bill Gates
// => Mark Zuckerberg
ファイル操作も簡単になりましたね。
最後に
いかがでしたか?これでJavaで関数型プログラミングが記述できるようになったのではないでしょうか。あとは、java.util.streamおよびjava.util.functionのパッケージを見ていろいろ試してみると良いでしょう。では。
環境
- Java: 11.0.4