Kion F
Kion F

Reputation: 37

Java8 Associate random point from stream with player object from other stream

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

Answers (2)

Holger
Holger

Reputation: 298469

Well, you can define a method returning a Stream of Points 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

Jack
Jack

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

Related Questions