Java
List
Random Selection
Programming
Algorithms

Take n random elements from a ListE?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Selecting n random elements from a Java List<E> sounds simple, but the correct solution depends on whether duplicates are allowed and whether the original list may be reordered. In most business code, the usual requirement is sampling without replacement, which means the same element should not appear twice in the result. A good implementation should also validate n and make the randomness strategy explicit.

Clarify the Sampling Rule First

There are two common interpretations:

  • sample without replacement, where each chosen element is unique
  • sample with replacement, where the same element may be chosen multiple times

Most developers mean the first one. If you do not define this up front, the code can look correct while returning the wrong type of random sample.

Simple Approach: Shuffle a Copy

For moderate list sizes, the easiest solution is to copy the list, shuffle it, and take the first n items.

java
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class SamplingDemo {
6    public static <E> List<E> sampleWithoutReplacement(List<E> source, int n) {
7        if (n < 0 || n > source.size()) {
8            throw new IllegalArgumentException("n must be between 0 and list size");
9        }
10
11        List<E> copy = new ArrayList<>(source);
12        Collections.shuffle(copy);
13        return new ArrayList<>(copy.subList(0, n));
14    }
15
16    public static void main(String[] args) {
17        List<String> values = List.of("A", "B", "C", "D", "E");
18        System.out.println(sampleWithoutReplacement(values, 3));
19    }
20}

This is readable and usually the best default when the list comfortably fits in memory.

Sampling With Replacement

If duplicates are allowed, choose random indices independently.

java
1import java.util.ArrayList;
2import java.util.List;
3import java.util.concurrent.ThreadLocalRandom;
4
5public class ReplacementSampling {
6    public static <E> List<E> sampleWithReplacement(List<E> source, int n) {
7        if (n < 0) {
8            throw new IllegalArgumentException("n must be non-negative");
9        }
10        if (source.isEmpty() && n > 0) {
11            throw new IllegalArgumentException("source must not be empty");
12        }
13
14        List<E> result = new ArrayList<>(n);
15        for (int i = 0; i < n; i++) {
16            int index = ThreadLocalRandom.current().nextInt(source.size());
17            result.add(source.get(index));
18        }
19        return result;
20    }
21}

Here duplicates are expected behavior, not a bug.

Do Not Shuffle the Original List Unless That Is Intentional

Calling Collections.shuffle(source) changes the list in place. That may be acceptable for local temporary lists, but it is risky when other code still depends on the original order.

Unsafe for shared state:

java
Collections.shuffle(source);
List<E> result = source.subList(0, n);

Safer default:

java
List<E> copy = new ArrayList<>(source);
Collections.shuffle(copy);

Mutation should be a conscious choice, not an accidental side effect.

Large Inputs: Consider Reservoir Sampling

If the input is very large or arrives as a stream, shuffling the full dataset may be wasteful. Reservoir sampling gives a uniform sample of n elements in one pass.

java
1import java.util.ArrayList;
2import java.util.List;
3import java.util.Random;
4
5public class ReservoirSampling {
6    public static <E> List<E> sample(List<E> source, int n) {
7        if (n < 0 || n > source.size()) {
8            throw new IllegalArgumentException("n must be between 0 and list size");
9        }
10
11        Random random = new Random();
12        List<E> reservoir = new ArrayList<>(source.subList(0, n));
13
14        for (int i = n; i < source.size(); i++) {
15            int j = random.nextInt(i + 1);
16            if (j < n) {
17                reservoir.set(j, source.get(i));
18            }
19        }
20
21        return reservoir;
22    }
23}

This matters when the list is huge or when the data source is effectively streaming.

Reproducibility for Tests

If you need deterministic behavior in tests, do not hard-code the global random source. Accept a Random instance or seed it deliberately.

java
1public static <E> List<E> sampleWithoutReplacement(List<E> source, int n, Random random) {
2    List<E> copy = new ArrayList<>(source);
3    Collections.shuffle(copy, random);
4    return new ArrayList<>(copy.subList(0, n));
5}

That makes unit tests stable and easier to debug.

Common Pitfalls

The biggest mistake is not defining whether duplicates are allowed. Sampling with replacement and without replacement solve different problems.

Another issue is shuffling the original list in place and surprising the rest of the program.

Developers also sometimes ignore invalid values of n. A negative sample size or a request larger than the list should fail clearly rather than produce ambiguous behavior.

Summary

  • Decide first whether sampling should allow duplicates.
  • For most lists, shuffle a copy and take the first n elements.
  • Use random-index selection only when sampling with replacement is intended.
  • Avoid mutating the original list unless reordering it is acceptable.
  • For huge inputs, reservoir sampling is a better fit than full-list shuffling.

Course illustration
Course illustration

All Rights Reserved.