「IntStreamからList<Integer>を作ろうとしたらコンパイルエラーになった」——Java Gold 1Z0-826の学習を進めていると、一度はこの壁にぶつかるのではないでしょうか。原因を調べるとboxed()というメソッドに行き着くものの、「結局いつ使えばいいのか」がはっきりしないまま、なんとなくエラーを回避するために書いている方も多いはずです。

この記事では、boxed()が必要になる場面を整理し、試験本番で見落としやすいひっかけポイントまで具体的なコード付きで解説します。読み終える頃には、IntStreamまわりの問題で迷わなくなります。

boxedとは何か

boxed()は、IntStream・LongStream・DoubleStreamといったプリミティブ型専用のストリームを、対応するラッパー型のストリームに変換するメソッドです。

  • IntStream → boxed() → Stream<Integer>
  • LongStream → boxed() → Stream<Long>
  • DoubleStream → boxed() → Stream<Double>

内部的にはオートボクシング(プリミティブ型をラッパー型に自動変換する仕組み)を明示的に行っているだけで、特別な計算をしているわけではありません。

IntStream intStream = IntStream.rangeClosed(1, 5);
Stream<Integer> stream = intStream.boxed();

stream.forEach(System.out::println);
// 1
// 2
// 3
// 4
// 5

なぜプリミティブ型専用のStreamが存在するのか

そもそもJavaにIntStreamのようなプリミティブ型専用のストリームが用意されているのは、パフォーマンスのためです。Stream<Integer>はint値を毎回Integerオブジェクトに変換して保持しますが、これはオートボクシングのコストがかかります。大量データを扱う集計処理では、このコストが無視できません。

そのためsum()・average()・max()・min()といった集計系メソッドは、IntStream側に直接用意されており、boxed()をする必要がありません。

int total = IntStream.rangeClosed(1, 100).sum();
System.out.println(total); // 5050

OptionalDouble avg = IntStream.rangeClosed(1, 100).average();
System.out.println(avg.getAsDouble()); // 50.5

「集計するならとりあえずboxed()」と覚えていると、この時点で無駄な変換を書いてしまうミスにつながります。

boxedが必要になる代表的な場面

boxed()が必要になるのは、主に「Stream<T>を前提としたAPIを使いたいとき」です。代表例は次の3つです。

1. Collectorsでコレクションを作りたいとき

IntStreamにはcollect(Collector)のオーバーロードが存在しません。Collectors.toList()などを使うには、事前にboxed()でStream<Integer>に変換する必要があります。

List<Integer> list = IntStream.rangeClosed(1, 5)
        .boxed()
        .collect(Collectors.toList());
System.out.println(list); // [1, 2, 3, 4, 5]

2. Comparatorを使ってソートしたいとき

IntStreamのsorted()は引数なしで自然順にソートするメソッドしか持ちません。Comparatorを渡して降順にしたい場合などは、boxed()が必須です。

List<Integer> desc = IntStream.rangeClosed(1, 5)
        .boxed()
        .sorted(Comparator.reverseOrder())
        .collect(Collectors.toList());
System.out.println(desc); // [5, 4, 3, 2, 1]

3. groupingByなどでグループ化したいとき

Collectors.groupingBy()はStream<T>にしか使えないため、IntStreamのままでは呼び出せません。

Map<Boolean, List<Integer>> grouped = IntStream.rangeClosed(1, 10)
        .boxed()
        .collect(Collectors.partitioningBy(i -> i % 2 == 0));
System.out.println(grouped);
// {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}

boxedを使わないとどうなるか

実際にコンパイルエラーになる例を見てみましょう。

List<Integer> list = IntStream.rangeClosed(1, 5)
        .collect(Collectors.toList()); // コンパイルエラー

IntStreamが持つcollectは次のシグネチャです。

<R> R collect(Supplier<R> supplier,
              ObjIntConsumer<R> accumulator,
              BiConsumer<R, R> combiner)

Collectors.toList()が返すCollectorオブジェクトは、この3引数版のシグネチャに一致しないため、コンパイルが通りません。「collectがあるからCollectorsが使えるはず」という思い込みが、このエラーの典型的な原因です。

boxedとmapToObjの違い

もう一つ、試験で狙われやすいのがboxed()とmapToObj()の違いです。boxed()は「ラッパー型への変換専用」であるのに対し、mapToObj()は任意の型への変換に使えます。

List<String> strList = IntStream.rangeClosed(1, 3)
        .mapToObj(i -> "No." + i)
        .collect(Collectors.toList());
System.out.println(strList); // [No.1, No.2, No.3]

ここで注意したいのが、文字列連結を行うCollectors.joining()です。joining()はCharSequence型の要素を前提としているため、boxed()で得られるStream<Integer>には使えません。

// 誤り:Integerはjoiningの対象外でコンパイルエラー
String joined = IntStream.rangeClosed(1, 3)
        .boxed()
        .collect(Collectors.joining(", "));

正しくはmapToObj(String::valueOf)で文字列に変換してからjoining()を呼び出します。

String joined = IntStream.rangeClosed(1, 3)
        .mapToObj(String::valueOf)
        .collect(Collectors.joining(", "));
System.out.println(joined); // 1, 2, 3

「boxed()さえ呼べば後は何でもできる」という誤解が、この種の問題の失点ポイントになります。

IntStreamとStream<Integer>の操作比較

操作IntStreamのまま呼べるかboxedが必要か
sum() / average() / max() / min()○ 呼べる不要
sorted()(自然順)○ 呼べる不要
sorted(Comparator)× 呼べない必要
distinct()○ 呼べる不要
collect(Collectors.toList()等)× 呼べない必要
collect(Collectors.groupingBy等)× 呼べない必要
collect(Collectors.joining())× 呼べないmapToObjが必要(boxedでは不可)

試験で問われやすい注意点まとめ

  • sum()・average()など集計系メソッドはIntStreamのままで完結し、boxed()は不要です。
  • sorted()は引数なしならIntStreamのままで呼べますが、Comparatorを渡す場合はboxed()が必要です。
  • Collectors系のメソッド(toList()・groupingBy()・partitioningBy()など)を使う場合は、必ず事前にboxed()が必要です。
  • Collectors.joining()だけはboxed()では対応できず、mapToObj(String::valueOf)のように文字列へ変換する必要があります。
  • Stream.of(1, 2, 3)は最初からStream<Integer>であり、IntStream.of(1, 2, 3)とは型が異なります。両者を混同しないよう注意してください。
  • boxed()はオートボクシングを伴うため、大量データの処理では性能面のトレードオフがある、という理解も問われることがあります。

まとめ

boxed()は「IntStreamなどのプリミティブ型ストリームを、CollectorsやComparatorといったStream<T>前提のAPIに橋渡しするための変換」です。集計処理では不要、コレクション化やソート、グループ化では必要、という基準を押さえておけば、試験本番でも迷わず判断できるようになります。特にjoining()のようにboxed()では対応できない例外があることまで押さえておくと、応用問題にも対応しやすくなります。