【用語解説】プログラミングにおける「ストリーム」とは?
こんにちは。インフラ担当のいせです。
ある日、「ストリームって何ですか?」と弊社の新人から質問されました。
Webで調べてみたそうなのですが、曰く「ストリームとはデータの流れ」。う~ん、わかったようなわからないような……
プログラミングをしていると時々出会う単語ですが、なんとなく雰囲気でやり過ごしがちで、いざ説明しようと思うとなかなか難しい単語ではないでしょうか。
今回はそんな「ストリーム」という単語を「ビュッフェ」と「コース料理」に例えて、噛み砕いて説明してみます!
ストリームとは?
例えるならストリームは「コース料理」、非ストリームは「ビュッフェ」という感じです。
何を言っているのかよくわからないですね。具体的なコードでストリーム的な処理と非ストリーム的な処理を見比べてみましょう。
以下はJavaのコードですが、そんなに難しくないはないですしちゃんと解説するのでJavaを知らない人もブラウザバックしないでください!
ポイントは3つあります。1) 6品の料理名の文字列が配列に格納されています。2) 非ストリーム的な処理では、for文で配列の添字をインクリメントすることで各要素を順番に取り出して画面に出力しています。3) ストリーム的な処理では、配列だったものでforEachメソッドを呼び出し、配列の各要素を先頭から順番に取り出して画面に出力しています。
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) throws Exception {
String[] dishes = new String[]{"シーザーサラダ", "オニオンスープ", "鮭のムニエル", "トマトのソルベ", "牛フィレステーキ", "ティラミス"};
//非ストリーム的な処理
for(int i = 0; i < dishes.length; i++){
System.out.println(dishes[i]);
}
//ストリーム的な処理
Stream<String> stream = Arrays.stream(dishes);
stream.forEach(dish-> {
System.out.println(dish);
});
}
}
ストリームや非ストリームというのは、プログラミングをする上で、データをどのように扱うか(どのように抽象化するか)という違いです。
非ストリーム的な処理ではデータを取り出すときに、「どこに欲しいデータがあるのか」(配列の何番目の要素か)ということを意識する必要があります。上記のコードでは配列からデータを取り出すときに添字を指定していますね。今回はたまたまfor文で添字がインクリメントされているため、配列の先頭から順番にデータが取り出されていますが、配列なので何番目の要素に欲しいデータが入っているかわかっていれば、それだけ取り出したりすることも可能です。例えるなら、いきなりメインのステーキから食べ始めたりサラダを残したりと自由自在です。
一方でストリーム的な処理では、先頭から最後まで順番に処理するので、非ストリーム的な処理のようにどこにデータがあるか(配列の何番目にどんなデータがあるか)を意識する必要はありません。それどころか元々のデータが配列であるかどうかさえ意識する必要がなくなります。「なんかよくわからないけど待ってたらデータが流れてくるから、流れてきたものを順々に処理する」みたいなコードの書き方ができます。
ファイルの操作はストリーム、ただし……
ファイルの読み書きは殆どの場合、ストリームであるかのように扱われています。ファイルを読み込むときにはよくforEachのようなイテレーターのメソッドを使いますよね。ある行だけ読み取りたいときもありますが、その場合はシークすることで実現します。先頭からその行の直前までスキップし、途中から読み込みを開始するということです。コース料理のシェフに前菜とスープは飛ばしてムニエルから出して、とお願いしているようなイメージです。
(ただしファイルは本質的には非ストリームです。HDDやSSDでデータを読み書きするには、どこに読み書きしたいデータがあるかという位置情報(アドレス)を指定する必要があるからです。ストリームの方が色々と便利なので、非ストリーム的であるファイルをOSがストリームに見えるように細工しています。その裏ではOSが、HDDやSSDのどこにあるデータを読み書きしたいかアドレスを使って非ストリーム的な操作を行っています。)
ネットワーク通信もストリーム
ネットワーク通信は本質的にストリームであると言えます。データを受信する側はどこにデータがあるかはわからないですが、待っていれば次々とデータが送られてきます。ただしネットワーク通信もメモリなどに一旦保存してしまえば位置を指定してデータにアクセスすることができるので非ストリーム的に見せかけることが可能です。
ストリームと非ストリームの関係
厨房では次々と料理が作られてくるのでストリーム的ですが、お皿に盛り付けてテーブルに置いておけばいつでも好きなときに料理を取りにいけるので非ストリーム的です。テーブルにストックしておいた料理をウェイターが順番に配膳すれば、お客さんからはストリームであるかのように見えます。
非ストリームは場所を指定してデータを操作し、ストリームは時間の経過とともに流れてくるデータを処理します。時間の対義語は空間と言いますが、ストリームは時間を意識したプログラミングの手法で、非ストリームは空間を意識した手法ということができます。
ストリームのメリット
最後にストリームを使うと便利な場面を紹介します。
ここに 超巨大なファイル.txt
があります。 むむ、どうやら10GBくらいあるみたいです。
まずは非ストリームで処理してみます。
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class NonStreamTest {
public static void main(String[] args) throws Exception {
// いったんファイルの中身を全て読み込んで変数に入れる
List<String> 超巨大ファイルの内容 = Files.readAllLines(Paths.get("超巨大ファイル.txt"));
// 読み込んだ変数を順次処理する
for (String line : 超巨大ファイルの内容) {
System.out.println(line);
}
}
}
おっと、残念ながら Files.readAllLines(Paths.get("超巨大ファイル.txt"))
の処理の途中、メモリに10GB分のデータを読み込むところでプログラムが死んでしまいました。確かに10GBもの変数を確保してデータを代入するのは難しいですよね。
では次にストリームでやってみましょう。
import java.nio.file.Files;
import java.nio.file.Paths;
public class StreamTest {
public static void main(String[] args) throws Exception {
Files.lines(Paths.get("超巨大ファイル.txt")).forEach(line -> {
System.out.println(line);
});
}
}
これならファイルを読み込みながら処理をして、使い終わった古いデータは捨てられるので同時に確保されるメモリはごくわずかです。そして処理は成功しました!
まとめ
今回はストリームという用語を解説してみました。
冒頭の新人さんの質問をみたとき、自分はきちんと説明できるかなとドキッとしてしまったのですが、わかりやすい説明になったでしょうか?
ストリームのような単語はなんとなく雰囲気で使いがちですが、よくよく考えてみると「どういう意味だっけ?」となることは少なくないかなと思います。一つひとつのことに立ち止まって自分の理解を確認できる人はきっとぐんぐん成長していくのではないでしょうか。
ラクーンホールディングスではそのような疑問を抱くことが得意なエンジニアを募集しています。疑問をSlackでつぶやくとそのスレッドで、さながら「誰が一番わかりやすく教えられるか選手権」の様相をみせる職場で技術力を磨いてみませんか?エントリーはこちらから、ぜひお待ちしています。