자바(JAVA)

자바 - 리스트(List)

BlueNoa 2023. 8. 21. 21:41
728x90
반응형

<목차>

 

반응형

• 리스트란?

리스트는 배열과 비슷하지만 좀 더 편의 기능이 많다고 볼 수 있다. 그 차이점은 좀 있다가 설명하고, 리스트에 대해서 서술하자면 리스트는 동일한 데이터 타입의 요소들을 순서대로 저장하는 자료 구조이다.

크기를 동적으로 조절할 수 있으며, 자바에서는 'java.util' 패키지에서 제공하는 'List' 인터페이스와 그를 구현한 클래스를 통해 사용할 수 있다.

 

- 리스트의 주요 특징

1. 크기 조절

리스트는 동적으로 크기를 조절할 수 있어 요소의 추가 및 삭제가 가능하다.

 

2. 중복 요소 허용

하나의 리스트 내에 같은 요소가 여러 번 포함될 수 있다.

 

3. 순서 유지

요소들은 추가된 순서대로 유지된다.

 

4. 인덱스

각 요소는 0부터 시작하는 인덱스로 접근이 가능하다.

 

5. 다양한 구현체

자바에서는 'ArrayList', 'LinkedList' 등 다양한 리스트 구현체를 지원한다.

 

- List 인터페이스의 주요 메서드

add() : 요소를 리스트에 추가한다.

get() : 인데스를 통해 요소를 가져온다.

remove() : 인덱스나 요소를 기준으로 해당 요소를 삭제한다.

size() : 리스트의 크기를 반환한다.

isEmpty() : 리스트가 비어있는지 여부를 확인

contains() : 지정한 요소가 리스트에 포함되어 있는지 여부를 반환한다.

 

728x90

• ArrayList와 LinkedList

자바의 리스트 구현의 대표적인 리스트 구현체중 두 개다.

간단히 정리하면 ArrayList는 배열을 기반으로 구현되어 빠른 접근이 가능하며, LinkedList는 링크드 리스트를 기반으로 구현되어 요소 추가 및 삭제에 특히 효율적이다.

 

<ArrayList 예시>

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

public class sample {
    public static void main (String[] args) {
        // ArrayList 생성
        List<String> names = new ArrayList<>();

        // 요소 추가
        names.add("한석봉");
        names.add("홍길동");
        names.add("권율");

        // 요소 접근
        System.out.println(names.get(1)); // 인덱스 위치1의 값을 가져온다.
        
        // 요소 삭제
        names.remove(0); // 인덱스 위치0의 값을 삭제

        // List의 크기
        System.out.println(names.size());
        System.out.println(names); // 각 요소 전체 출력
        
        // 해당 요소가 존재하는지 여부 확인
        System.out.println(names.contains("권율")); // true

    }
}

<결과>

홍길동
2
[홍길동, 권율]
true

 

<LinkedList 예시>

링크드 리스트는 데이터 요소들을 노드(Node)라는 개체로 구성하여 연결된 구조를 가지는 자료구조이다.

각 노드는 데이터와 다음 노드를 가리키는 참조(포인터)로 이루어져 있다.

링크드 리스트는 배열과 달리 데이터의 삽입, 삭제가 상대적으로 효율적으로 이루어지는 특징을 가지고 있습니다.

 

여기서는 다음 노드를 가리키는 참조 역할은 current가 한다.

// 정수형 데이터를 다루는 링크드 리스트의 예시

// 노드(데이터 요소) 정의
class Node {
    int data;
    Node next; // 다음 노드에 대한 정보가 담길 노드
    
    // 초기 노드의 값과 다음 노드의 주소를 받아옴
    public Node(int data){
        this.data = data;
        this.next = null;
    }
}

// 기본 동작 구현
class LinkedList {
    Node head; // 시작 노드에 대한 정보를 담을 head

    public LinkedList() {
        this.head = null; // LinkedList가 생성되면 자동으로 헤드에 null값 지정
    }
    
    // 요소 끝에 노드를 추가하는 메서드
    public void appendLst(int data) {
        Node newNode = new Node(data);
        // 처음 값이 없을 경우 appendFirst를 실행해도 된다.
        if (head == null){
            head = newNode;
            return;
        }
        // 기존에 값이 있는 경우
        Node current = head; // 마지막 노드의 정보를 담기 위함
        // 다음에 담길 노드(의 정보)가 null이 아닌 경우
        while (current.next != null){ // 다음 노드를 받아오기
            current = current.next;
        }
        current.next = newNode;
    }
    
