如何在 Java 中从数组创建可变列表,且不影响原始数组

JavaBeginner
立即练习

介绍

在 Java 编程中,使用不同的数据结构对于高效的程序开发至关重要。数组(Arrays)和列表(Lists)是两种基本的数据结构,它们服务于不同的目的。虽然数组提供固定大小的存储,但列表提供了动态大小的灵活性和额外的实用方法。

本教程重点介绍如何将 Java 数组转换为可变列表,而无需修改原始数组数据。通过完成这个实验(Lab),你将了解数组和列表之间的关系,并学习在它们之间进行转换同时保持数据完整性的实用技术。

理解 Java 数组和列表

在这一步中,我们将通过创建数组和列表的示例来探索 Java 数组和列表之间的基本区别。我们还将研究如何访问和显示它们的元素。

创建一个 Java 项目

让我们从创建一个简单的 Java 文件开始:

  1. 打开终端并导航到项目目录:

    cd ~/project
    
  2. 为我们的 Java 文件创建一个新目录:

    mkdir -p src/main/java
    cd src/main/java
    
  3. 使用 WebIDE 创建一个名为 ArrayListDemo.java 的新 Java 文件。单击 WebIDE 中的 Explorer 图标,导航到 project/src/main/java,右键单击,然后选择“New File”。将其命名为 ArrayListDemo.java

  4. 将以下代码添加到文件中:

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;

public class ArrayListDemo {
    public static void main(String[] args) {
        System.out.println("Java Arrays vs Lists Example");
        System.out.println("============================");

        // Creating an array of integers
        int[] numbersArray = {1, 2, 3, 4, 5};

        // Displaying array contents
        System.out.println("Array contents: " + Arrays.toString(numbersArray));

        // Accessing array elements by index
        System.out.println("Array element at index 2: " + numbersArray[2]);

        // Creating a List using ArrayList
        List<Integer> numbersList = new ArrayList<>();
        numbersList.add(1);
        numbersList.add(2);
        numbersList.add(3);
        numbersList.add(4);
        numbersList.add(5);

        // Displaying list contents
        System.out.println("List contents: " + numbersList);

        // Accessing list elements by index
        System.out.println("List element at index 2: " + numbersList.get(2));

        // Demonstrating List flexibility - adding a new element
        numbersList.add(6);
        System.out.println("List after adding element: " + numbersList);

        // Demonstrating List flexibility - removing an element
        numbersList.remove(1); // Removes element at index 1
        System.out.println("List after removing element at index 1: " + numbersList);
    }
}
  1. 编译并运行 Java 程序:

    cd ~/project
    javac src/main/java/ArrayListDemo.java
    java -cp src/main/java ArrayListDemo
    

你应该看到类似于这样的输出:

Java Arrays vs Lists Example
============================
Array contents: [1, 2, 3, 4, 5]
Array element at index 2: 3
List contents: [1, 2, 3, 4, 5]
List element at index 2: 3
List after adding element: [1, 2, 3, 4, 5, 6]
List after removing element at index 1: [1, 3, 4, 5, 6]

数组和列表之间的主要区别

  1. 大小灵活性

    • 数组具有固定大小,创建后无法更改
    • 列表可以根据需要动态增长或缩小
  2. 可用操作

    • 数组具有有限的内置功能
    • 列表提供了许多用于添加、删除和操作元素的方法
  3. 类型约束

    • 数组可以存储基本类型或对象
    • 列表只能存储对象(但自动装箱允许间接存储基本类型)

理解这些区别有助于你为你的特定需求选择正确的数据结构。

将数组转换为列表

现在我们了解了数组和列表的基础知识,让我们探索将数组转换为列表的不同方法。我们将特别关注从数组创建可变列表。

