Why is String.chars() a stream of ints in Java 8?

JavaStringJava 8

Java Problem Overview


In Java 8, there is a new method String.chars() which returns a stream of ints (IntStream) that represent the character codes. I guess many people would expect a stream of chars here instead. What was the motivation to design the API this way?

Java Solutions


Solution 1 - Java

As others have already mentioned, the design decision behind this was to prevent the explosion of methods and classes.

Still, personally I think this was a very bad decision, and there should, given they do not want to make CharStream, which is reasonable, different methods instead of chars(), I would think of:

  • Stream<Character> chars(), that gives a stream of boxes characters, which will have some light performance penalty.
  • IntStream unboxedChars(), which would to be used for performance code.

However, instead of focusing on why it is done this way currently, I think this answer should focus on showing a way to do it with the API that we have gotten with Java 8.

In Java 7 I would have done it like this:

for (int i = 0; i < hello.length(); i++) {
    System.out.println(hello.charAt(i));
}

And I think a reasonable method to do it in Java 8 is the following:

hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);

Here I obtain an IntStream and map it to an object via the lambda i -> (char)i, this will automatically box it into a Stream<Character>, and then we can do what we want, and still use method references as a plus.

Be aware though that you must do mapToObj, if you forget and use map, then nothing will complain, but you will still end up with an IntStream, and you might be left off wondering why it prints the integer values instead of the strings representing the characters.

Other ugly alternatives for Java 8:

By remaining in an IntStream and wanting to print them ultimately, you cannot use method references anymore for printing:

hello.chars()
        .forEach(i -> System.out.println((char)i));

Moreover, using method references to your own method do not work anymore! Consider the following:

private void print(char c) {
    System.out.println(c);
}

and then

hello.chars()
        .forEach(this::print);

This will give a compile error, as there possibly is a lossy conversion.

Conclusion:

The API was designed this way because of not wanting to add CharStream, I personally think that the method should return a Stream<Character>, and the workaround currently is to use mapToObj(i -> (char)i) on an IntStream to be able to work properly with them.

Solution 2 - Java

The answer from skiwi covered many of the major points already. I'll fill in a bit more background.

The design of any API is a series of tradeoffs. In Java, one of the difficult issues is dealing with design decisions that were made long ago.

Primitives have been in Java since 1.0. They make Java an "impure" object-oriented language, since the primitives are not objects. The addition of primitives was, I believe, a pragmatic decision to improve performance at the expense of object-oriented purity.

This is a tradeoff we're still living with today, nearly 20 years later. The autoboxing feature added in Java 5 mostly eliminated the need to clutter source code with boxing and unboxing method calls, but the overhead is still there. In many cases it's not noticeable. However, if you were to perform boxing or unboxing within an inner loop, you'd see that it can impose significant CPU and garbage collection overhead.

When designing the Streams API, it was clear that we had to support primitives. The boxing/unboxing overhead would kill any performance benefit from parallelism. We didn't want to support all of the primitives, though, since that would have added a huge amount of clutter to the API. (Can you really see a use for a ShortStream?) "All" or "none" are comfortable places for a design to be, yet neither was acceptable. So we had to find a reasonable value of "some". We ended up with primitive specializations for int, long, and double. (Personally I would have left out int but that's just me.)

For CharSequence.chars() we considered returning Stream<Character> (an early prototype might have implemented this) but it was rejected because of boxing overhead. Considering that a String has char values as primitives, it would seem to be a mistake to impose boxing unconditionally when the caller would probably just do a bit of processing on the value and unbox it right back into a string.

We also considered a CharStream primitive specialization, but its use would seem to be quite narrow compared to the amount of bulk it would add to the API. It didn't seem worthwhile to add it.

The penalty this imposes on callers is that they have to know that the IntStream contains char values represented as ints and that casting must be done at the proper place. This is doubly confusing because there are overloaded API calls like PrintStream.print(char) and PrintStream.print(int) that differ markedly in their behavior. An additional point of confusion possibly arises because the codePoints() call also returns an IntStream but the values it contains are quite different.

So, this boils down to choosing pragmatically among several alternatives:

  1. We could provide no primitive specializations, resulting in a simple, elegant, consistent API, but which imposes a high performance and GC overhead;

  2. we could provide a complete set of primitive specializations, at the cost of cluttering up the API and imposing a maintenance burden on JDK developers; or

  3. we could provide a subset of primitive specializations, giving a moderately sized, high performing API that imposes a relatively small burden on callers in a fairly narrow range of use cases (char processing).

We chose the last one.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAdam DygaView Question on Stackoverflow
Solution 1 - JavaskiwiView Answer on Stackoverflow
Solution 2 - JavaStuart MarksView Answer on Stackoverflow