    // 요소 처음에 추가하는 메서드
    public void appendFirst(int data) {
        Node newNode = new Node(data);
        newNode.next = head;
        head = newNode;
    }

   // 리스트의 개수(노드의 수)
    public int length() {
        int count = 0;
        Node current = head;
        while (current != null) {
            count++;
            current = current.next;
        }
        return count;
    }

    // 중간에 추가하는 메서드
    public void appendMiddle(int data, int i) { // i는 index를 뜻한다.
        // 인덱스 위치가 0보다 작거나 입력된 데이터의 수를 초과하는 경우
        if (i < 0 || i > length()) {
            System.out.println("Invalid index.");
            return;
        }
        // 인덱스의 위치가 0인 경우
        if (i == 0) {
            appendFirst(data);
        } else {
            Node newNode = new Node(data);
            Node prevNode = head;
            for (int j = 0; j < i - 1; j++) {
                prevNode = prevNode.next;
            }
            newNode.next = prevNode.next;
            prevNode.next = newNode;
        }
    }

    // 특정(값) 데이터를 삭제
    public void deleteNode(int key) { // 여기서 key는 data값이다.
        Node current = head;
        Node prevNode = null;
        // 현재 정보를 담은 노드가 null이 아니고 해당 데이터가 key와 일치한다면
        if (current != null && current.data == key){
            head = current.next; // 일치하는 경우 현재 노드의 정보를 다음 노드의 정보로 할당
            return;
        }
        // 현재 정보를 담은 노드가 null이 아니고 데이터가 키와 일치하지 않으면
        while (current != null && current.data != key) {
            prevNode = current;
            current = current.next;
        }
        // 현재 정보를 담은 노드가 null인 경우
        if (current == null) {
            System.out.println("Node with key " + key + " not found.");
            return;
        }
        // 이전 노드의 다음 주소를 현재 노드의 다음 주소로 할당(지우고자 하는 노드의 정보가 사라짐)
        // 말이 복잡해서 다시 정리하면 삭제 기준 앞 노드의 다음 노드로 삭제 기준 뒤 노드를 지정한다.
        prevNode.next = current.next;
        prevNode = null; // 지우고자 하는 노드의 연결을 끊는 것만으로 끝나면 안된다. 메모리 누수가 발생하기 때문에 null값을 부여.
    }

    // 리스트의 내용을 출력하는 메서드
    // 방법1 - 일반적인 출력
    public void printList() {
        Node current = head;
        while (current != null) {
            System.out.print(current.data + " ");
            current = current.next;
        }
        System.out.println();
    }

    // 방법2 - 배열이 출력되는 형식으로 출력
    public void str_printList() {
        if (head == null) {
            System.out.println("[]");
        }
        Node current = head;
        String str = "[";
        while (current.next != null) {
            str += current.data + ", ";
            current = current.next;
        }
        str += current.data;
        str += "]";
        System.out.println(str);
    }
}

// 메인
public class sample {
    public static void main (String[] args) {
       LinkedList list = new LinkedList();
       list.appendLst(100);
       list.appendLst(200);
       list.appendLst(300);
       list.appendFirst(50);
       list.appendMiddle(25, 2);

       System.out.println("Linked List : ");
       list.printList();

       list.deleteNode(25);
       list.str_printList();

        list.deleteNode(100);
        list.str_printList();
    }
}

<결과>

Linked List : 
50 100 25 200 300 
[50, 100, 200, 300]
[50, 200, 300]

• 제너릭스(Generics)

자바 제너릭스는 자바 프로그래밍 언어의 기능 중 하나로, 클래스나 메서드를 작성할 때 데이터 타입을 일반화(Generalize)하는 방법을 말한다.

제너릭스를 사용하면 클래스나 메서드를 특정 데이터 타입에 종속되지 않도록 만들 수 있다.

이는 코드의 재사용성과 유연성을 향상하며, 타입 안정성(type safety)을 보장하면서 컴파일 타임에 오류를 검출할 수 있도록 도와준다.

 

좀 더 쉽게 풀어서 설명하면 코드에서 사용할 데이터 타입을 명시하지 않고, 인스턴스화 또는 메서드 호출 시점에 해당 타입을 결정할 수 있으며 이를 통해 같은 로직을 다양한 데이터 타입에 대해 사용할 수 있고, 타입 간의 일관성과 안전성을 유지할 수 있다. 대표적인 예로 자료형을 강제로 바꿀 때 발생하는 캐스팅 오류를 줄일 수 있다.

 

- 2가지 사용 방법

ArrayList<String> 리스트명 = new ArrayList<String>();

 

