컬렉션

컬렉션 (Collections)

Groovy에서 컬렉션을 다루는 기본 문법을 볼게요. 리스트와 배열, 맵을 만드는 방법과 그 안의 값에 접근하는 방식을 자연스럽게 익힐 수 있을 거예요. 여러 언어에서 쓰던 표기법과 비슷하면서도, Groovy만의 편리한 표기 몇 가지가 눈에 띌 거예요.

출처: Groovy 공식 문서 - Syntax (Collections)

본문

7. 리스트 (Lists)

Groovy는 쉼표로 구분하고 대괄호로 감싼 값들을 리스트로 표현해요. Groovy는 자체 컬렉션 클래스를 정의하지 않으므로, Groovy 리스트는 그냥 JDK의 java.util.List예요. 리스트 리터럴을 정의할 때 사용되는 구체적인 구현은 기본적으로 java.util.ArrayList인데, 나중에 보겠지만 달리 지정할 수도 있어요.

def numbers = [1, 2, 3]         (1)

assert numbers instanceof List  (2)
assert numbers.size() == 3      (3)
  • (1) 쉼표로 구분하고 대괄호로 감싼 리스트 numbers를 정의해서 변수에 할당해요.
  • (2) 이 리스트는 자바의 java.util.List 인터페이스의 인스턴스예요.
  • (3) size() 메서드로 리스트의 크기를 물어보면, 우리 리스트에 3개 요소가 있다는 걸 알 수 있어요.

위 예시에서는 같은 타입만 담은 동종(homogeneous) 리스트를 썼는데, 서로 다른 타입의 값들을 담은 이종(heterogeneous) 리스트도 만들 수 있어요.

def heterogeneous = [1, "a", true]  (1)
  • (1) 여기 리스트에는 숫자, 문자열, boolean 값이 들어 있어요.

앞서 리스트 리터럴은 기본적으로 java.util.ArrayList의 인스턴스라고 했는데, as 연산자로 타입을 강제 변환(coercion)하거나 변수 타입을 명시적으로 선언해서 리스트의 뒷받침 타입(backing type)을 다르게 쓸 수도 있어요.

def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList

def linkedList = [2, 3, 4] as LinkedList    (1)
assert linkedList instanceof java.util.LinkedList

LinkedList otherLinked = [3, 4, 5]          (2)
assert otherLinked instanceof java.util.LinkedList
  • (1) as 연산자로 강제 변환해서 명시적으로 java.util.LinkedList 구현을 요청했어요.
  • (2) 리스트 리터럴을 담는 변수의 타입을 java.util.LinkedList로 선언할 수도 있어요.

리스트 요소에는 [] 첨자(subscript) 연산자로 접근할 수 있어요. 값을 읽을 때도, 설정할 때도 같은 표기법을 쓰고, 양수 인덱스나 리스트 끝에서부터 세는 음수 인덱스, 그리고 범위(range)로도 접근할 수 있어요. << leftShift 연산자로는 리스트에 요소를 덧붙일 수 있어요.

def letters = ['a', 'b', 'c', 'd']

assert letters[0] == 'a'     (1)
assert letters[1] == 'b'

assert letters[-1] == 'd'    (2)
assert letters[-2] == 'c'

letters[2] = 'C'             (3)
assert letters[2] == 'C'

letters << 'e'               (4)
assert letters[ 4] == 'e'
assert letters[-1] == 'e'

assert letters[1, 3] == ['b', 'd']         (5)
assert letters[2..4] == ['C', 'd', 'e']    (6)
  • (1) 리스트의 첫 번째 요소에 접근해요(0부터 세는 인덱스).
  • (2) 음수 인덱스로 마지막 요소에 접근해요. -1은 리스트 끝에서 첫 번째 요소예요.
  • (3) 할당을 사용해서 리스트의 세 번째 요소에 새 값을 설정해요.
  • (4) << leftShift 연산자로 리스트 끝에 요소를 덧붙여요.
  • (5) 두 요소를 한 번에 접근해서, 그 두 요소를 담은 새 리스트를 반환해요.
  • (6) 범위를 사용해서 리스트에서 시작 위치부터 끝 위치까지의 값 구간을 접근해요.

리스트는 본질적으로 이종일 수 있으니, 다른 리스트를 담아 다차원 리스트(make multidimensional lists)를 만들 수도 있어요.

def multi = [[0, 1], [2, 3]]     (1)
assert multi[1][0] == 2          (2)
  • (1) 숫자 리스트들로 이루어진 리스트를 정의해요.
  • (2) 가장 바깥 리스트의 두 번째 요소, 그 안쪽 리스트의 첫 번째 요소에 접근해요.

8. 배열 (Arrays)

Groovy는 배열에도 리스트 표기법을 재사용해요. 다만 그런 리터럴을 배열로 만들려면 강제 변환( coercion)이나 타입 선언으로 배열의 타입을 명시적으로 정의해야 해요.

String[] arrStr = ['Ananas', 'Banana', 'Kiwi']  (1)

assert arrStr instanceof String[]    (2)
assert !(arrStr instanceof List)

def numArr = [1, 2, 3] as int[]      (3)

assert numArr instanceof int[]       (4)
assert numArr.size() == 3
  • (1) 변수 타입을 명시적으로 선언해서 문자열 배열을 정의해요.
  • (2) 우리가 문자열 배열을 만들었는지 확인해요.
  • (3) as 연산자로 int 배열을 만들어요.
  • (4) 프리미티브 int 배열을 만들었는지 확인해요.

