자바

모던 자바 인 액션 -7-

정한_s 2021. 12. 17. 14:27

컬랙션 팩토리 

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

 

자바 9 이전 

// 일반 배열 만들기 // Set도 아래와 같다
List<String> friendsList = new ArrayList<>();
friendsList.add("Raphael");
friendsList.add("Olivia");
friendsList.add("Thibaut");
// Array.asList 사용
// 요소를 갱신할 수 있지만 추가나 삭제는 못함
List<String> friendsListArrayFactory = Arrays.asList("Raphael","Olivia","Thibaut");
// Array.asSet 이라는 팩토리 메서드는 없다 따라서 Set을 만들기 위해서는 다른 방법이 필요하다
// 이 경우 내부적으로 불필요한 객체 할등을 필요로 한다
Set<String> friendsSetArraysFactory = new HashSet<>(Arrays.asList("Raphael","Olivia","Thibaut"));
Set<String> friendsSetStream = Stream.of("Raphael", "Olivia", "Thibaut").collect(toSet());

자바 9 이후

// List.of, Set.of 메서드 제공
// 요소를 변경할 수 없음 (UnsupportedOperationException 발생)
List<String> friendsListFactory = List.of("Raphael", "Olivia", "Thibaut");
// 바꿀 수 없는 집합
// 요소에 중복된 값이 들어가 있으면 IllegalArgumentException 발생한다
Set<String> friendsSetFactory = Set.of("Raphael", "Olivia", "Thibaut");
// Map.of 메서드도 제공한다 (바꿀수 없는 맵) // 키와 값을 번달아 제공하는 방법 // 열개 이하의 작은맵에서 유용하다
Map<String, Integer> friendsMapFactory = Map.of("Raphael", 25, "Olivia", 32, "Thibaut", 43);
// Map.ofEntries 메서드
Map<String, Integer> friendsMapEntryFactory = Map.ofEntries
        (Map.entry("Raphael", 25), Map.entry("Olivia", 32), Map.entry("Thibaut", 43));

 

* List.of, Set.of, Map.of 에는 다양한 오버로드 버전이 있다. 이러한 오버로드 버전이 있는 이유는 작은 맵을 효율적으로 동작시키기 위해서이다. 가변 인수의 동작은 추가 배열을 할당해서 리스트로 감싼다. 따라서 배열을 할당하고 초기화하며 나중에 가비지 컬렉션을 하는 비용을 추가적으로 지불해야 한다. 따라서 오버로드를 통해 그 비용을 줄인 것이다.

* 열 개 초과의 요소를 가질 때 가변 인수를 이용하는 메서드 of(E ... elements)가 사용된다. (10개 이하는 효율적)

 

리스트 집합 처리

자바 8에서는 List, Set 인터페이스에 removeIf, replaceAll, sort를 추가했다. 이들 메서드는 새로운 결과를 만드는 스트림 동작과 달리 이들 메서드는 기존의 컬렉션을 바꾼다. 즉, 호출한 컬렉션 자체를 바꾼다.

 

// 초기의 값
List<String> transactions = new ArrayList<>();
transactions.add("soup");
transactions.add("2banana");
transactions.add("3apple");
transactions.add("juice");

삭제시 자바8 이전의 문제점

// 숫자로 시작되는 원소 삭제 // ConcurrentModificationException 발생
for(String transaction : transactions){
    if(Character.isDigit(transaction.charAt(0))){
        transactions.remove(transaction);
    }
}
// 위의 코드는 이와 같다
for(Iterator<String> iterator = transactions.iterator(); iterator.hasNext();){
    String transaction = iterator.next();
    if(Character.isDigit(transaction.charAt(0))){
        transactions.remove(transaction); // 반복하면서 별도의 두 객체를 통해 컬렉션을 바꾸고 있는 문제
    }
}

두 개의 개별 객체가 컬렉션을 관리한다.

  • Iterator 객체 : next(), hasNext()를 이용해서 소스를 질의한다.
  • Collection 객체 자체 : remove()를 호출해 요소를 삭제한다

결과적으로 반복자의 상태는 컬렉션의 상태와 서로 동기화되지 않는다.

 

해결

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

자바 8 이후 removeIf 메서드

removeIf 메서드는 삭제할 요소를 가리키는 프레디케이트를 인수로 받는다.

