컬렉션(Collection)은 여러 요소들을 담을 수 있는 자료구조다. 즉, 다수의 데이터 그룹이며 다른 말로 컨테이너(Container)라고도 부른다. 배열과 비슷하지만 크기가 고정된 배열을 보완하면 동적인 특성을 가진다.
자바 1.2 이후 표준적인 방식으로 컬렉션을 다루기 위해 컬렉션 프레임워크(Collection Framework)가 등장했다.
컬렉션 프레임워크란 널리 알려져 있는 자료구조를 바탕으로 객체나 데이터들을 효율적으로 관리(추가, 삭제, 검색, 저장)할 수 있도록 java.util 패키지에 컬렉션과 관련된 인터페이스와 클래스들을 포함시킨 것이다.
컬렉션 프레임워크 구성요소
컬렉션 인터페이스들은 제네릭(Generics)으로 표현되어 컴파일 시점에서 객체의 타입을 체크하기 때문에 런타임 에러를 줄이는 데 도움이 된다.
컬렉션 프레임워크 대표적인 인터페이스
컬렉션 프레임워크의 모든 컬렉션 클래스들은 List, Set, Map 중 하나를 구현하고 있으며, 구현한 인터페이스의 이름이 클래스 이름에 포함되지만 Vector, Stack, Hashtable, Properties와 같은 클래스들은 컬렉션 프레임워크가 만들어지기 이전부터 존재하던 것이기 때문에 컬렉션 프레임워크의 명명법을 따르지 않는다.
Vector나 Hashtable과 같은 기존의 컬렉션 클래스들은 호환을 위해 남겨진 것이므로 가급적 사용하지 않는 것이 좋다. 새로 추가된 ArrayList와 HashMap을 사용하면 된다.
List와 Set의 부모인 Collection 인터페이스에는 아래와 같은 메소드들이 정의되어 있다.
List 인터페이스는 중복을 허용하면서 저장 순서가 유지되는 컬렉션을 구혀하는 데 사용된다.
Set 인터페이스는 중복을 허용하지 않고 저장순서가 유지되지 않는 컬렉션 클래스르 구현하는 데 사용된다.
Map 인터페이스는 키(key)와 값(value)을 하나의 쌍으로 저장하는 컬렉션 클래스를 구현하는 데 사용된다.
값은 중복될 수 있지만 키의 중복은 허용하지 않는다.
기존에 저장된 데이터와 중복된 키와 값을 저장하면 기존의 값은 없어지고 마지막에 저장된 값이 남게 된다.
Map 인터페이스에서 값은 중복을 허용하기 때문에 Collection 타입으로 반환하고, 키는 중복을 허용하지 않기 때문에 Set 타입으로 반환한다.
Map.Entry 인터페이스는 Map 인터페이스의 내부 인터페이스이다.
Map에 저장되는 key-value 쌍을 다루기 위해 내부적으로 Entry 인터페이스가 정의되어 있다. 보다 객체지향적인 설계를 하도록 유도한 것으로 Map 인터페이스 구현하는 클래스에서는 Map.Entry 인터페이스도 함께 구현해야 한다.
java8에서 추가한 Stream은 람다를 활용할 수 있는 기술 중 하나이다. java8 이전에는 배열 또는 컬렉션 인스터스를 for
또는 foreach
문을 돌면서 요소 하나하나를 꺼내 다루었다면, java8 이후에는 Stream을 이용해 처리하였다.
Stream은 한마디로 데이터의 흐름이다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필러팅하고 가공된 결과를 얻을 수 있다. 또한 람다를 이용해 코드의 양을 줄이고 간결하게 표현할 수 있다.
즉, 배열과 컬렉션을 함수형으로 처리할 수 있다.
또 하나의 Stream의 장점은 간단하게 병렬처리(multi-threading)가 가능하다는 점이다.
map은 요소들을 특정조건에 해당하는 값으로 변환해 변경된 요소를 포함하고 있는 새로운 스트림 객체이다.
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.stream()
.map(num -> num * 10)
.forEach(System.out::println);
출력 결과
10
20
30
sorted는 stream의 요소들을 정렬해 새로운 stream을 생성한다.
sorted()는 매개변수가 없기 때문에 이를 사용하려면 정렬하려는 객체에 비교가능한 인터페이스가 구현되어 있어야 한다.
List<String> list = new ArrayList<>();
list.add("python");
list.add("java");
list.add("kotlin");
list.stream()
.sorted()
.forEach(System.out::println);
출력 결과
java
kotlin
python
stream에서 중복되는 요소들을 모두 제거해 새로운 stream을 반환한다.
동일한 객체인지 판단하는 기준은 Object.equals(Object)
의 결과 값이다.
List<String> list = new ArrayList<>();
list.add("java");
list.add("python");
list.add("java");
list.add("kotlin");
list.add("python");
list.stream()
.distinct()
.forEach(System.out::println);
출력 결과
java
python
kotlin
limit은 어떤 stream에서 일정 개수만큼만 가져와서 새로운 stream을 반환해준다.
Stream.limit(숫자)
로 사용하며, 숫자만큼 요소를 취하여 stream을 생성해 반환한다.
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
list.stream()
.limit(2)
.forEach(System.out::println);
출력 결과
1
2
stream의 각각의 요소를 최종 처리할 명령문이다.
매개값으로 람다식 또는 메소드 참조를 대입할 수 있다.
Collection.forEach는 따로 객체를 생성하지 않고 forEach 메소드를 호출한다. forEach 메소드는 Iterable 인터페이스의 default 메소드인데, Collection 인터페이스에서 Iterable 인터페이스를 상속하고 있기에 바로 호출할 수 있다.
반면 Stream.forEach는 Collection 인터페이스의 default 메소드 stream()으로 Stream 객체를 생성해야만 forEach를 호출할 수 있다.
단순 반복이 목적이라면 Stream.forEach는 stream()으로 생성된 Stream 객체가 버려지는 오버헤드가 있기 때문에 filter, map 등의 Stream 기능들과 함께 사용할 때만 Stream.forEach를 사용하고 나머지 경우엔 Collection.forEach를 쓰는 것이 좋다고 생각한다.