Skip to content

Java 및 Kotlin의 컬렉션

_컬렉션_은 해결하려는 문제에 중요하고 일반적으로 조작되는 가변 개수(0개일 수도 있음)의 항목 그룹입니다. 이 가이드에서는 Java 및 Kotlin의 컬렉션 개념과 연산을 설명하고 비교합니다. 이를 통해 Java에서 Kotlin으로 마이그레이션하고 코드를 진정한 Kotlin 방식으로 작성하는 데 도움이 될 것입니다.

이 가이드의 첫 번째 부분에는 Java 및 Kotlin에서 동일한 컬렉션에 대한 연산의 간략한 용어집이 포함되어 있습니다. Java 및 Kotlin에서 동일한 연산Java 표준 라이브러리에는 없는 연산으로 나뉩니다. 가이드의 두 번째 부분은 가변성부터 시작하여 특정 사례를 통해 몇 가지 차이점을 설명합니다.

컬렉션 소개는 컬렉션 개요를 참조하거나 Kotlin 개발자 옹호자인 Sebastian Aigner의 비디오를 시청하세요.

아래의 모든 예시는 Java 및 Kotlin 표준 라이브러리 API만 사용합니다.

Java 및 Kotlin에서 동일한 연산

Kotlin에는 Java의 해당 연산과 동일하게 보이는 컬렉션 연산이 많이 있습니다.

리스트, 세트, 큐, 덱 연산

설명공통 연산더 많은 Kotlin 대안
요소 또는 요소 추가add(), addAll()plusAssign(+=) 연산자를 사용합니다: collection += element, collection += anotherCollection.
컬렉션에 요소 또는 요소가 포함되어 있는지 확인contains(), containsAll()in 키워드를 사용하여 contains()를 연산자 형식으로 호출합니다: element in collection.
컬렉션이 비어 있는지 확인isEmpty()isNotEmpty()를 사용하여 컬렉션이 비어 있지 않은지 확인합니다.
특정 조건에서 제거removeIf()
선택된 요소만 남김retainAll()
컬렉션에서 모든 요소 제거clear()
컬렉션에서 스트림 가져오기stream()Kotlin에는 스트림을 처리하는 자체 방식이 있습니다: 시퀀스map()filter()와 같은 메서드.
컬렉션에서 이터레이터 가져오기iterator()

맵 연산

설명공통 연산더 많은 Kotlin 대안
요소 또는 요소 추가put(), putAll(), putIfAbsent()Kotlin에서 map[key] = value 할당은 put(key, value)와 동일하게 동작합니다. 또한 plusAssign(+=) 연산자를 사용할 수 있습니다: map += Pair(key, value) 또는 map += anotherMap.
요소 또는 요소 교체put(), replace(), replaceAll()put()replace() 대신 인덱싱 연산자 map[key] = value를 사용합니다.
요소 가져오기get()인덱싱 연산자를 사용하여 요소를 가져옵니다: map[index].
맵에 요소 또는 요소가 포함되어 있는지 확인containsKey(), containsValue()in 키워드를 사용하여 contains()를 연산자 형식으로 호출합니다: element in map.
맵이 비어 있는지 확인isEmpty()isNotEmpty()를 사용하여 맵이 비어 있지 않은지 확인합니다.
요소 제거remove(key), remove(key, value)minusAssign(-=) 연산자를 사용합니다: map -= key.
맵에서 모든 요소 제거clear()
맵에서 스트림 가져오기엔트리, 키 또는 값에 대한 stream()

리스트에만 있는 연산

설명공통 연산더 많은 Kotlin 대안
요소의 인덱스 가져오기indexOf()
요소의 마지막 인덱스 가져오기lastIndexOf()
요소 가져오기get()인덱싱 연산자를 사용하여 요소를 가져옵니다: list[index].
서브리스트 가져오기subList()
요소 또는 요소 교체set(), replaceAll()set() 대신 인덱싱 연산자를 사용합니다: list[index] = value.

약간 다른 연산