// 숫자로 시작하는 원소 삭제
transactions.removeIf(transaction->Character.isDigit(transaction.charAt(0)));

 

자바 8 이전 모든 요소 수정

// 모든 요소 대문자
for(ListIterator<String> listIterator = transactions.listIterator();listIterator.hasNext();){
    String transaction = listIterator.next();
    listIterator.set(transaction.toUpperCase());
}

자바 8 이후 replaceAll 메서드

// transactions.replaceAll(String::toUpperCase);
transactions.replaceAll(transaction -> transaction.toUpperCase());

맵 처리

자바 8에서 Map 인터페이스에 몇 가지 디폴트 메서드를 추가했다.

 

forEach 메서드

기존 방법

for(Map.Entry<String,Integer> entry : friendsMapFactory.entrySet()){
    String name = entry.getKey();
    Integer age = entry.getValue();
    System.out.println(name + " is age " + age);
}

개선된 방법

자바 8에서는 BiConsumer(키와 값을 인수로 받는다)를 인수로 받는 forEach 메서드 제공

friendsMapFactory.forEach((name,age)->System.out.println(name + " is age " + age));

 

정렬 메서드 - Entry.comparingByValue, Entriy.comparingByKey

// 정렬한 결과를 보여준다 
friendsMapFactory.entrySet()
 .stream()
 .sorted(Map.Entry.comparingByKey()) // key로 정렬한다 Entry.comparingByValue() value로 정렬// 기본 오름차순
 .forEachOrdered(System.out::println); // thread-safe하게, forEach는 병렬시 순서 보장 x

 

* HashMap 성능

더보기

자바 8에서는 HashMap의 내부 구조를 바꿔 성능을 개선하였음.

기존에 맵의 항목은 키로 생성한 해시코드로 접근할 수 있는 버켓에 저장했다. 많은 키가 같은 해시코드를 반환하는 상황이 되면 O(n)의 시간이 걸리는 LinkedList로 버킷을 반환해야 하므로 성능이 저하되었다 (캐시 충돌). 

최근에는 버킨이 너무 커질 경우 O(log(n))의 시간이 소요되는 정렬된 트리를 이용해 동적으로 치환해 충돌이 일어나는 요소 반환 성능을 개선했다. 하지만 키가 String, Number 클래스 같은 Comparable의 형태여만 정렬된 트리가 지원한다.

 

getOrDefault 메서드

기존의 찾으려는 키가 존재하지 않으면 null이 반환되다. 따라서 NullPointerException을 방지하려면 결과가 null인지 확인해야 한다. getOrDefault 메서드는 맵에 키가 존재하지 않으면 기본값을 반환한다.

// map에 Olivia=32 이므로 32를 반환한다
Integer newFriend = friendsMapFactory.getOrDefault("Olivia", 33);
// key에 값이 map에 없으므로 기본값인 33를 반환한다
Integer newFriend1 = friendsMapFactory.getOrDefault("newFriend", 22);

 

계산 패턴

맵에 키가 존재하는지 여부에 따라 동작을 실행하고 결과를 저장하는 함수이다

  • computeIfAbsent :  키에 해당하는 값이 없으면(값이 없거나 널), 키를 이용해 새 값을 계산하고 맵에 추가한다.
  • computeIfPresent : 키가 존재하면 새 값을 계산하고 맵에 추가한다
  • compute : 키로 새 값을 계산하고 맵에 저장한다
Map<String,Integer> mutableMap = new HashMap<>(friendsMapFactory);
// computeIfAbsent
// "newFriend" key가 없으므로 두번째 함수로 value를 계산해 map에 들어간다(key의 길이가 들어간다)
mutableMap.computeIfAbsent("newFriend",String::length);
System.out.println(mutableMap.get("newFriend")); // output 9
// computeIfPresent
// "newFriend" : 9 이 들어있는 상태이므로 두번째 인자의 함수로 계산된 값이 map에 들어간다
mutableMap.computeIfPresent("newFriend",(key,value)-> value+1);
System.out.println(mutableMap.get("newFriend")); // output 10
// compute 
// "newFriend" : 10에 key에 해당하는 값으로 두번째 함수를 통해 계산된 값이 map에 들어간다
mutableMap.compute("newFriend",(key,value)->value*2);
System.out.println(mutableMap.get("newFriend")); // output 20

 

