# 제네릭
# 제네릭 타입
public class StringBox {
private String value;
public void set(String object) {
this.value = object;
}
public String get() {
return value;
}
}
- 위 클래스는 String 데이터를 넣고 빼는 역할을 한다.
- 데이터가 확장되는 경우,
Object클래스 기반으로 다형성을 구현하거나 모든 타입별 박스 클래스를 생성할 수 있다.- 타입별 박스 구현: 코드 중복 과다 발생
- 다형성: 입력 데이터에 잘못된 타입의 데이터를 넣을 가능성 존재 / 리턴 타입이 Object가 되므로 타입 다운캐스팅이 필요
- 해결되지 않는 위 두 문제를 제네릭을 활용하여 해결 가능하다.
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// 생성시점
public static void main(String[] args) {
GenericBox<Integer> integerBox = new GenericBox<Integer>();
// ..
}
- 제네릭 클래스 생성 시점에 타입 매개변수에 타입 인자를 전달해주면 된다.
- 제네릭 타입 관례는 다음과 같다.
E - ElementK - KeyV - ValueN - NumberT - TypeS,U,V- 2nd, 4rd, 4th types
class Data<K, V>()
- 타입 매개변수에는 제한이 필요할때도 있다.
public class AnimalHospitalV2<T> {
private T animal;
public void set(T animal) {
this.animal = animal;
}
public void checkup() {
// 컴파일 에러 발생
// System.out.println("동물 이름: " + animal.getName());
}
}
- 제네릭을 사용하면 어떤 타입이 전달될지 모르기 때문에 컴파일러가 animal 인스턴스에 대한 내부 정보를 알수없다.
- 타입에 대한 제한이 없기 때문에 의도와 다른
Object,Integer등의 타입들을 마음대로 전달할 수 있다. - 이러한 문제들을 해결하기 위해 타입 제한을 사용한다.
- 타입 제한은
extends키워드를 사용한다.
- 타입 제한은
public class AnimalHospitalV2<T extends Animal> {
// ...
}
- T 타입 인자로 Animal과 그 자식들만 받을 수 있도록 제한을 둔다.
- 이러한 타입 제한을 통해 컴파일러가 T가 받을 수 있는 값 범위를 예측할 수 있다.
# 제네릭 메서드
- 메서드에도 제네릭을 사용한다.
- 메서드 시그니처에서 리턴타입 전에 제네릭을 정의한다.
- 메서드 호출 시점에 타입 인자를 전달한다.
public <T> T genericMethod(T t) {
return t;
}
//
//
instance.<Integer>genericMethod(3);
- 클래스의 제네릭 타입은 제네릭 static 메서드에서 사용 불가능하다.
- 클래스 제네릭 타입은 인스턴스 생성 시점에 결정되지만,
static메서드는 클래스 단위로 동작하기 때문이다.
public class Box<T> {
void instanceMethod(T t) { }
static <Z> void staticMethod(Z t) {}
}
- 자바는 타입 추론을 지원한다.
- 제네릭 메서드가 제네릭 클래스보다 타입 매개변수 우선순위가 높다.
- 제네릭 클래스의 타입은 타입 제한을 걸고 제네릭 메서드에서는 제한을 걸지 않으면 제네릭 메서드 기반으로 타입이 고정된다.
타입 공변성
- 타입에 있어서 공변성이란,
T가S의 하위 타입이면List<T>[]도List<S>[]의 하위 타입인 성질을 의미한다. - 제네릭은 타입 공변성이 없다.
List<Number>[]와List<Integer>[]는 별개이다.
# 타입 와일드 카드
- 자바에서는 와일드 카드로 제네릭 타입을 지정할 수 있다.
?키워드를 통해 정의한다. 모든 타입을 다 받을 수 있다는 뜻이다.? == <? extends Object>로 해석할 수 있다.?만 입력한 와일드 카드는 비제한 와일드카드라고 한다.- 제네릭 타입 및 제네릭 메서드 정의가 꼭 필요하지 않으면 와일드 카드 사용을 권장한다.
static <T> void printGeneric(Box<T> box) {
// ..
}
//..
static void printWildcard(Box<?> box) {
// ..
}
- 와일드카드도 extends를 통해 타입 제한을 둘 수 있다.
Box<? extends Animal>이는 상한 와일드 카드이다.Box<? super Animal>은 하한 와일드 카드이다. Animal 타입의 상위 타입들만 전달 가능하다.
타입 이레이저 (Type Erasure)
- 자바는 제네릭을 컴파일 단계에서만 사용하고 이후에는 제네릭 정보를 삭제한다.
- 타입 매개변수에 제한을 둔 경우 제한을 둔 타입으로 코드를 변경한다.
# ArrayList
- 선언 코드는 아래와 같다.
- 고정된 데이터 크기를 관리할때, 인덱싱 위주로 동작할때 효율적이다.
import java.util.Arrays;
public static void main(String[] args) {
int[] arr = new int[5];
}
- 배열과 리스트는 구분하여 이야기한다.
- 배열: 순서가 있고 중복을 허용하지만 크기가 정적으로 고정된다.
- 리스트: 순서가 있고 중복을 허용하지만 크기가 동적으로 변한다.
- ArrayList는 리스트 자료구조를 사용하되 내부 데이터는 배열에 보관하는 자료구조이다.
- 데이터 크기가 가변적이기 때문에 중간 삽입 및 삭제 등의 연산 시
O(N)복잡도가 발생한다. - 앞이나 중간에 데이터를 추가할때 성능이 좋지 않다.
# LinkedList
- 배열은 사용하지 않는 공간이 낭비된다.
- 중간 삽입 및 삭제 성능이 좋지 않다.
- 연결 리스트를 사용하면 참조만 변경하면 되므로 중간 삽입 및 삭제 연산이
O(1)로 수행된다.
| 연산 | ArrayList | LinkedList | 비고 |
|---|---|---|---|
| 인덱스 조회 | O(1) | O(n) | ArrayList는 배열 기반으로 직접 접근 가능 |
| 검색 | O(n) | O(n) | 둘 다 순차 탐색 |
| 앞에 추가/삭제 | O(n) | O(1) | ArrayList는 요소 전체 shift 필요 |
| 뒤에 추가/삭제 | O(1) | O(n) | ArrayList는 amortized O(1), 용량 초과 시 O(n) |
| 평균 추가/삭제 | O(n) | O(n) | LinkedList는 탐색 O(n) + 삽입 O(1) |
- 자바 연결리스트는 양방향 이동이 가능한 이중 연결리스트이다.
- 끝노드의 tail노드가 head가 되므로, O(1)시간에 접근 가능하다.
- 자바 언어상으로는 내부 구현의 차이이지, LinkedList 객체에서
tail,head속성을 제공하지는 않는다.
# List
BatchProcessor라는 클래스를 구현한다고 가정하자.- 데이터
add,remove등의 메서드를 구현할때,ArrayList기반으로 삽입 추가 메서드가 구현되어 있다. - 이때 내부 성능이 크게 저하되어 삽입 추가 메서드를
LinkedList기반으로 구현하려고 한다. - 만약
BatchProcessor클래스가 ArrayList클래스에 직접 의존하고 있었다면 내부 모든 코드와 구현을 LinkedList기반으로 전환해야 한다. - 이러한 번거로움을 줄이기 위해
ArrayList와LinkedList의 공통 함수를 추출하여 인터페이스로 빼두고, 다형성 기반으로 BachProcessor 내부 데이터 보관소 타입을 자유롭게 변경할 수 있다.- 리스트 기반 동작을 자바에서는
List인터페이스로 추출해두었다.
- 리스트 기반 동작을 자바에서는
- 이러한 구조를 외부에서 의존관계가 결정되어 인스턴스에 주입하는 것 같다고 하여, 의존관계 주입이라고 표현한다.
컴파일 타임 의존관계 vs 런타임 의존관계
- 컴파일 타임 의존관계: 자바 컴파일러가 보는 의존관계이다.
- 클래스에 바로 보이는 의존관계
- 실행하지 않은 소스 코드에 정적으로 나타나는 의존관계
- 런타임 의존관계: 실제 프로그램이 작동할 때 보이는 의존관계
- 인스턴스 간의 의존관계
- 실행 중 계속 변할 수 있다.
# Hash
- Set은 유일한 요소들의 컬렉션이다.
- 유일성을 보장한다.
- 순서는 보장하지 않는다.
- 조회가 빠르다.
- Set 내부 구현을 배열로 할 경우,
contains,add,remove구현을 할때 모든 동작에 대해O(n)복잡도를 갖는다. 이렇게 되면 성능이 매우 떨어진다.- 동적 배열이더라도 사용되지 않는 공간 최적화가 이루어지지 않으면 메모리가 낭비되기 쉽다.
- 해시 알고리즘을 사용하면 조회 성능이
O(1)으로 크게 개선된다.- 내부 데이터가 배열이라고 가정할때 int 데이터를 10으로 나머지 연산을 한 결과가 인덱스라고 가정해보자.
- 이 경우 값을 보고 인덱스를 즉시 알 수 있기 때문에 조회를 즉시 할수있다.
- 그러나 나머지 연산 시 동일한 값이 산출되면 해시의 충돌이 발생하게 된다.
- 해시 충돌 해결을 위해 2차원 배열을 활용할 수 있다.
- 이렇게 구현했을때 최악의 경우
O(n)의 조회 성능이 발생할 수 있다. - 해시 충돌은 발생 가능하지만 확률은 낮은 현상으로 본다.
- 이렇게 구현했을때 최악의 경우
- 통계학적으로 입력 데이터 수가 배열 크기의 75%를 넘지 않으면 해시 충돌 확률은 낮다고 본다.
- 해시의 시간복잡도는 다음과 같다.
- 데이터 저장
- 평균: O(1)
- 최악: O(N) / 해시충돌
- 데이터 조회
- 평균: O(1)
- 최악: O(N) / 해시 충돌
- 데이터 저장
- 조회를 위한 키값이 문자열인 경우
hashCode메서드와 같이 문자열 값을 특정 해시값으로 변경하여 조회한다.
해시 함수
- 해시 함수는 임의 길이의 데이터를 입력받아 고정 길이의 해시값을 출력하는 함수이다. (int의 경우 4byte를 의미하는 고정 길이)
- 해시 함수는 같은 데이터 입력시 항상 같은 해시 코드가 출력된다.
- 다른 데이터를 입력해도 같은 해시 코드가 출력될 수 있다. (해시 충돌)
- 클래스타입을 비롯한 수많은 타입들에 대해 해시 코드는
Object.hashCode()함수에서 얻을 수 있다. - 객체 참조값을 기반으로 해시 코드를 생성하는데, 인스턴스가 다르면 해시코드도 다르다.
- 해시 자료구조는
equals를 반드시 오버라이딩 해야한다.- 해시 충돌 시 동일 인덱스 내의 모든 데이터를 조회해보아야 하기 때문이다.
hashCode를 오버라이딩 하지 않은 클래스 인스턴스는 기본 Object타입의 hashCode 메서드를 사용하게 된다.- 참조 기반으로 동작하기 때문에, 프로퍼티에 id값이 있다고 가정할때 해당값이 동일해도 다른 해싱을 할 여지가 존재한다.
- 자바에서 제공하는 해시함수는 충돌 확률이 적고, 균일하게 분포한다.