Как получить случайный элемент из списка с помощью stream api?

каков наиболее эффективный способ получить случайный элемент из списка с помощью Java8 stream api?

Arrays.asList(new Obj1(), new Obj2(), new Obj3());

спасибо.

4 ответов


почему с потоками? Вам просто нужно получить случайное число от 0 до размера списка, а затем позвонить get по этому индексу:

Random r = new Random();
ElementType e = list.get(r.nextInt(list.size());

поток не даст вам ничего интересного здесь, но вы можете попробовать:

Random r = new Random();
ElementType e = list.stream().skip(r.nextInt(list.size()-1).findFirst().get();

идея в том, чтобы пропустить произвольное количество элементов (но не последний!), затем получите первый элемент, если он существует. В результате у вас будет Optional<ElementType который будет непустым, а затем извлеките его значение с помощью get. У тебя много вариантов здесь после того, как пропустить.

использование потоков здесь крайне неэффективно...

Примечание: ни одно из этих решений не учитывает пустые списки, но проблема определяется в непустых списках.


Если вы есть чтобы использовать потоки, я написал элегантный, хотя и очень неэффективный коллектор, который выполняет эту работу:

/**
 * 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);
}

есть гораздо более эффективные способы сделать это, но если это должен быть поток, самый простой способ-создать свой собственный компаратор, который возвращает случайный результат (-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));

Я думаю, что вам придется написать свою собственную реализацию коллектора, как эта, чтобы иметь однородную рандомизацию:

 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 потока и можете сделать то же самое с базовым итератором