삭제 패턴

자바 8에서는 키가 특정 값과 연관되었을 때만 항목을 제거하는 오버로드 버전 메서드를 제공한다.

// remove(k,v)
// "newFriend" : 20 인 상태이다
mutableMap.remove("newFriend",10); // 조건에 부합되지 않으므로 삭제 x
System.out.println(mutableMap.containsKey("newFriend")); // true
mutableMap.remove("newFriend",20); // 조건에 부합되므로 삭제 o 
System.out.println(mutableMap.containsKey("newFriend")); // false

 

교체 패턴

  • replaceAll : BiFunction을 적용한 결과로 각 항목의 값을 교체한다.
  • Replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 함수도 있음
Map<String, String> map = new HashMap<>();
map.put("key1","value1");
map.put("key2","value2");
// value 대문자로 변경
map.replaceAll((key,value)->value.toUpperCase());
// "key1" 라는 키값에 해당하는 value를 "value1"로 변경
map.replace("key1","value1");

합침

두개의 맵을 합칠 수 있다.

Map<String, String> firstMap  = new HashMap<>();
firstMap.put("firstMapKey1","firstMapValue1");
firstMap.put("firstMapKey2","firstMapValue2");
Map<String, String> secondMap=  new HashMap<>();
firstMap.put("secondMapKey1","secondMapValue1");
firstMap.put("secondMapKey2","secondMapValue2");
firstMap.put("secondMapKey3","secondMapValue3");

기존 방법

Map<String, String> firstMap  = new HashMap<>();
firstMap.put("firstMapKey1","firstMapValue1");
firstMap.put("firstMapKey2","firstMapValue2");
firstMap.put("testKey","testValue");
Map<String, String> secondMap=  new HashMap<>();
secondMap.put("secondMapKey1","secondMapValue1");
secondMap.put("secondMapKey2","secondMapValue2");
secondMap.put("secondMapKey3","secondMapValue3");
secondMap.put("testKey","testValue2");
firstMap.putAll(secondMap); // secondMap의 모든 항목을 firstMap으로 복사한다 // 중복된 key가 있다면 덮어쓴다

 중복된 키가 없다면 잘 동작하지만 중복된 키가 있으면 덮어쓴다. 값을 좀 더 유연하게 합쳐야 한다면 merge함수를 사용할 수 있다

 

merge 방법

Map<String,String> everyone= new HashMap<>(firstMap);
secondMap.forEach((k,v)->
        everyone.merge(k,v,(firstVal,secondVal)->firstVal+ "&" +secondVal));

중복된 키가 있을 때 merge를 통해서 map의 value를 변경한다.

 

개선된 ConcurrentHashMap

ConcurrentHashMap 클래스는 동시성 친화적이며 최신 기술을 반영한 HashMap이다. 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용한다. 따라서 동기화된 Hashtable 버전에 비해 읽기 쓰기 연산 성능이 월등하다

 

리듀스와 검색

세 가지 새로운 연산을 지원한다

  • forEach : 각 (키, 값) 쌍에 주어진 액션을 실행
  • reduce : 모든 (키, 값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
  • search : 널이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용

연산 형태

  • 키, 값으로 연산(forEach, reduce, search)
  • 키로 연산(forEachKey, reduceKeys, searchKeys)
  • 값으로 연산(forEachValue, reduceValues, searchValues)
  • Map, Entry 객체 연산(forEachEntry, reduceEntries, searchEntries)

이들 연산은 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행한다. 따라서 연산에 제공하는 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다.

 

이들 연산은 병렬성 기준값을 지정해야 한다. 맵의 크기가 주어진 기준값보다 작으면 순차적으로 연산을 지정한다. 

  • 기준값이 1일때 : 공통 스레드 풀을 이용해 병렬성 극대화
  • 기준값 Long,Max_VALUE : 한 개의 스레드로 연산을 실행

기존의 int를 반환하는 size 대신 long 을 반환하는 mappingCount 메서드 제공한다

 

ConcurrentHashMap 클래스는 ConcurrentHashMap을 집합 뷰로 반환하는 keySet이라는 새 메서드를 제공한다, 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다. newKeySet이라는 새 매서드를 이용해 ConcurrentHashMap으로 유지되는 집합을 만들 수 있다.