Каков наиболее эффективный способ получить случайный элемент из списка с потоком Java8 api?
Arrays.asList(new Obj1(), new Obj2(), new Obj3());
Спасибо.
Каков наиболее эффективный способ получить случайный элемент из списка с потоком Java8 api?
Arrays.asList(new Obj1(), new Obj2(), new Obj3());
Спасибо.
Почему с потоками? Вам просто нужно получить случайное число от 0 до размера списка, а затем вызвать get
по этому индексу:
Random r = new Random();
ElementType e = list.get(r.nextInt(list.size()));
Stream не даст вам ничего интересного, но вы можете попробовать:
Random r = new Random();
ElementType e = list.stream().skip(r.nextInt(list.size()-1)).findFirst().get();
Идея состоит в том, чтобы пропустить произвольное количество элементов (но не последний!), А затем получить первый элемент, если он существует. В результате у вас будет Optional<ElementType>
который будет не пустым, а затем извлеките его значение с помощью get
. У вас есть много вариантов здесь после того, как пропустить.
Использование потоков здесь крайне неэффективно...
Обратите внимание: ни одно из этих решений не учитывает пустые списки, но проблема определена в непустых списках.
Если вы используете HAVE для использования потоков, я написал элегантный, хотя и очень неэффективный сборщик, который выполняет эту работу:
/**
* Returns a random item from the stream (or null in case of an empty stream).
* This operation can't be lazy and is inefficient, and therefore shouldn't
* be used on streams with a large number or items or in performance critical sections.
* @return a random item from the stream or null if the stream is empty.
*/
public static <T> Collector<T, List<T>, T> randomItem() {
final Random RANDOM = new Random();
return Collector.of(() -> (List<T>) new ArrayList<T>(),
(acc, elem) -> acc.add(elem),
(list1, list2) -> ListUtils.union(list1, list2), // Using a 3rd party for list union, could be done "purely"
list -> list.isEmpty() ? null : list.get(RANDOM.nextInt(list.size())));
}
Использование:
@Test
public void standardRandomTest() {
assertThat(Stream.of(1, 2, 3, 4).collect(randomItem())).isBetween(1, 4);
}
Есть гораздо более эффективные способы сделать это, но если это нужно для Stream, самым простым способом является создание собственного Comparator, который возвращает случайный результат (-1, 0, 1) и сортирует ваш поток:
List<String> strings = Arrays.asList("a", "b", "c", "d", "e", "f");
String randomString = strings
.stream()
.sorted((o1, o2) -> ThreadLocalRandom.current().nextInt(-1, 2))
.findAny()
.get();
ThreadLocalRandom имеет готовый метод "из коробки" для получения случайного числа в вашем требуемом диапазоне для компаратора.
Если вы не знаете заранее размер вашего списка, вы можете сделать что-то вроде этого:
yourStream.collect(new RandomListCollector<>(randomSetSize));
Я предполагаю, что вам придется написать собственную реализацию Collector, подобную этой, чтобы получить однородную рандомизацию:
public class RandomListCollector<T> implements Collector<T, RandomListCollector.ListAccumulator<T>, List<T>> {
private final Random rand;
private final int size;
public RandomListCollector(Random random , int size) {
super();
this.rand = random;
this.size = size;
}
public RandomListCollector(int size) {
this(new Random(System.nanoTime()), size);
}
@Override
public Supplier<ListAccumulator<T>> supplier() {
return () -> new ListAccumulator<T>();
}
@Override
public BiConsumer<ListAccumulator<T>, T> accumulator() {
return (l, t) -> {
if (l.size() < size) {
l.add(t);
} else if (rand.nextDouble() <= ((double) size) / (l.gSize() + 1)) {
l.add(t);
l.remove(rand.nextInt(size));
} else {
// in any case gSize needs to be incremented
l.gSizeInc();
}
};
}
@Override
public BinaryOperator<ListAccumulator<T>> combiner() {
return (l1, l2) -> {
int lgSize = l1.gSize() + l2.gSize();
ListAccumulator<T> l = new ListAccumulator<>();
if (l1.size() + l2.size()<size) {
l.addAll(l1);
l.addAll(l2);
} else {
while (l.size() < size) {
if (l1.size()==0 || l2.size()>0 && rand.nextDouble() < (double) l2.gSize() / (l1.gSize() + l2.gSize())) {
l.add(l2.remove(rand.nextInt(l2.size()), true));
} else {
l.add(l1.remove(rand.nextInt(l1.size()), true));
}
}
}
// set the gSize of l :
l.gSize(lgSize);
return l;
};
}
@Override
public Function<ListAccumulator<T>, List<T>> finisher() {
return (la) -> la.list;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.singleton(Characteristics.CONCURRENT);
}
static class ListAccumulator<T> implements Iterable<T> {
List<T> list;
volatile int gSize;
public ListAccumulator() {
list = new ArrayList<>();
gSize = 0;
}
public void addAll(ListAccumulator<T> l) {
list.addAll(l.list);
gSize += l.gSize;
}
public T remove(int index) {
return remove(index, false);
}
public T remove(int index, boolean global) {
T t = list.remove(index);
if (t != null && global)
gSize--;
return t;
}
public void add(T t) {
list.add(t);
gSize++;
}
public int gSize() {
return gSize;
}
public void gSize(int gSize) {
this.gSize = gSize;
}
public void gSizeInc() {
gSize++;
}
public int size() {
return list.size();
}
@Override
public Iterator<T> iterator() {
return list.iterator();
}
}
}
Если вы хотите что-то проще и не хотите загружать весь свой список в память:
public <T> Stream<T> getRandomStreamSubset(Stream<T> stream, int subsetSize) {
int cnt = 0;
Random r = new Random(System.nanoTime());
Object[] tArr = new Object[subsetSize];
Iterator<T> iter = stream.iterator();
while (iter.hasNext() && cnt <subsetSize) {
tArr[cnt++] = iter.next();
}
while (iter.hasNext()) {
cnt++;
T t = iter.next();
if (r.nextDouble() <= (double) subsetSize / cnt) {
tArr[r.nextInt(subsetSize)] = t;
}
}
return Arrays.stream(tArr).map(o -> (T)o );
}
но тогда вы находитесь вдали от потокового API и можете сделать то же самое с базовым итератором
Другой идеей было бы реализовать собственный Spliterator
а затем использовать его в качестве источника для Stream
:
import java.util.List;
import java.util.Random;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class ImprovedRandomSpliterator<T> implements Spliterator<T> {
private final Random random;
private final T[] source;
private int size;
ImprovedRandomSpliterator(List<T> source, Supplier<? extends Random> random) {
if (source.isEmpty()) {
throw new IllegalArgumentException("RandomSpliterator can't be initialized with an empty collection");
}
this.source = (T[]) source.toArray();
this.random = random.get();
this.size = this.source.length;
}
@Override
public boolean tryAdvance(Consumer<? super T> action) {
if (size > 0) {
int nextIdx = random.nextInt(size);
int lastIdx = size - 1;
action.accept(source[nextIdx]);
source[nextIdx] = source[lastIdx];
source[lastIdx] = null; // let object be GCed
size--;
return true;
} else {
return false;
}
}
@Override
public Spliterator<T> trySplit() {
return null;
}
@Override
public long estimateSize() {
return source.length;
}
@Override
public int characteristics() {
return SIZED;
}
}
public static <T> Collector<T, ?, Stream<T>> toShuffledStream() {
return Collectors.collectingAndThen(
toCollection(ArrayList::new),
list -> !list.isEmpty()
? StreamSupport.stream(new ImprovedRandomSpliterator<>(list, Random::new), false)
: Stream.empty());
}
а потом просто:
list.stream()
.collect(toShuffledStream())
.findAny();
Подробности можно найти здесь.
... но это определенно излишнее, так что если вы ищете прагматичный подход. Определенно пойти на Джин решение.
В то время как все приведенные ответы работают, есть простая однострочная строка, которая делает свое дело без необходимости проверять, пуст ли список сначала:
List<String> list = List.of("a", "b", "c");
list.stream().skip((int) (list.size() * Math.random())).findAny();
Для пустого списка это вернет Optional.empty
.
Выбранный ответ имеет ошибки в своем потоковом решении... Вы не можете использовать Random # nextInt с неположительным длинным, в этом случае "0". Потоковое решение также никогда не выберет последнее в списке. Пример:
List<Integer> intList = Arrays.asList(0, 1, 2, 3, 4);
// #nextInt is exclusive, so here it means a returned value of 0-3
// if you have a list of size = 1, #next Int will throw an IllegalArgumentException (bound must be positive)
int skipIndex = new Random().nextInt(intList.size()-1);
// randomInt will only ever be 0, 1, 2, or 3. Never 4
int randomInt = intList.stream()
.skip(skipIndex) // max skip of list#size - 2
.findFirst()
.get();
Я бы порекомендовал использовать не потоковый подход, предложенный Жан-Батистом Юнесом, но если вам нужен потоковый подход, вы можете сделать что-то вроде этого (но это немного уродливо):
list.stream()
.skip(list.isEmpty ? 0 : new Random().nextInt(list.size()))
.findFirst();