【JavaScript】forEachやfilterなどのStream APIを使ってみよう

【JavaScript】forEachやfilterなどのStream APIを使ってみよう

エンジニアの皆さん、こんにちは!
今回はJavaScriptの要素技術の中でも私が愛してやまない「forEachやfilterなどのStream API」について説明します!
様々な言語の元となっているJavaScriptですが、その要素技術を覚えることは現在のエンジニアにとって必須と言っても過言ではなくなってきています。

様々な書き方がありますが、なるべく多く紹介しますので環境に応じて使い分けてみてください。

Stream APIとは?

Collectionや配列など、データの集合体から、個々のデータを取り出して「処理の流れ(Stream)」に引き渡すための仕組みのことです。
Stream APIは「中間操作」と「終端操作」があり、「中間操作」は処理結果を返しますが、「終端操作」は何も返しません。

これまで入れ子になっていた処理を変わりやすく記述することができたり、マルチスレッドで処理できるなどメリットが多いのが特徴です。

forEach(終端操作)

取り出した個々のデータに対して何か処理を行うのが、forEachになります。
例えばの例として以下のStream APIを使用しない場合と使用する場合を比べてみてください。

(例題)各データを2倍した値を表示しなさい(dataListにデータがあるものとする)

for文のみで実装した場合

for (let i = 0; i < dataList.length: i++) {
    console.log(dataList[i] * 2);
}

Stream API使用例①
インデックス指定が不要になったことが大きなポイントです。

dataList.forEach(function(x) {
    console.log(x * 2);
});

Stream API使用例②
ラムダ式を使用して、よりシンプルな書き方になります。

dataList.forEach(x -> {
    console.log(x * 2);
});

filter(中間操作)

取り出した個々のデータが特定の条件に合致するかを判定して、合致するもののみを取り出す操作が、filterになります。
例えばの例として以下のStream APIを使用しない場合と使用する場合を比べてみてください。

(例題)偶数のみのリストにしなさい(dataListにデータがあるものとする)

for文のみで実装した場合

let newDataList = [];
for (let i = 0; i < dataList.length: i++) {
    if (dataList[i] % 2 == 0) {
        newDataList.push(dataList[i]);
    }
}

Stream API使用例①
終端操作よりもコードの記述を減らせるのがうれしいですね!

let newDataList = dataList.filter(function(x) {
    return x % 2 == 0
});

Stream API使用例②
ラムダ式を使用して、よりシンプルな書き方になります。

let newDataList = dataList.filter(x -> {
    return x % 2 == 0
});

map(中間操作)

取り出した個々のデータを加工した結果を取り出す操作が、mapになります。
例えばの例として以下のStream APIを使用しない場合と使用する場合を比べてみてください。

(例題)各データを2倍した値を取り出しさなさい(dataListにデータがあるものとする)

for文のみで実装した場合

let newDataList = [];
for (let i = 0; i < dataList.length: i++) {
    newDataList.push(dataLsit[i] * 2);
}

Stream API使用例①
インデックス指定が不要になったことが大きなポイントです。
ここではereturnとして新しく返す要素を指定します。

let newDataList = dataList.map(function(x) {
    return x * 2;
});

Stream API使用例②
ラムダ式を使用して、よりシンプルな書き方になります。

let newDataList = dataList.map(x -> {
    return x * 2;
});

sorted(中間操作)

データの集合体を昇順もしくは降順に並び替える処理を行うのが、sortedになります。
例えばの例として以下のStream APIを使用しない場合と使用する場合を比べてみてください。

(例題)データを昇順に並び替えなさい(dataListにデータがあるものとする)

for文のみで実装した場合(バブルソート)

for (let i = 0; i < dataList.length - 1: i++) {
    for (let j = i + 1; j < dataList.length - i; j++) {
        let tmp;
        if (dataList[j - 1] > dataList[j]) {
            tmp = dataLsit[j - 1];
            dataLsit[j - 1] = dataList[j];
            dataList[j] = tmp;
        }
    }
}

Stream API使用例①
ソートアルゴリズムを記述する必要がないのが最も大きなポイントで、取り出すデータは2つになります。
なお、returnの値が正なら昇順、負なら降順に並び替える仕様になっています。

dataList = dataList.sorted(function(x, y) {
    return x - y;
});

Stream API使用例②
ラムダ式を使用して、よりシンプルな書き方になります。

dataList = dataList.sorted((x, y) -> {
    return x - y;
});

おまけ①
sortedでは単純に昇順に並べる際に「Comparator.naturalOrder」という便利なものがあります。

dataList = dataList.sorted(Comparator.naturalOrder());

おまけ②
sortedでは単純に降順に並べる際に「Comparator.reverseOrder」という便利なものもあります。

dataList = dataList.sorted(Comparator.reverseOrder());

入れ子の解消について

私が個人的にStream APIを使うメリットだと思っているのは以下の2つです。

  • 入れ子が解消できる
  • 処理速度が速い(GASで使用するとめちゃくちゃ実感できます!!)

ここでは入れ子が解消できるという点に着目してみましょう!!

例えば、以下のような処理を記述するとします。

(例題)データの中から偶数の値のみを取り出し、3倍にした結果を降順に表示しなさい(dataListにデータがあるものとする)

Stream APIを使わないと以下のようになります。
(拡張for文を使えばもう少し簡単になりますが、そこはご了承ください)

let newDataList = [];
for (let i = 0; i < dataList.length; i++) {
    if (dataList[i] % 2 == 0) {
        newDataList.push(dataList[i] * 3);
    }
}
for (let i = 0; i < newDataList.length - 1: i++) {
    for (let j = i + 1; j < newDataList.length - i; j++) {
        let tmp;
        if (newDataList[j - 1] < newDataList[j]) {
            tmp = newDataList[j - 1];
            newDataList[j - 1] = newDataList[j];
            newDataList[j] = tmp;
        }
    }
}
for (let i = 0; i < newDataList.length; i++) {
    console.log(newDataList[i]);
}

Stream APIを使うとここまで簡単になる&例題の順のままコードを記述できるのがわかると思います!!

dataList
    // 偶数のみを取り出す
    .filter(x -> { return x % 2 == 0; })
    // 3倍する
    .map(x -> { return x * 3; })
    // 降順に並べる
    .sorted((Comparator.reverseOrder())
    // 表示する
    .forEach(x -> console.log(x));

まとめ

今回は「forEachやfilterなどのStream API」処理について紹介しました。
Stream APIを使用することで処理がとても簡単に記述できることが分かったと思います。
配列に限らず、連想配列などでも同じようにすることができるので、ぜひとも覚えてみてください!!

大手SIerの新人教育や2~3年次を対象としたスキルアップ研修で培われてきたエッセンスをお伝えしましたが、少しでも役に立てたでしょうか?

研修講師に関するお問い合わせはこちらからお待ちしております。
LancersMENTAでも活動していますので、そちらからお仕事を依頼して頂けます。