본문 바로가기

프로그래밍 언어 기초/KOTLIN

[Kotlin] 연산자(Operator)와 컬렉션(Collection)

개요

오늘은 코틀린의 연산자(Operator)와 컬렉션(Collection)에 대해서 알아보도록 하겠습니다.

 


1. 코틀린 연산자(Kotlin Operator)

수학에서 수를 더하거나 빼거나 곱하거나 나누는 계산을 '사칙 연산'이라고 부릅니다.

코틀린에서도 이러한 사칙 연산을 아래와 같이 할 수 있습니다.

fun main(args: Array<String>) {

    // 코틀린의 사칙연산 + 나머지(모듈러) 연산
    var number = 10;
    println(number + 1); // 더하기, 11
    println(number - 2); // 빼기, 8
    println(number * 3); // 곱하기, 30
    println(number / 4); // 나누기(몫 연산) , 2
    println(number % 2); // 모듈러(나머지 연산), 0

}


 

사칙 연산 이외에도 변수에 담긴 데이터를 계산을 하기 위해서
여러 가지 연산자를 사용할 수 있는데요.

변수에 담긴 숫자 데이터의 부호를 나타낼 때 쓰이는 단항 연산자,

참, 거짓을 다루는 불리언 값에 대한 부정(=NOT 연산)연산자,

한 단계씩 수를 증가시키거나 감소시키는 증감 연산자,

변수에 담긴 값에 숫자에 사칙연산을 한 결과를 바로 적용시키는 대입 연산자,

두 변수에 담긴 값의 대소를 비교하거나, 일치 여부를 판단하는 비교 연산자 등이 있습니다.

fun main(args: Array<String>) {

    // 단항 연산자
    // var number = 10;
    var positiveNum = +10;
    var negativeNum = -10;
    println(positiveNum); // 10
    println(negativeNum); // -10


    // NOT 연산자, 부정 연산자
    val trueflag = true;
    val falseflag = false;
    println(trueflag); // true
    println(falseflag); // false
    println(!falseflag); // true

    // 증감 연산자
    // var number = 10;
    number++;
    println(number); // 11
    ++number;
    println(number); // 12
    number--;
    println(number); // 11
    --number;
    println(number); // 10
    println(number++); // 10
    println(number); // 11
    println(++number); // 12

    // 대입 연산자
    var myNumber = 10;
    myNumber += 1; // -> myNumber = myNumber + 1;
    myNumber -= 1; // -> myNumber = myNumber - 1;
    myNumber *= 2; // -> myNumber = myNumber * 2;
    myNumber /= 2; // -> myNumber = myNumber / 2;
    myNumber %= 3; // -> myNumber = myNumber % 3;
    println(myNumber); // 1

    // 비교연산자
    val number1 = 10;
    val number2 = 12;

    val result1:Boolean = number1 > number2;
    val result2:Boolean = number1 < number2;
    val result3:Boolean = number1 >= number2;
    val result4:Boolean = number1 <= number2;
    val result5:Boolean = number1 == number2;
    val result6:Boolean = number1 != number2;

    println(result1); // false
    println(result2); // true
    println(result3); // false
    println(result4); // true
    println(result5); // false
    println(result6); // true
    
}

 


2. 코틀린 컬렉션(Kotlin Collection)

자바와 코틀린의 Array는 모두 연속된 값을 담을 수 있는 자료형이지만,  

내부적으로 toString() 메서드가 구현되지 않았기 때문에,

 

코틀린에서 제공하는 contentToString() 메서드 등을 사용하지 않으면 주소값이 출력됩니다.

하지만, 코틀린의 List는 내부적으로 변수에 담긴 '요소'들을 출력할 수 있는 toString()이 구현되어 있기 때에 

contentToString() 메서드 등을 사용하지 않아도 내부 구성요소를 확인할 수 있습니다. 

 

본격적으로 코틀린의 컬렉션을 살펴보기 전,

코틀린 Collection의 구조에 대해서 알아보도록 하겠습니다.

 

https://kotlinlang.org/docs/collections-overview.html

 

Collections overview | Kotlin

 

kotlinlang.org

 

위의 공식 사이트 문서를 살펴보면, 코틀린 컬렉션의 구조는 아래와 같이 설계되어 있습니다.

 

fig 1.0. 코틀린 컬렉션의 구조

 

따라서, 변경이 가능한(Mutable)과, 변경이 불가능한(Immutable) 컬렉션으로 구분할 수 있는데,
Java에서는 이와 같은 기준으로 컬렉션이 구분되진 않습니다.

 

이 점에서 코틀린의 컬렉션의 구조(Architecture)는 코틀린만의 정체성을 잘 보여주는 사례라고 할 수 있습니다.

 