ArrayList<String> 리스트명 = new ArrayList<>(); <- 이 방법을 더 선호한다. 첫 번째 예시에서도 해당 방법을 사용했다.

 

 

<제네릭스 예시>

public class sample<T> {
    private T content;

    public void input(T item) {
        this.content = item;
    }

    public T output() {
        return content;
    }

    public static void main (String[] args) {
        sample<Integer> integer_sample = new sample<>();
        integer_sample.input(14);
        Integer intValue = integer_sample.output();
        System.out.println(intValue);

        sample<String> string_sample = new sample<>();
        string_sample.input("와우, 놀라워");
        String stringValue = string_sample.output();
        System.out.println(stringValue);
        // System.out.println(string_sample.output()); 바로 출력해도 된다.
    }
}

 


• 제네릭스의 특징

위 내용에서 제네릭스란 무엇인지 그 예시를 살펴보았다면 이번엔 제네릭스의 특징에 대해 정리해보려고 한다.

 

1. 타입 매개변수(Type Parameter)

제네릭 클래스나 메서드를 정의할 때 사용되는 변수로, 실제 타입으로 대체되는 변수다.

대부분의 경우 알파벳 대문자로 표기하며, 예를 들어 위 목차 예시에서 사용된 '<T>'와 같이 표기한다.

 

2. 타입 인자(Type Argument)

제네릭 클래스나 메서드를 실제로 사용할 때, 타입 매개변수에 대응하여 실제 타입을 지정하는 값이다.

예를 들어 위 예시에서 사용한 'sample<Integer>'에서 'Integer'는 타입 인자에 해당한다.

 

3. 타입 경계(Type Bounds)

제네릭 타입에 대한 특정 조건을 설정하는 것으로, 타입 매개변수가 특정 클래스나 인터페이스를 상속하거나 구현하도록 제한할 수 있다.

 

4. 와일드카드(Wildcard)

'?' 기호를 사용하여 제네릭 타입의 일반화된 표현을 나타낼 수 있다. 와이드카드는 특정 타입을 대신할 수 있고, 제한적인 조건으로 사용이 가능하다.

하지만 다음과 같은 상황에서 유용하게 사용될 수 있다.

- 타입을 알 수 없는 경우에 범용적으로 사용하는 경우

- 여러 다른 타입을 다룰 수 있는 유연한 메서드나 클래스를 구현하는 경우

- 타입 매개변수를 정확히 지정할 필요가 없는 경우

 

<제네릭스 특징과 관련된 예시>

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

public class sample {
    public static void printList(List<?> list) { // 와일드카드를 이용한 리스트 생성
        for (Object item : list) { // 오브젝트 타입은 자바의 모든 클래스의 최상위 클래스이며, 모든 객체 참조 가능
            // 그러나 타입 정보를 잃어버리기 때문에 런타임 캐스팅 오류가 발생할 수 있다.
            // 여기서는 어떤 객체든 참조가 가능하기 때문에 와일드카드에서 사용됨
            System.out.print(item + " ");
        }
        System.out.println();
    }

    public static void main (String[] args) {
        String[] sub = {"지구과학", "화학", "생물학", "물리학"};
        Integer[] sub_code = {1112, 1332, 2556, 4447};
        List<Integer> intList = new ArrayList<>(Arrays.asList(sub_code));
        List<String> strList = new ArrayList<>(Arrays.asList(sub));

        printList(intList);
        printList(strList);

        strList.sort(Comparator.naturalOrder());
        System.out.println(strList); // 정렬 기능도 추가

    }
}

<결과>

1112 1332 2556 4447 
지구과학 화학 생물학 물리학 
[물리학, 생물학, 지구과학, 화학]

• 리스트와 배열의 차이점

자바에서 리스트와 배열은 둘 다 데이터를 순차적으로 저장하는 자료 구조를 갖고 있어 비슷해 보이지만 몇 가지 차이점이 존재한다.

 

1. 크기 조절

- 배열 : 배열은 생성 시 크기가 고정된다. 크기를 변경하려면 새로운 배열을 생성하고 기존 배열의 데이터를 복사해야 한다.

- 리스트 : 리스트는 동적으로 크기를 조절할 수 있다. 요소의 추가 및 삭제가 자유롭다.

 

2. 타입

- 배열 : 정적인 타입을 가지며, 한 번 생성한 배열의 타입은 변경할 수 없다. 모든 요소는 동일한 데이터 타입을 가져야 한다.

