본문 바로가기
Book/모던 자바 인 액션

[모던 자바 인 액션] Chapter 08. 컬렉션 API 개선 (1)

by ghan2 2024. 11. 7.
💡 이 장의 내용
- 컬렉션 팩토리 사용하기
- 리스트 및 집합과 사용할 새로운 관용 패턴 배우기
- 맵과 사용할 새로운 관용 패턴 배우기

 

8.1 컬렉션 팩토리

자바 9에서는 작은 컬렉션 객체를 쉽게 만들 수 있는 몇 가지 방법을 제공한다. 

 

8.1.1) 리스트 팩토리

List.of 팩토리 메서드
  • List.of("Raphael", "Olivia", "Thibaut"); 간단하게 리스트를 만들 수 있다. 
  • 불변의 리스트가 만들어져서 요소를 추가하거나 삭제할 수 없다. 
  • null 요소를 허용하지 않는다 -> 의도치 않은 버그를 방지하고 간결한 내부 구현 달성. 

8.1.2) 집합 팩토리

Set.of 팩토리 메서드
  • Set.of("Raphael", "Olivia", "Thibaut"); 
  • 중복된 요소를 허용하지 않는다. 

8.1.3) 맵 팩토리

Map.of 팩토리 메서드
  • 열 개 이하의 키와 값 쌍을 가진 작은 맵을 만들 때 유용하다.
Map.ofEntries 팩토리 메서드
  • 열 개 이상의 맵에서는 Map.Entry<K, V> 객체를 인수로 받으며 가변 인수로 구현된 Map.ofEntries 팩토리 메서드를 이용하는 것이 좋다. 
  • 키와 값을 감쌀 추가 객체 할당을 필요로 한다. 
Map <String, Integer> ageOfFriends = Map.ofEntries(entry("Raphael", 30),
							entry("Olivia", 25),
                            entry("Thibaut", 26));

 

지금까지 자바 9의 새로운 팩토리 메서드를 이용해 컬렉션을 쉽게 만드는 방법을 살펴봤다. 

보통 컬렉션을 만들었으면 이를 처리해야 한다. 8.2 절에서는 리스트와 집합을 쉽게 처리할 수 있는 일반 패턴을 구현하는 새로운 개선사항을 배운다. 

 

8.2 리스트와 집합 처리

자바 8에서 List, Set 인터페이스에 다음과 같은 메서드를 추가했다. 
  • removeIf : 프레디케이트를 만족하는 요소를 제거한다. List나 Set을 구현하거나 그 구현을 상속받은 모든 클래스에서 이용할 수 있다. 
  • replaceAll : 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꾼다. 
  • sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.

이들 메서드는 호출한 컬렉션 자체를 바꾼다.  에러를 유발할 수 있는 이러한 기능을 왜 추가했을까?

 

8.2.1) removeIf 메서드

이 부분은 이전에 어리둥절 하며 지나갔던 부분을 다루고 있어서 지금 너무 기쁘기 때문에.. 자세히 적어보겠다

 

다음은 숫자로 시작되는 참조 코드를 가진 트랜잭션을 삭제하는 코드다.

for (Transaction transaction : transactions ) {
	if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
    	transactions.remove(transaction);
    }
}

안타깝게도 위 코드는 ConcurrentModificationException을 일으킨다. 왜 그럴까? 

내부적으로 for-each 문은 Iterator를 사용하기 때문이다. 위 코드는 다음과 같이 해석된다.

for (Iterator<Transaction> iterator = transactions.iterator(); iterator.hanNext(); ) {
	Transcaction transaction = iterator.next();
	
    if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
		transactons.remove(transaction);
	}
}
  • Iterator 객체, next(), hasNext()를 이용해 소스를 질의한다.
  • Collection 객체 자체, remove()를 호출해 요소를 삭제한다.

결과적으로 반복자의 상태는 컬렉션의 상태와 서로 동기화되지 않는다. Iterator 객체를 명시적으로 사용하고 그 객체의 remove() 메서드를 호출함으로 이 문제를 해결할 수 있다. 

for (Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext(); ) {
	Transaction transaction = iterator.next();
    
    if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
    	iterator.remove();
    }
}

이 코드 패턴은 자바 8의 removeIf 메서드로 바꿀 수 있다. 그러면 코드가 단순해질 뿐 아니라 버그도 예방할 수 있다. 

transactions.removeIf(transaction -> 
		Character.isDigit(transaction.getReferenceCode().charAt(0)));

 

하지만 때로는 요소를 제거하는게 아니라 바꿔야 하는 상황도 있다. 이런 상황에서는 replaceAll을 사용한다.

 

8.2.2) replaceAll 메서드

List 인터페이스의 replaceAll 메서드를 이용해 리스트의 각 요소를 새로운 요소로 바꿀 수 있다. 살짝 귀찮지만... 신기한 내용이니까 적어보겠다. 

referenceCodes.stream()
	.map(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1))
    .collect(Collectors.toList())
    .forEach(System.out::println);

 

하지만 이 코드는 새 문자열 컬렉션을 만든다. 우리가 원하는 것은 기존 컬렉션을 바꾸는 것이라면 이렇게 할 수 있다.

for (ListIterator<String> iterator = referenceCodes.listIterator(); iterator.hasNext(); ) {
	String code = iterator.next();
    
    iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1));
}

 

그리고 이러한 코드를 자바 8부터는 이렇게 구현할 수 있다.

referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));

 

8.3 절에서는 Map 인터페이스에 추가된 새 기능을 설명한다. 😃