다차원 배열도 만들 수 있어요.

def matrix3 = new Integer[3][3]         (1)
assert matrix3.size() == 3

Integer[][] matrix2                     (2)
matrix2 = [[1, 2], [3, 4]]
assert matrix2 instanceof Integer[][]
  • (1) 새 배열의 경계(bounds)를 정의할 수 있어요.
  • (2) 또는 경계를 지정하지 않고 배열을 선언할 수도 있어요.

배열 요소에 접근하는 방식은 리스트와 똑같아요.

String[] names = ['Cédric', 'Guillaume', 'Jochen', 'Paul']
assert names[0] == 'Cédric'     (1)

names[2] = 'Blackdrag'          (2)
assert names[2] == 'Blackdrag'
  • (1) 배열의 첫 번째 요소를 가져와요.
  • (2) 배열의 세 번째 요소 값을 새 값으로 설정해요.

8.1. Java 스타일 배열 초기화 (Java-style array initialization)

Groovy는 항상 대괄호를 사용한 리스트/배열 리터럴 정의를 지원해 왔고, 클로저 정의와 충돌하지 않도록 Java 스타일의 중괄호는 피해 왔어요. 다만 중괄호가 배열 타입 선언 바로 뒤에 오는 경우에는 클로저 정의와 모호함이 없기 때문에, Groovy 3 이상에서는 Java 배열 초기화 표현식의 그 변형도 지원해요. 예시를 볼게요:

def primes = new int[] {2, 3, 5, 7, 11}
assert primes.size() == 5 && primes.sum() == 28
assert primes.class.name == '[I'

def pets = new String[] {'cat', 'dog'}
assert pets.size() == 2 && pets.sum() == 'catdog'
assert pets.class.name == '[Ljava.lang.String;'

// traditional Groovy alternative still supported
String[] groovyBooks = [ 'Groovy in Action', 'Making Java Groovy' ]
assert groovyBooks.every{ it.contains('Groovy') }

9. 맵 (Maps)

다른 언어에서는 딕셔너리나 연관 배열(associative arrays)이라고도 부르는 것을 Groovy는 맵으로 제공해요. 맵은 키를 값에 연결하고, 키와 값은 콜론으로 구분하며, 각 키/값 쌍은 쉼표로 구분하고, 전체를 대괄호로 감싸요.

def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF']   (1)

assert colors['red'] == '#FF0000'    (2)
assert colors.green  == '#00FF00'    (3)

colors['pink'] = '#FF00FF'           (4)
colors.yellow  = '#FFFF00'           (5)

assert colors.pink == '#FF00FF'
assert colors['yellow'] == '#FFFF00'

assert colors instanceof java.util.LinkedHashMap
  • (1) 문자열 색상 이름과 그에 대응하는 16진수 HTML 색상을 연결한 맵을 정의해요.
  • (2) 첨자 표기법으로 red 키에 연결된 내용을 확인해요.
  • (3) 프로퍼티 표기법으로 green 색상의 16진수 표현을 확인할 수도 있어요.
  • (4) 마찬가지로 첨자 표기법으로 새 키/값 쌍을 추가해요.
  • (5) 프로퍼티 표기법으로 yellow 색상을 추가해요.

Note: 키에 이름을 쓸 때는 실제로 맵에 문자열 키를 정의하는 셈이에요.

Note: Groovy는 실제로 java.util.LinkedHashMap의 인스턴스인 맵을 만들어요.

맵에 없는 키에 접근하려고 하면:

assert colors.unknown == null

def emptyMap = [:]
assert emptyMap.anyKey == null

null 결과를 얻는답니다.

위 예시에서는 문자열 키를 썼는데, 다른 타입의 값을 키로 쓸 수도 있어요.

def numbers = [1: 'one', 2: 'two']

assert numbers[1] == 'one'

여기서는 숫자를 키로 썼어요. 숫자는 모호함 없이 숫자로 인식되기 때문에, 앞선 예시처럼 문자열 키를 만들지 않아요.

그런데 변수를 키 자리에 넣어서, 그 변수의 값이 키가 되도록 하고 싶은 경우를 생각해 볼게요.

def key = 'name'
def person = [key: 'Guillaume']      (1)

assert !person.containsKey('name')   (2)
assert person.containsKey('key')     (3)
  • (1) 'Guillaume' 이름과 연결된 키는 실제로 key 변수의 값이 아니라 "key" 문자열이 돼요.
  • (2) 맵에 'name' 키는 없어요.
  • (3) 대신 맵에 'key' 키가 들어 있어요.

Note: 키로 따옴표로 감싼 문자열을 쓸 수도 있어요: ["name": "Guillaume"]. 이는 키 문자열이 유효한 식별자가 아닌 경우, 예를 들어 ["street-name": "Main street"]처럼 대시를 포함한 문자열 키를 만들고 싶을 때 필수예요.

맵 정의에서 변수의 값을 키로 전달해야 한다면, 그 변수나 표현식을 괄호로 감싸야 해요.

person = [(key): 'Guillaume']        (1)

assert person.containsKey('name')    (2)
assert !person.containsKey('key')    (3)
  • (1) 이번에는 키 변수를 괄호로 감싸서, 문자열 키를 정의하는 게 아니라 변수를 전달하고 있다고 파서에 알려줘요.
  • (2) 이제 맵에 name 키가 들어 있어요.
  • (3) 하지만 이전처럼 key 키는 들어 있지 않아요.

더 알아보기 (Learn more)