티스토리 뷰

Java/Java 기초

[Java 기초] 제네릭

메성 2019. 12. 14. 22:41
반응형

제네릭

 

제네릭이란 무엇인가?

제네릭은 간단히 말해 데이터 타입을 명시하지 않은 상태를 말한다.

쉽게 생각해보면 클래스의 데이터 타입을 미리 정의하지 않고, 클래스가 인스턴스화 되는 시점에 데이터 타입을 지정해주는 방식이다.

제네릭은 <>를 활용하여 구현한다.

 

제네릭을 사용하는 이유는 무엇인가?

제네릭을 사용하는 이유 즉, 장점을 살펴보겠다.

  1. 제네릭을 활용하면 강제적인 타입 변환이 발생하지 않아 성능 저하를 방지할 수 있다.
  2. 중복 코드를 제거하고 코드의 재사용성을 증진시킨다.
  3. 컴파일 시에 타입 오류를 체크하여 안정적으로 데이터 타입을 체크할 수 있다.

 

하나씩 코드와 함께 살펴보자.

 

  • 제네릭을 활용하면 강제적인 타입 변환이 발생하지 않아 성능 저하를 방지할 수 있다.

제네릭을 사용하지 않는 코드를 확인해보자

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<Integer> 또한 다른 타입이기 때문이다.

즉, Box<Object>와 Box<String> 그리고 Box<Integer>는 상속관계가 성립되지 않는 것이다.

 

그렇다면 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

 

[2편] 제네릭이란?

이번 챕터의 중요한 내용은 제네릭과 와일드 카드의 차이점이다. 이것 저것 많은 내용을 참고해서 정리해보았다. 제네릭과 상속에 대한 개념을 알고 아래 내용을 보도록 하자. 해당 글은 effective java의 “..

vvshinevv.tistory.com

 

반응형

'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
링크
«   2024/04   »
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
글 보관함