Java 중급 핵심: 제네릭, 컬렉션 프레임워크, 반복자
김영한의 실전 자바 중급 2편에서 학습한 내용을 정리한 글입니다.
제네릭
제네릭이 필요한 이유
제네릭을 사용하면 코드 안정성과 다형성을 확보할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//제네릭 클래스
//T는 "타입 매개변수"라고 함
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
생성 시점에서 T의 타입을 결정해준다.
1
2
3
4
GenericBox<Integer> integerBox = new GenericBox<>(); //생성 시점에 T의 타입 결정
integerBox.set(10);
//integerBox.set("문자100"); // Integer 타입만 허용, 컴파일 오류
Integer integer = integerBox.get(); // Integer 타입 반환 (캐스팅 X)
- 제네릭은 자바 컴파일러가 지정한 타입으로 코드가 있다고 가정하고 컴파일 시 타입 정보를 반영한다. 실제로 해당 타입의 코드가 생기는 것은 아니다.
- 타입인자는 기본형은 사용할 수 없다 (int 등)
- 만약 다이아몬드(<>) 를 사용하지 않으면 원시타입(Object)이 된다. 과거 코드와 호환을 위해 보통 사용하는데 안정적이지 않기에 사용을 권장하지 않는다
제네릭 용어와 관례
- 메서드는 매개변수에 인자를 전달해서 사용할 값을 전달한다.
- 제네릭 클래스는 타입 매개변수에 타입 인자를 전달해서 사용할 타입을 결정한다.
- 제네릭 타입은 일반적으로 대문자를 사용한다. 보통 용도에 맞는 첫글자를 사용한다
- E - Element
- K - Key
- N - Number
- T - Type
- V -Value
- S, U, V - 2nd, 3rd, 4th type
타입 매개변수 제한
extends로 Object 타입 매개변수를 Aniaml로 제한하여 공통 기능들을 제공할 수 있다.
1
2
public class AnimalHospitalV3<T extends Animal> {
제네렉 메서드
- 반환값 앞에
<T>
로 제네릭 메서드라고 명시한다.
1
2
3
4
5
public static **<T>** T genericMethod(T t) {
System.out.println("generic print: " + t);
return t;
}
- 메서드를 호출할 때 어떤 타입으로 호출할지 지정한다.
1
2
3
Integer result = GenericMethod**.<Integer>**genericMethod(param)
//타입 인자 추론으로 코드 <Integer> 생략가능
Integer result = GenericMethod**.**genericMethod(param)
- 제네릭 메서드는 인스턴스 메서드와 static 메서드 모두 적용이 가능하다.
- 제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다.
- 제네릭 타입은 객체를 생성하는 시점에 타입이 정해지는데 static 메서드는 클래스 단위로 동작하기 때문에 제네릭 타입과 무관하다. static 메서드에 제네릭을 사용하려면 제네릭 메서드를 사용해야한다.
- 제네릭 타입과 마찬가지로 타입 매개변수를 제한할 수 있다
- ex)
extends Number
- ex)
제네릭 타입과 제네릭 메서드의 우선순위
- 제네릭 메서드과 제네릭 타입이 동일한 T를 사용한다면 제네릭 메서드가 제네릭 타입보다 우선순위가 높다
- 이름이 모호한 것은 좋지 않기 때문에 이름이 중복되지 않도록 사용한다.
와일드카드
이미 만들어진 제네릭 타입을 쉽게 사용할 수 있게 도와준다
- 비제한 와일드 카드 :
?
만 사용해 제한 없이 모든 타입을 받을 수 있는 와일드 카드 - 상한 와일드 카드 :
? extends 상한
- 상한과 하위 타입만 입력을 받는다. 다른 타입은 컴파일 에러를 발생시킨다.
- 하한 와일드 카드 :
? super 상한
- ? 가 상한보다 상위 타입이여야 한다.
메더스의 타입들을 특정 시점에 변경하려면 제네릭 타입 또는 제네릭 메서드를 사용해야한다. 그 외 경우엔 와일드 카드를 사용하는 것을 권장한다.
타입 이레이저
- 컴파일 단계에서 제네릭 타입으로 변환하여 코드를 확인한다.
- 컴파일 후 .class에서는 타입매개변수
T
가Object
로 된다. main에서는 매개변수가 필요할 때마다 타입 캐스팅 코드를 추가해준다. - 만약 상한 제한이 걸려 있는 경우엔 컴파일러가 반환 타입으로 타입 캐스팅을 해준다.
- 자바 제네릭 파일은 컴파일 시점에만 존재하고, 런타임 시에는 제네릭 정보가 없어지는데 이를 타입 이레이저라고 부른다.
한계
런타임에서 타입을 활용하는 instance of
키워드나 생성자는 사용할 수 없다.
- 타입 이레이저 때문에 런타임 시점에 instance of Object, new Object() 으로 변해버린다. 이는 개발자가 의도한 코드가 아니다.
Collection 프레임워크
Collection 인터페이스
- 자바에서 데이터 그룹을 다루는 메서드를 정의한다. (List, Set, Queue 등)
ArrayList
배열 내용 출력
1
Arrays.toString(arr)
배열 복사
1
Arrays.copyOf(기존배열, 새로운크기)
List
- 객체의 순서가 있는 컬렉션을 나타내며 같은 객체의 중복 저장을 허용한다. 크기가 동적으로 변하는 컬렉션을 다룰 때 사용하기 좋다.
- 이론적으론 LinkedList가 중간 삽입에 있어 더 효율적일 수 있다. 하지만 현대 컴퓨터의 메모리 접근 패턴, CPU 캐시 최적화, 메모리 고속 복사 등을 고려하면
ArrayList
가 실무에서 더 나은 성능을 보여줄 때가 많다. - LinkedList의 remove는 두 가지가 있다.
- int형만 넘기면 인덱스로 지운다.
- 만약 리스트에서 특정 숫자의 값을 가진 노드를 지우고 싶다면
remove(Integer.valueOf(value))
형태로 사용해야한다.
Set
- 중복된 요소가 없다. 이미 존재하는 요소는 추가하지 않고 무시한다.
- 순서를 보장하지 않는다.
- 요소의 유무를 빠르게 확인할 수 있다.
HashSet
Object.hashCode()
자바의 모든 객체는 자신만의 해시 코드를 표현할 수 있는 기능을 제공한다. 기본 구현은 참조값을 가지고 해시 코드를 계산한다. 이 경우 인스턴스마다 다른 값을 반환한다.
- 값이 같은 경우를 찾고 싶다면 참조값이 아닌 값으로 해시코드를 생성하도록 오버라이딩이 필요하다. (동등성 비교 시)
- 해시 자료 구조를 사용하려면 hashCode뿐만 아니라 인덱스가 충돌할 경우를 고려하여
equals
도 반드시 재정의해야 한다.
중요! 해시 자료 구조를 사용하려면 hashCode()와 equals() 를 반드시 오버라이딩해야한다. (IDE가 제공하는 오버라이딩 사용 권장)
Map
- Key는 중복이 허용되지 않기 때무에 Set 자료구조로 저장되어 있다.
- Value는 순서를 보장하지 않기 때문에 Collection으로 접근해야 한다.
- Key와 Value를 함께 가져오려면
entrySet()
을 사용한다. Entry는 키-값의 쌍으로 이루어진 객체이다.
Map의 put 메소드는 값을 입력할 때 이미 값이 있는 경우 덮어쓴다. 만약 값이 없는 경우에만 값을 넣고 싶다면 putIfAbsent
메소드를 사용한다.
자바의 HashSet구현은 내부적으로 HashMap을 사용한다. 즉, HashMap에서 value를 사용하지 않으면 HashSet이다.
Stack
자바의 Stack 클래스는 내부에서 Vector라는 구조를 사용하는데 지금은 사용되지 않은 느린 자료구조이다. 위와 같은 이유 Stack 대신 Deque를 사용을 권장한다.
Denque
Double Ended Queue의 약자로 양쪽 끝에서 요소를 추가하거나 제거할 수 있다. 일반적인 큐와 스택의 기능을 모두 포함하고 있다.
- stack
- push
- pop
- queue
- offer
- poll
구현체는 ArrayDequeu, LinkedList가 있는데 ArrayDeque
가 추가, 삭제, 조회 등 모든 면에서 더 빠르다.
그 이유는 ArrayDeque는 특별한 원형 큐 자료를 사용하기 때문이다.
Iterable, Iterator
Iterable 인터페이스의 주요 메서드
1
2
3
public interface Iterable<T> {
Iterator<T> iterator();
}
Iterator 인터페이스의 주요 메서드
1
2
3
4
5
public interface Iterator<E> {
boolean hasNext();
E Next();
}
hasNext()
: 다음 요소가 있는지 확인한다. 있으면 true, 없으면 falsenext()
: 다음 요소를 반환한다. 내부에 있는 위치를 다음으로 이동한다.
implements Iterable< >
- 순회 대상 자료구조에 Iterable달아주고, iterator를 오버라이딩한다. iterator 메소드에서는 자료구조의 반복자를 반환한다.
- Iterable을 달아주면 향상된 for문을 사용할 수 있다.
- 자바 ollection 인터페이스 상위에 Iterable 가 있어 모든 컬렉션은 Iterable과 Iterator를 사용할 수 있다.
- Map의 경우엔 Key뿐만 아니라 Value도 있어 바로 순회 할 수 없다. Key 또는 Value, EntrySet를 정하면 순회가 가능하다.
반복자 디자인 패턴
객체 지향 프로그래밍에서 컬렉션의 요소들을 순회할 때 사용 되는 디자인 패턴. 컬렉션의 내부 표현 방식을 노출시키지 않으면서 각 요소에 순차적으로 접근 할 수 있게 해준다. 구현과 독립적으로 요소들을 탐색할 수 있는 방법을 제공함으로써 코드의 복잡성은 줄이고 재사용성은 높힌다.
Comparable, Comparator
Comparator
비교자를 사용하면 두 값을 비교할 때 비교 기준을 직접 제공 할 수 있다.
1
2
3
public interface Comparator<T> {
int compare(T o1, T o2);
}
- 두 인수를 비교해서 결과 값을 반환한다
- 첫 번째 인수가 더 작으면 음수
- 같으면 0
- 첫 번째 인수가 더 크면 양수
reversed()
: compare 메소드의 결과에 -1을 곱한 것과 같다. 오름차순정렬을 reversed하면 내림차순 정렬로 바뀐다.
Comparable
직접 만든 객체를 구현할 땐 Comparable 인터페이스를 구현하고 compareTo
메소드를 오버라이딩해야한다.
Arrays.sort(array)
기본 정렬을 시도한다. 객체가 가지고 있는 Comparable 인터페이스를 사용해서 비교한다.
만약 기본 정렬이 아닌 다른 기준으로 정렬하고 싶을 땐 비교자 Comparator를 구현하고 compare
메소드를 오버라이딩한다. 생성한 Comparator를 sort 메소드의 두 번째 인자로 전달한다.
List 정렬
list.sort(비교자)로 리스트를 정렬할 수 있다
Tree 구조와 정렬
TreeSet 같은 이진 탐색 트리 구조는 데이터를 저장 시 정렬이 필수다. 따라서 TreeSet
, TreeMap
등 트리를 사용하는 자료구조는 Comparable 또는 Comparator 구현이 꼭 있어야 한다.
Collections 유틸
.of( )
: 불변 컬렉션 생성- 불변 리스트를 가변리스트로 만들고 싶으면 new + 해당 불변 컬렉션을 인자로 전달한다.
- 반대의 경우엔
unmodifiableXXX
메소드를 호출한다. Collections.emptyList()
orList.of()
: 빈 불변 리스트를 생성한다.- 자바 9 이상이면 Collections.emptyList보다는 List.of 사용을 권장한다.
- Arrays.asList도 List.of 로 대체하여 사용한다.
실무 컬렉션 프레임워크 선택 가이드
- 순서가 중요하고 중복이 허용되는 경우 : List
- 중복을 허용하지 않고, 순서가 중요하지 않은 경우 : HashSet.
- 만약 순서가 필요하면 LinkedHashSet, 정렬된 순서가 필요하면 TreeSet
- 요소의 키 - 값 쌍으로 저장하는 경우 : Map
- 순서가 중요하지 않는다면 HashMap, 순서를 유지해야하면 LinkedHashMap, 정렬된 순서가 필요하면 TreeMap
- 요소를 처리하기 전에 보관하는 경우 : Deque