이번 챕터는 자바 8, 자바 9에 추가된 새로운 컬렉션 API의 기능을 배운다. 리스트, 집합, 맵 등을 조작하고 쉽게 만드는 방법을 배운다고 생각하면 될 것 같다.
자바 9에서는 작은 컬렉션 객체를 쉽게 만들 수 있는 몇가지 방법을 제공한다.
자바에서 적은 요소를 포함하는 리스트를 어떻게 만들까? 한번 코드를 보자.
List<String> friends = new ArrayList<>();
friends.add("Raphael");
friends.add("Olivia");
friends.add("Thibaut");
단 3가지의 요소만 배열에 추가했음에도 많은 코드가 필요했다. 다음처럼 Arrays.asList() 펙토리 메서드를 이용하면 코드를 간단하게 만들 수 있다.
List<String> friends = Arrays.asList("Raphael","Olivia","Thibaut");
📌 고정 크기의 리스트를 만들었으므로 요소를 갱신할 순 있지만 새 요소를 추가하거나 요소를 삭제할 순 없다 !
예시 - UnsupportedOperationException
List<String> friends = Arrays.asList("Raphael", "Olivia"); friends.set(0, "Richard"); //Raphael -> Richard friends.add("Thibaut"); //UnsupportedOperationException - 고정 크기라 추가 불가능
위 예제와 같이 asList로 생성 시 요소의 갱신은 가능하지만 요소의 추가 삭제는 불가능하다.
내부적으로 고정된 크기의 변환할 수 있는 배열로 구현되었기 때문에 이와 같은 일이 발생한다.
그러면 집합은 어떨까? Arrays.asSet()이라는 메서드는 없으므로 다음 방법을 사용할 수 있다.
Set<String> friends = new HashSet<>(Arrays.asList("Raphael", "Olivia", "Thibaut")); //리스트를 인수로 받는 HashSet 생성자
Set<String> friends = Stream.of("Raphael", "Olivia", "Thibaut").collect(Collectors.toSet()); //toSet를 최종 연산에 적용하여 집합으로 만듦
하지만 두 방법 모두 내부적으로 불필요한 객체 할당을 필요로 한다.
📌 그래서 자바 9에서는 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 팩토리 메서드를 제공한다.
펙토리 메서드 : 기존 객체를 바탕으로 새로운 객체를 생성하는 메서드
List.of 팩토리 메서드를 이용해 간단하게 리스트를 만들 수 있다.
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
System.out.println(friends); //[Raphael, Olivia, Thibaut]
friends.add("Chih-Chun"); //UnsupportedOperationException
📌추가 - 오버로딩 vs 가변 인수
List 인터페이스를 살펴보면 List.of의 다양한 오버로드 버전이 존재한다.
static <E> List<E> of(E e1, E e2, E e3, E e4) static <E> List<E> of(E e1, E e2, E e3, E e4, E e5)
왜 아래처럼 다중 요소를 받을 수 있도록 자바 API를 만들지 않았을까?
static <E> List<E> of(E...elements)
- 내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싼다.
- 배열을 할당하고 초기화하며 나중에 Garbage Collection을 하는 비용을 지불해야 한다.
- 고정된 숫자 요소(최대 10개)를 API로 정의하므로 이런 비용을 제거할 수 있다.
- List.of로 10개 이상의 요소를 가진 리스트를 만들 수도 있지만, 이때는 가변 인수를 이용하는 메서드가 활용된다.
📌 불변적이고 간단한 구조를 가진 리스트를 생성할 때 비교적 간편한 팩토리 메서드를 사용할 수 있다.
List.of와 비슷한 방법으로 바꿀 수 없는 집합을 만들 수 있다.
Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
System.out.println(friends); //[Raphael, Olivia, Thibaut]
📌 중복된 요소를 포함하여 집합을 만들 시 IllegalArgumentException이 발생한다 !
예시를 한번 보자
Set<String> friends = Set.of("Raphael", "Olivia", "Olivia");
//IllegalArgumentException
집합은 오직 고유의 요소만 포함할 수 있다.
자바 9에서는 두 가지 방법으로 바꿀 수 없는 맵을 초기화할 수 있다.
Map<String, Integer> ageOfFriends = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
//key, value를 번갈아 제공하며 맵 만들기
System.out.println(ageOfFriends); //{Olivia=25, Raphael=30, Thibaut=26}
Map.Entry<K, V> 객체를 인수로 받고 가변 인수인 Map.ofEntries 팩토리 메서드 이용
import static java.util.Map.entry;
Map<String, Integer> ageOfFriends = Map.ofEntries(entry("Raphael", 30),
entry("Olivia", 25),
entry("Thibaut", 26));
System.out.println(ageOfFriends); //{Olivia=25, Raphael=30, Thibaut=26}
자바 8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.
새로운 결과를 만드는 스트림과 달리 이들 메서드는 호출한 컬렉션 자체를 바꾼다.
예시
//removeIf transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0))); //Predicate가 인수니 람다로 Predicate 구현 //replaceAll referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1)); //UnaryOpertaor 구현하여 요소를 바꿈
자바 8에서 Map 인터페이스에 몇 가지 Default Method를 추가했다.
forEach
BiConsumer(키와 값을 인수로 받음)를 인수로 받는 forEach 메서드 지원
ageOfFriends.forEach((friend, age) -> System.out.println(friend + "is" + age));
//key와 value 이용하여 출력
Entry.comparingByValue, Entry.comparingByKey
Map<String, String> favouriteMovies
= Map.ofEntries(entry("Raphael", "Star Wwars"),
entry("Cristina", "Matrix"),
entry("Olivia", "James Bond"));
favouriteMovies.entrySet() //Map에 포함된 모든 키-값 쌍을 Set 컬렉션으로 변경 -> {A=Apple, B= Banana etc...}
.stream()
.sorted(Entry.comparingByKey()) //키 값을 기준으로 정렬
.forEachOrdered(System.out::println); //사람의 이름을 알파벳 순으로 스트림 요소 처리
//결과
Cristina=Matrix
Olivia=James Bond
Raphael=Star wars
getOrDefault
요청한 키가 맵에 존재하지 않을때 어떻게 처리할건지 결정
첫번째 인수로 키를, 두번째 인수로 기본값을 받음
맵에 키가 존재하지 않으면 두번째 인수인 기본값 반환
키가 존재하더라도 값이 null이면 null 반환 가능
Map<String, String> favouriteMovies = Map.ofEntries(entry("Raphael", "Star wars"),
entry("Olivia", "James Bond"));
System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix"));
//키가 존재하므로 James Bond 출력
System.out.println(favouriteMovies.getOrDefault("Thibaut", "Matrix"));
//키가 존재하지 않으므로 Matrix 출력
computeIfAbsent
friendsToMovies.computeIfAbsent("Raphael", name -> new ArrayList<>()).add("Star Wars");
//{Raphael:[Star Wars]} - Raphael의 키의 값으로 새로운 배열 생성 후 Star Wars 추가
computeIfPresent
compute
remove
favouriteMovies.remove(key, value);
//key, value가 mapping 되었을때 삭제
replaceAll
BiFunction(T, U -> R)을 적용한 결과로 각 항목의 값 교체
List의 replaceAll과 비슷한 동작 수행
Map<String, String> favouriteMovies = new HashMap<>();
favouriteMovies.put("Raphael", "Star Wars");
favouriteMovies.put("Olivia", "James Bond");
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase()); //값을 대문자로 변경
System.out.println(favouriteMovies);
//결과 : {Olivia=JAMES BOND, Raphael=STAR WARS}
replace
putAll
Map<String, String> family = Map.ofEntries(entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(entry("Raphael", "Star Wars"));
Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends); //friends의 모든 항목을 everyone으로 복사
System.out.println(everyone);
//결과 : {Cristina=James Bond, Raphael=Star Wars, Teo=Star Wars}
merge
putAll에서 중복이 있는 경우 사용
중복된 키를 어떻게 합칠지 결정하는 BiFunction을 인수로 받음
Map<String, String> family = Map.ofEntries(entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"));
Map<String, String> everyone = new HashMap<>(family);
friends.forEach((k, v) -> everyone.merge(k,v, (movie1, movie2) -> movie1 + "&" + movie2));
//중복된 키가 있으면 두 값을 연결(BiFunction을 인수로 받았음)
System.out.println(everyone);
//{Raphael=Star Wars, Cristina=James Bond & Matrix, Teo=Star Wars}