코틀린은 변수를 var과 val를 통해 변수에 대한 재할당 가부를 결정하는 것과 더불어

컬렉션에서는 Mutable과 Mutable이 아닌것 즉, Immutable인 구조로 나뉩니다.

 

이러한 구분은 위의 그림과 같이 컬렉션을 설계했던 덕분이며,

이 때문에 개발자는 동적으로 컬렉션을 변경할 지 아니면, 첫 선언 이후 불변함을 보장할 지에 따라 선택할 수 있습니다.

 


2.1. 리스트(List)

코틀린에서 리스트는 선언할 때, 그 안에 담긴 요소를 바탕으로 자료형을 추론할 수 있습니다.

이를 타입 추론(Type Inference)이라고 부르며, 코틀린에서 리스트는 아래와 같이 선언할 수 있습니다.

 

// 컬렉션
// Array to List
val numberArr1 = arrayOf(1,2,3);
val numberArr2 = intArrayOf(1,2,3);
println(numberArr1); // 주소 값, [Ljava.lang.Integer;@f6f4d33
println(numberArr1.contentToString()); // 1,2,3
println(numberArr2.contentToString()); // 1,2,3

val numberList1 = numberArr1.toList(); // Array -> List
val numberList2 = numberArr1.toList(); // Array -> List
println(numberList1);
println(numberList2);

// 리스트 List
// Mutable vs Immutable
// 리스트에 담긴 요소를 바탕으로 데이터의 자료형을 추론할 수 있기에
// 아래와 같이 리스트를 선언할 수 있다.
val defaultList = listOf(1,2,3);

// defaultList.add(4); //  error: unresolved reference: add

val mutableList = mutableListOf(1,2,3);

mutableList.add(4); // error: unresolved reference: add

println(defaultList); // [1, 2, 3]
println(mutableList); // [1, 2, 3, 4]

 


위와 같이 코틀린에서 리스트는 변경 가능한(Mutable) 리스트와 그렇지 않은 리스트(Immutable)로 나뉩니다.

따라서, 리스트를 생성하고 나중에 그 값을 추가할 때에는 Mutable List를 사용해야 하며,

일반 List는 변수를 선언한 이후에는 수정할 수 없습니다.

 

그러나, 코틀린에서는 Mutable과 Immutable 간에 변환을 쉽게 할 수 있습니다.
만약, 이미 선언된 리스트를 다른 자료형으로 변환할 때에는 아래와 같이 사용할 수 있습니다.

 

 

val parsedDefaultList = mutableList.toList();
val parsedMutableList = defaultList.toMutableList();

parsedMutableList.add(4);

println(parsedDefaultList); // [1, 2, 3, 4]
println(parsedMutableList); // [1, 2, 3, 4]

 

 

리스트 요소의 비교

코틀린의 리스트에는 리스트를 구성하는 여러 개의 요소가 있습니다.

그리고 이 요소들은 null 값을 포함하여 같은 값을 가지는 요소가 1개 이상 존재할 수 있습니다.

코틀린에서 두 리스트의 크기가 서로 동일하고,구조적으로 동일한 위치에 동일한 요소가 있다면

비교 연산 '=='로 비교할 때, 두 목록을 동일한 것으로 간주합니다. 즉 true를 반환합니다.

val bob = Person("Bob", 31)
val people = listOf(Person("Adam", 20), bob, bob)
val people2 = listOf(Person("Adam", 20), Person("Bob", 31), bob)
println(people == people2) // true
bob.age = 32
println(people == people2) // false

val myNumbers1 = listOf(1,2);
val myNumbers2 = listOf(1,2);
println(myNumbers1 == myNumbers2); // true

 

이러한 특징은 코틀린이 자바(Java)와 구분되는 큰 특징 중 하나입니다.

List<Integer> myList1 = new ArrayList<Integer>();
List<Integer> myList2 = new ArrayList<Integer>();

myList1.add(1);
myList2.add(1);
myList1.add(2);
myList2.add(2);

System.out.println(myList1);
System.out.println(myList2);

System.out.println(myList1 == myList2); // false



만약 코틀린에서도 자바처럼 두 변수가 서로 엄밀히 다른지,

정확히 표한하자면 두 변수의 주소 값이 서로 다른지 확인을 하려면 === 연산자를 사용할 수 있습니다.

val myNumbers1 = listOf(1,2);
val myNumbers2 = listOf(1,2);

println(myNumbers1 === myNumbers2); // false

 


2.2. Set

여러 개의 데이터를 담을 수 있고, 중복을 제거하여 관리하는 자료형을 Set이라고 합니다.

