web-dev-qa-db-fra.com

Comment obtenir un élément aléatoire dans une liste avec stream api?

Quel est le moyen le plus efficace d’obtenir un élément aléatoire d’une liste contenant une api de flux Java8?

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

Merci.

9
aekber

Pourquoi avec des ruisseaux? Il vous suffit d'obtenir un nombre aléatoire compris entre 0 et la taille de la liste, puis appelez get dans cet index:

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

Stream ne vous donnera rien d'intéressant ici, mais vous pouvez essayer avec:

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

L'idée est de sauter un nombre arbitraire d'éléments (mais pas le dernier!), Puis obtenir le premier élément s'il existe. En conséquence, vous aurez un Optional<ElementType qui sera non vide, puis vous en extrairez la valeur avec get. Vous avez beaucoup d'options ici après avoir sauté.

Utiliser des flux ici est très inefficace ...

Remarque: aucune de ces solutions ne prend en compte les listes vides, mais le problème est défini sur des listes non vides .

11

Si vous DEVEZ utiliser des flux, j'ai écrit un collecteur élégant, bien que très inefficace, qui fait le travail:

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

Usage:

@Test
public void standardRandomTest() {
    assertThat(Stream.of(1, 2, 3, 4).collect(randomItem())).isBetween(1, 4);
}
3
KidCrippler

Vous pouvez faire quelque chose comme ça: 

 yourStream.collect(new RandomListCollector<>(randomSetSize));

Je suppose que vous devrez écrire votre propre implémentation Collector comme celle-ci pour avoir une randomisation homogène:

 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();
    }
}

}

Si vous voulez quelque chose de plus simple et que vous ne voulez toujours pas charger toute votre liste en mémoire: 

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

mais vous êtes alors loin de l’API de flux et pouvez faire la même chose avec un itérateur de base

1
Chris A

Il existe des moyens beaucoup plus efficaces de le faire, mais si cela doit être Stream, le plus simple est de créer votre propre comparateur, qui renvoie un résultat aléatoire (-1, 0, 1) et de trier votre flux:

 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 a une méthode "prête à l'emploi" pour obtenir un nombre aléatoire dans la plage requise pour le comparateur.

1
RichardK

Une autre idée serait d’implémenter votre propre Spliterator et de l’utiliser ensuite comme source pour 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) {
        int nextIdx = random.nextInt(size);
        int lastIdx = size - 1;

        action.accept(source[nextIdx]);
        source[nextIdx] = source[lastIdx];
        source[lastIdx] = null; // let object be GCed
        return --size > 0;
    }

    @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());
}

et ensuite simplement:

list.stream()
  .collect(toShuffledStream())
  .findAny();

Les détails peuvent être trouvés ici.

... mais c’est vraiment exagéré, alors si vous recherchez une approche pragmatique. Vraiment aller pour la solution de Jean .

1
Grzegorz Piwowarek

La réponse sélectionnée présente des erreurs dans sa solution de flux ... Vous ne pouvez pas utiliser Random # nextInt avec un long non positif, "0" dans ce cas. La solution de flux ne choisira jamais non plus le dernier de la liste Exemple:

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();

Ma recommandation serait de suivre l'approche non-stream que Jean-Baptiste Yunès a proposée, mais si vous deviez faire une approche par flux, vous pourriez faire quelque chose comme ça (mais c'est un peu moche):

list.stream()
    .skip(list.isEmpty ? 0 : new Random().nextInt(list.size()))
    .findFirst();
0
Tarandrus