创建一个新的 Java 文件

  1. 在同一目录下创建一个名为 ArrayToListConversion.java 的新 Java 文件:

    从 WebIDE 中,导航到 project/src/main/java,右键单击,然后选择“New File”。将其命名为 ArrayToListConversion.java

  2. 将以下代码添加到文件中:

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class ArrayToListConversion {
    public static void main(String[] args) {
        System.out.println("Converting Arrays to Lists");
        System.out.println("=========================");

        // Create an array of Strings
        String[] fruitsArray = {"Apple", "Banana", "Cherry", "Date", "Elderberry"};
        System.out.println("Original array: " + Arrays.toString(fruitsArray));

        // Method 1: Using Arrays.asList() - Creates a fixed-size list
        System.out.println("\nMethod 1: Arrays.asList()");
        List<String> fruitsListFixed = Arrays.asList(fruitsArray);
        System.out.println("List created with Arrays.asList(): " + fruitsListFixed);

        // Try to modify the list
        System.out.println("Changing element at index 0 to 'Apricot'");
        fruitsListFixed.set(0, "Apricot");
        System.out.println("List after change: " + fruitsListFixed);
        System.out.println("Original array after List change: " + Arrays.toString(fruitsArray));

        // This will cause UnsupportedOperationException
        try {
            System.out.println("Trying to add a new element to the fixed-size list...");
            fruitsListFixed.add("Fig");
        } catch (UnsupportedOperationException e) {
            System.out.println("Error: Cannot add to fixed-size list!");
        }

        // Reset the array for next example
        fruitsArray[0] = "Apple";

        // Method 2: Using new ArrayList<>(Arrays.asList()) - Creates a mutable list
        System.out.println("\nMethod 2: new ArrayList<>(Arrays.asList())");
        List<String> fruitsList = new ArrayList<>(Arrays.asList(fruitsArray));
        System.out.println("List created with new ArrayList<>(Arrays.asList()): " + fruitsList);

        // Modify the list
        System.out.println("Adding 'Fig' to the list");
        fruitsList.add("Fig");
        System.out.println("List after adding 'Fig': " + fruitsList);
        System.out.println("Original array after List modification: " + Arrays.toString(fruitsArray));

        // Method 3: Using Stream API (Java 8+)
        System.out.println("\nMethod 3: Using Stream API");
        List<String> fruitsListStream = Arrays.stream(fruitsArray)
                                              .collect(Collectors.toList());
        System.out.println("List created with Stream API: " + fruitsListStream);

        // Modify the list
        System.out.println("Adding 'Grape' to the list");
        fruitsListStream.add("Grape");
        System.out.println("List after adding 'Grape': " + fruitsListStream);
        System.out.println("Original array after List modification: " + Arrays.toString(fruitsArray));
    }
}
  1. 编译并运行程序:

    cd ~/project
    javac src/main/java/ArrayToListConversion.java
    java -cp src/main/java ArrayToListConversion
    

你应该看到类似于这样的输出:

Converting Arrays to Lists
=========================
Original array: [Apple, Banana, Cherry, Date, Elderberry]

Method 1: Arrays.asList()
List created with Arrays.asList(): [Apple, Banana, Cherry, Date, Elderberry]
Changing element at index 0 to 'Apricot'
List after change: [Apricot, Banana, Cherry, Date, Elderberry]
Original array after List change: [Apricot, Banana, Cherry, Date, Elderberry]
Trying to add a new element to the fixed-size list...
Error: Cannot add to fixed-size list!

Method 2: new ArrayList<>(Arrays.asList())
List created with new ArrayList<>(Arrays.asList()): [Apple, Banana, Cherry, Date, Elderberry]
Adding 'Fig' to the list
List after adding 'Fig': [Apple, Banana, Cherry, Date, Elderberry, Fig]
Original array after List modification: [Apple, Banana, Cherry, Date, Elderberry]

Method 3: Using Stream API
List created with Stream API: [Apple, Banana, Cherry, Date, Elderberry]
Adding 'Grape' to the list
List after adding 'Grape': [Apple, Banana, Cherry, Date, Elderberry, Grape]
Original array after List modification: [Apple, Banana, Cherry, Date, Elderberry]