코틀린에서도 마찬가지로 데이터의 중복을 제거하여 데이터를 담을 수 있는 Set을 제공합니다.

 

코틀린에서 Set은 한번 선언한 이후에 데이터를 추가할 수 없습니다.

Set 변수를 선언한 이후 데이터를 추가하려면 Mutable Set을 사용해야합니다.

 

코틀린에서 Set은 아래와 같이 사용할 수 있습니다.

 

// Set
val defaultSet = setOf(1.0f,2.0f,3.0f);

// defaultSet.add(4.0f);

val mutableSet = mutableSetOf(1.0f,2.0f,3.0f);

mutableSet.add(4.0f);

println(defaultSet); // [1.0, 2.0, 3.0]
println(mutableSet); // [1.0, 2.0, 3.0, 4.0]


코틀린 Set의 비교

코틀린에서 Set은 내부적으로 LinkedHashSet을 통해 추가된 순서를 기억합니다.

그래서 코틀린의 set은 first()나 last()와 같은 메서드로 순서에 따른 요소를 탐색할 수 있고,

아래와 같이 set의 요소에 대한 비교 연산이 가능합니다.

 

val numbers = setOf(1, 2, 3, 4)  // LinkedHashSet is the default implementation
val numbersBackwards = setOf(4, 3, 2, 1)

println(numbers.first() == numbersBackwards.first()) // false
println(numbers.first() == numbersBackwards.last()) // true

 


2.3. Map

맵(Map)은 키-값(key-Value) 형태로 저장하는 자료형입니다.

내부적으로는 해싱(Hashing)이라는 후속 작업이 일어나기 때문에,
특정한 키 값(key)을 기준으로 조회할 경우, 가장 빠른 조회 성능을 가집니다.

 

하지만, 연속된 값을 전체 탐색해야하는 작업의 경우 오히려 맵보다 리스트가 더 빠른 성능을 보일 수도 있습니다.

 

일반적으로 Map은 값의 변경이 자주 일어나거나, 사용자의 인증 등에 사용하는 토큰과 같은 고유한 값을 다루는 데 사용됩니다.

Map은 기본적으로 Key-Value의 쌍으로 값을 관리하기 때문에

아래와 같이 <> 안에 Key 값과 Value 값의 자료형을 명시해줄 수 있습니다.

 

그러나 코틀린에서 Map은 타입 추론이 가능하기 때문에 이를 생략할 수도 있습니다.

 

맵은 요소를 추가할 때, 키-값(Key-Value)의 형태로 추가해야 합니다.

코틀린에서는 Pair라는 클래스와 to라는 키워드를 통해 아래와 같이 요소를 추가할수 있습니다.

 

 val defaultMap = mapOf<String, Int>(
        Pair("Jhon", 23),
        Pair("Kevin", 34),
        "Anna" to 17,
    );

// defaultMap.put("Alice", 18); // Error!

val mutableMap = mutableMapOf<String, Int>(
    Pair("Jhon", 23),
    Pair("Kevin", 34),
    "Anna" to 17,
);

mutableMap.put("Alice", 18); // put을 통해서 새로운 값을 추가 가능
mutableMap["Konan"] = 31; // [] 를 통해서도 요소추가 가능

println(defaultMap); // {Jhon=23, Kevin=34, Anna=17}
println(mutableMap); // {Jhon=23, Kevin=34, Anna=17, Alice=18, Konan=31}


println(mutableMap.get("Konan")); // 31
println(mutableMap["Alice"]); // 18
println(mutableMap["Elvis"]); // null

// getOrDefault
// 값이 없을 경우 대응하기 위한 메서드
println(mutableMap.getOrDefault("Elvis", -1)); // -1

 


keys, values

코틀린에서 맵은 key와 value를 별도로 추출할 수 있습니다.

맵을 구성하는 key의 정보를 리스트로 확인할 때나,

맵을 구성하는 value의 정보를 리스트르 확인할 때 아래와 같이 사용할 수 있습니다.

 

val defaultMap = mapOf<String, Int>(
    Pair("Jhon", 23),
    Pair("Kevin", 34),
    "Anna" to 17,
);

val defaultMapKeys = defaultMap.keys; // [Jhon, Kevin, Anna]
val defaultMapValues = defaultMap.values; // [23, 34, 17]

 


Filter

코틀린의 맵은 filter 메서드를 지원합니다.

filter 메서드는 단어에서 기능을 유추할 수 있듯이, 맵에서 조건을 만족하는 데이터들의 집합을 맵으로 추출합니다.

또한, keys, values 와 같이 Key나 Value에 대해서만 필터링 된 데이터를 추출할 수도 있습니다.

