티스토리 뷰
제네릭
제네릭이란 무엇인가?
제네릭은 간단히 말해 데이터 타입을 명시하지 않은 상태를 말한다.
쉽게 생각해보면 클래스의 데이터 타입을 미리 정의하지 않고, 클래스가 인스턴스화 되는 시점에 데이터 타입을 지정해주는 방식이다.
제네릭은 <>
를 활용하여 구현한다.
제네릭을 사용하는 이유는 무엇인가?
제네릭을 사용하는 이유 즉, 장점을 살펴보겠다.
- 제네릭을 활용하면 강제적인 타입 변환이 발생하지 않아 성능 저하를 방지할 수 있다.
- 중복 코드를 제거하고 코드의 재사용성을 증진시킨다.
- 컴파일 시에 타입 오류를 체크하여 안정적으로 데이터 타입을 체크할 수 있다.
하나씩 코드와 함께 살펴보자.
- 제네릭을 활용하면 강제적인 타입 변환이 발생하지 않아 성능 저하를 방지할 수 있다.
제네릭을 사용하지 않는 코드를 확인해보자
public class Book {
private Object obj;
public Object get() {
return obj;
}
public set(Object obj) {
this.obj = obj;
}
}
//main
public class Main {
public static void main(String [] args) {
Book book = new Book();
book.set("제네릭 공부"); //Object = String으로 UpCasting 선행작업
String str = book.get(); //컴파일 에러 발생 (String)book.get(); 캐스팅 필요
System.out.println(str);
}
}
컴파일 에러가 발생한 이유는, book에서 꺼내온 값은 Object 타입의 변수이므로 String으로 타입 형변환을 해줘야한다.
이 때, DownCasting 작업이 왜 성능저하를 일어나는지 궁금해졌다.
- DownCasting을 하게 되면 JVM은 런타임 때 Object -> String(다운 캐스팅) 작업과 ClassCastException을 확인해주는 2가지 작업을 진행하게된다.
- 이런 작업을 반복적으로 진행하여 성능저하가 일어나는 것이다.
제네릭을 사용한 코드를 보자.
public class Book<T> {
private T t;
public T get() {
return t;
}
public set(T t) {
this.t = t;
}
}
//main
public class Main {
public static void main(String [] args) {
Book<String> book = new Book<String>();
book.set("제네릭 공부");
String str = book.get(); //강제 캐스팅을 안하므로 성능 저하 방지
System.out.println(str);
}
}
위 처럼 의 제네릭을 활용하여 Book 클래스를 정의하고, Book 클래스를 인스턴화 할 때 String 타입으로 정해주므로 강제적인 타입 형변환은 발생하지 않는다.
- 중복 코드를 제거하고 코드의 재사용성을 증진시킨다.
만약 제네릭을 사용하지 않고, Book의 String 타입뿐만 아니라 Integer, Boolean 등의 Wrraper Class를 사용하려고 할 시 타입만 다르고 내용은 같은 Book 클래스를 여러개 만들어야 한다.
제네릭을 사용함으로써, 이런 중복 코드 또한 제거할 수 있는 것이다.
- 컴파일 시에 타입 오류를 체크하여 안정적으로 데이터 타입을 체크할 수 있다.
class MyList<T> {
private Object [] obj;
private int index;
public MyList() {
obj = new Object[10];
index = 0;
}
public void add(T t) {
this.obj[index++] = t;
}
public T get() {
return (T) obj[index--];
}
}
public class CompileTest {
public static void main(String[] args) {
MyList<String> myList = new MyList<String>();
myList.add("제네릭");
myList.add("스터디");
myList.add(1); //컴파일 오류 발생
}
}
이 처럼 Collection을 만들 때, 제네릭을 사용할 시 개발자가 원하는 타입의 데이터가 아닌 다른 데이터가 들어가면 컴파일 오류가 발생하게 된다.
즉, 안전하게 해당 타입에 맞는 List를 만들 수 있어 버그나 에러를 줄일 수 있다.
제네릭에 대해서 좀 더 알아보자
제네릭 멀티 타입
말 그대로 제네릭을 2개 이상 사용하는 것을 말한다.
class Multi<T, E> {
private T t;
private E e;
public void setFirst(T t) {
this.t = t;
}
public void setSecond(E e) {
this.e = e;
}
public void print() {
System.out.println(t);
System.out.println(e);
}
}
public class MultiGeneric {
public static void main(String[] args) {
Multi<String, Integer> multi = new Multi<>();
multi.setFirst("하이");
multi.setSecond(222);
multi.print();
}
}
제네릭 메소드와 와일드 카드
제네릭 메소드는 클래스 전체가 아니라 하나의 메소드에 대해서만 제네릭을 선언하고 싶을 때 사용도가 높다.
소스를 통해 살펴보자
@Test
public void sampleCode1() {
List<Integer> integerList = Arrays.asList(1, 2, 3);
printList1(integerList);
printList2(integerList);
}
//제네릭 메소드
static <T> void printList1(List<T> list) { // 제네릭 메소드, 여기서 static 바로 옆에 <T>가 제네릭 메소드라는 것을 알리는 시그니쳐입니다.
list.forEach(System.out::println);
}
//와일드 카드를 사용한 일반 메소드
static void printList2(List<?> list) {
list.forEach(System.out::println);
}
두개의 메소드 기능을 살펴보면 완전히 똑같다... 그렇다면 제네릭 메소드와 와일드 카드의 차이점은 없는 것인가?
다음 소스를 확인해보자
//일반 메소드
public static void peekBox(Box<Object> box) {
System.out.println(box);
}
//제네릭 메소드
public static <T> void peekBox(Box<T> box) {
System.out.println(box);
}
두 메소드를 살펴보면 둘다 정상적으로 돌아갈 거 같지만 틀린 생각이다!
Box<String>과 Box<Integer>를 넘기고 싶을 시 제네릭 메소드에 넘겨야지만 제대로 실행된다.
why? Box<Object>와 Box<String>은 아예 다른 타입이며 Box<Object>
그렇다면 Box<?>의 경우는 어떤가?
//제네릭 메소드
public static <T> void peekBox(Box<T> box) {
System.out.println(box);
}
//와일드 카드가 파라미터인 일반 메소드
public static void peekBox(Box<?> box) {
System.out.println(box);
Box<?>는 제네릭 메소드와 마찬가지로 Box<String>과 Box<Integer>를 넘길 수 있다.
그럼 이제 여기서 의문이 들수 있다. Java는 같은 기능을 사용하는 제네릭 메소드와 와일드 카드를 두었는지 말이다
한마디로 정의를 하자면,
제네릭 : 지금은 이 타입을 모르지만, 이 타입이 정해지면 그 타입 특성에 맞게 사용할 것이다.
- 제네릭 타입에 관련된 파라미터를 받는 메소드들도 사용할 수 있다.
와일드 카드 : 나는 전혀 관심이 없다. 즉, 지금도 이 타입을 모르고 앞으로도 모를 것이다.
- 와일드 카드 타입에 관련된 파라미터를 받는 메소드들은 사용할 수 없다.
@Test
public void sampleCode2() {
List<Integer> integerList = Arrays.asList(1, 2, 3);
printList1(integerList);
printList2(integerList);
}
static <T> void printList2(List<T> list) {
//1. Object에 정의되어 있는 기능도 사용하겠다. equals(), toString()...
//2. 제네릭은 list에 담긴 타입에 관심을 갖기 때문에 타입과 관련된 add 메소드를 사용할 수 있다.
//3. 당연히 null도 들어갈 수 있다.
list.add(list.get(1)); // 컴파일 성공
}
static void printList1(List<?> list) {
//1. 단지 Object에 정의되어 있는 기능만 사용하겠다. equals(), toString()...
//2. 와일드 카드는 list에 담긴 타입에는 전혀 관심이 없다.
//3. 그로 인해, List 인터페이스에서 파라미터가 없는 size(), clear()만 사용하고, list에 담긴 타입에 관련된 파라미터를 받는 add(..)나 addAll(..)은 사용하지 않는다.
//단, null은 들어갈 수 있다.
list.add(list.get(1)); // 컴파일 실패
}
질문
컬렉션 API에서 제네릭을 어떻게 사용하는가?
- 클라이언트가 때에 따라 원하는 타입을 삽입할 수 있도록 한다. 그 이유는 컴파일 시점에 명시적으로 타입을 지정하여 해당하는 데이터를 받을 수 있기 때문이다.
- 또한, 제네릭을 사용하여 자동 캐스팅이 되기 때문에 형 조작을 위한 별도의 작업을 방지할 수 있다.
타입의 변화는 제네릭에 어떻게 영향을 미치는가?
public class A {...}
public class B extends A {...}
B는 A의 하위 클래스지만, List<B>는 List<A>의 하위 클래스가 아니다. B가 A를 상속받는다고 해서 List<B>가 List<A>를 상속받는 것은 아니다!
why? 본체가 List라는 것을 기억해라. <A>와<B>는 단지 List의 서브타입일 뿐이다.
이런 오류를 해결하기 위해 우리는 와일드 카드를 사용할 수 있다.
public static Stack<A> pushAll(final List<? extends A> list)
이로 인해 우리가 원하는 List 서브타입을 상속 개념을 받을 수 있는 것이다. 즉, A를 포함한 A 자손들을 서브타입으로 받을 수 있다. 당연히 B도 가능한 것이다.
구상화한다는 건 어떤 의미인가?
- 제네릭은 컴파일러가 컴파일 시점에 제네릭의 모든 타입 정보를 확인하기 때문에, 제네릭 타입에 맞는 형으로 변형시켜준다는 의미이다.
참고
https://vvshinevv.tistory.com/m/55
'Java > Java 기초' 카테고리의 다른 글
[Java 기초] 상속과 구성 (2) | 2020.01.04 |
---|---|
[Java 기초] 오버로딩 (0) | 2020.01.03 |
[Java 기초] enum (0) | 2020.01.01 |
[Java 기초] 변수의 Scope와 static (0) | 2019.12.31 |
[Java 기초] 팩토리 메서드 패턴 (0) | 2019.12.14 |
- Total
- Today
- Yesterday
- 이펙티브 자바
- 빈 순환 참조
- @Lazy
- 자바8
- java
- 연관관계
- java8
- 이펙티브자바
- junit
- 김영한
- try with resources
- 생성자
- package-private
- ifPresent
- 복사 팩토리
- 인프런
- Effective Java
- try catch finally
- mustache
- 팩토리 메소드 패턴
- effectivejava
- flatMap
- 점층적 생성 패턴
- springboot
- 스프링부트
- 빌더 패턴
- Spring
- 정적팩터리메서드
- JPA
- jdk버전
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |