Overview
컬렉션(Collection)은 많은 데이터 요소를 효율적으로 관리하기 위한 자료구조를 말하며, ArrayList, LinkedList, HashMap 등이 여기에 포함됩니다. 그리고 컬렉션들은 제네릭(Generic)으로 구현되어 있어 컬렉션과 함께 제네릭에 대해서도 알아보도록 하겠습니다.
1. Collection(컬렉션) ?
컬렉션(Collection)이란 많은 수의 데이터를 그 사용 목적에 적합한 자료구조로 묶어 하나로 그룹화한 객체를 말합니다. 자바에서는 이런 컬렉션을 위한 인터페이스와 클래스들이 있으며 자주 사용되는 클래스들의 상속 계층도는 아래 그림과 같습니다 !
종류는 ArrayList, LinkedList, Vector, Stack , HashSet, TreeSet, TreeMap 등이 있으며, 크게 아래와 같은 자료구조 유형에 따라 구분됩니다.
1. List : 이름과 같이 목록처럼 데이터를 순서에 따라 관리합니다.
- ArrayList : 가변의 길이 , 많은 데이터 처리시 성능이 떨어지지만 각 데이터에 대한 인덱스를 가져 검색이 빠르다.
- Vector : 비교적 성능이 좋지 않고 무거워 잘 사용하지 않는다.
- LinkedList : 데이터의 삽입/삭제는 빠르지만, 검색이 느리다.
2. Set : 중복이 허용되지 않는 데이터를 관리합니다.
- HashSet : 가장 빠른 접근속도를 가지고 있지만, 순서를 알 수 없다.
- LinkedHashSet : 추가된 순서대로 접근이 가능하다.
- TreeSet : 정렬된 순서대로 보관하며, 정렬방법을 지정할 수 있다.
3. Map : 데이터를 Key와 Value로 짝을 이루어 관리하며, Key 값은 중복을 허용하지 않습니다.
- HashMap : 중복을 허용하지 않고, 순서를 보장하지 않는다. (NULL 허용)
- HashTable : HashMap 보다는 느리지만, 동기화를 지원한다. (NULL 불가)
- TreeMap : 정렬된 순서대로 저장되어 검색은 빠르지만, 데이터 추가/삭제시 느리다.
4. Queue : 데이터 In/Out 순서를 FIFO 방식으로 관리합니다. (FIFO : First In First Out)
5. Stack : 데이터 In/Out 순서를 LIFO 방식으로 관리합니다. (LIFO : Last In First Out)
1 - 1. 컬렉션의 특징
컬렉션 클래스들이 데이터를 다룰 때 그 데이터는 기본적으로 객체만 가능합니다. 따라서 char,int,float와 같은 primitive type은 사용할 수 없고 대신 Wrapper 클래스를 사용해야 합니다. 하지만, 오토박싱(auto boxing)과 오토언박싱(auto unboxing)으로 인해 사용자는 마치 기본형을 다룰 수 있는 것처럼 사용할 수 있습니다.
컬렉션과 관련된 인터페이스 또는 클래스가 정의되어 있는 코드를 살펴보면 아래와 같이 제네릭(Generic) 형태로 구현되어 있는 것을 확인할 수 있습니다. 따라서 사용자는 사용하고자 하는 데이터 타입을 지정하여 사용할 수 있습니다.
public interface List<E> extends Collection<E> { ... }
위 상속도에서 살펴보았듯이 List, Set, Queue는 공통분모로 Collection 인터페이스를 상속받고 있으며, Collection 인터페이스가 가지는 주요 메소드는 아래와 같습니다.
메소드 | 기능 |
boolean add(E e) | 현재 컬렉션에 데이터 객체 e를 추가합니다. |
boolean addAll(Collection<? extends E> c) | 현재 컬렉션에 컬렉션 c의 모든 데이터를 추가합니다. |
boolean contains(Object o) | 현재 컬렉션에 객체 o의 포함 여부를 반환합니다. |
boolean containsAll(Collection<?> c) | 현재 컬렉션에 컬렉션 c의 모든 데이터가 포함되어 있는지 여부를 반환합니다. |
boolean remove(Object o) | 현재 컬렉션에서 객체 o를 삭제합니다. |
boolean removeAll(Collection<?> c) | 현재 컬렉션에서 컬렉션 c와 일치하는 데이터를 삭제합니다. |
boolean retainAll(Collection<?> c) | 현재 컬렉션에서 컬렉션 c와 일치하는 데이터만 남기고 나머지는 삭제합니다. |
void clear() | 현재 컬렉션의 모든 데이터를 삭제합니다. |
int size() | 현재 컬렉션에 포함된 데이터 개수를 반환합니다. |
boolean isEmpty() | 현재 컬렉션이 비어있는지 여부를 반환합니다. |
Iterator<E> iterator() | 현재 컬렉션의 모든 요소에 대한 iterator를 반환합니다. |
Object[] toArray() | 현재 컬렉션에 저장된 데이터를 Object 배열로 반환합니다. |
<T> T[] toArray(T[] a) | 현재 컬렉션에 저장된 데이터를 배열 a에 담고 배열 a를 반환합니다. |
2. Iterator
자바의 컬렉션 프레임워크(Collection Framework)는 컬렉션에 저장된 요소를 읽어오는 방법을 Iterator 인터페이스로 표준화하고 있어요. 자바에서 Iterator는 컬렉션 프레임워크에 저장된 값을 순회하면서 가져오거나, 삭제할 때 사용합니다.
[ Iterator 사용 ]
Iterator<데이터 타입> iterator명 = 컬렉션.iterator();
list의 경우 get(index) 메소드를 사용할 수 있어 Set이 아닌 경우 사용할 이유가 있나? 싶지만, Iterator를 사용할 시 얻을 수 있는 장단점들은 다음과 같습니다.
👍🏻 Iteraotr 장점
- Iterator는 모든 컬렉션 프레임워크에 공통으로 사용 가능
- 컬렉션 프레임워크에서 쉽게 값을 가져오고 제거할 수 있음
- 3개의 메소드만 알면 되어 사용하기 매우 쉬움
👎🏻 Iterator 단점
- 처음부터 끝까지의 단방향 순회만 가능
- 값의 변경이나 추가가 불가능
- 대량의 데이터를 제어할 시 속도가 느림
2 - 1. Iterator의 메소드
- hasNext() : Iterator 안에 다음 값이 있는지 확인 후 true/false 를 반환합니다.
- next() : iterator의 다음 값 가져오기
- remove() : iterator.next()로 가져온 값을 컬렉션 (List, Set, Map)에서 삭제 (반드시 next() 후에 사용해야 합니다.)
ex)
Iterator<Integer> iterator = Arrays.asList(1, 2, 3, 4, 5).iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
위와 같이 컬렉션 내의 모든 데이터를 출력하고 싶을 땐 while 문을 통해 hasNext() 메소드를 이용하여 출력할 수 있습니다.
3. Stream
스트림은 람다와 마찬가지로 Java 8에 추가된 것으로, 람다를 활용하여 데이터 처리 연산을 지원하도록 컬렉션, 배열 , I/O자원등에 대해 작성되어 있는 요소들을 추출하여 반복적인 처리를 가능케 하는 기능입니다.
- Java 8 이상에서 부터 사용 가능합니다.
- 데이터의 연속입니다.
- System.in / System.out 이것도 스트림 입니다.
- Collections.stream() 을 제공해줍니다. (Java8)
- filter, map, forEach 같은 고차함수(함수를 인자로 받는 함수)가 제공이 됩니다.
- Stream을 만들때는 Stream.generate 과 Stream.iterate 으로 만들 수 있습니다.
- 스트림을 사용하면 연속된 데이터에 대해서 풍부한 고차함수들을 사용하여 강력한 기능을 간결하게 표현할 수 있습니다.
💡 Iterator에서 Stream으로 ~
: iterator , stream 둘 다 반복적인 연산을 줄여주기 위해 만든 구분자입니다. 최근에는 iterator 보다는 stream을 많이 사용하는데요, 사용해보면 코드량도 줄어들고 중간처리를 할 수 있는 고차함수들이 존재해서 stream이 더 편하다는 것을 느낄 수 있습니다.
ex) List 컬렉션에 요일 요소들을 저장해두고, 수요일을 제거해볼게요.
List<String> stringList = Arrays.asList("월", "화", "수", "목", "금");
1) Iterator
List<String> list = new ArrayList<String>();
list.add("월");
list.add("화");
list.add("수");
list.add("목");
list.add("금");
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
if("수".equals(iter.next())){
iter.remove();
}
}
System.out.println(list);
2) Stream
List<String> list = new ArrayList<String>();
list.add("월");
list.add("화");
list.add("수");
list.add("목");
list.add("금");
List<String> newList = list.stream().filter(i -> !i.equals("수")).collect(Collectors.toList());
System.out.println(newList);
코드만 봐도 stream을 이용한 방법이 길지 않고 직관적인 것을 볼 수 있습니다. stream은 중간 처리과정도 있어 행여나 iterator에 생기는 elementException도 생기지 않는다고 해요 !
자주 사용하는 중개연산 메소드는 공식 doc 문서를 참고하도록 합시다 !
Stream (Java Platform SE 8 )
A sequence of elements supporting sequential and parallel aggregate operations. The following example illustrates an aggregate operation using Stream and IntStream: int sum = widgets.stream() .filter(w -> w.getColor() == RED) .mapToInt(w -> w.getWeight())
docs.oracle.com
4. Optional ?
[ NPE (NullPointerException) 이란? ]
개발을 할 때 가장 많이 발생하는 예외 중 하나가 바로 NPE(NullPointerException)입니다. NPE를 피하려면 null 여부를 검사해야 하는데, null 검사를 해야하는 변수가 많은 경우 코드가 복잡해지고 번거로워지죠. 그래서 null 대신 초기값을 사용하기를 권장하기도 합니다.
List<String> names = getNames();
names.sort(); // names가 null이라면 NPE가 발생함
List<String> names = getNames();
// NPE를 방지하기 위해 null 검사를 해야함
if(names != null){
names.sort();
}
[ Optional 이란 ? ]
Java8에서는 Option<T> 클래스를 사용해 NPE를 방지할 수 있도록 도와줍니다. Optional<T>는 null이 올 수 있는 값을 감싸서 Wrapper 클래스로, 참조하더라도 NPE가 발생하지 않도록 도와줍니다. Optional 클래스는 아래와 같은 value에 값을 저장하기 때문에 값이 null이더라도 바로 NPE가 발생하지 않으며, 클래스이기 때문에 각종 메소드를 제공합니다.
public final class Optional<T> {
// If non-null, the value; if null, indicates no value is present
private final T value;
...
}
4 - 1. Optional 활용하기
[ Optional.empty() - 값이 Null인 경우 ]
optional은 Wrapper 클래스이기 때문에 값이 없을수도 있는데, 이때는 Optional.empty()로 생성할 수 있습니다.
Optional<String> optional = Optional.empty();
System.out.println(optional); // Optional.empty
System.out.println(optional.isPresent()); // false
Optional 클래스는 내부에서 static 변수로 EMPTY 객체를 미리 생성해서 가지고 있어요. 이러한 이유로 빈 객체를 여러 번 생성해줘야 하는 경우에도 1개의 EMPTY 객체를 공유하기 때문에 메모리를 절약하고 있습니다.
public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>();
private final T value;
private Optional() {
this.value = null;
}
...
}
[ Optional.of() - 값이 Null이 아닌 경우 ]
만약 어떤 데이터가 절대 null이 아니라면 Optional.of()로 생성할 수 있습니다. 만약 Optional.of()로 Null을 저장하려고 하면NullPointerException이 발생합니다.
// Optional의 value는 절대 null이 아니다.
Optional<String> optional = Optional.of("MyName");
[ Optional.ofNullbale() - 값이 Null일수도, 아닐수도 있는 경우 ]
만약 어떤 데이터가 null이 올 수도 있고 아닐 수도 있는 경우에는 Optional.ofNullbale로 생성할 수 있습니다. 그리고 이후에 orElse 또는 orElseGet 메소드를 이용해서 값이 없는 경우라도 안전하게 값을 가져올 수 있습니다.
// Optional의 value는 값이 있을 수도 있고 null 일 수도 있다.
Optional<String> optional = Optional.ofNullable(getName());
String name = optional.orElse("anonymous"); // 값이 없다면 "anonymous" 를 리턴