skiwi
skiwi

Reputation: 69379

How to shuffle a stream using the Stream API?

I decided to take the functional approach in generating a string or random characters, so far I came up with this, it should perform better than boxing and then using a StringJoiner as collector:

Random random = new Random();
String randomString = IntStream.concat(
        random.ints(8, 'a', 'z'),
        random.ints(8, 'A', 'Z')
)
        .collect(
                StringBuilder::new,
                (sb, i) -> sb.append((char)i),
                (sb1, sb2) -> sb1.append(sb2)
        ).toString();

I want to generate a stream of 16 characters, ranging from a-z or A-Z, the problem I have is that I do not know how to shuffle both streams.

I know that I am using IntStream.concat here, which will simply concatenate both streams, I'm looking for either of the following:

Both ways are viable in my opinion, however I am especially intrigued by how to make an operator alike sorted(). The key point here is that it is an operator that is stateful as it needs to see the whole stream before it can operate, is there a way to inject a stateful operator in a stream sequence?

So far the operations, excluding the needed work to shuffle them, seem to not be appropiate for a functional approach in Java 8.

Upvotes: 10

Views: 7802

Answers (2)

Holger
Holger

Reputation: 298469

You are thinking too twisted

Random random = new Random();
String randomString=random.ints(16, 0, 26*2).map(i->(i>=26? 'a'-26: 'A')+i)
  .collect(StringBuilder::new,
           StringBuilder::appendCodePoint, StringBuilder::append)
  .toString();

Since you already have a source of random values there is no point in calling for a shuffle function (which would not work very well with streams).

Note that you also could define the allowed chars in a String explicitly and select them using: random.ints(16, 0, allowed.length()).map(allowed::charAt)

Similar pattern applies to selecting from a random access List.


Update: If you want to have code clearly showing the two ranges nature of the allowed characters you can combine your Stream.concat approach with the char selection solution described above:

StringBuilder allowed=
  IntStream.concat(IntStream.rangeClosed('a', 'z'), IntStream.rangeClosed('A', 'Z'))
    .collect(StringBuilder::new,
             StringBuilder::appendCodePoint, StringBuilder::append);
String randomString=random.ints(16, 0, allowed.length()).map(allowed::charAt)
  .collect(StringBuilder::new,
           StringBuilder::appendCodePoint, StringBuilder::append)
  .toString();

(Note: I replaced range with rangeClosed which I suspect to match your original intentions while it does not do what Random.ints(…, 'a', 'z') would do).

Upvotes: 11

Claude Martin
Claude Martin

Reputation: 765

This is probably not as elegant as you hoped for but it works:

final Random random = new Random();
String randomString = IntStream.concat(random.ints(8, 'a', 'z'+1), random.ints(8, 'A', 'Z'+1))
    .collect(StringBuilder::new, (sb, i) -> {
      int l = sb.length();
      if (l == 0) {
        sb.append((char) i);
      } else {
        int j = random.nextInt(l);
        char c = sb.charAt(j);
        sb.setCharAt(j, (char) i);
        sb.append(c);
      }
    }, (sb1, sb2) -> sb1.append(sb2)).toString();
System.out.println(randomString);

Alternatively you could do this:

final String randomString = random.ints(100, 'A', 'z' + 1)
        .filter(i -> i <= 'Z' || i >= 'a').limit(16)
        .collect(StringBuilder::new, (sb, i) -> sb.append((char) i), 
                StringBuilder::append).toString();

Upvotes: 1

Related Questions