모든 컬렉션 타입의 연산

설명JavaKotlin
컬렉션 크기 가져오기size()count(), size
중첩 컬렉션 요소에 대한 평면 접근collectionOfCollections.forEach(flatCollection::addAll) or collectionOfCollections.stream().flatMap().collect()flatten() or flatMap()
주어진 함수를 모든 요소에 적용stream().map().collect()map()
제공된 연산을 컬렉션 요소에 순차적으로 적용하고 누적 결과 반환stream().reduce()reduce(), fold()
분류자를 기준으로 요소를 그룹화하고 개수 세기stream().collect(Collectors.groupingBy(classifier, counting()))eachCount()
조건으로 필터링stream().filter().collect()filter()
컬렉션 요소가 조건을 만족하는지 확인stream().noneMatch(), stream().anyMatch(), stream().allMatch()none(), any(), all()
요소 정렬stream().sorted().collect()sorted()
첫 N개 요소 가져오기stream().limit(N).collect()take(N)
프레디케이트로 요소 가져오기stream().takeWhile().collect()takeWhile()
첫 N개 요소 건너뛰기stream().skip(N).collect()drop(N)
프레디케이트로 요소 건너뛰기stream().dropWhile().collect()dropWhile()
컬렉션 요소 및 관련 값으로 맵 구축stream().collect(toMap(keyMapper, valueMapper))associate()

위에 나열된 모든 연산을 맵에서 수행하려면 먼저 맵의 entrySet을 가져와야 합니다.

리스트 연산

설명JavaKotlin
리스트를 자연 순서로 정렬sort(null)sort()
리스트를 내림차순으로 정렬sort(comparator)sortDescending()
리스트에서 요소 제거remove(index), remove(element)removeAt(index), remove(element) or collection -= element
리스트의 모든 요소를 특정 값으로 채우기Collections.fill()fill()
리스트에서 고유 요소 가져오기stream().distinct().toList()distinct()

Java 표준 라이브러리에는 없는 연산

zip(), chunked(), windowed() 및 기타 연산에 대해 자세히 알아보려면 Kotlin의 고급 컬렉션 연산에 대한 Sebastian Aigner의 비디오를 시청하세요:

가변성

Java에는 가변 컬렉션이 있습니다:

java
// Java
// This list is mutable!
public List<Customer> getCustomers() { ... }

부분적으로 가변적인 것:

java
// Java
List<String> numbers = Arrays.asList("one", "two", "three", "four");
numbers.add("five"); // Fails in runtime with `UnsupportedOperationException`

그리고 불변적인 것:

java
// Java
List<String> numbers = new LinkedList<>();
// This list is immutable!
List<String> immutableCollection = Collections.unmodifiableList(numbers);
immutableCollection.add("five"); // Fails in runtime with `UnsupportedOperationException`

IntelliJ IDEA에서 마지막 두 코드 조각을 작성하면, IDE는 불변 객체를 수정하려고 한다고 경고할 것입니다. 이 코드는 컴파일은 되지만 런타임에 UnsupportedOperationException과 함께 실패합니다. 컬렉션의 타입을 보아서는 가변적인지 알 수 없습니다.

Java와 달리 Kotlin에서는 필요에 따라 가변 또는 읽기 전용 컬렉션을 명시적으로 선언합니다. 읽기 전용 컬렉션을 수정하려고 하면 코드가 컴파일되지 않습니다:

kotlin
// Kotlin
val numbers = mutableListOf("one", "two", "three", "four")
numbers.add("five")            // This is OK
val immutableNumbers = listOf("one", "two")
//immutableNumbers.add("five") // Compilation error - Unresolved reference: add

불변성에 대한 자세한 내용은 Kotlin 코딩 규칙 페이지에서 읽어보세요.

공변성

Java에서는 후손 타입의 컬렉션을 선조 타입의 컬렉션을 받는 함수에 전달할 수 없습니다. 예를 들어, RectangleShape을 확장하는 경우, Rectangle 요소 컬렉션을 Shape 요소 컬렉션을 받는 함수에 전달할 수 없습니다. 코드를 컴파일 가능하게 하려면 함수가 Shape의 모든 상속자를 포함하는 컬렉션을 받을 수 있도록 ? extends Shape 타입을 사용해야 합니다:

java
// Java
class Shape {}

class Rectangle extends Shape {}

public void doSthWithShapes(List<? extends Shape> shapes) {
/* If using just List<Shape>, the code won't compile when calling
this function with the List<Rectangle> as the argument as below */
}

public void main() {
    var rectangles = List.of(new Rectangle(), new Rectangle());
    doSthWithShapes(rectangles);
}

Kotlin에서 읽기 전용 컬렉션 타입은 공변성을 가집니다. 이는 Rectangle 클래스가 Shape 클래스를 상속하는 경우, List<Shape> 타입이 필요한 모든 곳에 List<Rectangle> 타입을 사용할 수 있다는 의미입니다. 즉, 컬렉션 타입은 요소 타입과 동일한 서브타이핑 관계를 가집니다. 맵은 값 타입에 대해서는 공변적이지만 키 타입에 대해서는 그렇지 않습니다. 가변 컬렉션은 공변적이지 않습니다. 이는 런타임 오류로 이어질 수 있습니다.

kotlin
// Kotlin
open class Shape(val name: String)

class Rectangle(private val rectangleName: String) : Shape(rectangleName)

fun doSthWithShapes(shapes: List<Shape>) {
    println("The shapes are: ${shapes.joinToString { it.name }}")
}

fun main() {
    val rectangles = listOf(Rectangle("rhombus"), Rectangle("parallelepiped"))
    doSthWithShapes(rectangles)
}

컬렉션 타입에 대해 여기에서 더 읽어보세요.

범위 및 진행

Kotlin에서는 범위를 사용하여 간격을 만들 수 있습니다. 예를 들어, Version(1, 11)..Version(1, 30)1.11부터 1.30까지의 모든 버전을 포함합니다. in 연산자를 사용하여 버전이 범위 내에 있는지 확인할 수 있습니다: Version(0, 9) in versionRange.

Java에서는 Version이 두 경계에 모두 맞는지 수동으로 확인해야 합니다:

java
// Java
class Version implements Comparable<Version> {

    int major;
    int minor;

    Version(int major, int minor) {
        this.major = major;
        this.minor = minor;
    }

    @Override
    public int compareTo(Version o) {
        if (this.major != o.major) {
            return this.major - o.major;
        }
        return this.minor - o.minor;
    }
}

public void compareVersions() {
    var minVersion = new Version(1, 11);
    var maxVersion = new Version(1, 31);

   System.out.println(
           versionIsInRange(new Version(0, 9), minVersion, maxVersion));
   System.out.println(
           versionIsInRange(new Version(1, 20), minVersion, maxVersion));
}

public Boolean versionIsInRange(Version versionToCheck, Version minVersion, 
                                Version maxVersion) {
    return versionToCheck.compareTo(minVersion) >= 0 
            && versionToCheck.compareTo(maxVersion) <= 0;
}

Kotlin에서는 범위를 하나의 전체 객체로 다룹니다. 두 개의 변수를 만들고 Version과 비교할 필요가 없습니다:

kotlin
// Kotlin
class Version(val major: Int, val minor: Int): Comparable<Version> {
    override fun compareTo(other: Version): Int {
        if (this.major != other.major) {
            return this.major - other.major
        }
        return this.minor - other.minor
    }
}

fun main() {
    val versionRange = Version(1, 11)..Version(1, 30)

    println(Version(0, 9) in versionRange)
    println(Version(1, 20) in versionRange)
}

버전이 최소 버전보다 크거나 같고(>=) 최대 버전보다 작거나(<) 같은지 확인하는 것처럼 경계 중 하나를 제외해야 하는 경우, 이러한 포함 범위는 도움이 되지 않습니다.

여러 기준으로 비교