理解不同的转换方法

  1. **Arrays.asList()**:

    • 创建一个由原始数组支持的固定大小的列表
    • 列表大小无法更改(不能添加/删除元素)
    • 对列表元素的更改会影响原始数组
  2. **new ArrayList<>(Arrays.asList())**:

    • 创建一个新的 ArrayList,其中包含数组中的所有元素
    • 列表是可变的(可以添加/删除元素)
    • 对列表的更改不会影响原始数组
  3. **Stream API (Java 8+)**:

    • 一种使用函数式编程的更现代的方法
    • 创建一个完全独立的列表
    • 在转换期间提供执行转换的灵活性

对于初学者来说,第二种方法(new ArrayList<>(Arrays.asList()))通常是最有用的,因为它创建了一个完全可变的列表,而不会影响原始数组。

使用基本类型数组

在之前的步骤中,我们使用了引用类型(String)的数组。但是,Java 数组也可以包含基本类型,例如 intdouble 等。将基本类型数组转换为列表需要额外的步骤,因为 Java 泛型仅适用于引用类型。

让我们创建一个新示例来演示这个过程。

创建一个新的 Java 文件

  1. 在同一目录下创建一个名为 PrimitiveArrayToList.java 的新 Java 文件:

    从 WebIDE 中,导航到 project/src/main/java,右键单击,然后选择“New File”。将其命名为 PrimitiveArrayToList.java

  2. 将以下代码添加到文件中:

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class PrimitiveArrayToList {
    public static void main(String[] args) {
        System.out.println("Converting Primitive Arrays to Lists");
        System.out.println("===================================");

        // Create a primitive int array
        int[] numbersArray = {10, 20, 30, 40, 50};
        System.out.println("Original primitive array: " + Arrays.toString(numbersArray));

        // Method 1: Manual conversion
        System.out.println("\nMethod 1: Manual conversion");
        List<Integer> numbersList1 = new ArrayList<>();
        for (int number : numbersArray) {
            numbersList1.add(number); // Autoboxing converts int to Integer
        }
        System.out.println("List after manual conversion: " + numbersList1);

        // Method 2: Using Java 8 Streams
        System.out.println("\nMethod 2: Using Java 8 Streams");
        List<Integer> numbersList2 = Arrays.stream(numbersArray)
                                          .boxed() // Converts IntStream to Stream<Integer>
                                          .collect(Collectors.toList());
        System.out.println("List after stream conversion: " + numbersList2);

        // Modify the lists to demonstrate independence from the array
        System.out.println("\nModifying the lists:");
        numbersList1.add(60);
        numbersList1.set(0, 15);
        numbersList2.add(70);
        numbersList2.remove(0);

        System.out.println("List 1 after modifications: " + numbersList1);
        System.out.println("List 2 after modifications: " + numbersList2);
        System.out.println("Original array after List modifications: " + Arrays.toString(numbersArray));

        // Create and convert other primitive type arrays
        System.out.println("\nOther primitive type examples:");

        double[] doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
        List<Double> doubleList = Arrays.stream(doubleArray)
                                      .boxed()
                                      .collect(Collectors.toList());
        System.out.println("Double array: " + Arrays.toString(doubleArray));
        System.out.println("Double list: " + doubleList);

        boolean[] boolArray = {true, false, true, true, false};
        List<Boolean> boolList = new ArrayList<>();
        for (boolean value : boolArray) {
            boolList.add(value);
        }
        System.out.println("Boolean array: " + Arrays.toString(boolArray));
        System.out.println("Boolean list: " + boolList);
    }
}
  1. 编译并运行程序:

    cd ~/project
    javac src/main/java/PrimitiveArrayToList.java
    java -cp src/main/java PrimitiveArrayToList
    

你应该看到类似于这样的输出:

Converting Primitive Arrays to Lists
===================================
Original primitive array: [10, 20, 30, 40, 50]

Method 1: Manual conversion
List after manual conversion: [10, 20, 30, 40, 50]

Method 2: Using Java 8 Streams
List after stream conversion: [10, 20, 30, 40, 50]

