Kim-Baek 개발자 이야기

자바에서 코틀린 - 6장 자바에서 코틀린 컬렉션으로 본문

개발/java basic

자바에서 코틀린 - 6장 자바에서 코틀린 컬렉션으로

김백개발자 2023. 4. 17. 07:36

자바의 컬렉션은 가변이다. 이 때문에 발생하는 문제를 먼저 살펴본다

6.1

package travelator;

public class Suffering {

    public static int sufferScoreFor(List<Journey> route) {
        Location start = getDepartsFrom(route);
        List<Journey> longestJourneys = longestJourneysIn(route, 3);
        return sufferScore(longestJourneys, start);
    }

}
  • start 가 별게 없어서 인라인 한다. (6.2)

6.2

public static int sufferScoreFor(List<Journey> route) {
        List<Journey> longestJourneys = longestJourneysIn(route, 3);
        return sufferScore(longestJourneys, getDepartsFrom(route));
    }
  • 이렇게 하고 난 뒤 문제가 발생한다.

6.3

package travelator;

import java.util.List;

public class Routes {

    public static Location getDepartsFrom(List<Journey> route) {
        return route.get(0).getDepartsFrom();
    }

}
  • 위처럼 수정한 후, 출발 로케이션을 찾는 곳에서 버그 발생

6.4

public static List<Journey> longestJourneysIn(
        List<Journey> journeys,
        int limit
    ) {
        journeys.sort(comparing(Journey::getDuration).reversed()); // <1>
        var actualLimit = Math.min(journeys.size(), limit);
        return journeys.subList(0, actualLimit);
    }
  • longestJourneysIn 에서 Sort 가 순서를 바꿔버렸다.
  • Collections.unmodifiableList() 를 썼으면?
    • 실행 시점에 확인이 가능
      • UnsupportedOperationException 에러가 났을 것이다.
    • 래핑을 하는 것이기 때문에 원본리스트를 변경할 수 있다면 역시나 문제가 생길 수 있음.

