ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바의 컬렉션 - 모던 자바 인 액션
    BackEnd/자바 2022. 3. 21. 23:32

    Java에서는 리스트, 집합(Set), 맵(Map)을 초기화하는 다양한 방법이 있습니다.

    특히 Java 9부터 도입된 컬렉션 팩토리 메서드는 코드의 간결성과 성능 면에서 유리합니다.

     

    기존 Arrays.asList()의 특징과 Java 9 이후의 List.of 의 차이

    Arrays.asList() – 고정 크기 리스트

    • 내부 구현: Arrays 클래스 내부의 정적 클래스 ArrayList (java.util과 다름)
    • 요소 수정 가능 (list.set(0, "z"))
    • 요소 추가/삭제 불가능  - 고정 크기 리스트
      • UnsupportedOperationException 발생
    • 배열 기반 리스트이기 때문에 크기 변경이 불가능합니다. 
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
    

    Arrays 클래스 내부에 선언된 ArrayList. 

    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;
    
        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

    요소에 추가, 삭제를 가능하게 하려면 Set으로 감싸줘야합니다.

    HashSet<String> strings1 = new HashSet<>(Arrays.asList("a", "v", "c"));

    자바 9부터는 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 팩토리 메서드를 제공한다.

    자바 9의 새로운 컬렉션 팩토리 메서드

    1. List.of 

     

    • 완전 불변 리스트
      • 요소 추가, 삭제, 변경 모두 불가능
    • 내부 클래스는 ImmutableCollections.ListN, List12 등 최적화된 불변 구현체
    • 성능을 고려해 0~10개까지는 오버로드 메서드, 11개 이상은 varargs 방식으로 처리

     

    List<String> list = List.of("a", "b", "c");

    List of 는 인자를 1~10개까지 받는 다양한 오버로드 버전이 있다.

    List of(E..elements) 이런식으로 다중 요소를 받을 수 있도록 되어있지 않은 이유는 

    내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싸는데, 이 때 배열을 할당,초기화, 가비지 컬렉션 비용이 발생한다. 그래서 이런 비용을 피하기 위해 고정된 숫자요소를 API 로 정의하고 10개 이상의 요소를 가진 리스트를 만들 때는 가변 인수를 이용하는 메서드를 사용한다.

     

    이 아이는 불변 리스트로 값의 추가, 삭제뿐만 아니라 요소 변경도 불가하다.

    static <E> List<E> of() {
        return ImmutableCollections.emptyList();
    }
    
     static <E> List<E> of(E e1) {
            return new ImmutableCollections.List12<>(e1);
    }
    
    static <E> List<E> of(E e1, E e2) {
            return new ImmutableCollections.List12<>(e1, e2);
    }
    ..
     static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) {
            return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5,
                                                    e6, e7, e8, e9, e10);
    }
        
    @SafeVarargs
        @SuppressWarnings("varargs")
        static <E> List<E> of(E... elements) {
            switch (elements.length) { // implicit null check of elements
                case 0:
                    return ImmutableCollections.emptyList();
                case 1:
                    return new ImmutableCollections.List12<>(elements[0]);
                case 2:
                    return new ImmutableCollections.List12<>(elements[0], elements[1]);
                default:
                    return new ImmutableCollections.ListN<>(elements);
            }
        }

     

    2. Set.of 

     

    • List.of()와 동일하게 불변 Set을 생성
    • 중복 요소를 허용하지 않으며, 중복 시 IllegalArgumentException 발생

     

    Set<String> set = Set.of("a", "b", "c");

     

    3. Map 팩토리

     

    • Map.of() → 최대 10쌍의 키-값 쌍
    • Map.ofEntries() → 10쌍 이상 가능
    • 불변 Map이며, 키 중복 시 IllegalArgumentException 발생

     

    맵은 두가지 방법으로 초기화 할 수 있다.

    열 개 이하의 키와 값 쌍을 가진 작은 맵을 만들 때는 Map.of 를,

    이상의 큰 맵에는 가변인수로 구현된 Map.ofEntries 를 이용하는 것이 좋다. 키와 값을 감쌀 추가 객체 할당을 필요로 한다.

    Map<String, Integer> rr = Map.of("R", 30, "A", 20);
    
    
    Map<String, Integer> map = Map.ofEntries(Map.entry("A", 30),
                                             Map.entry("B", 10));

    자바 8에서 추가된 컬렉션(List, Set, Map) 처리 기능 정리

    자바 8에서는 List, Set, Map 컬렉션에 유용한 디폴트 메서드들이 추가되었습니다. 이를 통해 컬렉션 요소를 더 안전하고 간결하게 다룰 수 있게 되었으며, 특히 람다와 함께 사용하면 코드 가독성과 유지보수성이 향상됩니다.

     

    List, Set 인터페이스의 추가 메서드

    List,Set 인터페이스에 아래와 같은 메서드가 추가되었다.

    1. removeIf

    • 조건(predicate)을 만족하는 요소를 제거합니다.
    • List, Set을 구현한 모든 컬렉션에서 사용할 수 있습니다.
    transactions.removeIf(transaction -> "2000".equals(transaction.getYear()));

    2. replaceAll

    • 리스트의 모든 요소를 주어진 함수(UnaryOperator)로 교체합니다.
    • List에서만 사용 가능합니다.
    transactions.replaceAll(transaction -> {
        transaction.setValue(2000);
        return transaction;
    });

    3. sort(Comparator)

    • 리스트를 주어진 정렬 기준에 따라 정렬합니다.
    transactions.sort(Comparator.comparing(Transaction::getYear));

     

    주의: 반복 중 컬렉션 수정 시 예외 발생

    이들 메서드는 호출한 컬렉션 자체를 바꾼다. 컬렉션을 바꾸는 동작은 에러 유발과 복잡함을 더하는데 왜 추가되었을까?

     

    아래와 같은 코드를 짜면 ConcurrentModificationException 을 일으키다.

     

    • for-each 문은 내부적으로 Iterator를 사용하지만,
    • 위 코드에서는 컬렉션 객체의 remove()를 직접 호출하므로,
    • Iterator와 컬렉션 상태가 불일치하게 되어 예외가 발생합니다.

     

    List<Transaction> transactions = new ArrayList<>();
    for (Transaction transaction : transactions) {
        if(Character.isDigit(transaction.getReferenceCode().charAt(0))){
            transactions.remove(transaction);
        }
    }

     

    해결 방법: 명시적으로 Iterator 사용

    반복자의 상태는 컬렉션의 상태와 서로 동기화되지 않는다. Iterator 객체를 명시적으로 사용하고,

    그 객체의 remove 메서드를 호출함으로 이 문제를 해결할 수 있다.

    for (Iterator<Transaction> iterator = transactions.iterator();
        iterator.hasNext();
         ) {
        Transaction transaction = iterator.next();
        System.out.println("getval = "+transaction.getValue());
        if("2000".equals(transaction.getYear())){
           iterator.remove();
        }
    }

     

    ListIterator를 사용하여 요소를 변경할 수도 있지만, 자바 8에서는 replaceAll로 훨씬 간단하게 처리할 수 있습니다.

    transactions.removeIf(transaction -> "2000".equals(transaction.getYear()));

    replaceAll 은 삭제가 아닌 요소 변경을 하고자 할 때 사용할 수 있다.

    //기존
    for (ListIterator<Transaction> iterator = transactions.listIterator();
         iterator.hasNext();
    ) {
        Transaction transaction = iterator.next();
        System.out.println("getval = "+transaction.getValue());
        if("2000".equals(transaction.getYear())){
            transaction.setValue(2000);
            iterator.set(transaction);
        }
    }

     

    //자바8 이후
    transactions.replaceAll(transaction-> {
        transaction.setValue(2000);
        return transaction;
    });

     

    Map 인터페이스의 기능 강화

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

    Map.Entry<K,V> 의 반복자를 이용해 맵의 항목 집합을 반복할 수 있다.

    for (Map.Entry<String, Integer> entry : ageOfFriends.entrySet()) {
        String friend = entry.getKey();
        Integer age = entry.getValue();
        System.out.println(friend + "is " + age + " years old");
    }
    ageOfFriends.forEach((friend,age)->{
        System.out.println(friend + "is " + age + " years old");
    });

     

    정렬은 반복과 관련된 오래된 고민거리다. 자바 8에서는 맵의 항목을 쉽게 비교할 수 있는 몇 가지 방법을 제공한다.

    Entry.comparingByValue

    Entry.comparingByKey

    Map<String, String> map = Map.ofEntries(Map.entry("c", "3"),
            Map.entry("a", "1"),
            Map.entry("b", "2"));
    
    map.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .forEach(System.out::println);
    
    

     

    HashMap의 성능 개선

    기존에는 HashMap의 버킷(bucket)에 LinkedList를 사용하여 키 충돌 시 O(n) 시간 복잡도가 발생했습니다.
    자바 8부터는 충돌이 많은 경우 TreeNode 구조로 전환하여 O(log n)으로 충돌이 일어나는 요소 반환 성능을 개선했습니다. 

    • 단, 키가 Comparable을 구현한 타입이어야 트리로 전환됩니다.
      • 예: String, Integer 등

     키가 존재하지 않을 때: getOrDefault

    기존에는 없는 키를 조회하면 null이 반환되어 NPE 위험이 있었습니다.
    getOrDefault는 기본값을 지정할 수 있어 이를 방지할 수 있습니다.

    String value = map.getOrDefault("key", "defaultValue");

    단, 키는 존재하지만 값이 null이면 null이 반환됩니다.

     


    자바 8 Map 계산 및 병합 메서드 정리 (compute, merge 등)

    컬렉션을 다루다 보면 맵(Map)의 키 존재 여부에 따라 특정 로직을 수행하거나, 값을 병합하거나, 조건에 따라 값을 수정하는 일이 자주 발생합니다.
    자바 8에서는 이러한 계산 및 병합 패턴을 간결하게 처리할 수 있도록 compute, computeIfAbsent, merge 등의 메서드를 추가했습니다.
    이 글에서는 이들 메서드의 사용법과 주의사항을 예제와 함께 정리합니다.

    computeIfAbsent – 값이 없을 때 계산해서 추가

    computeIfAbsent는 지정된 키가 맵에 없거나 null로 연결되어 있을 때, 새로운 값을 계산하여 맵에 추가합니다.
    캐시(Cache) 패턴에서 매우 자주 사용됩니다.

    dataToHash.computeIfAbsent(line, this::calculateDigest);

     

    파일 집합의 각 행을 파싱해 SHA-256을 계산한다고 가정하다. 기존에 이미 데이터를 처리했다면 이 값을 다시 계산할 필요가 없다.

    public byte[] calculateDigest(String key) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            return messageDigest.digest(key.getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    주의할 점

    • 계산 함수가 null을 반환하면 해당 키는 맵에서 제거됩니다.
    • null 처리는 명확하게 하시는 것이 좋고, 제거 목적이라면 remove()를 명시적으로 사용하는 것이 더 적절합니다.
    String key="Raphael";
    String value="Jack Reacher 2";
    HashMap<String, List<String>> favoriteMovies = new HashMap<>();
    if(favoriteMovies.containsKey(key) &&
            Objects.equals(favoriteMovies.get(key),value)){
        favoriteMovies.remove(key);
    }
    favoriteMovies.remove(key,value);

     

    기존 방식 vs computeIfAbsent

    //기존 방식
    String friend = "Rachel";
    HashMap<String, List<String>> friendsToMovies = new HashMap<>();
    List<String> movies = friendsToMovies.get(friend);
    if(movies==null){
        movies = new ArrayList<>();
        friendsToMovies.put(friend,movies);
    }
    movies.add("Stars");
    System.out.println(movies);
    
    //자바8방식
    friendsToMovies.computeIfAbsent(friend,name->new ArrayList<>()).add("Stars");

     

    computeIfPresent, compute

    • computeIfPresent: 키가 존재할 경우에만 새 값을 계산합니다.
    • compute: 존재 여부에 관계없이 항상 새 값을 계산하고 대체합니다.
    map.computeIfPresent(key, (k, v) -> v + 1);
    map.compute(key, (k, v) -> v == null ? 1 : v + 1);

     

    replace, replaceAll – 값 교체

    replace(key, value), replace(key, oldValue, newValue)

    • 키가 존재할 때만 값을 바꿉니다.
    • 조건부로 특정 값일 때만 교체도 가능합니다.

    replaceAll : BiFunction 을 적용한 결과로 각 항목의 값을 교체한다. 이 메서드는 이전에 List 의 replaceAll 과 비슷한 동작을 수행한다.

    replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전도 있다.

    HashMap<String, String> favoriteMovies2 = new HashMap<>();
    favoriteMovies2.replaceAll((k,v)->v.toUpperCase());

     

    merge – 키가 중복될 때 값 병합 처리

    맵을 병합할 때 putAll을 사용하면 덮어쓰기만 됩니다.
    반면 merge는 중복 키에 대해 어떻게 처리할지 지정할 수 있는 BiFunction을 제공합니다.

      Map<String, String> family = Map.ofEntries(
              Map.entry("teo", "star wars"),
              Map.entry("christina", "james bond")
              //Map.entry("c", "")
              );
    
      Map<String, String> friends = Map.ofEntries(
              Map.entry("rachel", "star wars"),
              Map.entry("christina", "matrix")
              );
      HashMap<String, String> everyone = new HashMap<>(family);
    
      friends.forEach((k,v)->everyone.merge(k,v,(m1,m2)->{
          System.out.println(m1+"&" + m2);
          return m1+ "&"  + m2;
      }));

    merge 동작 방식

    • 키가 없으면 → 새 값을 그대로 추가
    • 키가 있고 기존 값이 있으면 → BiFunction을 적용
    • 함수가 null을 반환하면 해당 항목은 제거됨

    카운팅 로직에 merge 활용할 수도 있다. 

    Map<String,Long> moviesToCount = new HashMap<>();
    String movieName = "JamesBond";
    Long count = moviesToCount.get(movieName);
    if(count==null){
        moviesToCount.put(movieName,1L);
    } else {
        moviesToCount.put(movieName,count+1L);
    }
    moviesToCount.merge(movieName,1L,(key,c) -> c+1L);

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    반응형
Designed by Tistory.