Java 배열에서 원본 배열을 변경하지 않고 가변 리스트 생성하는 방법

JavaBeginner
지금 연습하기

소개

Java 프로그래밍에서 다양한 데이터 구조를 다루는 것은 효율적인 프로그램 개발에 필수적입니다. 배열과 리스트는 서로 다른 목적을 수행하는 두 가지 기본적인 데이터 구조입니다. 배열은 고정 크기 스토리지를 제공하는 반면, 리스트는 동적 크기 조정과 추가 유틸리티 메서드를 통해 유연성을 제공합니다.

이 튜토리얼은 원래 배열 데이터를 수정하지 않고 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 에서 탐색기 아이콘을 클릭하고 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. 유형 제약 조건:

    • 배열은 기본형 (primitive) 또는 객체를 저장할 수 있습니다.
    • 리스트는 객체만 저장할 수 있습니다 (하지만 오토박싱 (autoboxing) 을 통해 간접적으로 기본형을 저장할 수 있습니다).

이러한 차이점을 이해하면 특정 요구 사항에 맞는 올바른 데이터 구조를 선택하는 데 도움이 됩니다.

배열을 리스트로 변환하기

이제 배열과 리스트의 기본 사항을 이해했으므로 배열을 리스트로 변환하는 다양한 방법을 살펴보겠습니다. 특히 배열에서 가변 리스트를 생성하는 데 중점을 둡니다.

새 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 배열은 int, double 등과 같은 기본형도 포함할 수 있습니다. 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 는 컬렉션에 추가할 때 기본형 값을 해당 래퍼 클래스 객체로 자동 변환합니다. 예를 들어, intInteger로 변환됩니다.

  2. 스트림용 박싱 메서드: 기본형 배열과 함께 스트림을 사용할 때는 기본형 스트림을 객체 스트림으로 변환하기 위해 .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 기능을 활용할 수 있습니다.

  3. 피해야 할 일반적인 함정:

    • Arrays.asList()는 크기를 늘리거나 줄일 수 없는 고정 크기 리스트를 생성한다는 점을 기억하십시오.
    • 기본형 배열은 Arrays.asList()에 직접 전달할 수 없습니다. 루프 또는 박싱이 있는 스트림을 사용해야 합니다.
  4. 모범 사례: 대부분의 사용 사례에서 가장 안전한 방법은 객체 배열의 경우 new ArrayList<>(Arrays.asList(array))를 사용하고 기본형 배열의 경우 스트림 메서드를 사용하는 것입니다.

이러한 패턴과 함정을 이해하면 Java 애플리케이션에서 배열과 리스트를 효과적으로 사용하여 각 상황에 맞는 올바른 데이터 구조와 변환 방법을 선택할 수 있습니다.

요약

이 랩에서는 원래 배열 데이터를 보존하면서 Java 배열에서 가변 리스트를 만드는 방법을 배웠습니다. 다음 내용을 살펴보았습니다.

  1. 구조, 기능 및 사용 사례를 포함하여 Java 배열과 리스트 간의 근본적인 차이점.

  2. 배열을 리스트로 변환하는 여러 가지 방법:

    • 배열의 고정 크기 보기를 위해 Arrays.asList() 사용
    • 완전히 가변적인 리스트를 위해 new ArrayList<>(Arrays.asList()) 사용
    • 기본형 배열에 .boxed()를 사용한 Stream API 사용
  3. 래퍼 객체의 리스트로 변환하기 위해 명시적인 박싱이 필요한 기본형 배열에 대한 특별 고려 사항.

  4. 구성 관리 및 레거시 API 와의 상호 작용을 포함하여 배열 및 리스트로 작업할 때의 실용적인 응용 분야 및 일반적인 함정.

이러한 기술은 배열과 리스트를 모두 사용해야 하는 많은 실제 Java 애플리케이션에서 유용하며, 특히 조작을 위해 유연하고 가변적인 컬렉션을 만들면서 원래 데이터를 보존해야 할 때 유용합니다. 배열과 리스트 간의 관계를 이해하면 보다 효율적이고 유지 관리 가능한 Java 코드를 작성할 수 있습니다.