ZetCode

Java Collections.shuffle 方法

上次修改时间:2025 年 4 月 20 日

Collections.shuffle 方法是 Java 集合框架的一部分。它使用默认或指定的随机源随机排列指定的列表。此方法对于 Java 中的随机化任务很有用。

洗牌通常在游戏、模拟和统计抽样等应用程序中需要。该方法在线性时间内运行,并修改原始列表,而不是返回新的洗牌列表。

Collections.shuffle 概述

Collections.shuffle 方法有两个重载版本。一个只接受一个 List,使用默认的随机源。另一个同时接受 List 和 Random 对象,用于受控的随机性。这两个版本都在原位洗牌。

该方法内部使用 Fisher-Yates 洗牌算法。该算法生成列表元素的均匀随机排列。时间复杂度为 O(n),其中 n 是列表大小。

基本洗牌示例

此示例演示了 Collections.shuffle 的最简单用法。我们创建一个数字列表并对它们进行洗牌。输出显示了洗牌前后的列表。

BasicShuffle.java
package com.zetcode;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class BasicShuffle {

    public static void main(String[] args) {
        
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        
        System.out.println("Original list: " + numbers);
        Collections.shuffle(numbers);
        System.out.println("Shuffled list: " + numbers);
    }
}

此代码使用 Arrays.asList 创建一个不可变的数字列表。我们打印原始顺序,然后调用 Collections.shuffle 来随机化列表。最后,我们打印洗牌后的结果。

每次运行都会产生不同的输出,因为洗牌是随机的。如果需要保留原始列表,请先创建一个副本。

使用自定义随机源进行洗牌

此示例演示了如何将特定的 Random 对象与 Collections.shuffle 一起使用。通过使用带种子的 Random 实例,您可以实现可重复的洗牌,这对于测试、调试或需要一致随机行为的场景特别有用。

SeededShuffle.java
package com.zetcode;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class SeededShuffle {

    public static void main(String[] args) {

        List<String> originalColors = Arrays.asList("Red", "Green", "Blue", "Yellow");
        Random random = new Random(42); // Fixed seed for reproducibility

        System.out.println("Original: " + originalColors);

        // First shuffle with original list
        List<String> colors = Arrays.asList("Red", "Green", "Blue", "Yellow");
        Collections.shuffle(colors, random);
        System.out.println("First shuffle: " + colors);

        // Reset to original list and reseed for deterministic shuffle
        colors = Arrays.asList("Red", "Green", "Blue", "Yellow");
        random.setSeed(42);
        Collections.shuffle(colors, random);
        System.out.println("Second shuffle: " + colors);
    }
}

在此示例中,我们初始化一个颜色列表,并使用一个带有固定种子 (42) 的 Random 对象。在执行第一次洗牌后,列表顺序重置为其原始状态。通过重置 Random 实例的种子并重新洗牌,我们确保两次洗牌的结果相同。

这种方法突出了带种子的随机性在可预测结果中的实际应用。它对于单元测试等用例很有价值,其中跨执行的一致结果至关重要,或者对于调试依赖于随机的操作也很重要。请注意,在每次洗牌之前将列表重置为其原始状态对于获得相同结果至关重要。

洗牌自定义对象列表

Collections.shuffle 适用于包含任何对象类型的任何 List 实现。此示例演示了洗牌自定义 Person 对象列表。

ShuffleCustomObjects.java
package com.zetcode;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

record Person(String name, int age) {}

public class ShuffleCustomObjects {

    public static void main(String[] args) {

        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 25));
        people.add(new Person("Bob", 30));
        people.add(new Person("Charlie", 22));
        people.add(new Person("Diana", 28));

        System.out.println("Original order:");
        people.forEach(System.out::println);

        Collections.shuffle(people);

        System.out.println("\nShuffled order:");
        people.forEach(System.out::println);
    }
}

我们定义一个简单的 Person 记录,其中包含姓名和年龄字段。创建并填充 Person 对象列表后,我们使用 Collections.shuffle 对其进行洗牌。输出显示了随机重新排序。

这表明洗牌适用于任何对象类型,而不仅仅是基本类型包装器或字符串。Person 对象保持其完整性,而它们在列表中的顺序发生变化。

洗牌列表的一部分

要仅对列表的一部分进行洗牌,我们可以使用 List.subList 来创建所需范围的视图。此示例显示了如何只洗牌列表的前半部分。

PartialShuffle.java
package com.zetcode;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class PartialShuffle {

    public static void main(String[] args) {
        
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            numbers.add(i);
        }
        
        System.out.println("Original list: " + numbers);
        
        // Shuffle first half
        int halfSize = numbers.size() / 2;
        Collections.shuffle(numbers.subList(0, halfSize));
        
        System.out.println("Half-shuffled: " + numbers);
    }
}

我们创建一个从 1 到 10 的数字列表。使用 subList(0, halfSize),我们创建了列表前一半的视图。洗牌此子列表仅影响原始列表中的那些元素。

输出显示前五个数字被随机化,而后五个数字保持其原始顺序。当您只需要随机化列表的特定部分时,此技术很有用。

