Java의 정석_기초편

지네릭스란 & 타입 변수

DJDU 2022. 11. 7. 14:39

지네릭스(Generics)

  • 컴파일 시 타입을 체크해주는 기능(compile-time type check) - JDK1.5
  • 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여줌
더보기

컴파일 시 타입을 체크해주는 기능(compile-time type check) - JDK1.5

 

그동안에도 컴파일 시 타입 체크가 가능하긴 했지만 '한계'가 분명히 있었다. 그 한계를 넘어서게 해주는 것이 '지네릭스'이다. 

 

 

객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여줌

  • 타입의 안정성 ↑ 
  • 형변환의 번거로움 ↓ - 코드 간결

 

 

형변환의 번거로움을 줄여준다는 건, 원래는 꺼낼 때 get(0)가 Object 타입을 반환하기 때문에 (Tv) 타입으로 형변환을 해주어야 한다. 그런데 지네릭스를 사용해서 ArrayList클래스에 대해 <Tv>타입을 지정해주면. 들어있는 게 Tv밖에 없으니까 꺼내도 당연히 Tv타입만 나온다. 따라서 형변환할 필요가 없다.

 사실은 컴파일러가 '형변환'을 해주기 때문에 안 써도 되는 것이다. 어쨌든, 형 변환 하나 안 쓰는게 코드를 굉장히 간결하게 만들어준다. 


지네릭스의 장점

  1. 타입 안정성을 제공한다.
  2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

 

1. 타입 안정성을 제공한다.

컴파일러한테 타입 정보를 제공해줘서 타입 체크 기능을 강화한다. 지정된 타입 이외의 타입이 저장되는 것을 막을 수 있다. 
 ClassCastException(형 변환 에러)은 형변환을 잘못하면 발생하는 에러이다. 지네릭스토 타입을 지정해줌으로써 이 실행 시 에러를 방지할 수 있다. 

2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

게다가 코드를 간결하게 만들 수 있다. 이는 부수적으로 얻게 되는 장점이다.

 

예제GenericTest

더보기

예제GenericTest.java

코드

import java.util.ArrayList;

public class GenericTest {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(10);
        list.add(20);
        list.add("30");  // String을 추가
        
        System.out.println(list);
    }
}

결과

[10, 20, 30]

 

사실은 ArrayList에 '숫자'만 저장하고 싶었는데 문자열("30")을 추가했다.

 

Integer 타입의 값을 I에 저장해야 하는데 Object타입이기 때문에 '컴파일 에러'가 발생했다.
 Integer 타입으로 형변환해주자.(Cast to 'java.lang.Integer')

 

컴파일 에러가 사라졌다.

프로그램을 실행해보자.

Exception in thread "main" java.lang.ClassCastException: 
class java.lang.String cannot be cast to class java.lang.Integer 
(java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
	at ch12.GenericTest.main(GenericTest.java:12)

ClassCastException

ClassCastException - 형변환 에러가 발생한다. 컴파일할 때는 에러가 발생하지 않는데, 실행 시 에러가 발생한다. 왜 이런 현상이 발생하냐면, 컴파일러의 한계 때문이다.

 

컴파일 에러가 실행 시 에러보다 낫다. 실행 시 에러는 프로그램이 죽기 때문이다. 그래서 가능하면 컴파일 때 체크하는 게 좋은데, 컴파일러의 한계가 있어서, 컴파일러가 못 잡는 에러에 대해 실행 시 에러가 발생하는 것이다.

 실행 시 에러를 어떻게 하면 컴파일러가 잡아낼 수 있을까 에 대한 고민에 결과가 '지네릭스'이다.

 

 

ArrayList에 ArrayList<Integer>라 적어줌으로써 타입 정보를 제공한다. 컴파일러에게 더 많은 정보를 제공하는 것이다. 
 이렇게 하면 컴파일러가 체크를 해준다. 실수로, list.add("30");을 작성하면 컴파일러가 체크를 해주는 것이다. 그래서 잘못된 타입이 들어가는 것을 막아준다. 이걸 고치면 된다. 

코드

package ch12;

import java.util.ArrayList;

public class GenericTest {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(10);    // list.add(new Integer(10)); - 오토박싱
        list.add(20);   
        list.add(30);    // '지네릭스' 덕분에 '타입 체크'가 강화됨

//      Integer i = (Integer)list.get(2); // 컴파일 OK
        Integer i = list.get(2); // 형변환 생략 가능

        System.out.println(list);

    }
}

결과

[10, 20, 30]

지네릭스의 장점은 형변환을 생략할 수 있다는 것이다.

 

ArrayList<Object>

옛날 처럼 여러 종류의 객체를 저장하고 싶으면 다음과 같이 코드를 작성하면 된다.

ArrayList<Object> list = new ArrayList<Object>();

Object 자손들이 다 들어갈 수 있기 때문이다. 
 대신에, 반드시 형변환을 해주어야 한다.

//  Integer i = (Integer)list.get(2); // 컴파일 OK
    String i = (String)list.get(2);   // 형변환 생략 불가
        ArrayList<Object> list = new ArrayList<Object>();
        list.add(10);    // list.add(new Integer(10)); - 오토박싱
        list.add(20);
        list.add("30");    // '지네릭스' 덕분에 '타입 체크'가 강화됨

