백준이나 프로그래머스 문제를 풀 때 항상 발목을 잡는 것이 시간복잡도였다. 같은 방식으로 코드를 다뤄도 어떤 것은 메모리를 절약하면서 좀 더 빠르게 원하는 값을 계산하거나 도출할 수 있다. 프로젝트의 규모가 커질 수록 코드를 효율적으로 작성하는 것이 필요하다.
자바에서는 관련된 자료구조를 적절하게 활용할 수 있도록 미리 관련된 클래스 및 인터페이스를 컬렉션 프레임워크에 추가해뒀다. 다만, 자료구조의 개념적인 내용을 다루는 것은 현재 내가 깃허브에 기록으로 남기고 있는 알고리즘 & 자료구조에서 다룰 내용이고 여기서는 자바의 코드 작성과 관련돼서 유념해야 할 개념을 중점적으로 볼 예정
얼마 만의 자바 공부(?)
Java 17
에서 다룰 자료구조에는 List
, Set
, Map
, LIFO(Stack)
, FIFO(Queue)
가 있다.
List
컬렉션은 대표적인 특징은 객체를 인덱스로 관리한다는 점이다. 저장된 객체마다 인덱스가 부여되고, 그 인덱스를 통해서 객체를 검색, 삭제하는 기능을 제공한다. 그렇기 때문에 List
기반 컬렉션들은 보통 인덱스를 매개값으로 갖는 메소드가 많은 편이다.
자바에는 List
인터페이스를 구현한 대표적인 컬렉션으로 ArrayList
, Vector
, LinkedList
등이 있다. 추후의 Set
컬렉션과도 비교할 부분이지만 인덱스로 관리한다는 것 때문에 순서가 중요 키워드가 된다.
int[] ints; // 일반 배열
List<Integer> list = new ArrayList<>(); // ArrayList
자바에서의 ArrayList
와 일반 배열의 차이점은 제한의 유무일 것이다. 일반 배열을 선언하면 해당 배열의 길이를 지정하게 되는데, 이는 추후에 변경할 수 없다. 반대로 ArrayList
에서는 제한 없이 객체를 추가할 수 있다.
'자바에서의'라는 키워드를 강조한 이유는, 자바만이 지니고 있는 특징이기 때문이다. 예를 들어서 자바스크립트는 일반 배열의 길이를 능동적으로 늘이고 줄일 수 있다.
앞서 언급했듯, List
컬렉션은 인덱스를 통해 객체를 관리한다. 이 말인 즉슨, List
에 저장하는 것은 객체를 직접 저장하는 것이 아닌, 객체를 참조하는 번지를 저장하게 된다. 만약 동일한 객체를 중복 저장하면 동일한 참조가 저장(즉, 해당 객체로 갈 수 있는 길이 한 길에서 두 길이 되는 것)되며, null
또한 저장이 가능하다.
위의 그림을 보면, 인덱스 3의 위치에 int 타입의 값인 55
가 새로운 값으로 들어가면서, 기존의 인덱스 3부터 마지막 인덱스 9까지의 값들이 전부 한 칸씩 뒤로 밀려나간다. 특정 인덱스의 삭제 역시 동일한 매커니즘으로 차순의 인덱스들이 전부 한 칸씩 움직여야 한다는 특징이 있다. 이런 점 때문에 객체의 빈번한 삽입과 삭제가 발생하는 곳에서는 ArrayList
를 쓰지 않는 것이 좋고, 대신 LinkedList
를 사용하는 것이 좋다.
그렇지만, 인덱스를 통해서 순서를 관리할 수 있다는 특징 때문에 요소에 쉽게 접근할 수 있어서 검색이 빠르다는 장점이 있다.
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개 미만의 값으로 추가가 이뤄진다.
연결 리스트라고 부르는 LinkedList
는 ArrayList
와의 사용 방법은 동일하지만 내부 구조는 완전히 다르다. 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
List
컬렉션과 비교할 수 있는 Set
컬렉션의 대표적인 특징은 저장 순서를 유지하지 않으며, 중복을 허용하지 않는다. 머릿속으로 집합의 벤 다이어그램을 생각하면 편할 듯? 집합은 중복 허용 안 하고 벤 다이어그램은 순서가 무관하니까.
Set
인터페이스를 구현한 자료구조에는 HashSet
, TreeSet
, LinkedHashSet
등이 있다. 당연히 인덱스를 매개값으로 갖는 메소드가 없으며, 그렇기 때문에 수치와 관련된 메소드는 단순히 개수를 세리는 것으로 인식할 것.
예를 들어서 size()
메소드는 Set
인터페이스 구현 객체 내부에 저장된 요소의 개수를 세려서 그 값을 반환하는 메소드다.
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()로 가져온 특정 객체를 컬렉션에서 제거한다.
}
}
TreeSet
은 이진 트리를 기반으로 한 Set
컬렉션이다. 반복문 혹은 Iterator
를 사용하기 때문에 Set
컬렉션의 검색이 느리다는 단점을 보완 및 강화하는 특징을 지녔다.
TreeSet<T> treeSet = new TreeSet<>();
Set
인터페이스를 구현해도 되지만 TreeSet
타입으로 대입한 이유는 Set
인터페이스에는 없는 검색 관련 메소드들이 TreeSet
에만 정의되어 있어서 기왕이면 TreeSet
타입으로 선언하자.
검색 메소드들 중에서 눈에 틔는 건, descendingSet()
, headSet()
, tailSet()
, subSet()
이었다. 이들은 리턴 타입이 NavigableSet
이기 때문.
NavigableSet
인터페이스는 SortedSet
인터페이스를 구현해서 생성된다. SortedSet
은 순서 개념이 없는 Set
에 요소의 정렬이라는 개념을 포함시킨 인터페이스다. 이것의 기능을 확장한(정확히는 구현한)NavigableSet
은 SortedSet
의 정렬에서 요소의 검색 기능까지 덧붙여진 인터페이스다.
'자바스크립트의 객체'처럼 키와 값의 쌍으로 구성된 객체를 저장한다. 이 키와 값의 쌍으로 구성된 객체를 엔트리(Entry)라고 한다.또한, 키와 값은 각각 객체로 구성되며, 키는 중복 저장이 불허되지만, 값은 중복 저장이 가능하다. 만약 중복 키를 저장할 경우, 기존에 저장된 키의 값은 새롭게 저장된 키의 값으로 대치, 즉 덮어씌워진다.
Map
컬렉션은 주로 키를 매개값으로 하는 메소드가 많다. 왜냐하면 관리 대상이 되는 객체는 주로 키의 값으로 저장하기 때문이다.
아까 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 객체에 담아서 리턴
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보다 적은 엔트리가 저장된다.
Properties
는 Hashtable
의 자식 클래스라서 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);
}
}
검색 기능이 전무한 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>
타입으로 반환하는 메소드 또한 존재한다.
위에서 언급했던 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
를 활용해서 필요 작업을 수행한다.
비교 기능이 있는 Comparable
인터페이스를 구현한 객체를 TreeSet
혹은 TreeMap
의 키에 저장하는 것이 원칙이나, 비교 기능이 없는 Comparable
비구현 객체를 저장할 수도 있다. 그 방법은 TreeSet
과 TreeMap
을 생성할 때, 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
)의 인스턴스를 넣어주는 것을 잊지 말자.
LIFO
는 후입선출이고 스택(Stack
) 자료구조에서 볼 수 있으며, FIFO
는 선입선출이고 큐(Queue
) 자료구조에서 볼 수 있다. 여담으로 LinkedList
클래스는 Queue
인터페이스를 구현한 것이다.
둘의 자세한 개념은 알고리즘 & 자료구조에서 공부한 내용 생각하기.
Vector
나 Hashtable
등은 동기화된 메소드를 제공하기 때문에 멀티 스레드 환경에서 안전하게 객체를 추가하고 삭제할 수 있다. 하지만 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의 메소드가 동기화됨
컬렉션 내부에서 요소를 추가, 삭제할 수 없는 컬렉션을 만들 수 있다. 주로 생성한 컬렉션의 저장된 요소를 변경하고 싶지 않을 때 유용하게 사용 가능하다. 방법은 총 3가지가 있다.
List
,Set
,Map
인터페이스의 정적 메소드of()
List
,Set
,Map
인터페이스의 정적 메소드copyOf()
- 배열로부터 얻기
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, /*...*/);
기존 컬렉션을 생성한 다음, 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);
배열을 사용해서 불변 컬렉션을 생성할 수 있다. 단, 이 내용은 List
에만 적용된다.
// List에만 해당
String[] stringArray = {"A", "B", "C"};
List<String> immutableList = Arrays.asList(stringArray);