70
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Java 8 Stream API にテキストを流してみて(中間操作編)

Last updated at Posted at 2017-04-08

今さらですが、昔書きかけた投稿のお蔵出しすることにします。
まだ途中ですが、書き溜めたコードを随時追記していく予定です。

[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ドキュメントにはデバッグ用だとはっきり書いてある。

peek()の基本
    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() で観察できる。

Unicode正規化形式の確認
    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	マルクカイリカイリトンナノピコセントエーカー

...

Streamにデバッグコードを埋め込む

// ログレベルによってデバッグコードの実行を制御する。
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データの値を差し替える
    // Map の List
    List<Map<String, Object>> rows = ...;

    rows.stream()
            .peek(e -> {
                // デフォルト値を設定
                e.putIfAbsent("nick_name", e.get("first_name"));
            })
            .count();  // 回すだけ
}
nullチェック
    // 例外を投げて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 個を捨てたり取得したりするような中間操作は提供されていない。

skip()/limit()の基本
    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);
        });
Setからランダムに要素を取得する
    //...のは案外むずかしい
    // 一組のカード
    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回だけ。
ページネーションのような複数領域へ分割をする目的には不向きだ。

CSVファイルをMapのListに変換する(その2)
@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 処理全体の実行を制御する様は電気回路のスイッチのようにも見える。

limit(0)の効果
        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 に追加されるという。

これで、「~になるまで」捨てる/拾うができるようになり、使いどころが増える。

メールの本文を抽出する(Java9)
    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() は、どちらもよく使う中間操作であり、また両者を組み合わせることも多い。

filter()の基本
        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);
                });
distinct()の基本
    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
distinctの例
    // 追記予定
ステートフルなfilterの例
    // 追記予定

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 にキャストする必要があったり、優先順位が分かりにくかったりして、思ったほど使い勝手はよくない。

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で使用可能な別名アドレス(エイリアス) - 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() を使ったほうがシンプルになる。

sorted()の基本
    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 は はあと はぁと ハート はつ ハット はと はは ババ パパ ははは 

Mapのキーを順序付ける
    // 追記予定
ディレクトリを再帰的に削除する
// コマンド `/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() を適用すると、帰って来れない。

無限Streamをソートできるか
    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);
    });
CSVデータをソートする
    // 追記予定

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 の要素の型を変更してもよい。

map()の基本
    //関数の用意(先頭文字を大文字にする)
    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を使った単純な変換の例
    // 追記予定
遺伝子コード翻訳
    // コドン→アミノ酸の変換テーブル
    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 にはパターンマッチングとかガードとかいうかっちょいい構文がないので、ラムダ式でロジックを埋め込むとどうしても簡潔さが失われる。

FizzBuzz
    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 を使った方が軽い。

DOSコンソール入力
    // 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);
            });
java.util.DateクラスからLocallDateTimeクラスに変換する
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 される。

flatMap()の基本
// 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にする
色々なソースの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"));
2次元配列を1次元配列に伸す
   String[][] matrix = {{"a", "b"}, {"c", "d"}};
   
   String arr[] = Arrays.stream(matrix)
           .flatMap(Arrays::stream)
           .toArray(String[]::new);
多次元配列を1次元配列に伸す
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);
            });
    }
1文字分解
    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);
            });
MeCabで分かち書き
// 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 ループのかわりに使えることもある。

Stringの16進ダンプ
    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);
            });
base64変換
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. 参考

《追記予定》

70
78
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
70
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?