Modifying the lists:
List 1 after modifications: [15, 20, 30, 40, 50, 60]
List 2 after modifications: [20, 30, 40, 50, 70]
Original array after List modifications: [10, 20, 30, 40, 50]

Other primitive type examples:
Double array: [1.1, 2.2, 3.3, 4.4, 5.5]
Double list: [1.1, 2.2, 3.3, 4.4, 5.5]
Boolean array: [true, false, true, true, false]
Boolean list: [true, false, true, true, false]

理解基本类型数组转换

在使用基本类型数组时,需要考虑两个关键因素:

  1. 自动装箱(Autoboxing):Java 在添加到集合时,会自动将基本类型值转换为它们的包装类对象。例如,int 转换为 Integer

  2. 流的装箱方法(Boxing Methods for Streams):将流与基本类型数组一起使用时,你需要调用 .boxed() 方法将基本类型流转换为对象流。

转换过程会创建完全独立于原始数组的新列表。这意味着:

  1. 修改列表不会影响原始数组
  2. 列表是完全可变的(你可以添加、删除或更改元素)
  3. 列表中的每个元素都是一个包含数组中值的新的对象(包装器)

当你需要操作数据而又不想冒着更改原始数组的风险时,这种独立性特别有用。

实际应用和常见陷阱

现在我们了解了如何将数组转换为可变列表,让我们探索一些实际应用和常见陷阱。我们将创建一个最终示例,演示实际用例以及如何避免常见错误。

创建一个新的 Java 文件

  1. 在同一目录下创建一个名为 ArrayListPractical.java 的新 Java 文件:

    从 WebIDE 中,导航到 project/src/main/java,右键单击,然后选择“New File”。将其命名为 ArrayListPractical.java

  2. 将以下代码添加到文件中:

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

public class ArrayListPractical {
    public static void main(String[] args) {
        System.out.println("Practical Applications and Common Pitfalls");
        System.out.println("========================================");

        // Use case 1: Creating a mutable list from a configuration array
        String[] defaultSettings = {"dark-mode", "auto-save", "notifications"};
        System.out.println("Default settings array: " + Arrays.toString(defaultSettings));

        // Create a mutable list of user settings starting with defaults
        List<String> userSettings = new ArrayList<>(Arrays.asList(defaultSettings));
        System.out.println("Initial user settings list: " + userSettings);

        // User can add or remove settings
        userSettings.add("sync-enabled");
        userSettings.remove("notifications");
        System.out.println("Modified user settings: " + userSettings);
        System.out.println("Original default settings (unchanged): " + Arrays.toString(defaultSettings));

        // Use case 2: Working with legacy APIs that return arrays
        System.out.println("\nUse case: Working with legacy APIs");

        // Simulate a legacy API that returns an array
        String[] legacyData = getLegacyData();
        System.out.println("Data from legacy API (array): " + Arrays.toString(legacyData));

        // Convert to a mutable list for modern processing
        List<String> modernData = new ArrayList<>(Arrays.asList(legacyData));

        // Process with modern methods
        modernData.removeIf(item -> item.startsWith("OLD_"));
        modernData.replaceAll(item -> item.toLowerCase());
        System.out.println("Processed data (list): " + modernData);

        // Common Pitfall 1: Forgetting that Arrays.asList creates a fixed-size list
        System.out.println("\nCommon Pitfall 1: Fixed-size list");
        Integer[] numberArray = {1, 2, 3, 4, 5};

        // This creates a fixed-size list
        List<Integer> wrongWay = Arrays.asList(numberArray);
        System.out.println("Fixed-size list: " + wrongWay);

        try {
            wrongWay.add(6); // This will fail
        } catch (UnsupportedOperationException e) {
            System.out.println("Error: Cannot add to fixed-size list!");
        }

        // Correct way
        List<Integer> rightWay = new ArrayList<>(Arrays.asList(numberArray));
        rightWay.add(6); // This works fine
        System.out.println("Mutable list: " + rightWay);

        // Common Pitfall 2: Array of primitives
        System.out.println("\nCommon Pitfall 2: Array of primitives");
        int[] primitiveArray = {10, 20, 30};

        // This won't compile: Arrays.asList(primitiveArray)
        // List<Integer> primitiveList = Arrays.asList(primitiveArray);

        // Correct ways
        List<Integer> primitiveList1 = new ArrayList<>();
        for (int value : primitiveArray) {
            primitiveList1.add(value);
        }

        List<Integer> primitiveList2 = Arrays.stream(primitiveArray)
                                            .boxed()
                                            .toList();

        System.out.println("Primitive array: " + Arrays.toString(primitiveArray));
        System.out.println("Converted list (loop method): " + primitiveList1);
        System.out.println("Converted list (stream method): " + primitiveList2);
    }

