자바 공부 기록 2회독(12) - 2024.2.2

동준·2024년 2월 2일
0

개인공부(자바)

목록 보기
14/16

10. 컬렉션 자료구조

백준이나 프로그래머스 문제를 풀 때 항상 발목을 잡는 것이 시간복잡도였다. 같은 방식으로 코드를 다뤄도 어떤 것은 메모리를 절약하면서 좀 더 빠르게 원하는 값을 계산하거나 도출할 수 있다. 프로젝트의 규모가 커질 수록 코드를 효율적으로 작성하는 것이 필요하다.

자바에서는 관련된 자료구조를 적절하게 활용할 수 있도록 미리 관련된 클래스 및 인터페이스를 컬렉션 프레임워크에 추가해뒀다. 다만, 자료구조의 개념적인 내용을 다루는 것은 현재 내가 깃허브에 기록으로 남기고 있는 알고리즘 & 자료구조에서 다룰 내용이고 여기서는 자바의 코드 작성과 관련돼서 유념해야 할 개념을 중점적으로 볼 예정

얼마 만의 자바 공부(?)

Java 17에서 다룰 자료구조에는 List, Set, Map, LIFO(Stack), FIFO(Queue)가 있다.

1) List 컬렉션

List 컬렉션은 대표적인 특징은 객체를 인덱스로 관리한다는 점이다. 저장된 객체마다 인덱스가 부여되고, 그 인덱스를 통해서 객체를 검색, 삭제하는 기능을 제공한다. 그렇기 때문에 List 기반 컬렉션들은 보통 인덱스를 매개값으로 갖는 메소드가 많은 편이다.

자바에는 List 인터페이스를 구현한 대표적인 컬렉션으로 ArrayList, Vector, LinkedList 등이 있다. 추후의 Set 컬렉션과도 비교할 부분이지만 인덱스로 관리한다는 것 때문에 순서가 중요 키워드가 된다.

(1) ArrayList

int[] ints; // 일반 배열

List<Integer> list = new ArrayList<>(); // ArrayList

자바에서의 ArrayList와 일반 배열의 차이점은 제한의 유무일 것이다. 일반 배열을 선언하면 해당 배열의 길이를 지정하게 되는데, 이는 추후에 변경할 수 없다. 반대로 ArrayList에서는 제한 없이 객체를 추가할 수 있다.

'자바에서의'라는 키워드를 강조한 이유는, 자바만이 지니고 있는 특징이기 때문이다. 예를 들어서 자바스크립트는 일반 배열의 길이를 능동적으로 늘이고 줄일 수 있다.

앞서 언급했듯, List 컬렉션은 인덱스를 통해 객체를 관리한다. 이 말인 즉슨, List에 저장하는 것은 객체를 직접 저장하는 것이 아닌, 객체를 참조하는 번지를 저장하게 된다. 만약 동일한 객체를 중복 저장하면 동일한 참조가 저장(즉, 해당 객체로 갈 수 있는 길이 한 길에서 두 길이 되는 것)되며, null 또한 저장이 가능하다.

위의 그림을 보면, 인덱스 3의 위치에 int 타입의 값인 55가 새로운 값으로 들어가면서, 기존의 인덱스 3부터 마지막 인덱스 9까지의 값들이 전부 한 칸씩 뒤로 밀려나간다. 특정 인덱스의 삭제 역시 동일한 매커니즘으로 차순의 인덱스들이 전부 한 칸씩 움직여야 한다는 특징이 있다. 이런 점 때문에 객체의 빈번한 삽입과 삭제가 발생하는 곳에서는 ArrayList를 쓰지 않는 것이 좋고, 대신 LinkedList를 사용하는 것이 좋다.

그렇지만, 인덱스를 통해서 순서를 관리할 수 있다는 특징 때문에 요소에 쉽게 접근할 수 있어서 검색이 빠르다는 장점이 있다.

(2) Vector

List<Integer> list = new Vector<>();

Vector는 내부 구조는 ArrayList와 동일하지만, 차이점은 Vector는 동기화(synchronized)된 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 Vector() 메소드를 실행할 수 없다. 동기화 키워드에서도 짐작되지만, Vector는 멀티 스레드 환경에서의 객체 추가에서 안전하게 사용할 수 있다.

정확히 표현하자면, List 인터페이스를 기반으로 구현된 컬렉션에는 요소 추가나 삭제 등의 기능 관련 메소드들이 존재한다. 여기서 Vector를 구현하게 되면 해당 List 메소드들은 동기화 메소드가 되는 것이다.