- 리스트 : 리스트는 동적인 타입을 가지며, 여러 종류의 데이터 타입을 하나의 리스트에 저장할 수 있다.

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

public class MixedTypeList {
    public static void main(String[] args) {
        List<Object> mixedList = new ArrayList<>();

        mixedList.add(42);             // 정수
        mixedList.add("Hello");        // 문자열
        mixedList.add(3.14);           // 실수
        mixedList.add(true);           // 불린
        mixedList.add(new ArrayList<>()); // 또 다른 리스트도 추가 가능

        for (Object item : mixedList) {
            System.out.println(item);
        }
    }
}

 

3. 기능

- 배열 : 자체적으로 제공하는 메서드가 제한적이다. 길이를 제외한 많은 작업은 직접 구현해야 한다.

- 리스트 : 'List' 인터페이스와 그를 구현한 클래스들이 풍부한 메서드가 있다. 요소의 추가, 삭제, 검색, 정렬 등의 작업을 간단하게 수행할 수 있다.

 

4. 메모리 할당

- 배열 : 배열은 메모리에 연속적으로 할당되기 때문에 큰 크기의 배열을 생성할 때 메모리 관리에 주의해야 한다.

- 리스트 : 내부 구조에 따라 메모리가 연속적으로 할당되지 않을 수 있으며, 대규모 데이터 저장이나 빈번한 크기 변경이 필요한 경우 메모리 효율성을 고려해야 한다.

 

5. 다차원

- 배열 : 다차원 배열은 행렬(Matrix) 등 다차원 데이터를 표현해야 할 때 유용하다.

- 리스트 : 리스트는 일차원 데이터를 순차적으로 저장하는 데 주로 사용한다. 다차원 데이터를 표현하려면 중첩된 리스트를 사용해야만 한다.

 


• 추가내용(기존 내용 포함)

1. 요소 추가 및 삭제
   - add(element) : 리스트에 요소를 추가
   - addAll(collection) : 다른 컬렉션의 모든 요소를 리스트에 추가
   - remove(element) : 지정한 요소를 리스트에서 삭제
   - remove(index) : 지정한 인덱스에 위치한 요소를 삭제

2. 요소 접근 
   - get(index) : 지정한 인덱스에 위치한 요소를 반환
   - set(index, element) : 지정한 인덱스에 위치한 요소를 다른 값으로 변경

3. 크기 및 존재 여부 
   -  size() : 리스트에 포함된 요소의 개수를 반환
   -  isEmpty() : 리스트가 비어있는지 여부를 반환

4. 요소 포함 여부 검사 
   -  contains(element) : 지정한 요소가 리스트에 포함되어 있는지 여부를 반환
   - containsAll(collection) : 다른 컬렉션의 모든 요소가 리스트에 포함되어 있는지 여부를 반환

5. 인덱스 및 순회 
   -  indexOf(element) : 지정한 요소의 첫 번째 등장하는 인덱스를 반환
   -  lastIndexOf(element) : 지정한 요소의 마지막으로 등장하는 인덱스를 반환
   -  iterator() : 리스트를 순회하기 위한 반복자(iterator)를 반환
   -  listIterator() : 리스트를 순회하기 위한 양방향 반복자(list iterator)를 반환

6.  하위 리스트 생성 
   -  subList(fromIndex, toIndex) : 지정한 범위의 요소들로 이루어진 하위 리스트를 생성

7. 배열 변환
   -  toArray() : 리스트의 요소들을 배열로 변환
   -  toArray(array) : 리스트의 요소들을 지정한 배열에 복사

8. 리스트 비교 
   - equals(otherList) : 두 리스트가 같은지 여부를 반환

9. 정렬 및 검색
   -  sort(comparator) : 요소들을 지정한 컴패레이터(comparator)를 사용하여 정렬
   -  sort(null) : 요소들을 기본 정렬 순서로 정렬
   -  binarySearch(element) : 정렬된 리스트에서 이진 검색을 통해 요소의 인덱스를 찾는다.

10. 클리어 및 복사
    -  clear() : 리스트의 모든 요소를 삭제
    -  clone() : 리스트의 얕은 복사본을 생성

그 밖에도 많은 추가 기능이 존재한다.

 

 

 

728x90
반응형

'자바(JAVA)' 카테고리의 다른 글

자바 - 형 변환(Type Conversion)  (0) 2023.08.30
자바 - Map, 집합, 상수 집합  (0) 2023.08.29
자바 - 배열(Array)  (0) 2023.08.14
자바 - StringBuffer 클래스  (0) 2023.08.11
자바 - 문자  (0) 2023.08.10