공유된 컬렉션을 변경하지 말라

  • 다른 코드에서 공유되는 컬렉션이 있으면 불변 컬렉션으로 취급하라
  • 불변이 아닐지라도 이 원칙을 적용해라
  • 생성하되 변경하지 말라. ( 전략 )
  • 자바 컬렉션
    • 가변
  • 스칼라
    • 불변 컬렉션 ( 자바와 쓰기 위해서 컬렉션 복사 필요 )
  • 코틀린
    • 자바의 컬렉션 인터페이스에서 상태를 변경하는 메소드 제거
      • List<E> , Collection<E>
      • 상태 변경을 위해서, 위의 인터페이스를 확장한 MutableCollection<E>, MutableList<E>
  • 코틀린의 불변 List / MutableList 에 자바의 List 를 대입해 사용된다.
    • 문제는 불변 값인 List에 가변 리스트가 들어가 값이 변경될 수 있다는 것.
    val aMutableList = mutableListOf("0","1")
    val aList: List<String> = aMutableList //코틀린에서 List 는 불변
    
    aMutableList[0] = "123123"
    
    assertEquals("0", aList[0] )
    
    • 이걸 제대로 해결하려면 불변과 가변의 하위 타입 관계를 없애는 것이다.
      • 대신 StringBuilder / String 처럼 매번 복사를 해야한다.
    • 그런데 왜 코틀린은 이렇게 안했을까?
      • 공유된 컬렉션을 변경하지 말라” → 이걸 지킨다면 안전할 것이라 생각
      • 자바와의 상호 운영성을 가져가기 위해서.
    inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
      val result = ArrayList<R>()
      for (item in this)
        result.add(transform(item))
      return result
    }
    
    • 코틀린 표준 라이브러리도 그런 식으로 설계가 되어 있다.
    자바코드 고치기
    • 자바의 sort는 복사본을 정렬 후, 원본을 복사본에 일치시킨다. ( 가변 )
    • 이걸 바꾸자
    6.6
  • package travelator; public class Collections { @SuppressWarnings("unchecked") public static <E> List<E> sorted( Collection<E> collection, Comparator<? super E> by ) { var result = (E[]) collection.toArray(); // 새로운 어레이 리턴 Arrays.sort(result, by); return Arrays.asList(result); // Array. 를 리턴하는데 원소 추가삭제 불가 } }

•The returned array will be "safe" in that no references to it are maintained by this list. (In other words, this method must allocate a new array). The caller is thus free to modify the returned array.

6.7

static List<Journey> longestJourneysIn(
        List<Journey> journeys,
        int limit
    ) {
        var actualLimit = Math.min(journeys.size(), limit);
        return sorted(
            journeys,
            comparing(Journey::getDuration).reversed()
        ).subList(0, actualLimit);
    }
  • 우리가 새로 만든 sorted 사용
    • 불변!

6.9

public static int sufferScoreFor(List<Journey> route) {
        Location start = getDepartsFrom(route);
        List<Journey> longestJourneys = longestJourneysIn(route, 3);
        return sufferScore(longestJourneys, start);
    }

==========================================================

public static int sufferScoreFor(List<Journey> route) {
        return sufferScore(
            longestJourneysIn(route, 3),
            getDepartsFrom(route));
    }
  • 이제 값이 불변이니 다시, 인라이닝 한다.

6.10

public static List<List<Journey>> routesToShowFor(String itineraryId) {
        var routes = routesFor(itineraryId);
        removeUnbearableRoutes(routes);
        return routes;
    }

    private static void removeUnbearableRoutes(List<List<Journey>> routes) {
        routes.removeIf(route -> sufferScoreFor(route) > 10);
    }
  • removeUnbearableRoutes 는 void 를 리턴하면서, 내부에서 파라미터를 변경해버리는 구나… 를 알게 해준다.
private static List<List<Journey>> removeUnbearableRoutes
        (List<List<Journey>> routes
    ) {
        routes.removeIf(route -> sufferScoreFor(route) > 10);
        return routes;
    }

=============================================

private static List<List<Journey>> bearable
        (List<List<Journey>> routes
    ) {
        return routes.stream()
            .filter(route -> sufferScoreFor(route) <= 10)
            .collect(toUnmodifiableList());
    }
  • 기존 값 변경이 아닌, 복사본을 리턴하도록 수정

이제 코틀린으로 변환

package travelator

import travelator.Collections.sorted
import travelator.Other.SOME_COMPLICATED_RESULT
import java.util.Comparator.comparing
import java.util.stream.Collectors

object Suffering {
    @JvmStatic
    fun sufferScoreFor(route: List<Journey>): Int {
        return sufferScore(
            longestJourneysIn(route, 3),
            Routes.getDepartsFrom(route)
        )
    }

    @JvmStatic
    fun longestJourneysIn(
        journeys: List<Journey>,
        limit: Int
    ): List<Journey> {
        val actualLimit = Math.min(journeys.size, limit)
        return sorted(
            journeys,
            comparing { obj: Journey -> obj.duration }.reversed()
        ).subList(0, actualLimit)
    }

    fun routesToShowFor(itineraryId: String?): List<List<Journey>> {
        return bearable(Other.routesFor(itineraryId))
    }

    private fun bearable(routes: List<List<Journey>>): List<List<Journey>> {
        return routes.stream()
            .filter { route -> sufferScoreFor(route) <= 10 }
            .collect(Collectors.toUnmodifiableList())
    }

    private fun sufferScore(
        longestJourneys: List<Journey>,
        start: Location
    ): Int {
        return SOME_COMPLICATED_RESULT()
    } 
}
@JvmStatic
    fun longestJourneysIn(
        journeys: List<Journey>,
        limit: Int
    ): List<Journey> {
        val actualLimit = Math.min(journeys.size, limit)
        return sorted(
            journeys,
            comparing { obj: Journey -> obj.duration }.reversed()
        ).subList(0, actualLimit)
    }

======================================================

@JvmStatic
    fun longestJourneysIn(journeys: List<Journey>, limit: Int): List<Journey> =
        journeys.sortedByDescending { it.duration }.take(limit)

======================================================

// 확장함수
@JvmStatic
    fun List<Journey>.longestJourneys(limit: Int): List<Journey> =
        sortedByDescending { it.duration }.take(limit)
  • 확장함수는 클래스에 새로운 함수를 추가하는 것.
@JvmStatic
    fun sufferScoreFor(route: List<Journey>): Int {
        return sufferScore(
            route.longestJourneys(limit = 3), // 확장함수 호출
            Routes.getDepartsFrom(route)
        )
    }
  • 확장함수 호출하도록 수정한다.
private fun bearable(routes: List<List<Journey>>): List<List<Journey>> {
        return routes.stream()
            .filter { route -> sufferScoreFor(route) <= 10 }
            .collect(Collectors.toUnmodifiableList())
    }

======================================================

private fun bearable(routes: List<List<Journey>>): List<List<Journey>> =
        routes.filter { sufferScoreFor(it) <= 10 }
  • list 가 filter 를 확장함수로 제공
    • List 반환하기에 toUnmodifiableList 필요 없음
반응형
Comments