public class VectorExample {
    public static void main(String[] args) {
        List<Board> list = new Vector<>(); 
        // 동기화된 메소드(여기서는 Vector의 add())로 구성

        Thread threadA = new Thread() {
            @Override
            public void run() {
                for(int i=1; i<=1000; i++) {
                    list.add(new Board("제목" + i, "내용" + i, "글쓴이" + i));
                }
            }
        };

        Thread threadB = new Thread() {
            @Override
            public void run() {
                for(int i=1; i<=1000; i++) {
                    list.add(new Board("제목" + i, "내용" + i, "글쓴이" + i));
                }
            }
        };

        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
            // 현재 실행 중인 스레드(main 스레드)가 threadA와 threadB가 모두 종료될 때까지 기다림
        } catch(Exception e) {}

        int size = list.size();
        System.out.println("총 객체 수 : " + size);
    }
}

위의 코드를 실행하면 정확히 2000개의 결과가 추가되는 것을 확인할 수 있다. 정확한 과정은 두 개의 스레드(threadA, threadB)가 내부 작업으로 공유 객체인 Vector 타입의 list를 각각 1000개씩 객체를 추가한다.

여기서 Vector 타입의 list에 대한 메소드이기 때문에 객체에 대한 잠금이 발생하기 때문에 정확히 각각 1000개씩 추가하는 작업을 수행할 수 있다.

또한, 두 개의 스레드가 작업이 끝나고 종료 상태에 진입할 때까지 메인 스레드는 일시 대기 상태에 진입시키도록 각 스레드의 join() 메소드를 호출해뒀다. 이를 통해서 list에 저장된 객체 개수를 정확하게 측정할 수 있다.

만약 Vector 대신, ArrayList를 써서 동일한 작업을 수행해보면 다른 결과가 나온다. 동기화 메소드로써의 추가가 이뤄지지 않기 때문에 list 내부에 객체 추가가 병렬적으로 동시에 이뤄지면서 2000개 미만의 값으로 추가가 이뤄진다.

(3) LinkedList

연결 리스트라고 부르는 LinkedListArrayList와의 사용 방법은 동일하지만 내부 구조는 완전히 다르다. LinkedList의 구조는 인접 객체들끼리 서로 연결돼서 관리한다. 흡사 줄로 꿰서 연결하는 모습이다.

ArrayList의 특징은 인덱스를 통해 관리하므로 객체의 검색이 빠르지만, 어느 한 인덱스에 추가나 삭제 등의 영향을 주면 차순의 인덱스들한테 전부 영향을 준다. 반대로 LinkedList는 특정 객체를 중간에 삽입하기 위해서는 삽입하려는 위치의 앞과 뒤에 존재하는 객체들의 연결만 수정해주면 되기 때문에 ArrayList보다 객체의 삭제와 삽입에 있어서 빠른 성능을 낸다.

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

public class LinkedListExample {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();

        long startTime;
        long endTime;

        startTime = System.nanoTime();
        for (int i = 0; i < 10000; i++) {
            arrayList.add(0, String.valueOf(i));
        }
        endTime = System.nanoTime();
        System.out.printf("%-17s %8d ns\n", "ArrayList 걸린 시간 :", (endTime - startTime));

        startTime = System.nanoTime();
        for (int i = 0; i < 10000; i++) {
            linkedList.add(0, String.valueOf(i));
        }
        endTime = System.nanoTime();
        System.out.printf("%-17s %8d ns\n", "LinkedList 걸린 시간 :", (endTime - startTime));
    }
}
ArrayList 걸린 시간 : 14430083 ns
LinkedList 걸린 시간 :  1804250 ns

2) Set 컬렉션

List 컬렉션과 비교할 수 있는 Set 컬렉션의 대표적인 특징은 저장 순서를 유지하지 않으며, 중복을 허용하지 않는다. 머릿속으로 집합의 벤 다이어그램을 생각하면 편할 듯? 집합은 중복 허용 안 하고 벤 다이어그램은 순서가 무관하니까.

Set 인터페이스를 구현한 자료구조에는 HashSet, TreeSet, LinkedHashSet 등이 있다. 당연히 인덱스를 매개값으로 갖는 메소드가 없으며, 그렇기 때문에 수치와 관련된 메소드는 단순히 개수를 세리는 것으로 인식할 것.

예를 들어서 size() 메소드는 Set 인터페이스 구현 객체 내부에 저장된 요소의 개수를 세려서 그 값을 반환하는 메소드다.

