简体   繁体   中英

Java Stream stateful findFirst

The below method is part of a weighted random selection algorithm for picking songs.

I would like to convert the below method to use streams, to decide if it would be clearer / preferable. I am not certain it is possible at all, since calculation is a stateful operation, dependent on position in the list.

public Song songForTicketNumber(long ticket)
{
    if(ticket<0) return null;
    long remaining = ticket;
    for(Song s : allSongs)  // allSongs is ordered list
    {
        rem-=s.numTickets; // numTickets is a long and never negative
        if(remaining<0)
            return s;
    }
    return null;
}

More formally: If n is the sum of all Song::numTickets for each Song object in allSongs , then for any integer 0 through n-1 , the above method should return a song in the list. The number of integers which will return a specific Song object x , would be determined by x.numTickets . The selection criteria for a specific song is a range of consecutive integers determined by both its numTickets property and the numTickets property for each item in the list to its left. As currently written, anything outside the range would return null.

Note: Out of range behavior can be modified to accommodate Streams (other than returning null)

The efficiency of a Stream compared to a basic for or for-each loop is a matter of circumstance. In yours, it's highly likely that a Stream would be less efficient than your current code for, among others, these major reasons:

  1. Your function is stateful as you mentioned. Maintaining a state with this method probably means finagling some kind of anonymous implementation of a BinaryOperator to use with Stream.reduce , and it's going to turn out bulkier and more confusing to read than your current code.
  2. You're short circuiting in your current loop, and no Stream operation will reflect that kind of efficiency, especially considering this in combination with #1.
  3. Your collection is ordered , which means the stream will iterate over elements in a manner very similar to your existing loop anyway. Depending on the size of your collection, you might get some efficiency out of parallelStream , but having to maintain the order in this case will mean a less efficient stream.

The only real benefit you could get from switching to a Stream is the difference in memory consumption (You could keep allSongs out of memory and let Stream handle it in a more memory-efficient way), which doesn't seem applicable here.

In conclusion, since the Stream operations would be even more complex to write and would probably be harmful, if anything, to your efficiency, I would recommend that you do not pursue this change.

That being said, I personally can't come up with a Stream based solution to actually answer your question of how to convert this work to a Stream . Again, it would be something complex and strange involving a reducer or similar... (I'll delete this answer if this is insufficient.)

Java streams do have the facility to short circuit evaluation, see for example the documentation for findFirst() . Having said that, decrementing and checking remaining , requires state mutation which is not great. Not great, but doable:

    public Optional<Song> songForTicketNumber(long ticket, Stream<Song> songs) {
        if (ticket < 0) return Optional.empty();

        AtomicLong remaining = new AtomicLong(ticket);
        return songs.filter(song -> decrementAndCheck(song, remaining)).findFirst();
    }

    private boolean decrementAndCheck(Song song, AtomicLong total) {
        total.addAndGet(-song.numTickets);
        return total.get() < 0;
    }

As far as I can tell, the only advantage of this approach is that you could switch to parallel streams if you wanted to.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM