Reputation: 37
So this is one that's really left me puzzled. Lets say I have a Player
object, with Point p
containing an x
and y
value:
class Player {
void movePlayer(Point p) {
...
}
}
If I have a bunch of static points (certainly more than players) that I need to randomly, yet uniquely, map to each player's movePlayer function, how would I do so? This process does not need to be done quickly, but often and randomly each time. To add a layer of complication, my points are generated by both varying x and y values. As of now I am doing the following (which crashed my JVM):
public List<Stream<Point>> generatePointStream() {
Random random = new Random();
List<Stream<Point>> points = new ArrayList<Stream<Point>>();
points.add(random.ints(2384, 2413).distinct().mapToObj(x -> new Point(x, 3072)));
points.add(random.ints(3072, 3084).distinct().mapToObj(y -> new Point(2413, y)));
....
points.add(random.ints(2386, 2415).distinct().mapToObj(x -> new Point(x, 3135)));
Collections.shuffle(points);
return points;
}
Note that before I used only one stream with the Stream.concat
method, but that threw errors and looked pretty ugly, leading me to my current predicament. And to assign them to all Player objects in the List<Player> players
:
players.stream().forEach(p->p.movePlayer(generatePointStream().stream().flatMap(t->t).
findAny().orElse(new Point(2376, 9487))));
Now this almost worked when I used some ridiculous abstraction Stream<Stream<Point>>
, except it only used points from the first Stream<Point>
.
Am I completely missing the point of streams here? I just liked the idea of not creating explicit Point
objects I wouldn't use anyways.
Upvotes: 1
Views: 115
Reputation: 298469
Well, you can define a method returning a Stream of Point
s like
public Stream<Point> allValues() {
return Stream.of(
IntStream.range(2384, 2413).mapToObj(x -> new Point(x, 3072)),
IntStream.range(3072, 3084).mapToObj(y -> new Point(2413, y)),
//...
IntStream.range(2386, 2415).mapToObj(x -> new Point(x, 3135))
).flatMap(Function.identity());
}
which contains all valid points, though not materialized, due to the lazy nature of the Stream. Then, create a method to pick random elements like:
public List<Point> getRandomPoints(int num) {
long count=allValues().count();
assert count > num;
return new Random().longs(0, count)
.distinct()
.limit(num)
.mapToObj(i -> allValues().skip(i).findFirst().get())
.collect(Collectors.toList());
}
In a perfect world, this would already have all the laziness you wish, including creating only the desired number of Point
instances.
However, there are several implementation details which might make this even worse than just collecting into a list.
One is special to the flatMap
operation, see “Why filter() after flatMap() is “not completely” lazy in Java streams?”. Not only are substreams processed eagerly, also Stream properties that could allow internal optimizations are not evaluated. In this regard, a concat
based Stream is more efficient.
public Stream<Point> allValues() {
return Stream.concat(
Stream.concat(
IntStream.range(2384, 2413).mapToObj(x -> new Point(x, 3072)),
IntStream.range(3072, 3084).mapToObj(y -> new Point(2413, y))
),
//...
IntStream.range(2386, 2415).mapToObj(x -> new Point(x, 3135))
);
}
There is a warning regarding creating too deep concatenated streams, but if you are in control of the creation like here, you can care to create a balanced tree, like
Stream.concat(
Stream.concat(
Stream.concat(a, b),
Stream.concat(c, d)
),
Stream.concat(
Stream.concat(a, b),
Stream.concat(c, d)
)
)
However, even though such a Stream allows to calculate the size without processing elements, this won’t happen before Java 9. In Java 8, count()
will always iterate over all elements, which implies having already instantiated as much Point
instances as when collecting all elements into a List
after the count()
operation.
Even worse, skip
is not propagated to the Stream’s source, so when saying stream.map(…).skip(n).findFirst()
, the mapping function is evaluated up to n+1
times instead of only once. Of course, this renders the entire idea of the getRandomPoints
method using this as lazy construct useless. Due to the encapsulation and the nested streams we have here, we can’t even move the skip
operation before the map
.
Note that temporary instances still might be handled more efficient than collecting into a list, where all instance of the exist at the same time, but it’s hard to predict due to the much larger number we have here. So if the instance creation really is a concern, we can solve this specific case due to the fact that the two int
values making up a point can be encapsulated in a primitive long
value:
public LongStream allValuesAsLong() {
return LongStream.concat(LongStream.concat(
LongStream.range(2384, 2413).map(x -> x <<32 | 3072),
LongStream.range(3072, 3084).map(y -> 2413L <<32 | y)
),
//...
LongStream.range(2386, 2415).map(x -> x <<32 | 3135)
);
}
public List<Point> getRandomPoints(int num) {
long count=allValuesAsLong().count();
assert count > num;
return new Random().longs(0, count)
.distinct()
.limit(num)
.mapToObj(i -> allValuesAsLong().skip(i)
.mapToObj(l -> new Point((int)(l>>>32), (int)(l&(1L<<32)-1)))
.findFirst().get())
.collect(Collectors.toList());
}
This will indeed only create num
instances of Point
.
Upvotes: 2
Reputation: 133609
You should do something like:
final int PLAYERS_COUNT = 6;
List<Point> points = generatePointStream()
.stream()
.limit(PLAYERS_COUNT)
.map(s -> s.findAny().get())
.collect(Collectors.toList());
This outputs
2403, 3135
2413, 3076
2393, 3072
2431, 3118
2386, 3134
2368, 3113
Upvotes: 2