4

This question

Is there a concise way to iterate over a stream with indices in Java 8?

describes how to drive one stream based on another stream of indexes. My goal is to take an array of strings

one two three four five six seven eight

and all-cap the elements having an array index between 2 and 5:

one two THREE FOUR FIVE SIX seven eight

I've figured out how to do this, but my instinct says there should be a more elegant way of going about it. In the below example, the first stream filters out all elements except those in range. It's not directly what I want, of course, but it's similar in how it uses a stream to filter by index-range. The second alters the array elements in place, capitalizing the ones I want.

I don't like how it alters the array in place, and how it requires two streams. Is there a better way?

import static java.util.stream.Collectors.joining;

import java.util.Arrays;
import java.util.stream.IntStream;

public class UpperCaseElementsInIndexRangeViaIntStream {

   public static void main(String[] ignored) {
      final String input = "one two three four five six seven eight";
      final int MIN_IDX = 2;
      final int MAX_IDX = 5;
      final String[] splits = input.split(" ");

      //Filter only those in range

      String output = IntStream.range(0, splits.length).
            filter(idx -> MIN_IDX <= idx && idx <= MAX_IDX).
            mapToObj(i -> splits[i]).
            collect(joining(" "));

      System.out.println(output);

      //Change the array in place, capitalizing only those in range

      IntStream.range(0, splits.length).forEach(idx -> {
         final String split = splits[idx];
         splits[idx] = (MIN_IDX <= idx && idx <= MAX_IDX)
                       ? split.toUpperCase() : split;
      });

      output = Arrays.stream(splits).collect(joining(" "));

      System.out.println(output);
   }
}

Output:

three four five six
one two THREE FOUR FIVE SIX seven eight
Community
  • 1
  • 1
aliteralmind
  • 19,847
  • 17
  • 77
  • 108

4 Answers4

3

Here's a solution without Stream. Use subList to get a view of the range you want as a List. Then use replaceAll to transform the elements in place.

List<String> all = Arrays.asList(splits);
all.subList(MIN_IDX, MAX_IDX + 1).replaceAll(String::toUpperCase);
// use 'all' as needed

Note that the second parameter of replaceAll is an exclusive index value.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • Well my goal is to learn streams, so perhaps it not the best question for that purpose. But this is seriously interesting. Can’t believe I never heard of subList before. Seems like its been around since Java 6. – aliteralmind Aug 08 '15 at 01:42
3

Here's the Stream API-based solution which is parallel friendly:

final String[] splits = input.split(" ");

String output = IntStream.range(0, splits.length).parallel()
    .mapToObj(idx -> MIN_IDX <= idx && idx <= MAX_IDX ?
              splits[idx].toUpperCase() : splits[idx])
    .collect(Collectors.joining(" "));

Note that I'm posting this answer just for the completeness. Streams are not well-suitable for indexed operations. I vote for @SotiriosDelimanolis solution, it's really simple and elegant.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
1

I have also thought there should be a map(predicate, functionIfTrue, functionIfFalse) kind of method, but there isn't. Instead, use a ternary in your map:

final int[] idx = {-1}; // trick to get around "effectively final"
String result = Arrays.stream(input.split(" "))
   .map(s -> MIN_IDX <= ++idx[0] && idx[0] <= MAX_IDX ? s.toUpperCase() : s)
   .collect(joining(" "));

This employs a trick to get the index of the iteration by using a single element array that is itself final, but whose contents are mutable.


Some test code:

final String input = "one two three four five six seven eight";
final int MIN_IDX = 2;
final int MAX_IDX = 5;
final int[] idx = {-1};
String result = Arrays.stream(input.split(" "))
    .map(s -> MIN_IDX <= ++idx[0] && idx[0] <= MAX_IDX ? s.toUpperCase() : s)
    .collect(joining(" "));
System.out.println(result);

Output:

one two THREE FOUR FIVE SIX seven eight
Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • 1
    I actually though about adding such `map(predicate, functionIfTrue, functionIfFalse)` to my StreamEx library, but is there any benefit of such method over ternary operator? Usually ternary is shorter. – Tagir Valeev Aug 08 '15 at 13:02
  • Neat trick with the index. I wonder if there are any consequences doing that. Just the answer I was looking for. Thanks. – aliteralmind Aug 08 '15 at 14:14
  • 2
    @aliteralmind, the consequence is that this solution will not work for parallel streams. – Tagir Valeev Aug 08 '15 at 16:05
  • @TagirValeev Any alternatives that can work in parallel then? It seems an IntStream is required. And perhaps two streams, too :( – aliteralmind Aug 08 '15 at 16:27
  • 2
    @aliteralmind, single stream is enough (though `input.split(" ")` is still sequential). Posted an answer. – Tagir Valeev Aug 08 '15 at 17:14
0

functionally similar but more streamlined

IntStream.range( 0, splits.length ).boxed().collect( Collector.of(
    StringBuilder::new,
    (buf, idx) -> buf.append( idx > 0 ? " " : "" ).append(
        idx >= MIN_IDX && idx <= MAX_IDX ? splits[idx].toUpperCase() : splits[idx] ),
    (buf1, buf2) -> buf1 ) ).toString();

streamlined:
– allocation of only one StringBuilder
– the mapping is omitted

Kaplan
  • 2,572
  • 13
  • 14