简体   繁体   中英

How to get random objects from a stream

Lets say I have a list of words and i want to create a method which takes the size of the new list as a parameter and returns the new list. How can i get random words from my original sourceList?

public List<String> createList(int listSize) {
   Random rand = new Random();
   List<String> wordList = sourceWords.
      stream().
      limit(listSize).
      collect(Collectors.toList()); 

   return wordList;
}

So how and where can I use my Random?

I've found a proper solution. Random provides a few methods to return a stream. For example ints(size) which creates a stream of random integers.

public List<String> createList(int listSize)
{
   Random rand = new Random();
   List<String> wordList = rand.
      ints(listSize, 0, sourceWords.size()).
      mapToObj(i -> sourceWords.get(i)).
      collect(Collectors.toList());

   return wordList;
}

I think the most elegant way is to have a special collector.

I am pretty sure the only way you can guarantee that each item has an equal chance of being picked, is to collect, shuffle and re-stream. This can be easily done using built-in Collectors.collectingAndThen(...) helper.

Sorting by a random comparator or using randomized reducer, like suggested on some other answers, will result in very biased randomness.

List<String> wordList = sourceWords.stream()
  .collect(Collectors.collectingAndThen(Collectors.toList(), collected -> {
      Collections.shuffle(collected);
      return collected.stream();
  }))
  .limit(listSize)
  .collect(Collectors.toList());

You can move that shuffling collector to a helper function:

public class CollectorUtils {

    public static <T> Collector<T, ?, Stream<T>> toShuffledStream() {
        return Collectors.collectingAndThen(Collectors.toList(), collected -> {
            Collections.shuffle(collected);
            return collected.stream();
        });
    }

}

I assume that you are looking for a way to nicely integrate with other stream processing functions. So following straightforward solution is not what you are looking for :)

Collections.shuffle(wordList)
return wordList.subList(0, limitSize)

Here's a solution I came up with which seems to differ from all the other ones, so I figured why not add it to the pile.

Basically it works by using the same kind of trick as one iteration of Collections.shuffle each time you ask for the next element - pick a random element, swap that element with the first one in the list, move the pointer forwards. Could also do it with the pointer counting back from the end.

The caveat is that it does mutate the list you passed in, but I guess you could just take a copy as the first thing if you didn't like that. We were more interested in reducing redundant copies.

private static <T> Stream<T> randomStream(List<T> list)
{
    int characteristics = Spliterator.SIZED;
    // If you know your list is also unique / immutable / non-null
    //int characteristics = Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.SIZED;
    Spliterator<T> spliterator = new Spliterators.AbstractSpliterator<T>(list.size(), characteristics)
    {
        private final Random random = new SecureRandom();
        private final int size = list.size();
        private int frontPointer = 0;

        @Override
        public boolean tryAdvance(Consumer<? super T> action)
        {
            if (frontPointer == size)
            {
                return false;
            }

            // Same logic as one iteration of Collections.shuffle, so people talking about it not being
            // fair randomness can take that up with the JDK project.
            int nextIndex = random.nextInt(size - frontPointer) + frontPointer;
            T nextItem = list.get(nextIndex);
            // Technically the value we end up putting into frontPointer
            // is never used again, but using swap anyway, for clarity.
            Collections.swap(list, nextIndex, frontPointer);

            frontPointer++;
            // All items from frontPointer onwards have not yet been chosen.

            action.accept(nextItem);
            return true;
        }
    };

    return StreamSupport.stream(spliterator, false);
}

This is my one line solution:

 List<String> st = Arrays.asList("aaaa","bbbb","cccc");
 st.stream().sorted((o1, o2) -> RandomUtils.nextInt(0, 2)-1).findFirst().get();

RandomUtils are from commons lang 3

Try something like that:

List<String> getSomeRandom(int size, List<String> sourceList) {
    List<String> copy = new ArrayList<String>(sourceList);
    Collections.shuffle(copy);
    List<String> result = new ArrayList<String>();
    for (int i = 0; i < size; i++) {
        result.add(copy.get(i));
    }

    return result;
}

If you want non repeated items in the result list and your initial list is immutable:

  • There isn't a direct way to get it from the current Streams API.
  • It's not possible to use a random Comparator because it's going to break the compare contract.

You can try something like:

public List<String> getStringList(final List<String> strings, final int size) {
    if (size < 1 || size > strings.size()) {
        throw new IllegalArgumentException("Out of range size.");
    }

    final List<String> stringList = new ArrayList<>(size);

    for (int i = 0; i < size; i++) {
        getRandomString(strings, stringList)
                .ifPresent(stringList::add);
    }

    return stringList;
}

private Optional<String> getRandomString(final List<String> stringList, final List<String> excludeStringList) {
    final List<String> filteredStringList = stringList.stream()
            .filter(c -> !excludeStringList.contains(c))
            .collect(toList());

    if (filteredStringList.isEmpty()) {
        return Optional.empty();
    }

    final int randomIndex = new Random().nextInt(filteredStringList.size());
    return Optional.of(filteredStringList.get(randomIndex));
}

@kozla13 improved version:

List<String> st = Arrays.asList("aaaa","bbbb","cccc");
st.stream().min((o1, o2) -> o1 == o2 ? 0 : (ThreadLocalRandom.current().nextBoolean() ? -1 : 1)).orElseThrow();
  1. Used java built-in class ThreadLocalRandom
  2. nextInt generates one from sequence [-1, 0, 1], but return 0 in compare func means equals for the elements and side effect of this - first element (o1) will be always taken in this case.
  3. properly handle object equals case

If the source list is generally much larger than the new list, you might gain some efficiencies by using a BitSet to get random indices:

List<String> createList3(int listSize, List<String> sourceList) {
  if (listSize > sourceList.size()) {
    throw new IllegalArgumentException("Not enough words in the source list.");
  }

  List<String> newWords = randomWords(listSize, sourceList);
  Collections.shuffle(newWords); // optional, for random order
  return newWords;
}

private List<String> randomWords(int listSize, List<String> sourceList) {
  int endExclusive = sourceList.size();
  BitSet indices = new BitSet(endExclusive);
  Random rand = new Random();
  while (indices.cardinality() < listSize) {
    indices.set(rand.nextInt(endExclusive));
  }
  
  return indices.stream().mapToObj(i -> sourceList.get(i))
    .collect(Collectors.toList());
}

A stream is probably overkill. Copy the source list so you're not creating side-effects, then give back a sublist of the shuffled copy.

public static List<String> createList(int listSize, List<String> sourceList) {
  if (listSize > sourceList.size()) {
    throw IllegalArgumentException("Not enough words for new list.");
  }
  List<String> copy = new ArrayList<>(sourceList);
  Collections.shuffle(copy);
  return copy.subList(0, listSize);
}

The answer is very simple(with stream):

List<String> a = src.stream().sorted((o1, o2) -> {
        if (o1.equals(o2)) return 0;
        return (r.nextBoolean()) ? 1 : -1;
    }).limit(10).collect(Collectors.toList());

You can test it:

List<String> src = new ArrayList<String>();
for (int i = 0; i < 20; i++) {
    src.add(String.valueOf(i*10));
}
Random r = new Random();
List<String> a = src.stream().sorted((o1, o2) -> {
        if (o1.equals(o2)) return 0;
        return (r.nextBoolean()) ? 1 : -1;
    }).limit(10).collect(Collectors.toList());
System.out.println(a);

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