val defaultMap2 = mapOf<String, Int>(
    Pair("Jhon", 23),
    Pair("Kevin", 34),
    "Anna" to 17,
);

val mFilteredMap = defaultMap2.filter{(key, value) -> key.endsWith("a") && value > 10 }
println(mFilteredMap); // {Anna=17}

val mFilteredKeyMap  = defaultMap2.filterKeys { it.endsWith("a") }
val mFilteredValueMap  = defaultMap2.filterValues { it < 20 }

println(mFilteredKeyMap); // {Anna=17}
println(mFilteredValueMap); // {Anna=17}

 

여기서 기억 해야 할 점은, filter 메서드를 통해 맵 변수에서 데이터를 추출했을 때,

그 구성 요소가 완전히 같다면 비교 연산자 '=='를 통해 참(true)값을 얻을 수 있습니다.

 

하지만, 기존의 맵 변수 자체를 바꾸는 것은 아니기 때문에  fitler 메서드는 새롭게 생성된 맵 변수를 반환합니다.

 

val singleElementMap = mutableMapOf("Elvis" to 18);
val mFilteredMap2 = singleElementMap.filter{(key, value) -> true };

println(singleElementMap); // {Elvis=18}
println(mFilteredMap2); // {Elvis=18}
println(singleElementMap == singleElementMap); // true
println(singleElementMap === singleElementMap); // true
println(singleElementMap == mFilteredMap2); // true
println(singleElementMap === mFilteredMap2); // false

singleElementMap["Alice"] = 33;
println(singleElementMap == singleElementMap); // true
println(singleElementMap == mFilteredMap2); // false

 


컬렉션의 순회(loop)

코틀린에서 컬렉션 클래스는 하나의 변수에, 여러 개의 값을 담을 수 있습니다.

따라서, 그 변수를 하나씩 모두 꺼내어 탐색해볼 수 있는데요.

이러한 작업에는 다음에 다룰 for문을 사용하는 방법도 있지만,

 

코틀린에서 컬렉션 자료형은 모두 forEach 메서드를 지원합니다.

코틀린의 forEach는  컬렉션에서 모두 사용 가능합니다.

 

그리고, List와 Set은 모두 내부적으로 입력된 순서를 기억합니다.

그렇기 때문에 이 두 자료는 순서를 나타낼 수 있는 인덱스(index)정보를 활용할 수 있는데요.

List와 Set은 forEach와 더불러 forEachIndexed 메서드를 지원하여, 변수에 저장된 요소의 인덱스도 함께 조회할 수 있습니다.

 

하지만, Map은 forEach 메서드만 사용할 수 있고, 아래와 같이 key, value를 forEach 문으로 조회할 수 있습니다.

 

// 컬렉션의 forEach, forEachIndex
val people3 = listOf(Person("Amy", 20), bob, bob)
people3.forEach({ element ->
    println(element); // Person(name=Amy, age=20) ...
})
/*
    Person(name=Amy, age=20)
    Person(name=Bob, age=32)
    Person(name=Bob, age=32)
 */

// () 가 생략된 문법, 코드의 간결성을 확보해줌
people3.forEach{ element ->
    println(element); // Person(name=Amy, age=20) ...
}
/*
    Person(name=Amy, age=20)
    Person(name=Bob, age=32)
    Person(name=Bob, age=32)
 */

// index와 함께 전체 컬렉션을 순회할 수 있음.
people3.forEachIndexed{ index, element ->
    println("index : $index , $element"); // index : 0 , Person(name=Amy, age=20)
}
/*
    index : 0 , Person(name=Amy, age=20)
    index : 1 , Person(name=Bob, age=32)
    index : 2 , Person(name=Bob, age=32)
 */

val numberSet = setOf(1,2,3);
numberSet.forEach { item ->
    println(item);
}
/*
    1
    2
    3
 */

numberSet.forEachIndexed{ index, element ->
    println("index : $index, element : $element");
}
/*
    index : 0, element : 1
    index : 1, element : 2
    index : 2, element : 3
 */

val ageMap = mapOf("Jhon" to 16, "Kevin" to 26, "Amy" to 14);
ageMap.forEach { item,  ->
    println(item);
}
/*
    Jhon=16
    Kevin=26
    Amy=14
 */

ageMap.forEach { (key, value),  ->
    println("key = $key, value = $value");
}
/*
    key = Jhon, value = 16
    key = Kevin, value = 26
    key = Amy, value = 14
 */

// error: cannot infer a type for this parameter. Please specify it explicitly.
// agaMap.forEachIndexed{ index, element ->
//     println("index : $index, element : $element");
// }

 


 

참고자료

https://kotlinlang.org/docs/

 

You will be redirected shortly

 

kotlinlang.org