    // Simulate a legacy API that returns an array
    private static String[] getLegacyData() {
        return new String[] {"OLD_RECORD1", "ACTIVE_DATA", "OLD_RECORD2", "CURRENT_INFO"};
    }
}
  1. 编译并运行程序:

    cd ~/project
    javac src/main/java/ArrayListPractical.java
    java -cp src/main/java ArrayListPractical
    

你应该看到类似于这样的输出:

Practical Applications and Common Pitfalls
========================================
Default settings array: [dark-mode, auto-save, notifications]
Initial user settings list: [dark-mode, auto-save, notifications]
Modified user settings: [dark-mode, auto-save, sync-enabled]
Original default settings (unchanged): [dark-mode, auto-save, notifications]

Use case: Working with legacy APIs
Data from legacy API (array): [OLD_RECORD1, ACTIVE_DATA, OLD_RECORD2, CURRENT_INFO]
Processed data (list): [active_data, current_info]

Common Pitfall 1: Fixed-size list
Fixed-size list: [1, 2, 3, 4, 5]
Error: Cannot add to fixed-size list!
Mutable list: [1, 2, 3, 4, 5, 6]

Common Pitfall 2: Array of primitives
Primitive array: [10, 20, 30]
Converted list (loop method): [10, 20, 30]
Converted list (stream method): [10, 20, 30]

来自实际应用的关键要点

  1. 配置管理:使用数组进行默认设置,使用列表进行用户特定设置,可以在保留默认设置的同时提供灵活性。

  2. 使用旧版 API:许多较旧的 Java API 返回数组,你可以将其转换为列表,以利用现代 Java 功能,如 lambda 表达式和方法引用。

  3. 要避免的常见陷阱

    • 记住 Arrays.asList() 创建一个固定大小的列表,该列表无法增长或缩小
    • 基本类型数组不能直接传递给 Arrays.asList() - 你必须使用循环或带有装箱的流
  4. 最佳实践:对于大多数用例,最安全的方法是使用 new ArrayList<>(Arrays.asList(array)) 用于对象数组,并使用流方法用于基本类型数组。

通过理解这些模式和陷阱,你可以在 Java 应用程序中有效地使用数组和列表,为每种情况选择正确的数据结构和转换方法。

总结

在这个实验中,你已经学习了如何从 Java 数组创建可变列表,同时保留原始的数组数据。你已经探索了:

  1. Java 数组和列表之间的基本区别,包括它们的结构、功能和用例。

  2. 将数组转换为列表的多种方法:

    • 使用 Arrays.asList() 获取数组的固定大小视图
    • 使用 new ArrayList<>(Arrays.asList()) 获取完全可变的列表
    • 使用 Stream API 和 .boxed() 处理基本类型数组
  3. 基本类型数组的特殊考虑事项,这些数组需要显式装箱才能转换为包装器对象的列表。

  4. 在使用数组和列表时的实际应用和常见陷阱,包括配置管理和与旧版 API 的交互。

这些技术在许多实际的 Java 应用程序中都很有价值,在这些应用程序中,你需要同时使用数组和列表,尤其是在你需要保留原始数据,同时创建灵活、可变集合以进行操作时。理解数组和列表之间的关系可以让你编写更高效、更易于维护的 Java 代码。