(1) HashSet

Set<Integer> set = new HashSet<>();

Set 컬렉션 중에서 가장 많이 쓰이는 HashSet중복을 허용하지 않는다. 동일한 객체의 판단은 동등성의 개념으로 판별한다. 즉, hashCode() 메소드의 리턴값이 동일하고, equals() 메소드의 리턴값이 true를 반환하면 동등 객체가 되고, 이 동등 객체를 HashSet에서는 중복 저장하지 않는 것이다.

hashCode() 메소드와 equals() 메소드를 오버라이딩해서 중복의 기준을 작성자가 설정해줄 수 있음을 잊지 말 것.

public class Member {
    public String name;
    public int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode() {
        return name.hashCode() + age; // name값과 age가 같으면 동일한 해시코드 리턴
    }

    @Override
    public boolean equals(Object object) {
        if(object instanceof Member target) {
            return target.name.equals(name) && (target.age == age);
        } else {
            return false;
        } // name과 age가 같으면 true 리턴
    }

    // 최종적으로 동등성 확보
}
// 총 3번의 중복 저장으로 set 크기는 1이 된다.
public class HashSetExample {
    public static void main(String[] args) {
        Set<Member> set = new HashSet<>();

        set.add(new Member("홍길동", 30));
        set.add(new Member("홍길동", 30));
        set.add(new Member("홍길동", 30));
        set.add(new Member("홍길동", 30));

        System.out.println("총 객체 수 : " + set.size());
    }
}

Set 컬렉션은 인덱스가 없기 때문에 객체를 검색해서 찾는 메소드가 없다. 대신, 객체를 한 개씩 반복해서 뒤지면서 찾는 방법이 있다. 하나는 일반적인 반복문을, 다른 하나는 반복자(Iterator)를 사용하는 것이다.

// 반복문 사용하기
Set<T> set = new HashSet<>();

for(T t: set) {
	// ...
}
// 반복자(iterator) 사용하기
Set<T> set = new HashSet<>();

Iterator<T> iterator = set.iterator(); // set의 반복자 얻기

while(iterator.hasNext()) { // hasNext() 메소드로 가져올 다음 값이 있는지 확인
	T t = iterator.next(); // 컬렉션에서 하나의 객체를 가져온다.
    
    if (t.eqauls(something)) {
    	iterator.remove(); // next()로 가져온 특정 객체를 컬렉션에서 제거한다.
    }
}

(2) TreeSet

TreeSet이진 트리를 기반으로 한 Set 컬렉션이다. 반복문 혹은 Iterator를 사용하기 때문에 Set 컬렉션의 검색이 느리다는 단점을 보완 및 강화하는 특징을 지녔다.

TreeSet<T> treeSet = new TreeSet<>();

Set 인터페이스를 구현해도 되지만 TreeSet 타입으로 대입한 이유는 Set 인터페이스에는 없는 검색 관련 메소드들이 TreeSet에만 정의되어 있어서 기왕이면 TreeSet 타입으로 선언하자.

검색 메소드들 중에서 눈에 틔는 건, descendingSet(), headSet(), tailSet(), subSet()이었다. 이들은 리턴 타입이 NavigableSet이기 때문.

NavigableSet 인터페이스는 SortedSet 인터페이스를 구현해서 생성된다. SortedSet은 순서 개념이 없는 Set요소의 정렬이라는 개념을 포함시킨 인터페이스다. 이것의 기능을 확장한(정확히는 구현한)NavigableSetSortedSet의 정렬에서 요소의 검색 기능까지 덧붙여진 인터페이스다.

3) Map 컬렉션

'자바스크립트의 객체'처럼 키와 값의 쌍으로 구성된 객체를 저장한다. 이 키와 값의 쌍으로 구성된 객체를 엔트리(Entry)라고 한다.또한, 키와 값은 각각 객체로 구성되며, 키는 중복 저장이 불허되지만, 값은 중복 저장이 가능하다. 만약 중복 키를 저장할 경우, 기존에 저장된 키의 값은 새롭게 저장된 키의 값으로 대치, 즉 덮어씌워진다.

Map 컬렉션은 주로 키를 매개값으로 하는 메소드가 많다. 왜냐하면 관리 대상이 되는 객체는 주로 키의 값으로 저장하기 때문이다.

(1) HashMap

