[Android/Kotlin] 9 배열과 컬렉션
데이터 타입 배열과 컬렉션에 대해 알아보자!
- Int와 Double 같은 타입은 모두 하나의 변수에 하나의 값만 저장하도록 설계되어 있는데 프로그래밍을 하다 보면 하나의 변수에 여러 개의 값을 저장해야 할 필요성이 있다.
- Kotlin 뿐만 아니라 대부분의 컴퓨터 언어들은 이처럼 여러 개의 값을 하나의 변수에 저장할 수 있도록 배열(Array)과 컬렉션(Collection)이라는 데이터 타입을 제공한다.
배열
배열이란? 여러 개의 값을 담을 수 있는 대표적인 자료형이다.
먼저 배열 공간의 개수를 할당하거나 초기화 시에 데이터를 저장해두면 데이터의 개수만큼 배열의 크기가 결정된다.
개수를 정해 놓고 사용해야 하며, 중간에 개수를 추가하거나 제거할 수 없다.
배열은 다른 데이터 타입과 마찬가지로 변수에 저장해서 사용할 수 있으며 다음과 같은 형태로 선언한다.
var 변수 = Array(개수)
- 배열 객체는 Int, Long, Char 등과 같은 타입 뒤에 Array를 붙여서 만든다.
var students = IntArray(10)
var longArray = LongArray(10)
var CharArray = CharArray(10)
var FloatArray = FloatArray(10)
var DoubleArray = DoubleArray(10)
위 코드 중 첫 번째 줄은 변수 students에 Int(정수형) 공간을 10개 할당하라는 의미이다.
students라는 이름으로 정수형 데이터를 담을 수 있는 10개의 공간을 가진 배열이 만들어지고, 각 공간에는 아직 무슨 값이 들어 있는지 모른다.
우리는 보통 첫 번째라고 하면 1이라고 생각하지만 컴퓨터는 0을 첫 번째로 인식하도록 설계되어 있다.
그래서 10개의 공간을 가지는 위와 같은 배열의 인덱스(Index)는 0부터 시작해서 9에서 끝난다.
인덱스(Index): 배열은 여러 공간을 가지고 있으며 각 몇 번째의 공간을 가리킬 때 사용한다.
예를 들어 배열의 3번째 공간을 가리키기 위해서는 인덱스(Index) 2를 사용한다.
배열뿐만 아니라 컴퓨터 언어에서 위치를 나타내는 인덱스(Index)는 0부터 시작한다.
다양한 견해가 있지만 컴퓨터의 시작이 0과 1로 만들어진 하드웨어이고 수의 표현을 2진수(Binary)로 하기 때문에 셈의 시작을 1부터 했을 때와 0부터 했을 때, 그 값이 커질수록 표현 범위에 큰 차이가 난다는 것에 기인했다는 견해가 가장 설득력이 있다.
문자 배열에 빈 공간 할당하기
- String은 Int, Double 등과 같은 기본 타입이 아니기 때문에 StringArray는 없지만 다음과 같이 사용할 수 있다.
var stringArray = Array(10, { item -> "" } )
- 수식이 조금 특이해 보일 수 있지만 일단은 괄호 안의 첫 번째 숫자인 10만 변경해서 사용하면 그 숫자만큼 빈 문자열로 된 배열 공간을 할당한다.
값으로 배열 공간 할당하기
- arrayOf 함수를 사용해서 String 값을 직접 할당할 수도 있다.
var dayArray = arrayOf("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")
배열에 값 입력하기
- 배열을 선언한 변수명 옆에 대괄호 [ ] 를 사용하고, 대괄호 안에 값을 저장할 위치의 인덱스 번호를 작성합니다. 그리고 등호(=)를 사용해서 값을 입력할 수 있다.
배열명[인덱스] = 값
- set 함수를 사용할 수 있다. 배열이 제공하는 set 함수에 인덱스와 값을 파라미터로 넘겨주면 된다.
배열명.set(인덱스, 값)
- 다음은 첫 번째부터 열 번째까지의 인덱스 값을 바꾸는 예제 코드이다.
students[0] = 90 students.set(1, 91) ... sutdents.set(8, 98) students[9] = 99
여기서 잠깐!
배열의 범위를 벗어난 인덱스에 값을 넣을 경우 어떻게 될까?
- 10개의 공간이 할당된 배열에서 열한 번째에 해당하는 10번 인덱스에 값을 넣으려고 하면 범위를 넘어섰다는 Exception이 발생하고 프로그램이 종료된다.
var intArray = IntArray(10)
intArray[10] = 100 // Exception 발생. intArray의 마지막 인덱스는 9입니다.
--> ArrayIndexOutOfBoundsException 발생
배열에 있는 값 꺼내기
- 저장할 때와 마찬가지로 대괄호 안에 인덱스를 입력해서 값을 가져올 수 있으며 get()을 이용할 수도 있다.
배열명[인덱스]
배열명.get(인덱스)
- 배열 intArray의 일곱 번째 값을 seventhValue 변수에 저장한다.
var seventhValue = intArray[6]
- 배열 intArray의 열 번째 값을 get 함수를 사용해서 tenthValue 변수에 저장한다.
var tenthValue = intArray.get(9)
- 다음은 Array 프로젝트의 전체 코드이다.
package net.flow9.thisiskotlin.basicsyntax
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 기본타입 배열 선언하기 - 각 기본타입별로 10개의 빈 공간이 할당된다
var students = IntArray(10)
var longArray = LongArray(10)
var CharArray = CharArray(10)
var FloatArray = FloatArray(10)
var DoubleArray = DoubleArray(10)
// arrayOf를 사용하면 선언과 동시에 값을 입력할 수 있다
var intArray = intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// intArray 변수에는 1 부터 10까지의 값이 각각의 배열공간에 저장되어 있다
// 2. 문자열타입 배열 선언하기
var stringArray = Array(10, { item -> "" })
// arrayOf 함수로 값을 직접 입력해서 배열을 생성할 수 있다
var dayArray = arrayOf("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")
// 3. 앞에서 선언한한 studens 변수에 값 넣기
// 가. 대괄호를 사용하는 방법
students[0] = 90
students[1] = 91
students[2] = 92
students[3] = 93
students[4] = 94
// 나. set 함수를 사용하는 방법
students.set(5, 95)
students.set(6, 96)
students.set(7, 97)
students.set(8, 98)
students.set(9, 99)
// 4. 값 변경해 보기
intArray[6] = 137 // 6번 인덱스인 7번째 값 7이 137로 변경된다
intArray.set(9, 200) // 9번 인덱스인 10번째 값 8이 200으로 변경된다
// 5. 배열 값 사용하기
var seventhValue = intArray[6]
Log.d("Array", "8번째 intArray의 값은 ${seventhValue}입니다")
var tenthValue = intArray.get(9)
Log.d("Array", "10번째 intArray의 값은 ${tenthValue}입니다")
Log.d("Array", "1번째 dayArray 값은 ${dayArray[0]}입니다")
Log.d("Array", "6번째 dayArray 값은 ${dayArray.get(5)}입니다")
}
}
/** [로그캣 출력 내용]
8번째 intArray의 값은 137입니다
10번째 intArray의 값은 200입니다
1번째 dayArray 값은 MON입니다
6번째 dayArray 값은 SAT입니다
*/
컬렉션
컬렉션(Collection)은 다른 이름으로는 동적 배열이라고도 한다.
배열과는 다르게 공간의 크기를 처음 크기로 고정하지 않고 임의의 개수를 담을 수 있기 때문이다.
컬렉션은 크게 세 가지로 리스트(List), 맵(Map), 셋(Set)이 있으며 각각은 다음과 같은 용도로 사용할 수 있다.
리스트(List)
리스트는 저장되는 데이터에 인덱스를 부여한 컬렉션이며 중복된 값을 입력할 수 있다.
Kotlin에서 동적으로 리스트를 사용하기 위해서는 리스트 자료형 앞에 뮤터블(Mutable)이라는 접두어(Prefix를 붙여야 한다.
예를 들면 mutableList, mutableMap, mutableSet이 있다.
배열과 같이 ‘데이터 타입Of’ 형태로 사용할 수 있다.
var list = mutableListOf("MON", "TUE", "WED")
여기서 잠깐!
뮤터블이란?
뮤터블(Mutable)이란 변할 수 있다라는 의미를 가지고 있다.
Kotlin은 컬렉션 데이터 타입을 설계할 때 모두 이뮤터블(Immutable: 변할 수 없는)로 설계하였다.
기본 컬렉션인 리스트(List), 맵(Map), 셋(Set)은 모두 한 번 입력된 값을 바꿀 수 없다.
그래서 컬렉션의 원래 용도인 동적 배열로 사용하기 위해서는 뮤터블로 만들어진 데이터 타입을 사용해야 한다.
리스트 생성하기: mutableListOf
- 다음과 같이 작성하면 변수에 “MON”, “TUE”, “WED” 3개의 값을 가진 크기가 3인 동적 배열 리스트가 생성된다.
var mutableList = mutableListOf("MON", "TUE", "WED)
리스트에 값 추가하기: add
add 함수를 사용해서 값을 추가할 수 있다.
값이 추가되면서 동적으로 리스트의 공간이 자동으로 증가한다.
mutableList.add("THU")
add 함수를 사용하면 입력될 위치인 인덱스를 따로 지정해주지 않아도 입력되는 순서대로 인덱스가 지정된다.
“MON”: [0], “TUE”: [1], “WED”: [2] –> “MON”: [0], “TUE”: [1], “WED”: [2], “THU”: [3]
리스트에 입력된 값 사용하기: get
get 함수로 리스트에서 값을 꺼낼 수 있다.
입력과는 다르게 사용할 때는 인덱스를 지정해서 몇 번째 값을 꺼낼 것인지 명시해야 한다.
다음은 두 번째 값을 변수에 저장하는 예시이다.
var variable = mutableList.get(1)
리스트값 수정하기: set
set 함수를 사용해서 특정 인덱스의 값을 수정할 수 있다.
다음은 두 번째 값을 수정하는 예시이다.
mutalbeList.set(1, "수정할 값")
리스트에 입력된 값 제거하기: removeAt
removeAt 함수로 리스트에 입력된 값의 인덱스를 지정해서 삭제할 수 있다.
다음은 두 번째 값을 삭제하는 예시이다.
mutableList.removeAt(1)
두 번째 값을 삭제하면 세 번째 값부터 인덱스가 하나씩 감소하면서 빈자리의 인덱스로 이동한다.
“MON”: [0], “TUE”: [1], “WED”: [2] –> “MON”: [0], “WED”: [1]
- “TUE”가 삭제되면서 3번 째 값인 “WED”의 인덱스는 2에서 1로 바뀌게 된다.
빈 리스트 사용하기
아무것도 없는 빈 리스트를 생성하면 앞으로 입력되는 값의 데이터 타입을 알 수 없기 때문에 값의 타입을 추론할 수 없다.
- 그러므로 빈 컬렉션의 경우 앞에서처럼 ‘데이터 타입Of’만으로는 생성되지 않고 데이터 타입을 직접적으로 알려주는 방법을 사용해야 한다.
다음과 같이 사용할 수 있다.
var 변수명 = mutableListOf<컬렉션에 입력될 값의 타입>()
var stringList = mutableListOf<String>()
- 다음 예제와 같이 문자열로 된 빈 리스트를 생성하고 조작할 수 있다.
// 생성
var stringList = mutableListOf<String>()
// 입력
stringList.add("월")
stringList.add("화")
// 사용
Log.d("Collection", "stringList에 입력된 두 번째 값은 ${stringList.get(1)}입니다.")
// 수정
stringList.set(1, "수정된 값")
// 삭제
stringList.removeAt(1) // 두 번째 값이 삭제 됩니다.
여기서 잠깐!
제네릭(Generic)
리스트 컬렉션을 생성하면서 < > 괄호를 사용했는데, 이 괄호의 정식 명칭은 제네릭이다.
제네릭은 컬렉션이 사용하는 값의 타입을 지정하기 위한 도구이다.
Kotlin에서 컬렉션은 제네릭을 사용하지 않으면 사용할 수 없다.
단, 값으로 초기화 할 때는 입력되는 값으로 타입을 추론할 수 있기 때문에 제네릭을 쓰지 않아도 생성할 수 있다.
컬렉션 개수 가져오기: size
size 프로퍼티를 사용하면 컬렉션의 개수를 가져올 수 있다. (mutableList.size)
위에서 set, get 등은 ‘함수’라고 하고, size는 ‘프로퍼티’라는 용어를 사용했는데, 이 둘을 구분하는 방법은 괄호의 유무이다.
괄호가 있으면 함수
괄호가 없으면 프로퍼티
다음은 컬렉션 List를 사용한 예제이다. 안드로이드 스튜디오에서 해당 코드를 작성해서 로그를 확인해보자!
package net.flow9.thisiskotlin.basicsyntax
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 값으로 컬렉션 생성하기
var mutableList = mutableListOf("MON", "TUE", "WED")
// 값 추가하기
mutableList.add("THU")
// 값 꺼내기
Log.d("Collection", "mutableList의 첫 번째 값은 ${mutableList.get(0)}입니다")
Log.d("Collection", "mutableList의 두 번째 값은 ${mutableList.get(1)}입니다")
// 2. 빈 컬렉션 생성하기기
var stringList = mutableListOf<String>() // 문자열로 된 빈 컬렉션 생성
// 값 추가하기
stringList.add("월")
stringList.add("화")
stringList.add("수")
// 값 변경하기
stringList.set(1, "날짜 변경")
// 사용
Log.d("Collection", "stringList에 입력된 두 번째 값은 ${stringList.get(1)}입니다")
// 삭제
stringList.removeAt(1) // 두 번째 값이 삭제된다.
Log.d("Collection", "stringList에 입력된 두 번째 값은 ${stringList.get(1)}입니다")
}
}
/** [로그캣 출력 내용]
mutableList의 첫 번째 값은 MON입니다
mutableList의 두 번째 값은 TUE입니다
stringList에 입력된 두 번째 값은 날짜 변경입니다
stringList에 입력된 두 번째 값은 수입니다
*/
셋(Set)
셋은 중복을 허용하지 않는 리스트라고 할 수 있다.
리스트와 유사한 구조이지만 인덱스로 조회할 수 없고, get 함수도 지원하지 않는다.
String 타입의 값을 입력받기 위해 다음과 같이 선언할 수 있다.
var set = mutableListOf<String>()
빈 셋으로 초기화하고 값 입력하기
- 셋은 중복을 허용하지 않기 때문에 다음 코드를 보시면, 네 번째 줄에서 입력한 “JAN”은 입력되지 않는다.
var set = mutableSetOf<String>()
set.add("JAN")
set.add("FEB")
set.add("MAR")
set.add("JAN") // 동일한 값은 입력되지 않습니다.
셋의 값 인덱싱?
- 셋은 인덱스로 조회하는 함수가 없기 때문에 특정 위치의 값을 사용할 수 없습니다.
셋의 값 삭제하기
- 셋은 값이 중복되지 않기 때문에 다음과 같이 값을 직접 입력하여 삭제하여야 합니다.
set.remove("FEB")
- 다음은 컬렉션 Set을 사용한 예제이다. 안드로이드 스튜디오에서 해당 코드를 작성해서 로그를 확인해보자!
package net.flow9.thisiskotlin.basicsyntax
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 셋 생성하고 값 추가하기
var set = mutableSetOf<String>()
set.add("JAN")
set.add("FEB")
set.add("MAR")
set.add("JAN") // 동일한 값은 입력되지 않는다.
// 2. 전체 데이터 출력해보기
Log.d("Collection", "Set 전체출력 = ${set}")
// 3. 특정 값 삭제하기
set.remove("FEB")
Log.d("Collection", "Set 전체출력 = ${set}")
}
}
/** [로그캣 출력 내용]
Set 전체출력 = [JAN, FEB, MAR]
Set 전체출력 = [JAN, MAR]
*/
맵(Map)
맵은 키(Key)와 값(Value)의 쌍으로 입력되는 컬렉션이다.
맵의 키는 리스트의 인덱스와 비슷한데 맵에서는 키를 직접 입력해야 한다.
제네릭으로 키와 값의 데이터 타입을 지정해서 맵을 생성한다.
다음은 키와 값의 타입을 모두 String으로 사용하기 위한 생성 예제이다.
var map = mutableMapOf<String, String>()
- 인덱스에 해당하는 키를 직접 지정해서 사용해야 한다.
빈 맵으로 생성하고 값 추가하기
- 값을 추가하기 위해 제공되는 맵에서 제공되는 put 함수에 키와 값을 입력하면 된다.
var map = mutableMapOf<String, String>()
map.put("key1", "value2")
map.put("key2", "value2")
map.put("key3", "value3")
- 키와 값을 추가할 때마다 리스트처럼 맵의 공간이 늘어난다.
맵 사용하기
- get 함수에 키를 직접 입력해서 값을 꺼낼 수 있다.
Log.d("CollectionMap", "map에 입력된 key1의 값은 ${map.get("key1")}입니다.")
// [로그캣 출력 내용]
// map에 입력된 key1의 값은 value2입니다.
맵 수정하기
- put 함수를 사용할 때 동일한 키를 가진 값이 있으면 키는 유지된 채로 그 값만 수정한다.
map.put("key2", "수정")
- 기존 “key2”의 값인 “value2”는 “수정”으로 바뀌게 된다.
맵 삭제하기
remove 함수에 키를 입력해서 값을 삭제할 수 있다.
리스트와는 다르게 인덱스에 해당하는 키의 값이 변경되지 않고 그대로 유지된다.
map.remove("key2")
- 다음은 컬렉션 Map을 사용한 예제이다. 안드로이드 스튜디오에서 해당 코드를 작성해서 로그를 확인해보자!
package net.flow9.thisiskotlin.basicsyntax
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 맵 생성하기
var map = mutableMapOf<String, String>()
// 2. 값 넣기
map.put("키1", "값2")
map.put("키2", "값2")
map.put("키3", "값3")
// 3. 값 사용하기
var variable = map.get("키2")
Log.d("Collection", "키2의 값은 ${variable}입니다")
// 4. 값 수정하기
map.put("키2", "두 번째 값 수정")
Log.d("Collection", "키2의 값은 ${map.get("키2")}입니다")
// 5. 값 삭제하기
map.remove("키2")
// 5.1 없는 값을 불러오면 null값이 출력된다
Log.d("Collection", "키2의 값은 ${map.get("키2")}입니다")
}
}
/** [로그캣 출력 내용]
키2의 값은 값2입니다
키2의 값은 두 번째 값 수정입니다
키2의 값은 null입니다
*/
여기서 잠깐!
컬렉션 값의 단위 = 엘리먼트
컬렉션에 입력되는 값은 각각을 엘리먼트(Element)라고 한다.
값이라고 해도 되지만 맵을 지칭할 때 맵의 값(엘리먼트 자체)을 가리키는 건지 엘리먼트의 값(실제 값)을 가리키는 건지, 2개의 용어가 충돌할 수 있기 때문에 엘리먼트라고 이해하고 있는 것이 좋다.
엘리먼트는 맵의 입력 단위인 키와 값을 합친 것을 말하는데 이것은 리스트와 셋에서도 동일한 용어로 사용된다.
즉 리스트의 값 또한 엘리먼트라고 부른다.
리스트 엘리먼트 = 리스트의 (값)
맵 엘리먼트 = 맵의 (키와 값)
이뮤터블 컬렉션(Immutable Collection)
Kotlin은 일반 배열처럼 크기를 변경할 수 없으면서 값 또한 변경할 수 없는 이뮤터블 컬렉션을 지원한다.
이뮤터블 컬렉션은 다음과 같이 기존 컬렉션에서 mutable 이라는 접두어가 제거된 형태로 사용된다.
var list = listOf("1", "2")
불변형 컬렉션은 한 번 입력된 값을 변경할 수 없기 때문에 add나 set 함수는 지원하지 않고 최초 입력된 값만을 ‘사용’할 수 있다.
배열과 다른 점은 크기뿐만 아니라 값의 변경 또한 불가능하다는 것이다.
즉 불변형 컬렉션은 수정, 추가, 제거 모두 안된다.
var immutableList = listOf("JAN", "FEB", "MAR") // 생성
Log.d("Collection", "리스트의 두 번째 값은 ${immtuableList.get(1)}입니다.) // 사용
// [로그캣 출력 내용]
// 리스트의 두 번째 값은 FEB입니다.
그렇다면 이뮤터블 컬렉션은 언제 사용할 수 있을까?
일반 변수 var와 읽기 전용 변수 val의 관계에세 그 사용법을 유추할 수 있는데, 기준이 되는 어떤 값으 모음을 하나의 변수에 저장할 필요가 있거나 또는 여러 개의 값을 중간에 수정하지 않고 사용할 필요가 있을 때 이뮤터블 컬렉션을 사용한다.
대표적인 예로 요일 데이터가 있습니다!
- 다음처럼 7개의 요일을 이뮤터블 리스트로 선언하면 중간에 바뀌지 않기 때문에 계속 같은 값을 유지하면서 사용할 수 있다.
var dayList = listOf("월", "화", "수", "목", "금", "토", "일)
복습
배열(Array)
하나의 변수에 여러 개의 정해진 값을 담을 수 있게 해주는 데이터 타입이다.
처음 정해진 값의 개수는 바꿀 수 없다.
컬렉션(Collection)
여러 개의 값을 담을 수 있는 배열은 값의 개수가 증가하면 사용할 수 없는 단점이 있기 때문에 동적으로 크기를 변경할 수 있도록 만들어진 동적 배열이다.
동적 배열에는 크게 리스트(List), 셋(Set), 맵(Map) 세 가지의 데이터 타입이 있으며 이것들을 모두 통칭해서 부르는 용어가 컬렉션이다.
뮤터블(Mutable)
입력된 값을 변경할 수 있는 것을 말한다.
대표적으로 var로 선언된 변수는 모두 뮤터블이다.
리스트(List)
컬렉션의 한 종류로 인덱스를 사용하는 데이터 타입이다.
인덱스가 있기 때문에 중복된 값을 넣을 수도 있다.
셋(Set)
리스트에서 인덱스가 빠진 데이터 타입이다.
값을 중복해서 넣을 수 없다.
맵(Map)
키(Key)와 값(Value)을 쌍으로 갖는 데이터 타입이다.
맵의 키는 리스트의 인덱스처럼 사용할 수 있다.