8장 첫문장.

컬렉션 API가 자바 개발자의 삶을 외롭지 않게(….) 해 주었으나, 극혐인건 여전하다. 예상치 못한 위치에서 에러도 뿜뿜 내뱉고, 사용하는 것 자체도 성가시다.
하지만 모던 자바로 돌입하면서(8,9) 이 컬렉션 API를 사용하기 조금 더 편하게 많은 개선이 되었다.
- 리스트(List)
- 집합(Set)
- 맵(Map)
이 대표적인 컬렉션들을 마음껏 manipulate 할 수 있도록 해 주는 것들에 대해 살펴보기로 하자
컬렉션 팩토리
별 생각없이 리스트를 만든다.
List<String> friends = new ArrayList<>();
friends.add("John");
friends.add("Tom");
friends.add("Lee");
리스트에 고작 3개의 원소만 넣는데 4줄의 코드를 쓰고 앉았다. 손가락 아픈건 덤….
그래서 조금 있어보이는 방법으로 코드를 바꿔본다
List<String> friends = Arrays.asList("John","Tom","Lee);
깔끔하다. 근데 새 친구가 생겼다
List<String> friends = Arrays.asList("John","Tom","Lee);
friends.add(0,"Doe"); // UnsupportedOperationException 발생
그렇다. friends가 생성시 고정크기의 Array로 구현 되었디 때문에, 익셉션이 발생한다.
난 친구를 더 추가할 수 없는, ‘컬렉션 API가 있는데도 많이 외로운 자바 개발자’ 가 되었다…..
그럼 어떻게 하지?
방법이 있다고 한다(?) List.of 팩토리 메소드를 이용하면 된다고 한다.
List<String> friends = List.of("John","Tom","Lee");
근데 얘도 사실 friends.add() 로 요소를 추가하려 하면 마찬가지로 UnsupportedOperationException이 발생한다……
책에서 훼이크를 줬다. 마치 방법이 있는 것처럼 써놓고,

뭐, 틀린 말은 아니다. 대부분의 코드는 & 내가 일하는 실무에서의 코드는 혼자 짜서 돌리는 소규모의 간단한 코드가 아니고, 여러명이 협업하는 코드인데… 대부분의 오류는 컬렉션에서 터지고(NPE라던가, ArrayIndexOutOfBounds라던가..등등…), 그 중 절대 다수는 ‘의도치 않은 값의 변경’ 에 의해서 터졌기 때문이다. 따라서 객체 생성시 immutable하게 생성되는 패러다임 자체는 귀찮을지언정&객체생성의 오버헤드가 더 생길지언정 신뢰성 측면에서 더 좋은 상황이기는 하다.
? 그럼 같은거 아님?
asList 와 of 는 분명한 차이가 있다.
mutable vs immutable의 차이.
List<String> friends1 = Arrays.asList("John","Tom","Lee");
List<String> friends2 = List.of("John","Tom","Lee");
friends1.set(0,"Doe"); // 가능
friends2.set(0,"Doe"); // 불가능. UnsupportedOperationException 발생.
friends1.contains(null); // false 리턴. 익셉션이 나는 밑줄보다 이게 더 좋은 것 같은가?ㅋㅋ ㄴㄴ함. 애초에 List.of로 리스트를 생성하면 하등 쓸 이유도, 쓸 필요도, 써서도 안되는 없는 구문임....
friends2.contains(null); // NPE 남. 아싸.
///////////////////////////////////////////////////////////////////////
List<String> friends3 = Arrays.asList("John","Tom","Lee", null); //가능. 근데 보기만 해도 소름돋는 ㅎㄷㄷ한 코드.
List<String> friends4 = List.of("John","Tom","Lee", null); // NPE 남. 아싸.
///////////////////////////////////////////////////////////////////////
암튼 mutable한 객체가 이렇게 무섭다. 쓰지말자…(8이상에서도 몇번 쓴 적이 있는 기억이 불현듯 난다…쓰지 말아야지..)
집합, 맵 팩토리
//집합 팩토리 사용법.
Set<String> friends = Set.of("John", "Tom", "Lee");
Set<String> friends = Set.of("John", "Tom", "Lee", "Lee"); // IllegalArgumentException 발생.
//맵 팩토리 사용법.
Map<String,Integer> ageOfFriends =
Map.of(
"John", 20,
"Tom", 30,
"Lee", 40,
);
Map<String, Integer> ageOfFriends2 =
Map.ofEntries(
entry("John", 20),
entry("Tom", 30),
entry("Lee", 40),
);
리스트 처리(removeIf, replaceAll)
레거시 자바 기준, 리스트 컬렉션에서 요소를 변경하고자 하는 경우가 있다.
List<Transaction> 에서 숫자로 시작되는 참조코드를 삭제하는 코드를 작성한다고 가정해 보자.
for (Transaction elem : transactions) {
if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
transactions.remove(elem);
}
}
정상적으로 잘 동작할까? transactions 에서 숫자로 시작되는 참조코드가 아예 없지 않는 이상, 십중팔구 익셉션이 난다.
for (Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext(); ) {
Transaction transaction = iterator.next();
if (Character.isDigit(transaction.getReferenceCode(). charAt(0))) {
transactions.remove(transaction);
}
}
for-each를 풀면 이 for문으로 해석할 수 있는데,
iterator가 for문 진입 시 transactions 를 최초 1번만 참조하고, 그 이후에 transactions 가 변경되면서 두개의 결과가 동기화 되지 않기 떄문이다.
풀어서 말해보면, 최초 transactions.iterator(); 를 참조하면서 숫자가 존재하는 참조코드를 갖고 있다가, if를 거쳐 4번 라인을 실행한 후에, transactions는 원소 1개가 remove 되었지만, iterator는 최초에 1번만 transactions를 참조하였으므로… ‘아직도 4번 라인이 실행되기 전인 최초의 transactions의 iterator를 갖고 있는’ 상황이라는 것임..ㅇㅇ
따라서 이 복잡복잡한 상황을 해결하고 싶으면….편-안 하게 removeIf를 사용하면 된다.
transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0))); // 프리디케이트 사용!
리스트에 있는 모든 요소를 replaceAll 을 통해 변경할 수도 있다.
스트림을 이용하여 .stream().map() 처리를 해도 되지만, stream을 이용하는 순간 ‘새로운 컬렉션’ 이 생기게 되는 현상이 발생하므로(기존 컬렉션은-사용하지 않는다면- 가비지 컬렉팅 대상이 되어 시스템의 성능부담을 주는 효과는 덤) 되도록이면 지양하자.
code.replaceAll(code -> Character.toUpperCase(code.charAt(0))); // 프리디케이트 사용!
맵 처리(forEach, getOrDefault)
맵을 순회하면서 무언가를 하는 경우…. 레거시 자바는 다음과 같이 구현한다.
for(Map.Entry<String,Integer> entry : ageOfFriends.entrySet()) {
entry.getKey();
entry.getValue();
}
극혐. 하지만 모던 자바의 forEach()를 사용하면?
ageOfFriends.forEach((name, age) -> sysout(friend + age));
편-안
맵의 키를 조회하여 요소를 받고자 할 때…. 레거시 자바는 다음과 같이 처리한다
Integer johnAge = ageOfFriends.get("Johnnnnnnn");
if (johnAge == null) { // 좋으나 싫으나 필수......
johnAge = 0;
sysout("존이 없어.");
}
이렇듯 null이 발생할 수 있는 부분에 대해 처리를 해야 한다.
하지만 모던 자바의 getOrDefault() 를 사용하면?
Integer johnAge = ageOfFriends.getOrDefault("john12321321", 0);
이렇게 별도의 체크 구문 필요 없이 편-안 하게 default 값을 줄 수 있다.
계산패턴,교체패턴,삭제패턴
사실 읽어보니 개념적으로 그렇게 중요한 부분은 아닌 것 같고….. 일종의 ‘기법’을 소개해 보는 것이니 간단하게…. 코드로 한번 쓱 훑어본다.
계산패턴
// forEach 처럼 '묻지마. 무조건 다 계산' 할 게 아니라, 미처 계산되지 못한 것들에 대해서만 계산하고자 할 때 lines.forEach(line -> dataToHash.computeIfAbsent(line, this::calculateDigest); );
뭐 이런식으로 조금 ‘스마트’ 하게 계산할 수 있다.
- computeIfAbsent : 첫번째 argument로 들어오는 키값이 없으면 계산(두번째 argument)식 수행.
- computeIfPresent : 그 반대….
- compute : 제공된 키로 새 값을 계산
삭제패턴
myMap.remove(key); //묻지마 삭제. myMap.remove(key,value); //value까지 일치 해야 삭제함. 'remove if value equals' 정도라고 이해하면 되겠다
교체패턴
favouriteMovies.replaceAll((friend, movie) => movie.toUpperCase());
머징
중복된 키가 없다는 것을 알고 있는 상황이라면, aMap.putAll(bMap); 과 같은 방법으로 머징을 할 수 있다. 중복된 키가 없는 경우엔, forEach 와 merge 를 통해 충돌을 해결할 수 있다.
Map<String,String> everyone = new HashMap<>(family); friends.forEach((k,v) -> everyone.merge(k,v, (movie1, movie2) -> movie1 + " & " + movie2)); // 겹치는 key에 대해, 세번째 argument 펑션을 사용하여 처리. 이 경우는 1,2번 영화를 더하는 동작으로 중복을 처리함
마치며(정리)
- 모던 자바는 immutable한 객체를 생성하게 해 주는 컬렉션 팩토리가 있다.
- List.of
- Set.of
- Map.of
- Map.ofEntries
- List 인터페이스는 removeIf,replaceAll,sort 세가지 디폴트 메소드를 지원한다.
- Set 인터페이스는 removeIf 디폴트 메소드를 지원한다.
- Map은 좀 더 다양하게 지원해 준다.
- computeIfAbsent, computeIfPresent, compute
- remove
- replaceAll
- merge