- Java 8 Stream API にテキストを流してみる(生成編)- Qiita
- Java 8 Stream API にテキストを流してみた(終端操作編)- Qiita
- Java 8 Stream API にテキストを流してみて(中間操作編)← 今ココ
今さらですが、昔書きかけた投稿のお蔵出しすることにします。
まだ途中ですが、書き溜めたコードを随時追記していく予定です。
[2017-04-09] 3.4 sorted() を追記
[2017-04-15] 3.5 map() 追記
[2017-04-23] 3.6 flatMap() 追記
ストリームとパイプライン
《省略》
Java 関数イディオム
《省略》
3 中間操作
《省略》
peek(Consumer<? super T> action)
skip(long maxSize)
limit(long maxSize)
filter(Predicate<? super T> predicate)
distinct()
sorted() / sorted(Comparator<? super T> comparator)
map(Function<? super T,? extends R> mapper)
flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
parallel()
sequencial()
unordered()
3.1 peek()
peek() は Stream を操作しない中間操作だ。
peek() に渡す Consumer では、要素を見られるだけで Stream に影響を与えることはない。
なんらかの実効性を持たせるためには副作用として外部に出力するしかないのは forEach() と同様だ。
peek() はデバックなどでパイプラインの途中に差し込んで、要素内容を確認する目的で用意されたもののようだ。
APIドキュメントにはデバッグ用だとはっきり書いてある。
List<String> list = Arrays.asList("Wow!", "Oh!", "No way!", "HENTAI!");
// ラムダ式
String text = list.stream()
.peek(s -> {
System.out.println(s);
})
.collect(Collectors.joining(","));
// メソッド参照
list = Stream.of(text.split(","))
.peek(System.out::println)
.collect(Collectors.toList());
peek() をパイプラインの複数個所に差し込んで関数と要素の呼び出し順を観察してみてほしい。
中間操作毎に要素がまとめて処理されるわけではなく、あるいはバケツリレー的に同時処理されているわけでもない。
要素が律儀に一度に1個ずつ順にパイプライン(一通りの中間操作と終端操作)に流されているのがわかる。
ただその動作は保証されているわけではないので、そこをあてにしたロジックを組むのはお行儀が悪いというものだ(本稿のことだ)。
また、sorted() 中間操作を挿入したり、unordered() や parallel() で動作モードを変更する事によって、その流れは大きく変わるが、それも peek() で観察できる。
String[] samples = {
"ABC012", // 全角英数
"バカボン", // 半角カナ
"㍆㌋㌋㌧㌨㌰㌣㌈", // 組み文字
"11①⑴⒈₁ⅠⅡⅪ½", // 数字
"[㎉][㎠][㎨][㎕][µℓ][Å][℃]", // 単位
"㈱㊑㏍㍿℡©", // 機種依存文字
"神社", // CJK互換漢字
"[が][か゛][か\u3099][あ゛][あ\u3099]", // 濁点
"ÁÄA\u0301", // 合字
"한글", // ハングル
"¥\\―-¬|~£…", // 気になる文字
};
Stream.of(samples)
.peek(s -> {
System.out.printf("%s:\t%5d\t%s\n", "SRC", s.length(), s);
})
.peek(s -> {
s = Normalizer.normalize(s, Normalizer.Form.NFD); // 正規分解
System.out.printf("%s:\t%5d\t%s\n", "NFD", s.length(), s);
})
.peek(s -> {
s = Normalizer.normalize(s, Normalizer.Form.NFC); // 正規分解とそれに続く正規合成
System.out.printf("%s:\t%5d\t%s\n", "NFC", s.length(), s);
})
.peek(s -> {
s = Normalizer.normalize(s, Normalizer.Form.NFKD); // 互換分解
System.out.printf("%s:\t%5d\t%s\n", "NFKD", s.length(), s);
})
.peek(s -> {
s = Normalizer.normalize(s, Normalizer.Form.NFKC); // 互換分解とそれに続く正規合成
System.out.printf("%s:\t%5d\t%s\n", "NFKC", s.length(), s);
})
.forEach(s -> {
System.out.println();
});
SRC: 6 ABC012
NFD: 6 ABC012
NFC: 6 ABC012
NFKD: 6 ABC012
NFKC: 6 ABC012
SRC: 6 バカボン
NFD: 6 バカボン
NFC: 6 バカボン
NFKD: 6 バカボン
NFKC: 4 バカボン
SRC: 8 ㍆㌋㌋㌧㌨㌰㌣㌈
NFD: 8 ㍆㌋㌋㌧㌨㌰㌣㌈
NFC: 8 ㍆㌋㌋㌧㌨㌰㌣㌈
NFKD: 23 マルクカイリカイリトンナノピコセントエーカー
NFKC: 22 マルクカイリカイリトンナノピコセントエーカー
...
// ログレベルによってデバッグコードの実行を制御する。
public static <E> Consumer<E> debug(Consumer<E> consumer) {
return log.isDebugEnabled() ? consumer : (E e) -> {};
}
...
System.setProperty("debug", "true");
...
List<Order> orders = ... ;
orders = orders.stream()
.peek(debug(order -> {
log.debug("DUMP 1: Order:{}", order);
}))
.sorted() // せき止められる
.peek(debug(order -> {
log.debug("DUMP 2: Order:{}", order);
}))
.collect(Collectors.toList());
peek() に渡したラムダ式から外部のローカル変数に代入できない事情は forEach() と同じだ。
カウンタのようなちょっとした状態を持ちたいだけでも、工夫を強いられる。
// Consumer にメンバ変数を持たせる
public static <V, E> Consumer<E> aboo(V initial, BiFunction<V,E,V> bifunc) {
return new Consumer<E>() {
V memo = initial;
@Override
public void accept(E element) {
memo = bifunc.apply(memo, element);
}
};
}
...
List<String> list = Stream.of("a","b","c")
.peek(aboo(1, (i, s) -> {
System.out.format("%d %s", i, s);
return ++i;
}))
.map(s -> s.toUpperCase())
.peek(aboo(1, (i, s) -> {
System.out.format("%s", i, s);
return ++i;
}))
.collect(Collectors.toList());
ところで peek() は本当にデバッグにしか使えないものなのだろうか。
確かに Consumer からは要素を差し替えるような事はできない。
かと言って、要素の内部データの操作・変更を妨げるものでもない。
// Map の List
List<Map<String, Object>> rows = ...;
rows.stream()
.peek(e -> {
// デフォルト値を設定
e.putIfAbsent("nick_name", e.get("first_name"));
})
.count(); // 回すだけ
}
// 例外を投げてStream を中断してしまう
List<String> list = Stream.of(array)
.peek(e -> Objects.requireNonNull(e, "ガッ")) // NullPointerException
.map(e -> e.toString());
.collect(Collectors.joining());
3.2 skip() / limit()
skip(n) はそこを通る最初の n 個の要素を捨てる。
limit(n) はそこを通る最初の n 個までの要素を見過ごし、それ以降を捨てる(短絡操作)。
n は負数だと例外(IllegalArgumentException)になるが、要素数を超えも例外にはならない。
また 0 を指定することもできる。
逆に最後からの n 個を捨てたり取得したりするような中間操作は提供されていない。
String[] arr = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"};
int n = 2;
int m = 5;
// n 番目から m + 1 番目までを取得
Stream.of(arr)
.limit(m)
.skip(n)
.forEach(e -> {
System.out.println(e);
});
// n 番目から m 個を取得
Stream.of(arr)
.skip(n)
.limit(m)
.forEach(e -> {
System.out.println(e);
});
//...のは案外むずかしい
// 一組のカード
EnumSet<Card> deck = EnumSet.allOf(Card.class);
// カードを一枚引く
Card aCard = deck.stream()
.skip(new Random().nextInt(set.size()))
.findAny()
.get();
deck.remove(aCard);
List<String> list = ... ;
int index = 100;
Optional<String> item = list.stream()
.skip(index) // 万一リストサイズを超えてもエラーにはならない
.findFirst();
skip()とlimit()を組み合わせれば、部分 Stream を得ることができる ―― ただし、1回だけ。
ページネーションのような複数領域へ分割をする目的には不向きだ。
@SuppressWarnings("serial")
public static List<Map<String, String>> csvToRows(Path path) throws IOException {
final Pattern delim = Pattern.compile(",\\s*");
try (Stream<String> header = Files.lines(path).limit(1); // 先頭行
Stream<String> lines = Files.lines(path).skip(1)) { // それ以降
// 先頭行を見出し行とみなす
String[] flds = header
.flatMap(line -> delim.splitAsStream(line))
.toArray(String[]::new);
if (flds.length == 0)
return Collections.emptyList();
return lines
.map(line -> delim.split(line, flds.length))
.map(vals -> Arrays.copyOf(vals, flds.length)) // 要素不足を許容したい
.map(vals -> new LinkedHashMap<String,String>(){{
for (int i=0; i<flds.length; i++)
put(flds[i], Objects.toString(vals[i], ""));
}})
.collect(Collectors.toList());
}
}
// substring()では char インデックスで切り出すので補助文字に対応できない。
// そしてなぜか substring() のコードポイント版は提供されていない。
public static String substringByCodePoints(String str, int start, int end) {
if (start < 0 || end - start < 0)
throw new StringIndexOutOfBoundsException("" + start + ", " +end);
int[] cps = str.codePoints()
.skip(start)
.limit(end - start)
.toArray();
//if (cps.length < end - start)
// throw new StringIndexOutOfBoundsException("" + start + ", " +end);
return new String(cps, 0, cps.length);
}
...
String str = "𠀀𠀁𠀂𠀃𠀄𠀅𠀆𠀇𠀈𠀉";
System.out.println(str.substring(2,6)); // 𠀁𠀂
System.out.println(substringByCodePoints(str, 1,3)); // 𠀁𠀂
limit() は Stream を中断(短絡操作)できる唯一の中間操作だ。
(今のところ。次期 Java 9 で takeWhile() がそれに加わる予定だ)
無限 Stream は迂闊なパイプラインで帰ってこれなくなることがあるので、保険で適当な limit() を入れておくことは結構重要かもしれない。
まあ、テキスト処理では自動生成でも目論まない限り、そのような危惧はないと思うが。
// ズンドコキヨシまとめ - Qiita
// http://qiita.com/shunsugai@github/items/971a15461de29563bf90
public enum ZunDokoKiyoshi {
ドコ, ズン;
final static int MIN_POINT = 4;
static int score = 0;
public static void main(String[] args) {
final int repeat = 1;
// 無限ズンドコ
new Random().ints(0, values().length)
.mapToObj(i -> values()[i])
.peek(e -> System.out.print(e))
.peek(e -> score += e.ordinal())
.filter(e -> e == ドコ)
.filter(e -> 0 < (score /= MIN_POINT))
.limit(repeat)
.forEach(e -> {
System.out.println("キ・ヨ・シ!");
score = 0;
});
}
}
// 追記予定
細かい話をもう一つ、limit() に 0 を指定するとどうなるか。
基本的に、パイプラインに limit(0) が一つでもあると、Stream 全体の処理が実行されない。つまり、空 Stream を処理するのと同じことになる。(ただし上流に sorted() がある場合は別(後述))
パイプラインの動作を考えれば当たり前なのだが、limit() に要素が到達するかどうかに係わらず Stream 処理全体の実行を制御する様は電気回路のスイッチのようにも見える。
IntStream.range(0, 100) // 0 ~ 99
.peek(i -> {
System.out.println(i); // (A)
})
.skip(1000) // 1000 以下は通れない
.peek(i -> {
System.out.println("ここに来ないはず"); // (B)
})
//.limit(1) // (A)は全て表示される。(B)は表示されない。
.limit(0) // (A)も(B)は表示されない。
.count();
さて、skip() にしろ limit() にしろ、目当ての要素が得られるのは、あらかじめその位置(インデックス)がわかっていてこそだが、テキスト処理でそのような状況は少ない。
本当にやりたいのは位置よりも要素の内容評価による領域の取得だ。
次期 Java 9 では、正にそのための中間操作である 「dropWhile()」 と 「takeWhile()」 が、 Stream API に追加されるという。
- Java 9 Streamに追加された3つの新機能 - Qiita
- http://download.java.net/java/jdk9/docs/api/java/util/stream/Stream.html#dropWhile-java.util.function.Predicate-
- http://download.java.net/java/jdk9/docs/api/java/util/stream/Stream.html#takeWhile-java.util.function.Predicate-
これで、「~になるまで」捨てる/拾うができるようになり、使いどころが増える。
List<String> body = Files.lines(path)
.dropWhile(s -> !s.isEmpty()) // 最初の空行以降
.skip(1) // 空行自体はいらない
.takeWhile(s -> !"-- ".equals(s)) // 署名以前
.collect(Collectors.toList());
3.3 filter() / distinct()
filter() と distinct() は、Stream の要素をその内容で間引く。
filter() は要素を条件によって絞り込む中間操作で、引数には判定のための述語関数(Predicate)を与える。
述語が要素の値だけを見ている限り、filter() はステートレス中間操作だ。
distinct() は要素の重複を排除する中間操作だ。
重複をチェックするためには過去の要素の何らかの情報を保持するはずなので、これはステートフル中間操作ということになる。
逆に重複する要素のみや、重複しない要素のみを抽出するような中間操作は用意されていない。
filter() と disctinct() は、どちらもよく使う中間操作であり、また両者を組み合わせることも多い。
Stream.of(array)
.filter(Objects::nonNull) // メソッド参照
.forEach(s -> {
System.out.println(s);
});
Stream.of(array)
.filter(s -> s.endsWith(".txt")) // ラムダ式
.forEach(s -> {
System.out.println(s);
});
String data = "aaa aaa bbb aaa ccc bbb ccc";
String uniq = Stream.of(data.split("\\s"))
.peek(s -> {
System.out.print("\n" + s);
})
.distinct()
.peek(s -> {
System.out.print("\t" + s);
})
.collect(Collectors.joining(","));
System.out.println();
System.out.println(uniq);
String[] names = {"Taro Yamada", "ヤマダ タロウ", "山田 太郎"};
// String#matches は正規表現を都度コンパイルするので効率が悪い
Stream.of(names)
.filter(s -> s.matches("\\p{IsLetter}+ \\p{IsLetter}+"))
.forEach(s -> {
System.out.println(s);
});
// Taro Yamada
// ヤマダ タロウ
// 山田 太郎
// コンパイル済みの正規表現を用意する
Pattern isKatakana = Pattern.compile("\\p{IsKatakana}+[ ]\\p{IsKatakana}+");
Stream.of(names)
.filter(isKatakana.asPredicate()) // Java 8 で追加
.forEach(s -> {
System.out.format("氏:%s、名:%s\n", (Object[])s.split(" +"));
});
// 氏:ヤマダ、名:タロウ
// キャプチャする(ミドルネームを飛ばす)
Pattern isEimei = Pattern.compile("(\\w+) (\\w\\. )?(\\w+)");
Stream.of(names)
.map(s -> isEimei.matcher(s)) // isEimei::matcher
.filter(m -> m.matches()) // Matcher::matches
.forEach(m -> {
System.out.format("氏:%s、名:%s\n", m.group(3), m.group(1));
});
// 氏:Yamada、名:Taro
// 追記予定
// 追記予定
filter() にラムダ式を使うときには、できるだけ単純な式にしないと後で何をやっているのか自分でも分からなくなる。
一目で何をやっているのか分からないようなラムダ式は、いったん変数にとって適切な名前をつける。
またビジネスロジックを含むような関数は、ラムダ式を避けて別途メソッドに定義し、処理内容の明確なメソッド名にした上で、メソッド参照等で適用する。
Predicate<LocalDate> isOysterable = d -> d.getMonth().name().contains("R");
Stream.of(LocalDate.now())
.filter(isOysterable)
.forEach(d -> {
System.out.printf("%tBのカキ(゚д゚)ウマー\n", d);
});
// 追記予定
複雑な述語でも、複数条件を AND している所で個々の条件にばらせれば、単純な複数の filter() に分ける事で分かりやすくできる。
しかし OR とか NOT とかあるとそうもいかない。
Predicateインターフェースには、述語関数をプログラム的に組み合わせるためのデフォルトメソッドが用意されていて、なにか使えそうな気がする。
negate()
and(Predicate<T> other)
or(Predicate<T> other)
ところが、これらをメソッド参照やラムダ式で使ってみると、どうも相性が悪い。
最初の関数を Predicate にキャストする必要があったり、優先順位が分かりにくかったりして、思ったほど使い勝手はよくない。
// 残念な感じ
Stream.of(array)
.filter(((Predicate<String>)(Objects::isNull)).negate())
.count();
// 別に分かりやすくもない
Stream.of(list)
.filter(((Predicate<String>)(s -> s.startsWith("#")))
.or(String::isEmpty)
.negate())
.forEach(s -> {
System.out.println(s);
});
public static <T> Predicate<T> duplicated() {
// Set の add() の戻り値を反転した述語
// 重複があると true を返す
return ((Predicate<T>)new HashSet<T>()::add).negate();
}
...
boolean hasDups = Stream.of(array)
.filter(duplicated())
.findFirst()
.isPresent();
distinct() は常に完全一致している String のみを同一とみなす。
言語表現には揺らぎが付きもので、完全に一致していなくても、たとえば大文字と小文字のように同一視したいことはよくある。
残念ながら distinct() にはそのような等価性を評価する関数を与えられる口は用意されていない。
言葉の揺らぎを吸収するためには distinct() する前に何らかの正規化を施すことになる。
// Gmailで使用可能な別名アドレス(エイリアス) - Gmailの使い方
// https://www.ajaxtower.jp/gmail/sent/index18.html
public String normGmailAddress(final String address) {
String gmail = address.trim()
.toLowerCase(); // 大文字小文字を区別しない
gmail = gmail.replaceFirst("@googlemail.com$", "@gmail.com"); // googlemail.com も可
if (!gmail.endsWith("@gmail.com"))
return address;
gmail = gmail.replaceFirst("\\+.*(?=.*@)", ""); // +以降は無視
if (gmail.contains("..") || gmail.contains(".@")) // 連続.と末尾.は不正
return address;
return gmail.replaceAll("\\.(?=.*@)", ""); // アカウント名の .を無視
}
...
String[] addr = {
"KINDO.Nichiyo@gmail.com",
"kin.do.nichi.yo+qiita@googlemail.com",
"soji.okita@qiita.com",
"sojiokita+hoge@qiita.com",
};
Stream.of(addr)
.map(this::normGmailAddress)
.distinct()
.forEach(e -> {
System.out.println(e);
});
}
// テキストからアルファベット文字を抽出する
public String alphabeticsIn(final String text) {
return Normalizer.normalize(text, Normalizer.Form.NFKD).codePoints()
.filter(Character::isAlphabetic)
.map(Character::toUpperCase)
.distinct()
.sorted()
.mapToObj(cp -> String.format("%c", cp))
.collect(Collectors.joining());
}
...
// 英文パングラムを検出
final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
String[] samples = {
"The quick brown fox jumps over the lazy dog.",
"Bright vixens jump; dozy fowl quack.",
"She sells sea shells by the seashore.",
};
Stream.of(samples)
.filter(text -> ALPHABET.equals(alphabeticsIn(text)))
.forEach(text -> {
System.out.printf("PANGRAM DETECTED : '%s'\n", text);
});
// いろは歌は完全パングラム
final String KYU_KANA47 = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑを";
String iroha_uta = ""
+ "いろはにほへど ちりぬるを わがよたれぞ つねならむ "
+ "うゐのおくやま けふこえて あさきゆめみじ ゑひもせず ";
iroha_uta = iroha_uta.replaceAll("[\\s ]", "");
Stream.of(iroha_uta)
.filter(text -> KYU_KANA47.equals(alphabeticsIn(text)))
.peek(s -> {
System.out.println("パングラムです!");
})
.filter(text -> KYU_KANA47.length() == text.length())
.forEach(text -> {
System.out.println("か、完全パングラムです!");
});
正規化により、元テキストの表現が失われてしまう。
それもある程度生かしたいとなったら distinct() の代わりに filter() などを使って、自前で重複チェックするしかない。
// 重複を判定する述語関数を返す。
public static Predicate<String> distinctIgnoreCase() {
// SortedSet インターフェースの実装クラスは、等価性の判定に Comparator を使う
// 大文字小文字を区別しない Comparator を設定する
// Set.add() は重複登録で false を返す述語として使える
return new ConcurrentSkipListSet<>(String.CASE_INSENSITIVE_ORDER)::add;
//return new TreeSet<>(String.CASE_INSENSITIVE_ORDER)::add;
}
...
String text = "Java perl PERL JAVA AWK";
String uniq = Stream.of(text.split(" "))
.filter(distinctIgnoreCase())
.collect(Collectors.joining(" "));
System.out.println(uniq);
3.4 sorted()
sorted() は、Stream の要素をソートした Stream を返す。
sorted() には引数あり・なしの2つのオーバーロードが用意されている。
- sorted()
- sorted(Comparator super T> comparator)
引数なしの sorted() は要素を単に自然順序昇順でソートする。
ただし要素クラスは Comaprable でなければならない。
うかつに素の POJO なクラスを流すと、実行時にエラー(ClassCastException)になる。
もちろん String クラスは Comparable だ。
”不自然な”順序や降順でソートしたい場合には、引数ありの soreted() に、カスタムの Comparator を関数として渡す。
ラムダで与えるなら Comparator#comparing() を使ったほうがシンプルになる。
List<String> list = ... ;
// デフォルト
list.stream()
.sorted() // 自然順序昇順
.forEach(s -> {
System.out.println(s);
});
// Comparator
list.stream()
.sorted(Comparator.reverseOrder()) // 自然順序降順
.forEach(s -> {
System.out.println(s);
});
// ラムダ式
list.stream()
.sorted((l, r) -> l.length() - r.length()) // 文字列長降順
.forEach(s -> {
System.out.println(s);
});
// メソッド参照(Comparable)
list.stream()
.sorted(String::compareToIgnoreCase) // 大小文字無視
.forEach(s -> {
System.out.println(s);
});
// 関数オブジェクト
list.stream()
.sorted(String.CASE_INSENSITIVE_ORDER) // 大小文字無視
.forEach(s -> {
System.out.println(s);
});
// Comparable
Stream<Path> paths = … ;
paths.stream()
.sorted(Comparator.comparing(path -> path.getFileName()))
.forEach(s -> {
System.out.println(s);
});
// java.util.List` にも 1.8 で `sort()` がデフォルトメソッドで追加されている。
// List 自体の要素順が変更するのが目的ならこちらの方が経済的だ。
// Comparator を指定する
list.sort(Comparator.naturalOrder());
// null を指定するとデフォルトで自然順序昇順
list.sort(null);
String nums = "1 12 123 2 21 3";
nums = Stream.of(nums.split(" "))
.sorted(Comparator.comparingInt(Integer::parseInt))
.collect(Collectors.joining(" "));
// "1 2 3 12 21 123"
String[] words = {
"は", "はつ", "ハット", "はと", "ハート", "はあと","はぁと",
"はは", "パパ", "ババ", "ははは", "a", "A", "b", "B"
};
// デフォルトでは自然順序(文字コード順)
Stream.of(words)
.sorted()
.forEach(w -> {
System.out.print(w + " ");
});
// A B a b は はぁと はあと はつ はと はは ははは ハット ハート パパ ババ
// 日本語かな辞書順でソートするには java.text.Collator を使う。
Collator jaCmp = Collator.getInstance(Locale.JAPANESE);
jaCmp.setStrength(Collator.IDENTICAL);
jaCmp.setDecomposition(Collator.FULL_DECOMPOSITION);
Stream.of(words)
.sorted(jaCmp)
.forEach(w -> {
System.out.print(w + " ");
});
// a A b B は はあと はぁと ハート はつ ハット はと はは ババ パパ ははは
// 追記予定
// コマンド `/bin/rm -rf`
public class Rmrf {
static final int DEPTH = 100; // 最大ディレクトリレベル
public static void main(String[] args) throws IOException {
Path dir = Paths.get(args[0]).normalize();
// パス名降順で下層ファイルから順番に削除していく
Files.find(dir, DEPTH, (p, e) -> true)
.sorted(Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch(IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
sorted() は、とてもステートフルな中間操作だ。
それはもう、めっちゃステートフルだ。
例えば、無限 Stream に sorted() を適用すると、帰って来れない。
IntStream.iterate(1, i -> i + 1)
.peek(i -> System.out.println(i))
.sorted()
.limit(0) // 効かぬ
.findFirst();
//java.lang.OutOfMemoryError: Java heap space
要素の順序を決定するために、sorted() は全ての要素がそろうまで、パイプラインを塞き止める。
中間操作であるにもかかわらず、終端操作のようにループを持っているわけで、言わばStream の中ボス的存在だ。
それはまた、全要素を sorted() で保持していることも意味する。
Stream を使えば、テキストファイルのソートなど簡単に記述できてしまうわけだが、古いプログラマとしてはなかなかシビれる。
// sort コマンドの模倣
// 引数に与えられた複数のファイルをマージしてソートする("-" は標準入力)
public class Sort {
public static void main(String[] args) throws IOException {
final String STDIN = "-";
String[] files = {STDIN};
if (args.length > 0)
files = args;
// 各ファイルのテキスト行の Stream の Stream
Stream.Builder<Stream<String>> builder = Stream.builder();
for (String file : files) {
final Stream<String> lines;
if (file.equals(STDIN)) {
lines = Stream.of(System.in)
.map(InputStreamReader::new)
.map(BufferedReader::new)
.flatMap(BufferedReader::lines); // close されない
} else {
try {
lines = Files.lines(Paths.get(file)); // close される
} catch (IOException e) {
System.err.printf("Sort: cannot read: %s: No such file or directory", file);
return;
}
}
builder.add(lines);
}
builder.build()
.flatMap(l -> l)
.sorted()
.forEach(line -> {
System.out.println(line);
});
}
}
ところで、Java 最古参の interface である java.util.Comparator は、 Java 8 にきて多くの static メソッドと default メソッドを授けられた。
- default メソッド
- reversed()`
- thenComparing(Comparator super T> other)
- thenComparing(Function super T, ? extends U> keyExtractor,Comparator super U> keyComparator)
- static メソッド
- naturalOrder()
- reverseOrder()
- nullsFirst(Comparator super T> comparator)
- nullsLast(Comparator super T> comparator)
これらを組み合わせれば、SQL の ORDER BY のようなノリで複数のキーによる複雑なソートを制御する事も可能だ。
しかし Predicate と同様、そう単純な話ではない。
Stream プログラミンとしては、比較関数としてのラムダ式やメソッド参照とうまく絡めたいところだが、この相性がまた悪い。
関数を Comparator へキャストしたり、型推論が効かずにジェネリクス型を明示する必要があったりして、お世辞にも簡潔とは言えない。
また型推論のエラーを直そうにも、内臓が飛び出したようなエラーメッセージで途方に暮れる。
List<User> list = new ArrayList<>();
// reversed() を使いたいだけの人生だった・・・
list.stream()
//.sorted(((l, r) -> l.getName().compareTo(r.getName())).reversed()) // The target type of this expression must be a functional interface
.sorted(((Comparator<User>)((l, r) -> l.getName().compareTo(r.getName()))).reversed())
//.sorted(Comparator.comparing(e -> e.getName()).reversed()) // The method getName() is undefined for the type Object
.sorted(Comparator.comparing((User e) -> e.getName()).reversed()) // OK
.sorted(Comparator.<User, String>comparing(e -> e.getName()).reversed()) // OK
.sorted(Comparator.comparing(User::getName).reversed()) // OK
.sorted(Comparator.comparing(User::getName, Comparator.reverseOrder())) // OK
.forEach(e -> {
System.out.println(e);
});
Map<String, Integer> map = ... ;
// Map の値で降順にしたい
map.entrySet().stream()
// .sorted(Map.Entry.comparingByValue().reversed()) // 意味不明なエラーメッセージ
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed()) // OK
.sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) // OK
.forEach(e -> {
System.out.println(e);
});
import static java.util.Comparator.*;
...
// 単語 -> 発生件数
Map<String, Integer> wordCounts = ... ;
// Map を値で降順にソートする
Map<String, Integer> top10 = wordCounts.entrySet().stream()
.sorted(comparing(e -> - e.getValue())) // 負数で比較
.limit(10)
.collect(Collectors.toMap(
e -> e.getKey(),
e -> e.getValue(),
(l, r) -> r, // dummy
LinkedHashMap::new)); // 登録順を維持するMap
top10.forEach((k, v) -> {
System.out.println(v + " : " + k);
});
// 追記予定
sorted の比較評価対象が文字列そのものではく、それを変換処理をしたものである場合、インスタンス生成や文字列データのコピーというコストがパフォーマンスに与える影響は馬鹿にならない。
// mapで変換
// 変換処理はN回だが、その分のオブジェクトのメモリを消費する。
list.stream()
.map(s -> s.toUpperCase())
.sorted()
.forEach(s -> {
System.out.println(s);
});
// Comparator で変換
// 同時に必要な対象は2個だけだが、変換処理が O(nLog n) 回起こる。
list.stream()
.sorted(Comparator.comparing(s -> s.toUpperCase()))
.forEach(s -> {
System.out.println(s);
});
// 部分文字列のコピーを回避するため、CharBufferを使う
public static int[] suffixArray(CharBuffer buff) {
int end = buff.length();
return IntStream.range(0, end)
.boxed() // IntStream で Comparator が使えないはどーゆーつもりなのか小1時間ry
.sorted(Comparator.comparing(
i -> buff.subSequence((int)i, end))) //文字配列をコピーしない
.mapToInt(i -> (int)i)
.toArray();
}
public static int[] suffixArray(String text) {
return suffixArray(CharBuffer.wrap(text));
}
// テキストファイルをメモリマップする事もできる。
// ファイルのエンコーディングはUTF16BE。
public static int[] suffixArray(Path file) throws IOException {
try(FileChannel fc = FileChannel.open(file)) {
return suffixArray(fc.map(MapMode.READ_ONLY, 0, fc.size()).asCharBuffer());
}
}
3.5 map()
map() は、与えられた関数で要素を変換した Stream を返す。
関数の戻り値の型で、 Stream の要素の型を変更してもよい。
//関数の用意(先頭文字を大文字にする)
Function<String, String> capitalize = (String s) -> {
return String.format("%c%s",
Character.toTitleCase(s.charAt(0)),
s.substring(1));
};
List<String> words = …;
words = words.stream()
.map(s -> s.trim()) // ラムダ式
.filter(s -> !s.isEmpty())
.map(String::toLowerCase) // メソッド参照
.map(capitalize) // 関数オブジェクト
.collect(Collectors.toList());
// エンティティのIDを収集する
List<User> users ...
List<String> names = users.stream()
.map(item -> item.getName()) // Stream の型を変更
.collect(Collectors.toList());
これまで Java で map と言えば java.util.Map ということで通じていたが、Java Stream の map の登場でもっと広く抽象的な用語となった。
コンピュータ用語一般で言う「map」は、取りあえず日本語にすれば「写像」と言ったところか。
写像関係を定義するには2つのアプローチが考えられて、集合間の要素対応をデータベースとして持つか、入力値から出力値への計算としてルールベースで記述するかになる。
まあどちらにしろテキスト処理としては、文字列を「変換」すると言っても差し支えないだろう。
// 追記予定
// 追記予定
// 追記予定
// コドン→アミノ酸の変換テーブル
Map<String, String> geneticCodes = = new HashMap<String, String>() {{
put("UUU","Phe"); put("UUC","Phe"); put("UUA","Leu"); put("UUG","Leu");
・・・
}};
Pattern codingRegion = Pattern.compile("(?<gene>(?<start>AUG)(...)+?)(?<stop>UAA|UAG|UGA)");
Pattern codonFrame = Pattern.compile("(?<=\\G...)");
String mRNA = "UCAGUCAGAUGUUUCCCAAAGGGUAAGUCAGUCAGAAAAAAAAAAAAAAA";
String protein = Stream.of(mRNA)
.map(codingRegion::matcher)
.filter(seq -> seq.find())
.map(seq -> seq.group("gene"))
.flatMap(gene -> {
return codonFrame.splitAsStream(gene);
})
.map(codon -> {
return geneticCodes.get(codon); // データマッピング
})
.collect(Collectors.joining("-"));
System.out.println(protein); // Met-Phe-Pro-Lys-Gly
public static UnaryOperator<String> slim() {
Map<String, String> map = new HashMap<>();
return s -> map.computeIfAbsent(s, k -> k);
}
// 同一文字列のオブジェクトを大量に持つのは忍びない・・・
List<String> words = Arrays.asList("x x A x".split("\\s"));
words.forEach(w -> {
System.out.printf("%08X %s\n", System.identityHashCode(w), w);
});
// 66CD51C3 x
// 4DCBADB4 x
// 4E515669 A
// 17D10166 x
// 要素文字列に重複があれば、初出のオブジェクトで置き換える
words = words.stream()
.map(slim())
.collect(Collectors.toList());
words.forEach(w -> {
System.out.printf("%08X %s\n", System.identityHashCode(w), w);
});
// 66CD51C3 x
// 66CD51C3 x
// 4E515669 A
// 66CD51C3 x
テキスト処理的にはルールベースで変換したいことも多いが、Java にはパターンマッチングとかガードとかいうかっちょいい構文がないので、ラムダ式でロジックを埋め込むとどうしても簡潔さが失われる。
IntStream.range(1, 100)
.mapToObj(
i -> i % 15 == 0 ? "FizzBuss"
: i % 3 == 0 ? "Fizz"
: i % 5 == 0 ? "Buzz"
: String.valueOf(i))
.forEach(s -> {
System.out.println(s);
});
public static Function<String, String> getCharacterReferenceDecoder() {
// 実体文字参照を置換するデータベース
@SuppressWarnings("serial")
Map<String, String> entities = new HashMap<String, String>() {{
put("nbsp"," "); put("lt","<"); put("gt",">"); put("amp","&"); put("quot","\"");
}};
// 数値文字参照を変換する関数
BiFunction<String, Integer, String> unref =
(String s, Integer r) -> String.format("%c", Integer.parseUnsignedInt(s, r));
// 文字参照抽出パターン
Pattern charRef = Pattern.compile("^" + "&(#(?<dec>\\d+?)|#x(?<hex>[0-9A-F]+?)|(?<ent>.{1,8}?));",
Pattern.CASE_INSENSITIVE);
return (String text) -> Pattern.compile("(?=&.{1,8}?;)").splitAsStream(text) // 文字参照を行頭に持つ文字列の Stream に変換
.map(s -> charRef.matcher(s))
.map(m -> {
String c = "";
if (m.find()) {
if (m.group("hex") != null) { // 数値参照16進
c = unref.apply(m.group("hex"), 16);
} else if (m.group("dec") != null) { // 数値参照10進
c = unref.apply(m.group("dec"), 10);
} else if (m.group("ent") != null) { // 実体参照
c = entities.getOrDefault(m.group("ent"), m.group());
}
}
return m.replaceFirst(c);
})
.collect(Collectors.joining());
}
map() によるパイプラインを「プログラム可能なセミコロン」として見なせないこともない。
Java でよく見るメソッドを重ねて何度も変換をかけるような醜いコーディングバターンは、map で見通しよく書き換えられるかもしれない。
要素一つだけなら、java.util.Optional
を使った方が軽い。
// Windows コマンドプロンプトはShiftJIS
final Charset sjis = Charset.forName("Shift_JIS");
Stream.of(System.in)
.map(is -> new InputStreamReader(is, sjis))
.map(isr -> new BufferedReader(isr))
.flatMap(br -> br.lines()) // Ctrl+Zで終了(DOS)
.forEach(line -> {
System.out.println(line);
});
public static LocalDateTime dateToLocalDateTime(final java.util.Date date) {
return Optional.of(date)
.map(d -> d.toInstant())
.map(i -> i.atZone(ZoneId.systemDefault()))
.map(z -> z.toLocalDateTime())
.get();
}
3.6 flatMap()
flatMap() は、関数が返した Stream を全て連結した Stream を返す。
map() 同様、flatMap() も渡された関数で要素を変換するが、さらにそれを Stream にして返さなければならない点が map() とは異なる。
関数は、Stream の要素型も変更してよいし、空 Stream を返してもよい。
関数の返した各 Stream は、 flatMap() 内部で close される。
// Stream の連結
Stream<String> stream1 = …
Stream<String> stream2 = …
Stream<String> stream2 = …
Stream.of(stream1, stream2, stream3)
.flatMap(stream -> stream) // ラムダ式
.forEach(e -> {
System.out.println(s);
});
// 配列の連結
String[] arr1 = {};
String[] arr2 = {};
String[] arr3 = {};
String[] ret = Stream.of(arr1, arr2, arr3)
.flatMap(Array::stream) // メソッド参照
.toArray(String[]::new);
// 要素の分解
Pattern delim = Pattern.compile(“\\s+”);
List<String> lines = …;
List<String> words = lines.stream()
.flatMap(line -> {
return delim.splitAsStream(line);
})
.collect(Collectors.toList());
int[] chars = words.stream()
.flatMapToInt(word -> {
return word.codePoints(); // IntStream に変更
})
.toArray();
flatMap() は、flatMap と言うからにはプログラミング的にやはり map して flatten するものなのだろう。
flatten は高次構造を平坦化するという意味合いで、他の言語でも flatMap はリストのリストをリストにしたり、2次元配列を1次元配列にしたりするのに使われる。
そのノリで言うと、Java Stream の flatMap は、「Stream の Stream」を Stream に平坦化するということになる。
Java Stream は、データ構造ではなく操作の集まり(パイプライン)なのだから、なんでそれをまた Stream にして何をどうするというのか。
手を動かしてみて、テキスト処理の実用的なところでは、flatMap の使いどころは大体以下のようなパターンになるようだ。
- 複数の Stream を連結する
- ソースとなる高次の配列やリストの次数を下げる
- 文字列を分解した文字列や文字の Streamにする
Map<String, String> header = ... ;
String file = ... ;
String footer = ... ;
String message = Stream.of(
header.entrySet().stream(), // Collection
Stream.of(""),
Files.lines(Paths.get(file)), // ファイル
Stream.of(""),
Stream.of(footer.split("\\r?\\n"))) // 配列
.flatMap(Function.identity())
.map(Objects::toString)
.collect(Collectors.joining("\r\n"));
String[][] matrix = {{"a", "b"}, {"c", "d"}};
String arr[] = Arrays.stream(matrix)
.flatMap(Arrays::stream)
.toArray(String[]::new);
class ArrayStreams {
public static Stream<Object> flatten(Object... o) {
if (o.length == 0)
return Stream.empty();
if (!o[0].getClass().isArray())
return Stream.of(o);
return Stream.of(o).flatMap(a -> flatten((Object[])a));
}
}
String[][][] a3d = {
{{"a","b","c"}, {"d","e","f"},},
{{"g"}, {"h","i"}, {"j","k"},},
};
String[] flat = Arrays.stream(a3d)
.flatMap(ArrayStreams::flatten)
.toArray(String[]::new);
System.out.println(Arrays.toString(flat)); // [a, b, c, d, e, f, g, h, i, j, k]
Path file = Paths.get("sample.txt");
Pattern lineBreak = Pattern.compile("(?<=\\G.{40})"); // 1行40文字まで
try (Stream<String> lines = Files.lines(file)) {
lines
.flatMap(lineBreak::splitAsStream)
.forEach(line -> {
System.out.println(line);
});
}
List<String> lines = ・・・;
// chars
lines.stream()
.map(l -> l.split(""))
.flatMap(Arrays::stream)
.forEach(s -> {
System.out.println(s);
});
// code points
Pattern one = Pattern.compile("(?!\\p{InLowSurrogates})");
lines.stream()
.flatMap(one::splitAsStream)
.forEach(s -> {
System.out.println(s);
});
// 64-bit Windows で MeCab の Java バインディングを使いたい - Qiita
// http://qiita.com/kumazo/items/5c5c57d94b2080ffbb5a
public class Wakati {
static {System.loadLibrary("MeCab");}
private static Stream<Node> nodeStreamOf(String line) {
Tagger tagger = new Tagger();
tagger.parse(""); // おまじない
Iterator<Node> nodeIterator = new Iterator<Node>() {
Node node = tagger.parseToNode(line);
@Override public boolean hasNext() {
return node != null;
}
@Override public Node next() {
Node nextNode = node;
node = node.getNext();
return nextNode;
}
};
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
nodeIterator, Spliterator.ORDERED | Spliterator.NONNULL), false);
}
public static void main(String[] args) throws IOException {
final String file = "kokoro01.txt"; //args[0];
try (Stream<String> lines = Files.lines(Paths.get(file))) {
lines.flatMap(line -> {
return nodeStreamOf(line);
})
.map(Node::getSurface)
.forEach(w -> {
System.out
.printf(w)
.printf(" ");
});
}
}
// 追記予定
flatMap は for ループのかわりに使えることもある。
Charset charset = StandardCharsets.UTF_8;
String text = "あいうえお";
String dump = Stream.of(text)
.map(s -> s.getBytes(charset))
.flatMap(b ->
IntStream.range(0, b.length)
.map(i -> Byte.toUnsignedInt(b[i]))
.mapToObj(c -> String.format("%02X", c)))
.collect(Collectors.joining());
String[] pais = "萬筒索".split("");
String[] nums = "一二三四伍六七八九".split("");
String[] jipais = "東西南北白發中".split("");
List<String> tiles = Stream.of(
Stream.of(pais)
.flatMap(pai -> Stream.of(nums)
.map(su -> su + pai)),
Stream.of(jipais))
.flatMap(t -> t)
.flatMap(f -> Stream.of(f, f, f, f))
.collect(Collectors.toList());
flatMapで文字列を分割したStreamにするの分には簡単なのだが、逆に複数要素を一定数でまとめて、そのStream にするような処理は難しい。
そこは素直にバッファとループを書いたほうがいいだろう。
// 改行1個は体裁の折り返しとみなし削除する。
// 改行2個(空行)は段落区切りとみなしそのまま残す。
// バッファをStream外に持たなければならない。
final StringBuilder buff = new StringBuilder();
// 空文字ストリープを連結しているのは、バッファの最後の内容を吐き出すための苦肉の策。
Stream.concat(Files.lines(Paths.get("long_story.txt")), Stream.of(""))
.flatMap(line -> {
if (line.isEmpty()) {
// 空行(2個めの改行)が来たら、バッファの内容を取り出しリセットする
String para = buff.toString();
buff.setLength(0);
// 空行も付け加えて送り出す
return Stream.of(para, "");
} else {
// 空行でなければ、それをバッファに追加し・・・
buff.append(line);
// 空 Stream でやり過ごす。
return Stream.empty();
}
})
.forEach(line -> {
System.out.println(line);
});
public static String base64enc(byte[] data) {
String[] base64 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
.split("");
String[] pads = {"", "!!!", "==", "="};
Pattern triplet = Pattern.compile("(?<=\\G...)");
// LCM(8, 6) = 3 * 8 = 4 * 6 = 24
int len = (data.length * 4 + (3 - 1)) / 3;
return Stream.of(data)
.map(d -> Arrays.copyOf(d, d.length + 3))
.map(b -> new String(b, StandardCharsets.ISO_8859_1))
.flatMap(s -> triplet.splitAsStream(s))
.mapToInt(b8 -> b8.chars()
.reduce(0, (l, r) -> l << 8 | r))
.flatMap(b24 -> IntStream.of(3, 2, 1, 0)
.map(p -> b24 >> p * 6 & 0b111111))
.limit(len)
.mapToObj(b6 -> base64[b6])
.collect(Collectors.joining("", "", pads[len % 4]));
}
3.6 sequential() / parallel() / unordered()
《省略》
4 まとめ
《追記予定》
A. 参考
《追記予定》