Reputation: 9532
I am wondering if IntStream
and lambdas can be used somehow to quickly (one-line) create an array containing a random subset of an existing array of elements.
For example, say I have a pool of players:
Player[] allPlayers;
And I want to get a random subset of those players, given the dimension of the required subset. Traditionally I would do something like:
List<Player> players = new ArrayList<Player>(Arrays.asList(allPlayers));
int subsetSize = 8;
Player[] subset = new Player[subsetSize];
for (int i = 0; i < subsetSize; i++) {
int randIndex = new Random().nextInt(players.size());
subset[i] = players[randIndex];
players.remove(randIndex);
}
return subset;
But can this process be done with Java 8 features? Which I assume would make it more condensed, which is what I am trying to achieve. I am still getting the hang of these new Java 8 features, like IntStream
and lambdas and I wouldn't know how to use them for this particular case.
Upvotes: 4
Views: 949
Reputation: 37655
An efficient way to do this is:
List<String> players = Arrays.asList("a", "b", "c", "d", "e");
int subsetSize = 3;
for (int i = 0; i < subsetSize; i++)
Collections.swap(players, i, ThreadLocalRandom.current().nextInt(i, players.size()));
System.out.println(players.subList(0, subsetSize));
This doesn't require an inefficient search for unused indices if subsetSize
is large. It doesn't require calling shuffle
on the entire list, so is more appropriate if subsetSize
is small. It also avoids calling ArrayList.remove
which has linear time complexity.
This code does not use Stream
operations, but I do not think it is possible to write a simple, efficient solution using Stream
without using a lambda that modifies state (considered bad practice).
Upvotes: 2
Reputation: 137229
In this case, you want to select streamSize
distinct elements from an input array.
You can use the Random#ints(randomNumberOrigin, randomNumberBound)
method:
Returns an effectively unlimited stream of pseudorandom int values, each conforming to the given origin (inclusive) and bound (exclusive).
This returns a stream of random integers in the specified range. To ensure distinct values, distinct()
is called and limit(...)
allows to only keep the number of elements we want.
Random random = new Random();
Player[] subset = random.ints(0, allPlayers.length)
.distinct()
.limit(subsetSize)
.mapToObj(i -> allPlayers[i])
.toArray(Player[]::new);
I would note however, that, even if this is a one-liner, it is not as efficient as JB Nizet's solution since this continues generating integers until a distinct one is found.
Upvotes: 4
Reputation: 692073
You could use the following, which doesn't use a Stream, isn't a one-liner, but is already more condensed.
List<Player> copy = new ArrayList<>(Arrays.asList(allPlayers));
Collections.shuffle(copy);
return copy.subList(0, subsetSize).toArray(new Player[0]);
Upvotes: 4