//      Integer i = (Integer)list.get(2); // 컴파일 OK
        String i = (String)list.get(2); // 형변환 생략 불가

질문 | 그렇다면 예전처럼 쓰면 되지, 뭐하러 ArrayList<Object>라고 쓰는 거지?

JDK1.5 도입되기 전에는 위 말처럼 사용했다.

ArrayList list = new ArrayList();  // JDK1.5이전

JDK1.5 이후에는 반드시 아래와 같이 작성해야 한다. 위처럼 작성하면 안 된다.

ArrayList<Object> list = new ArrayList<Object>();  // JDK1.5이후

 

에러가 발생하진 않지만, 좋은 코드가 아니다. 지금까지 '지네릭스'를 안 배웠기 때문에 그냥 작성했는데, 12장 이후로는 위와 같이 작성해야 한다. 

지네릭 클래스

모든 클래스에 대해 이렇게 써야 하는 건 아니다. 지네릭 타입을 써주어야 하는 클래스들이 따로 있다.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

ArrayList클래스 선언문을 보면, 옛날에는 <E>가 없었다. 

  1. ArrayList         : 일반 클래스
  2. ArrayList<E>  : 지네릭 클래스

모든 클래스가 다 바뀐 건 아니고, 클래스 안에 Object타입이 있는 것들은 일반 클래스에서 지네릭 클래스로 바뀌었다. 그래서 지네릭 클래스를 사용할 때 '타입'을 <E>처럼 지정해주어야 한다. 

 

지네릭스 탄생 배경

더보기

RuntimeException 상속계층도

Exception : Runtime Error, 실행 시 발생하는 에러(프로그램 종료) 

RuntimeException : 프로그래머 실수로 발생 에러

ClassCastException : 형 변환 에러

NullPointerException : 참조변수 null

IndexOutOfBoundsException : 배열 범위 벗어남

어떻게 하면 Runtime Error를  Compile time Error로 끌어올 수 있을까?

 프로그램을 실행하기 전에 수정할 수 있게 하기 위함이다. 문서 작성 중 Runtime Error가 발생하면 작성한 문서를 수정할 기회도 없이 종료된다. 그런 것 보다는 프로그램을 개발할 때 Compile time Error가 확인되면 수정할 수 있다.
 

ClassCastException

ClassCastException은 compile time에 '타입 정보'를 제공함으로써 끌고 올 수 있다. 그게 바로 '지네릭스'이다. compile time에서 체크할 수 있게 실행 중 발생하는 '형 변환'을 '컴파일 타임'으로 끌고 오는 것이다. compile time에서 체크할 수 있으면 실행 중 에러가 덜 발생할 것이다. 

 

NullPointerException

String str = null;

위와 같이 초기화를 하지 않고, 다음과 같이 초기화해야 더 좋은 코드이다.

String str = "";

str.length()를 호출해서 문자열 길이를 얻는 경우를 생각해보자. null인 경우에는 null.length()을 하면 NullPointerException이 발생한다. 반면 빈 문자열인 경우, "".length()를 호출해도 NullPointerException이 발생하지 않는다. 0을 반환한다. 따라서 1번보다 2번처럼 초기화하는 것이다.

배열의 경우도 마찬가지이다.

// Object[] objArr = null;           // 안 좋은 코드.

   Object[] objArr = new Object[0];  // 좋은 코드. 길이가 0인 배열
// Object[] objArr = {};             // 위 코드와 동일한 의미.

 

정리

실행 중 NullPointerException을 덜 발생하게 하려고 이와 같이 코드를 작성하는 것이다. Runtime에러가 발생하는 것보다 Compiletime에러를 발생하게 해서 우리가 수정할 수 있게 만드는 것이 제일 좋다. 프로그래머 실수로 발생할 수 있는 에러인RuntimeException 중에서 어떤 것들을 compiletime으로 끌어올 수 있겠는가 고민하는 것이다. 그 중 하나가 ClassCastException은 우리가 compiletime에 '타입 정보'를 추가해주면 compiletime에 체크해줄 수 있겠다 싶어서 나온 것이 '지네릭스'이다.

 


타입 변수

  • 지네릭 클래스 작성 시, Object타입 대신 타입 변수(E)를 선언해서 사용.
더보기

지네릭 클래스 작성 시, Object타입 대신 타입 변수(E)를 선언해서 사용

 

타입 변수에 대입하기

  • 객체를 생성 시, 타입 변수(E) 대신 타입(Tv)을 지정(대입)
  • 타입 변수 대신 실제 타입이 지정되면, 형 변환 생략가능
더보기

객체를 생성 시, 타입 변수(E) 대신 타입(Tv)을 지정(대입)

// 타입 변수 E 대신에 실제 타입 Tv를 대입
ArrayList<Tv> tvList = new ArrayList<Tv>();

원래 정의된 ArrayList<E>();에 실제 타입인 <Tv>를 대입한다.
메서드 호출할 때 매개변수에 값을 넘겨주는 것처럼, 타입 변수에 타입을 대입한다.

 

타입변수의 실제 타입을 '참조변수'와 '생성자'에 넣어준다.

참주변수의 타입과 생성자의 타입이 일치해야 한다.

타입 변수 대신 실제 타입이 지정되면, 형 변환 생략가능

 

예제GenericTest2

더보기

GenericTest2