Java에서 여러 기준으로 객체를 비교하려면 Comparator 인터페이스의 comparing()thenComparingX() 함수를 사용할 수 있습니다. 예를 들어, 사람들을 이름과 나이로 비교하려면:

java
class Person implements Comparable<Person> {
    String name;
    int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return this.name + " " + age;
    }
}

public void comparePersons() {
    var persons = List.of(new Person("Jack", 35), new Person("David", 30), 
            new Person("Jack", 25));
    System.out.println(persons.stream().sorted(Comparator
            .comparing(Person::getName)
            .thenComparingInt(Person::getAge)).collect(toList()));
}

Kotlin에서는 비교하려는 필드를 열거하기만 하면 됩니다:

kotlin
data class Person(
    val name: String,
    val age: Int
)

fun main() {
    val persons = listOf(Person("Jack", 35), Person("David", 30), 
        Person("Jack", 25))
    println(persons.sortedWith(compareBy(Person::name, Person::age)))
}

시퀀스

Java에서는 다음과 같이 숫자 시퀀스를 생성할 수 있습니다:

java
// Java
int sum = IntStream.iterate(1, e -> e + 3)
    .limit(10).sum();
System.out.println(sum); // Prints 145

Kotlin에서는 _시퀀스_를 사용합니다. 시퀀스의 다단계 처리는 가능할 때 지연 실행됩니다. 실제 계산은 전체 처리 체인의 결과가 요청될 때만 발생합니다.

kotlin
fun main() {
    // Kotlin
    val sum = generateSequence(1) {
        it + 3
    }.take(10).sum()
    println(sum) // Prints 145
}

시퀀스는 일부 필터링 연산을 수행하는 데 필요한 단계 수를 줄일 수 있습니다. IterableSequence의 차이를 보여주는 시퀀스 처리 예시를 참조하세요.

리스트에서 요소 제거

Java에서 remove() 함수는 제거할 요소의 인덱스를 받습니다.

정수 요소를 제거할 때는 remove() 함수의 인수로 Integer.valueOf() 함수를 사용합니다:

java
// Java
public void remove() {
    var numbers = new ArrayList<>();
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
    numbers.add(1);
    numbers.remove(1); // This removes by index
    System.out.println(numbers); // [1, 3, 1]
    numbers.remove(Integer.valueOf(1));
    System.out.println(numbers); // [3, 1]
}

Kotlin에는 removeAt()를 사용한 인덱스 기반 요소 제거와 remove()를 사용한 값 기반 요소 제거의 두 가지 유형이 있습니다.

kotlin
fun main() {
    // Kotlin
    val numbers = mutableListOf(1, 2, 3, 1)
    numbers.removeAt(0)
    println(numbers) // [2, 3, 1]
    numbers.remove(1)
    println(numbers) // [2, 3]
}

맵 순회

Java에서는 forEach를 통해 맵을 순회할 수 있습니다:

java
// Java
numbers.forEach((k,v) -> System.out.println("Key = " + k + ", Value = " + v));

Kotlin에서는 Java의 forEach와 유사하게 for 루프 또는 forEach를 사용하여 맵을 순회합니다:

kotlin
// Kotlin
for ((k, v) in numbers) {
    println("Key = $k, Value = $v")
}
// Or
numbers.forEach { (k, v) -> println("Key = $k, Value = $v") }

비어 있을 수 있는 컬렉션의 첫 번째 및 마지막 항목 가져오기

Java에서는 컬렉션의 크기를 확인하고 인덱스를 사용하여 첫 번째 및 마지막 항목을 안전하게 가져올 수 있습니다:

java
// Java
var list = new ArrayList<>();
//...
if (list.size() > 0) {
    System.out.println(list.get(0));
    System.out.println(list.get(list.size() - 1));
}

또한 Deque 및 그 상속자를 위해 getFirst()getLast() 함수를 사용할 수 있습니다:

java
// Java
var deque = new ArrayDeque<>();
//...
if (deque.size() > 0) {
    System.out.println(deque.getFirst());
    System.out.println(deque.getLast());
}

Kotlin에는 특별한 함수인 firstOrNull()lastOrNull()가 있습니다. 엘비스 연산자를 사용하여 함수의 결과에 따라 즉시 추가 작업을 수행할 수 있습니다. 예를 들어, firstOrNull():

kotlin
// Kotlin
val emails = listOf<String>() // Might be empty
val theOldestEmail = emails.firstOrNull() ?: ""
val theFreshestEmail = emails.lastOrNull() ?: ""

리스트에서 세트 생성

Java에서 List로부터 Set을 생성하려면 Set.copyOf 함수를 사용할 수 있습니다:

java
// Java
public void listToSet() {
    var sourceList = List.of(1, 2, 3, 1);
    var copySet = Set.copyOf(sourceList);
    System.out.println(copySet);
}

Kotlin에서는 toSet() 함수를 사용합니다:

kotlin
fun main() {
    // Kotlin
    val sourceList = listOf(1, 2, 3, 1)
    val copySet = sourceList.toSet()
    println(copySet)
}

요소 그룹화

Java에서는 Collectors 함수의 groupingBy()를 사용하여 요소를 그룹화할 수 있습니다:

java
// Java
public void analyzeLogs() {
    var requests = List.of(
        new Request("https://kotlinlang.org/docs/home.html", 200),
        new Request("https://kotlinlang.org/docs/home.html", 400),
        new Request("https://kotlinlang.org/docs/comparison-to-java.html", 200)
    );
    var urlsAndRequests = requests.stream().collect(
            Collectors.groupingBy(Request::getUrl));
    System.out.println(urlsAndRequests);
}

Kotlin에서는 groupBy() 함수를 사용합니다:

kotlin
data class Request(
    val url: String,
    val responseCode: Int
)

fun main() {
    // Kotlin
    val requests = listOf(
        Request("https://kotlinlang.org/docs/home.html", 200),
        Request("https://kotlinlang.org/docs/home.html", 400),
        Request("https://kotlinlang.org/docs/comparison-to-java.html", 200)
    )
    println(requests.groupBy(Request::url))
}

요소 필터링

Java에서 컬렉션에서 요소를 필터링하려면 Stream API를 사용해야 합니다. Stream API에는 intermediateterminal 연산이 있습니다. filter()는 스트림을 반환하는 중간 연산입니다. 출력으로 컬렉션을 받으려면 collect()와 같은 종단 연산을 사용해야 합니다. 예를 들어, 키가 1로 끝나고 값이 10보다 큰 쌍만 남기려면:

java
// Java
public void filterEndsWith() {
    var numbers = Map.of("key1", 1, "key2", 2, "key3", 3, "key11", 11);
    var filteredNumbers = numbers.entrySet().stream()
        .filter(entry -> entry.getKey().endsWith("1") && entry.getValue() > 10)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    System.out.println(filteredNumbers);
}

Kotlin에서는 필터링이 컬렉션에 내장되어 있으며, filter()는 필터링된 것과 동일한 컬렉션 타입을 반환합니다. 따라서 filter()와 해당 프레디케이트만 작성하면 됩니다:

kotlin
fun main() {
    // Kotlin
    val numbers = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
    val filteredNumbers = numbers.filter { (key, value) -> key.endsWith("1") && value > 10 }
    println(filteredNumbers)
}

맵 필터링에 대해 여기에서 더 알아보세요.

타입별 요소 필터링

Java에서 타입별로 요소를 필터링하고 작업을 수행하려면 instanceof 연산자로 타입을 확인한 다음 타입 캐스트를 수행해야 합니다:

java
// Java
public void objectIsInstance() {
    var numbers = new ArrayList<>();
    numbers.add(null);
    numbers.add(1);
    numbers.add("two");
    numbers.add(3.0);
    numbers.add("four");
    System.out.println("All String elements in upper case:");
    numbers.stream().filter(it -> it instanceof String)
        .forEach( it -> System.out.println(((String) it).toUpperCase()));
}

Kotlin에서는 컬렉션에 filterIsInstance<NEEDED_TYPE>()를 호출하기만 하면, 스마트 캐스트에 의해 타입 캐스트가 수행됩니다:

kotlin
// Kotlin
fun main() {
    // Kotlin
    val numbers = listOf(null, 1, "two", 3.0, "four")
    println("All String elements in upper case:")
    numbers.filterIsInstance<String>().forEach {
        println(it.uppercase())
    }
}

프레디케이트 테스트

일부 작업에서는 모든, 없는, 또는 일부 요소가 조건을 만족하는지 확인해야 합니다. Java에서는 Stream API 함수인 allMatch(), noneMatch()anyMatch()를 통해 이 모든 검사를 수행할 수 있습니다:

java
// Java
public void testPredicates() {
    var numbers = List.of("one", "two", "three", "four");
    System.out.println(numbers.stream().noneMatch(it -> it.endsWith("e"))); // false
    System.out.println(numbers.stream().anyMatch(it -> it.endsWith("e"))); // true
    System.out.println(numbers.stream().allMatch(it -> it.endsWith("e"))); // false
}

Kotlin에서는 모든 Iterable 객체에 확장 함수none(), any(), all()이 제공됩니다:

kotlin
fun main() {
// Kotlin
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.none { it.endsWith("e") })
    println(numbers.any { it.endsWith("e") })
    println(numbers.all { it.endsWith("e") })
}

프레디케이트 테스트에 대해 더 알아보세요.

컬렉션 변환 연산

요소 결합

Java에서는 두 컬렉션의 동일한 위치에 있는 요소를 동시에 반복하여 쌍을 만들 수 있습니다:

java
// Java
public void zip() {
    var colors = List.of("red", "brown");
    var animals = List.of("fox", "bear", "wolf");

    for (int i = 0; i < Math.min(colors.size(), animals.size()); i++) {
        String animal = animals.get(i);
        System.out.println("The " + animal.substring(0, 1).toUpperCase()
               + animal.substring(1) + " is " + colors.get(i));
   }
}

출력에 요소 쌍을 단순히 출력하는 것보다 더 복잡한 작업을 수행하려면 레코드를 사용할 수 있습니다. 위 예시에서 레코드는 record AnimalDescription(String animal, String color) {}가 됩니다.

Kotlin에서는 동일한 작업을 수행하기 위해 zip() 함수를 사용합니다:

kotlin
fun main() {
    // Kotlin
    val colors = listOf("red", "brown")
    val animals = listOf("fox", "bear", "wolf")

    println(colors.zip(animals) { color, animal -> 
        "The ${animal.replaceFirstChar { it.uppercase() }} is $color" })
}

zip()Pair 객체의 List를 반환합니다.

컬렉션의 크기가 다른 경우, zip()의 결과는 더 작은 크기가 됩니다. 더 큰 컬렉션의 마지막 요소는 결과에 포함되지 않습니다.

요소 연결

Java에서는 Stream API를 사용하여 요소와 특성을 연결할 수 있습니다:

java
// Java
public void associate() {
    var numbers = List.of("one", "two", "three", "four");
    var wordAndLength = numbers.stream()
        .collect(toMap(number -> number, String::length));
    System.out.println(wordAndLength);
}

Kotlin에서는 associate() 함수를 사용합니다:

kotlin
fun main() {
    // Kotlin
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.associateWith { it.length })
}

다음 단계는 무엇인가요?

  • Kotlin Koans를 방문하세요 – Kotlin 구문을 배우기 위한 연습을 완료하세요. 각 연습은 실패하는 단위 테스트로 생성되며, 이를 통과시키는 것이 여러분의 임무입니다.
  • 다른 Kotlin 관용구를 살펴보세요.
  • Java-Kotlin 변환기를 사용하여 기존 Java 코드를 Kotlin으로 변환하는 방법을 알아보세요.
  • Kotlin의 컬렉션을 알아보세요.

마음에 드는 관용구가 있다면, 풀 리퀘스트를 보내 공유해 주세요.