Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

Given a string, S, of length N that is indexed from 0 to N−1, I want to print its even-indexed and odd-indexed characters as 2 space-separated strings on a single line:

Hacker -> Hce akr

My code works, but I feel it could have been done more elegantly and maybe in a single stream instead of in two... Any ideas how to improve this?

private static String reindex(String input) {
    StringBuilder result = new StringBuilder();

    IntStream.range(0, input.length())
             .filter(index -> index % 2 == 0)
             .forEach(index -> result.append(input.charAt(index)));

    result.append(" ");

    IntStream.range(0, input.length())
             .filter(index -> index % 2 == 1)
             .forEach(index -> result.append(input.charAt(index)));

    return result.toString();
}
share|improve this question

The main issue with your code is that it operates with side-effects by leveraging the forEach operation. If you were to run your code in parallel, it would be broken and you wouldn't have the expected result. In this case, what you want is to use a mutable reduction approach, that is collect elements into a container.

All propose solutions hardcodes the fact that we reindex even and odd indexes. However, it would be possible to make them generic easily.


The first simple approach is to use 2 Stream pipeline (as you have done). The first one creates the first part of the String and the second one creates the second. Instead of using forEach to add elements to a StringBuilder, we collect the elements into a new StringBuilder. As such, each index is mapped to the character at that index and accumulated into a new StringBuilder with appendCodePoint. The 3 arguments to the collect call corresponds to:

  1. the supplier of the mutable container, (StringBuilder::new);
  2. the accumulator of each element into the container (appending the code point of the character);
  3. a combiner combining two containers into one (used in parallel processing).
private static String reindex(String input) {
    StringBuilder result = new StringBuilder(input.length() + 1);
    result.append(filter(input, index -> index % 2 == 0));
    result.append(' ');
    result.append(filter(input, index -> index % 2 == 1));
    return result.toString();
}

private static StringBuilder filter(String input, IntPredicate predicate) {
    return IntStream.range(0, input.length())
             .filter(predicate)
             .map(input::charAt)
             .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append);
}

The duplicate logic has been extracted to a utility method that filter the indexes matching the given predicate and collects the filtered characters into a StringBuilder.


But the issue is that it loops around the String two times when such an operation should be possible using a single traversal. This is a bit more complicated to do and I would first measure the performance of the first solution before doing this one: it is possible that your concerned String do not have a length big enough to warrant such an approach.

A possible solution is to collect the elements inside an array of StringBuilder. The array to append the current character to is determined by the result of i % 2: when i is even, this will be 0 and it will append it to the first array; when i is odd, it will append it to the second array.

private static String reindex(String input) {
    StringBuilder[] result = 
        IntStream.range(0, input.length())
             .collect(
                () -> new StringBuilder[] { new StringBuilder(), new StringBuilder() },
                (b, i) -> b[i % 2].appendCodePoint(input.charAt(i)),
                (b1, b2) -> {
                    b1[0].append(b2[0]);
                    b1[1].append(b2[1]);
                }
             );

    return result[0] + " " + result[1];
}
share|improve this answer
    
Okay. I see I really need to read up about the collect method since I'm not really understanding what happens inside there in the second approach. Seems like the most concise solution, though I would probably try to refactor some of the lambda expressions into separate statements. – AdHominem yesterday

The Java-8 approach suggested by @Tunaki is valid, but still there is a rather high degree of complexity, because:

  • there is an intermediate array of StringBuilders, with final results concatenated from items taken from this array.
  • there are three lambdas among collect method args and they are not easily readable without remembering the respective signature of collect method and clear understanding of the algorithm.

Here is a solution which uses a single StringBuilder, no Strings concatenation, staying within a single pass through the input stream:

private static String reindex2(String input) {
  StringBuilder bld = new StringBuilder(" "); // init it with a space in the middle
  IntStream.range(0, input.length())
         .forEach(i -> {
            if (i % 2 == 0) { // even chars
              int insertIndex = i / 2;
              bld.insert(insertIndex, input.charAt(i));
            } else { // odd chars
              bld.append(input.charAt(i));
            }
          });
  return bld.toString();
}

The principle is the following. Even characters are inserted at the beginning of the StringBuilder, at the index that equals to the index of the original character divided by two. Odd characters are appended to the StringBuilder.

A test case to compare with the original:

@Test
public void testReindex() {
  final String testString = "abcdefghijklmnopqrstuvwxyz";
  final String original = reindex(testString);
  final String improved = reindex2(testString);
  assertEquals(original, improved);
}
share|improve this answer
    
But if you run it in parallel on longer Strings, the results won't be correct. To have a correct output, you would need forEachOrdered and then it defeats the parallel purpose. In fact, I just tested with a parallel Stream (and forEach) and it crashed with ArrayIndexOutOfBoundsException. – Tunaki 20 hours ago
    
I don't recommend inserting. It would scale poorly performance-wise, and it would not be parallelizable. – 200_success 20 hours ago
    
Thank you for these comments, I agree that there can be this parallelization drawback. However, the original question said nothing about that requirement. To improve characters insertion , the StringBuilder can be initialized with input.length + 1, which will be its target capacity. – Antot 20 hours ago
    
instead of inserting - create the stringbuilder with the correct length (use setLength) and then use setCharAt to set characters at the correct indexes (front and back). But there's a serious question as to whether parallelizing this with the built in streams would help (due to cache effects). You'd really want to control the parallelization so that each thread wrote contiguous sections of the stringbuilder without crossing cache lines ... I bet measurement would show that for this case the stream api is worse (much worse) than the "naïve" loop. Streams aren't a panacea in Java 8. – davidbak 18 hours ago

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.