通过列表转换洗牌数组

虽然 Collections.shuffle 适用于 Lists,但我们可以通过首先将数组转换为 List 来洗牌数组。此示例使用 Arrays.asList 演示了该技术。

ShuffleArray.java
package com.zetcode;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class ShuffleArray {

    public static void main(String[] args) {
        
        Integer[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        
        System.out.println("Original array: " + Arrays.toString(numbers));
        
        // Convert to List and shuffle
        List<Integer> list = Arrays.asList(numbers);
        Collections.shuffle(list);
        
        System.out.println("Shuffled array: " + Arrays.toString(numbers));
    }
}

我们创建一个 Integer 数组(注意:这不适用于原始的 int 数组)。Arrays.asList 创建数组的 List 视图。洗牌此列表会洗牌底层数组。

输出显示数组元素以其新的随机顺序排列。请记住,Arrays.asList 返回一个由数组支持的固定大小的列表,因此不允许进行结构性修改。

性能注意事项

此示例比较了洗牌不同列表实现的性能。由于其内部结构,ArrayList 和 LinkedList 具有不同的洗牌性能特征。

ShufflePerformance.java
package com.zetcode;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

public class ShufflePerformance {

    static final int SIZE = 100000;
    
    public static void main(String[] args) {
        
        // Create lists
        List<Integer> arrayList = new ArrayList<>(SIZE);
        List<Integer> linkedList = new LinkedList<>();
        
        Random random = new Random();
        for (int i = 0; i < SIZE; i++) {
            int num = random.nextInt();
            arrayList.add(num);
            linkedList.add(num);
        }
        
        // Time ArrayList shuffle
        long start = System.currentTimeMillis();
        Collections.shuffle(arrayList);
        long duration = System.currentTimeMillis() - start;
        System.out.println("ArrayList shuffle time: " + duration + "ms");
        
        // Time LinkedList shuffle
        start = System.currentTimeMillis();
        Collections.shuffle(linkedList);
        duration = System.currentTimeMillis() - start;
        System.out.println("LinkedList shuffle time: " + duration + "ms");
    }
}

我们用相同的随机数填充 ArrayList 和 LinkedList。然后,我们计时 Collections.shuffle 在每个实现上花费的时间。ArrayList 通常更快,因为它具有更好的内存局部性。

输出显示了洗牌这两种列表类型之间的时间差。对于大型列表,ArrayList 可以快得多。根据您的应用程序需求选择您的列表实现。

使用多线程进行线程安全洗牌

此示例演示了当多个线程访问或修改同一集合时,同步如何确保线程安全的操作。我们将使用多个线程同时对同一个列表进行洗牌和读取,突出显示对同步的需要。

Collections.synchronizedList 是 Java 中的一个实用程序方法,它使用线程安全的同步包装器包装给定的列表。这确保了对列表的所有访问,例如添加、删除或修改,都是同步的,从而防止了多线程环境中的并发修改问题。但是,对于复合操作(例如,迭代或条件更新),仍然需要在返回的列表对象上进行显式同步以保持线程安全性。

ThreadSafeShuffleWithThreads.java
package com.zetcode;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ThreadSafeShuffleWithThreads {

    public static void main(String[] args) {

        // Initialize a list with sample data
        List<String> unsafeList = new ArrayList<>();
        Collections.addAll(unsafeList, "Task1", "Task2", "Task3", "Task4", "Task5");

        // Wrap the list to make it thread-safe
        List<String> safeList = Collections.synchronizedList(unsafeList);

        // Thread to shuffle the list
        Thread shuffleThread = new Thread(() -> {
            synchronized (safeList) {
                Collections.shuffle(safeList);
                System.out.println("Shuffled safely: " + safeList);
            }
        });

        // Thread to read the list
        Thread readThread = new Thread(() -> {
            synchronized (safeList) {
                System.out.println("Thread-safe read: " + safeList);
            }
        });

        // Start both threads
        shuffleThread.start();
        readThread.start();

        // Wait for threads to complete
        try {
            shuffleThread.join();
            readThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在此示例中,两个线程(一个用于洗牌,另一个用于读取)同时访问同一个列表。同步块确保一次只有一个线程可以处理列表,从而防止潜在的数据损坏或异常。synchronized 块在操作期间锁定共享列表,以保证线程安全的访问和修改。

这表明同步如何提供对共享资源的可靠处理,确保即使在多线程场景下也能保持一致和安全的行为。如果没有同步,此类操作可能会导致不一致的状态或运行时错误。

来源

Java Collections.shuffle 文档

在本教程中,我们深入探讨了 Collections.shuffle 方法。我们涵盖了基本用法、自定义随机性、性能考虑因素和线程安全性。洗牌是许多应用程序的基本操作。

作者

我叫 Jan Bodnar,是一位经验丰富的程序员,在该领域拥有多年经验。我于 2007 年开始撰写编程文章,此后撰写了 1,400 多篇文章和八本电子书。凭借超过八年的教学经验,我致力于分享我的知识并帮助其他人掌握编程概念。

列出所有Java教程