아까 HashSet은 요소의 중복 저장을 불허했다. HashMap도 이와 유사하게, 동일한 키의 중복 저장을 불허한다. 키의 동등성을 판단하기 때문에, 키 객체의 hashCode() 리턴값이 동일하고, 키 객체의 equals() 리턴값이 동일하면 동등 키 객체로 판단하고 중복 저장을 허용하지 않는다.

Map<K, V> map = new HashMap<>();
// K : 키 객체 타입, V : 값 객체 타입

다만 기본적인 Set과는 달리, 특정 요소를 찾아내는 관련 메소드를 어느 정도 제공한다. 정확히 말하자면 후술할 TreeMap이 아닌 이상, Map에서 직접적으로 검색을 할 수는 없고 키, 혹은 엔트리를 Set 타입으로 반환해서 기본 Set 컬렉션에서의 요소들을 세려서(반복문, Iterator) 찾아내는 방법을 활용할 수 있다.

Set<Map.Entry<K,V>> entrySet()
// 엔트리로 구성된 모든 Map.Entry 객체를 Set에 담아서 리턴

Set<K> keySet()
// 모든 키 객체를 Set 객체에 담아서 리턴

(2) Hashtable

List 컬렉션에서 ArrayList와 동일한 구조를 지니고 있지만, 동기화 메소드로 구성된 Vector가 있었다. Map에서도 비슷한 녀석이 있다.

HashMap과 동일한 구조를 지녔지만 동기화(synchronized) 메소드로 구성된 Hashtable 역시 멀티 스레드 환경에서 안전하게 객체의 추가, 삭제를 수행할 수 있다.

Map<K, V> map = new Hashtable<>(); 

메소드가 동기화되면서 공유 Map 객체가 잠금이 발생하며 엔트리의 추가, 삭제가 멀티 스레드 환경에서 경합을 일으키지 않고 이뤄질 수 있다. 아래 예시를 보자.

public class HashTableExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new Hashtable<>();

        Thread threadA = new Thread(){
            @Override
            public void run() {
                for (int i = 1; i <= 1000; i++) {
                    map.put(String.valueOf(i), i);
                }
            }
        };

        Thread threadB = new Thread(){
            @Override
            public void run() {
                for (int i = 1001; i <= 2000; i++) {
                    map.put(String.valueOf(i), i);                   
                }
            }
        };

        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        int size = map.size();
        System.out.println("총 엔트리 수 : " + size);
    }
}

정확하게 엔트리 2000개가 저장한다. 만약 Hashtable이 아닌 기존의 HashMap을 사용하면 동시에 호출된 Map 객체의 메소드가 두 스레드 간에서 경합이 발생하고 결국은 하나만 저장되기 때문에 2000보다 적은 엔트리가 저장된다.

(3) Properties

PropertiesHashtable의 자식 클래스라서 Hashtable의 특징을 그대로 지녔지만, 키와 값을 String으로 제한했다. Properties는 주로 확장자가 .properties인 프로퍼티 파일을 읽을 때 사용한다.

.properties는 응용 프로그램의 구성 가능한 파라미터들을 저장하기 위해서 자바 관련 기술을 주로 사용하는 파일을 위한 파일 확장자이며, 키와 값이 =으로 연결되어 있는 텍스트 파일이다.

// database.properties

driver=oracle.jdbc.OracleDriver
url=jdbc:oracle:thin:@localhost:1521:orcl
username=scott
password=tiger
...
Properties properties = new Properties(); // Properties 객체 생성

properties.load(Xxx.class.getResourceAsStream("database.properties"));
// 해당 클래스 파일 기준, 상대 경로의 리소스 파일 InputStream으로 리턴

이런 류의 느낌? Properties 객체를 생성해서 load() 메소드로 프로퍼티 파일의 내용을 메모리로 로드한다. 보통 프로퍼티 파일은 클래스 파일(.class)들과 함께 저장돼서 클래스 파일을 기준으로 상대 경로를 이용해서 읽는 것이 편하다.

load() 메소드의 매개값에서 사용하는, Class 객체의 getResourceAsStream() 메소드는 주어진 상대 경로의 리소스 파일을 읽는 InputStream을 리턴하게 된다.

public class PropertiesExample {
    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();

        properties.load(PropertiesExample.class.getResourceAsStream(
                "database.properties"));

        String driver = properties.getProperty("driver");
        String url = properties.getProperty("url");
        String userName = properties.getProperty("username");
        String password = properties.getProperty("password");
        String admin = properties.getProperty("admin");
        
