모던자바인액션 - 8

이건희·2023년 7월 25일
0

모던자바인액션

목록 보기
8/9

이번 챕터는 자바 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()이라는 메서드는 없으므로 다음 방법을 사용할 수 있다.

  1. 리스트를 인수로 받는 HashSet 생성자 사용
    Set<String> friends = new HashSet<>(Arrays.asList("Raphael", "Olivia", "Thibaut"));
    //리스트를 인수로 받는 HashSet 생성자
  2. 스트림 API 사용
    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
  • 요소를 추가하려하면 Exception이 발생한다.
  • 왜냐하면 변경할 수 없는 리스트가 만들어졌기 때문이다.
  • set()으로 요소를 바꾸려해도 Exception이 발생한다.
  • 불변성이 있어 컬렉션이 의도치 않게 변하는 것을 막을 수 있다.

📌추가 - 오버로딩 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에서는 두 가지 방법으로 바꿀 수 없는 맵을 초기화할 수 있다.

  1. Map.of 팩토리 메서드에 키와 값을 번갈아 제공하는 방법
    Map<String, Integer> ageOfFriends = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
    //key, value를 번갈아 제공하며 맵 만들기
    System.out.println(ageOfFriends); //{Olivia=25, Raphael=30, Thibaut=26}
  • 10개 이하의 키와 값 쌍을 가진 맵을 만들때 유용
  1. 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}
  • 10개 이상의 키 값 쌍이 있을때 유용
  • Map.ofEntries 메서드는 키와 값을 감쌀 추가 객체 할당을 필요로 함
  • Map.entry는 Map.Entry 객체를 만드는 새로운 팩토리 메서드

리스트와 집합 처리

자바 8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.

  • removeIf
    • Predicate를 만족하는 요소 제거
    • List나 Set을 구현하거나 구현을 상속 받은 모든 클래스에서 이용 가능
  • replaceAll
    • 리스트에서 이용 가능
    • UnaryOperator 함수를 이용해 요소를 바꿈
  • sort
    • List 인터페이스에서 제공하는 기능으로 리스트 정렬

새로운 결과를 만드는 스트림과 달리 이들 메서드는 호출한 컬렉션 자체를 바꾼다.

예시

//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

    • 키에 해당하는 맵을 제거하는 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}
profile
백엔드 개발자가 되겠어요

0개의 댓글