1

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
Kion F
  • 37
  • 4
  • 1
    I don’t understand the rationale behind these nested streams. It also doesn’t match your descriptions, e.g. you say “I have a bunch of static points” but `generatePointStream()` create different points each time. Where are these static points? – Holger Jul 08 '16 at 11:10
  • So I have a long list of static points but I only need `n` of them, one for every player. I used multiple streams because Java has no great way of merging Streams (it is done lazily so often throws runtime errors). What I opted for in my solution was to make a stream for every "trend" of points, containing the points in random order. I then added the streams to a list in random order. The problem with this is that only the first "random" stream is used. A big part of this is not wanting to create an object for ALL the points when I only need a few. – Kion F Jul 08 '16 at 11:40
  • 1
    You don’t have static points here, the points are random and different on each invocation. Further, you explain the problems with these multiple streams, but fail to explain why you have them in the first place. Why don’t you simply generate `n` random points when you need `n` random points? – Holger Jul 08 '16 at 11:52
  • 1
    Just a few hints: you create *infinite* streams here. This makes no sense when you create, e.g. random integers like with `ints(2384, 2413)` and then request `.distinct()`. There are exactly `2413-2384` distinct value possible, if you request more, the stream will hang, trying to find another unique value in the infinite stream. Since you plan to shuffle the points afterwards anyway, there is no point in requesting these distinct values in a random fashion, a simple `IntStream.range(2384, 2413)` would provide all possible distinct values, then `concat` all and `shuffle` the result would work. – Holger Jul 08 '16 at 11:58
  • 1
    Maybe, we can reformulate the problem as follows: you have a bunch of *static lines* and want to get distinct *random points* lying on these lines. The remaining question is, do these points have to be on distinct lines as well, or is it permitted to pick more than one random point from the same line? – Holger Jul 08 '16 at 12:09
  • That is a good point, I suppose it is the lines the points lie on that are static. It does not matter what line a point comes from (i.e. more than one point from one line). The problem I had with `shuffle` was that I had to collect the points, defeating the purpose of streaming them in my mind. And like I said in response to @jack, `concat` works lazily, not guaranteeing the Streams being combined. – Kion F Jul 08 '16 at 12:45

2 Answers2

2

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
Jack
  • 131,802
  • 30
  • 241
  • 343
  • Awesome stuff! How would you recommend building a stream of all those random points? Or did you use what I had? – Kion F Jul 08 '16 at 01:16
  • 1
    It depends what you need exactly because your situation is very specific. You have 6 points which must be generated within their specific ranges and then shuffled. A generic stream of random points is quite easy to implement by extending `AbstractSpliterator`. – Jack Jul 08 '16 at 01:18
  • What if I have a variable amount of players? Do I need that limit, cause it would be nice not to count the size of the player list (will require a loop for my application) – Kion F Jul 08 '16 at 02:15
  • 1
    Honestly, I didn’t understand why the OP creates a `List` at all, so your decision to do just `findAny()` on these streams, which will simply return the *first* element in this specific use case, does the right thing. However, it still makes no sense to generate `n` streams of points, when only the first elements of these streams are ever considered, in other words, just generating `n` points in the first place would produce exactly the same result, making the entire nested stream operation obsolete. – Holger Jul 08 '16 at 11:27
  • Only problem is that my points don't follow an easy trend (that I see) that can be created using one stream. To fight this I tried doing a `flatMap` like such: `stream()....flatMap(t->t)....`. This made one large stream with all the points instead of a stream of a stream. Your comment makes me think the best solution may be to make a custom `Point` `Supplier` object. – Kion F Jul 08 '16 at 11:45
2

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.

Community
  • 1
  • 1
Holger
  • 285,553
  • 42
  • 434
  • 765
  • Well since the total amount of points doesn't change I can calculate it and store it once in startup. Great write-up by the way, thanks! – Kion F Jul 08 '16 at 19:47