        // getProperty() 메소드는 해당 .properties 파일의 키를 바탕으로 값을 읽는다

        System.out.println("driver : " + driver);
        System.out.println("url : " + url);
        System.out.println("userName : " + userName);
        System.out.println("password : " + password);
        System.out.println("admin : " + admin);
    }
}

(4) TreeMap

검색 기능이 전무한 Set에서 이진 트리를 기반으로 검색 기능을 강화한 TreeSet처럼 Map 컬렉션에서 검색 기능을 강화한 TreeMap이 있다. 얘 역시 이진 트리를 기반으로 구현됐다.

TreeMap<K, V> treeMap = new TreeMap<>();

역시나 Map 타입으로 선언하지 않는 이유는, 검색과 관련된 메소드가 Map 인터페이스에는 존재하지 않기 때문이다. 이 부분 역시 Treeset과 개념이 유사하다.

다만 검색과 이후의 작업을 혼동해서는 안 된다. TreeSet이든, TreeMap이든 검색은 강화됐지만 그것을 바탕으로 수행하는 차후의 작업은 기존의 Set 방식을 따르게 된다. 정확히는 키 또는 키와 값으로 이루어진 엔트리를 Set 타입 객체에 담아서 반복문 혹은 Iterator를 활용해서 출력 등의 작업을 수행하게 된다.

TreeSet의 메소드들 중, 특정 메소드들은 NavigableSet 타입을 리턴하는 것을 확인했었다. 그와 마찬가지로 TreeSet이 지닌 메소드 중에서 descendingMap(), headMap(), tailMap(), subMap() 등은 NavigableMap<K, V> 타입의 값을 리턴한다.

NavigableSet처럼, NavigableMap 인터페이스는 SortedMap 인터페이스를 구현하는 서브 인터페이스다. SortedMap키의 정렬을 구현하며, 그것을 바탕으로 NavigableMap키의 검색을 갖추게 된다.

descendingKeySet()처럼 키를 내림차순으로 정렬해서 NavigableSet<K> 타입으로 반환하는 메소드 또한 존재한다.

4) 검색 기능 강화 컬렉션

(1) Comparable 인터페이스

위에서 언급했던 TreeSet 객체나 TreeMap의 키 객체는 자체적으로 저장하면서 오름차순으로 정렬된다. 그 이유는 해당 객체가 Comparable 인터페이스를 구현하고 있어서 그렇다. 즉, 어떤 객체든지 정렬될 수 있는 것은 아니고, 그 객체가 Comparable 인터페이스를 구현해야만 정렬이 가능하다.

Integer, Double, String 타입은 모두 Comparable 인터페이스를 구현하고 있기 때문에 상관 없지만, 사용자가 직접 정의하는 객체를 저장할 때는 반드시 Comparable 인터페이스를 구현해야 한다.

Comparable 인터페이스는 compareTo() 메소드가 정의되어 있다.

int compareTo(T o)
// 주어진 객체와 같으면 0을 리턴
// 주어진 객체보다 적으면 음수를 리턴
// 주어진 객체보다 크면 양수를 리턴

보통 Comparable 인터페이스를 구현할 때, 제네릭 타입으로 선언한다. 다음은 Comparable 인터페이스를 활용해서 Person 클래스를 정렬하는 예시다. 정렬 기준은 Person 클래스의 age 필드다.

public class Person implements Comparable<Person>{
    public String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

	// Comparable<Person> 인터페이스를 구현하고 compareTo 메소드를 구현
    @Override
    public int compareTo(Person person) {
        if(this.age < person.age) return -1;
        else if(this.age == person.age) return 0;
        else return 1;
    }
}

이후의 정렬은 반복문을 쓰거나 Set의 반복자인 Iterator를 활용해서 필요 작업을 수행한다.

(2) Comparator 비교자

비교 기능이 있는 Comparable 인터페이스를 구현한 객체를 TreeSet 혹은 TreeMap의 키에 저장하는 것이 원칙이나, 비교 기능이 없는 Comparable 비구현 객체를 저장할 수도 있다. 그 방법은 TreeSetTreeMap을 생성할 때, Comparator(비교자)의 구현 객체를 생성자 매개값으로 제공하는 것이다.

TreeSet<E> treeSet = new TreeSet<>(new ComparatorImpl());
  
TreeMap<K, V> treeMap = new TreeMap<>(new ComparatorImpl());

// Comparator 인터페이스를 구현한 객체를 생성자에 제공한다.

Comparable 인터페이스의 compareTo() 메소드처럼 Comparator 인터페이스에는 compare() 메소드가 있다.

int compare(T o1, T o2)
// 두 객체 o1과 o2가 동등하다면 0을 리턴
// o1이 o2보다 앞에 오게 하려면 음수를 리턴
// o1이 o2보다 뒤에 오게 하려면 양수를 리턴

보통 Comparator 인터페이스를 구현할 때, 제네릭 타입으로 선언한다. 다음은 Comparator 인터페이스를 활용해서 FruitComparator 클래스를 정렬하는 예시다. 정렬 기준은 Fruit 클래스의 price 필드다.

public class Fruit {
    public String name;
    public int price;

    public Fruit(String name, int price) {
        this.name = name;
        this.price = price;
    }
}

public class FruitComparator implements Comparator<Fruit> {
    @Override
    public int compare(Fruit fruit1, Fruit fruit2) {
        if(fruit1.price < fruit2.price) return -1;
        else if(fruit1.price == fruit2.price) return 0;
        else return 1;
    }
}

이후의 정렬은 반복문을 쓰거나 Set의 반복자인 Iterator를 활용해서 필요 작업을 수행한다. TreeSet 혹은 TreeMap을 구현할 때, 생성자의 매개값으로 비교자 구현 객체(FruitComparator)의 인스턴스를 넣어주는 것을 잊지 말자.

5) LIFO & FIFO

LIFO는 후입선출이고 스택(Stack) 자료구조에서 볼 수 있으며, FIFO는 선입선출이고 큐(Queue) 자료구조에서 볼 수 있다. 여담으로 LinkedList 클래스는 Queue 인터페이스를 구현한 것이다.

둘의 자세한 개념은 알고리즘 & 자료구조에서 공부한 내용 생각하기.

6) 동기화 컬렉션

VectorHashtable 등은 동기화된 메소드를 제공하기 때문에 멀티 스레드 환경에서 안전하게 객체를 추가하고 삭제할 수 있다. 하지만 ArrayList, HashSet, HashMap은 동기화된 메소드를 제공하지 않아서 멀티 스레드 환경에서 안전하지 않다.

경우에 따라서는 저 세 가지를 멀티 스레드 환경에서 사용할 수 있다. 즉, 원래는 동기화되지 않은 메소드지만, Collections에서 제공하는 비동기화된 메소드를 동기화된 메소드로 래핑하는 synchronizedXXX() 메소드를 활용하면 된다.

List<T> list = Collections.synchronizedList(new ArrayList<>());
// ArrayList의 메소드가 동기화됨

Set<T> set = Collections.synchronizedSet(new HashSet<>());
// HashSet의 메소드가 동기화됨

Map<K, V> map = Collections.synchronizedMap(new HashMap<>());
// HashMap의 메소드가 동기화됨

7) 수정 불가 컬렉션

컬렉션 내부에서 요소를 추가, 삭제할 수 없는 컬렉션을 만들 수 있다. 주로 생성한 컬렉션의 저장된 요소를 변경하고 싶지 않을 때 유용하게 사용 가능하다. 방법은 총 3가지가 있다.

  • List, Set, Map 인터페이스의 정적 메소드 of()
  • List, Set, Map 인터페이스의 정적 메소드 copyOf()
  • 배열로부터 얻기

1) 정적 메소드 of()

List, Set, Map 인터페이스의 정적 메소드 of()으로 생성해서 내부 매개값으로 저장할 객체들을 대입하면 된다.

List<E> immutableList = List.of(E... elements);

Set<E> immutableSet = Set.of(E... elements);

Map<K,V> immutableMap = Map.of(K k1, V v1, K k2, V v2, /*...*/);

2) 정적 메소드 copyOf()

기존 컬렉션을 생성한 다음, List, Set, Map 인터페이스의 정적 메소드 copyOf()을 이용해서 복사하면 불변 컬렉션이 생성된다.

List<E> immutableList = List.copyOf(Collection<E> collection);

Set<E> immutbaleSet = Set.copyOf(Collection<E> collection);

Map<K,V> immutableMap = Map.copyOf(Map<K,V> map);

3) 배열로부터 복사

배열을 사용해서 불변 컬렉션을 생성할 수 있다. 단, 이 내용은 List에만 적용된다.

// List에만 해당

String[] stringArray = {"A", "B", "C"};

List<String> immutableList = Arrays.asList(stringArray);
profile
scientia est potentia / 벨로그 이사 예정...

0개의 댓글