코드스테이츠/section1

코드스테이츠 4주차 - 컬렉션(열거형, 제네릭, 예외처리, 컬렉션 프레임워크)

강예은 2023. 3. 11. 04:50

열거형(Enum)

 

앞서 설명했듯이, 열거형(enum)은 서로 연관된 상수들의 집합을 의미합니다.

우리가 앞서 배웠듯이, 상수변하지 않는 값을 의미하며 final 키워드를 사용하여 선언할 수 있었습니다.

열거형은 이러한 상수들을 보다 간편하게 관리할 때 유용하게 사용할 수 있는 자바의 문법 요소이며, 주로 몇 가지로 한정된 변하지 않는 데이터를 다루는데 사용합니다.

원래 JDK 1.5 이전 버전에서는 enum 문법을 지원하지 않았습니다. 따라서 여러 상수를 정의해서 사용하기 위해서는 public static final 을 통해 전역변수로 상수를 설정하여 아래와 같이 사용했습니다.

// 여러 상수를 정의하기 위한 예전 방식 
public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int FALL = 3;
public static final int WINTER = 4;

참고로, 상수에 부여된 1, 2, 3, 4의 값은 각 상수를 구분하기 위해서 사용합니다.

그런데 이렇게 정수값을 통해서 상수를 할당하면, 아래의 예시처럼 상수명이 중복되는 경우가 종종 발생할 수 있습니다.

public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int FALL   = 3;
public static final int WINTER = 4;

public static final int DJANGO  = 1;
public static final int SPRING  = 2; // 계절의 SPRING과 중복 발생!
public static final int NEST    = 3;
public static final int EXPRESS = 4;

그리고 이렇게 상수의 이름이 중복되면 컴파일 에러가 발생합니다.

이 문제는 인터페이스를 사용하여 상수를 구분함으로써 다음과 같이 일차적으로 해결할 수 있습니다.

interface Seasons {
	int SPRING = 1, SUMMER = 2, FALL = 3, WINTER = 4;
}

interface Frameworks {
	int DJANGO = 1, SPRING = 2, NEST = 3, EXPRESS = 4;
}

그러나 이 경우 중복상수명의 문제는 피할 수 있지만, 타입 안정성이라는 새로운 문제가 생깁니다.

예를 들면, 위의 예시 코드에서 Seasons.SPRING 의 정수값 1과 Frameworks.SPRING 의 정수값 2는 상수를 열거하기 위해 임의로 주어진 값이고, 그 외에 어떤 의미가 있는 값이 아님에도 아래와 같이 비교하는 코드를 작성할 수 있습니다.

if (Seasons.SPRING == Frameworks.SPRING) {...생략...}

어떤가요?

Seasons의 SPRING과 Frameworks의 SPRING은 의미적으로 다른 개념임에도 불구하고, 이 둘을 비교하면 에러가 발생하지 않기 때문에 타입 안정성이 떨어집니다.

이런 문제를 해결하기 위해서는 다시 아래와 같이 서로 다른 객체로 만들어주어야 합니다.

class Seasons {
    public static final Seasons SPRING = new Seasons();
    public static final Seasons SUMMER = new Seasons();
    public static final Seasons FALL   = new Seasons();
    public static final Seasons WINTER = new Seasons();
}

class Frameworks {
    public static final Frameworks DJANGO  = new Frameworks();
    public static final Frameworks SPRING  = new Frameworks();
    public static final Frameworks NEST    = new Frameworks();
    public static final Frameworks EXPRESS = new Frameworks();
}

위의 코드와 같이 객체를 생성해주면, 상수명 중복과 타입 안정성 문제를 모두 해결할 수 있습니다. 하지만 어떤가요?

보기에도 굉장히 코드가 길어지고, 사용자 정의 타입이기 때문에 switch문에 활용할 수 없다는 문제가 다시 발생합니다.

이런 맥락에서, 이같은 문제들을 효과적으로 해결하기 위해서 만들어진 것이 바로 enum입니다.

이제 eum을 활용한 상수 정의를 한번 살펴봅시다.

enum Seasons { SPRING, SUMMER, FALL, WINTER }
enum Frameworks { DJANGO, SPRING, NEST, EXPRESS }

어떤가요?

이처럼 enum을 사용하면 앞서 발생했던 문제들을 모두 효과적으로 해결할 수 있을 뿐 아니라 코드를 단순하고 가독성이 좋게 만들 수 있다는 장점이 있습니다. 또한 enum으로 정의한 상수는 switch문에서도 사용이 가능합니다.

이제 마지막으로 switch문을 사용할 때 public static final 을 사용하여 정의한 상수와 enum을 사용하여 정의한 상수가 각각 어떻게 작동하는 지 확인해보겠습니다.

class Seasons {
    public static final Seasons SPRING = new Seasons();
    public static final Seasons SUMMER = new Seasons();
    public static final Seasons FALL   = new Seasons();
    public static final Seasons WINTER = new Seasons();
}

public class Main {
    public static void main(String[] args) {
        Seasons seasons = Seasons.SPRING;
        switch (seasons) {
            case Seasons.SPRING:
                System.out.println("봄");
                break;
            case Seasons.SUMMER:
                System.out.println("여름");
                break;
            case Seasons.FALL:
                System.out.println("가을");
                break;
            case Seasons.WINTER:
                System.out.println("겨울");
                break;
        }
    }
}

/*
출력값 

java: incompatible types: Seasons cannot be converted to int
*/

먼저 전자의 경우, 코드를 실행하면 호환되지 않는 타입이라는 에러가 발생합니다.

그 이유는 switch문의 조건은 char, byte, short, int, Character, Byte, Short, Integer, String, enum 타입만 가능하지만, 위의 seasons 는 사용자 정의 타입이기 때문입니다.

그럼 이제 enum를 사용하는 경우, 제대로 출력이 되는지 확인해보겠습니다.

enum Seasons {SPRING, SUMMER, FALL, WINTER}

public class Main {
    public static void main(String[] args) {
        Seasons seasons = Seasons.SPRING;
        switch (seasons) {
            case SPRING:
                System.out.println("봄");
                break;
            case SUMMER:
                System.out.println("여름");
                break;
            case FALL:
                System.out.println("가을");
                break;
            case WINTER:
                System.out.println("겨울");
                break;
        }
    }
}

//출력값 
봄

위의 코드 예제처럼 enum을 사용하면 switch문을 사용할 수 있다는 것을 확인할 수 있습니다.

정리하면, 자바에서 열거형은 여러 상수들을 보다 편리하게 선언하고 관리할 수 있게하며, 상수명의 중복을 피하고, 타입에 대한 안정성을 보장합니다.

또한 같은 효과를 낼 수 있는 다른 코드에 반해 훨씬 더 간결하고 가독성이 좋은 코드를 작성할 수 있으며 switch문에서도 작동이 가능합니다.

열거형의 사용

 

이제 열거형을 어떻게 정의하고 사용할 수 있는 지 살펴보도록 합시다.

직전의 예시에서 봤었던 것처럼, 열거형을 정의하는 방법은 아주 간단합니다.

다음과 같이 코드 블럭 안에 선언하고자 하는 상수의 이름을 나열하기만 하면 됩니다.

enum 열거형이름 { 상수명1, 상수명2, 상수명3, ...}


앞서 봤었던 사계절을 예로 들어보겠습니다. enum을 사용하여 사계절을 상수로 정의하면 다음과 같습니다.

enum Seasons { 
    SPRING, //정수값 0 할당
    SUMMER,  //정수값 1 할당
    FALL, //정수값 2 할당
    WINTER //정수값 3 할당
}

참고로, 상수는 대소문자로 모두 작성이 가능하지만 관례적으로 대문자로 작성합니다.

또한 각각의 열거 상수들은 객체이기 때문에, 위의 예시에서 Seasons 라는 이름의 열거형은 SPRING, SUMMER, FALL, WINTER 는 총 네 개의 열거 객체를 포함하고 있다고 말할 수 있습니다.

마지막으로 각각의 상수들에는 따로 값을 지정해주지 않아도 자동적으로 0부터 시작하는 정수값이 할당되어 각각의 상수를 가리키게 됩니다.

그러면 이렇게 선언한 열거형을 어떻게 사용할 수 있을까요?

아래 예시를 한번 살펴보도록 합시다.

enum Seasons { SPRING, SUMMER, FALL, WINTER }

public class EnumExample {
    public static void main(String[] args) {
        System.out.println(Seasons.SPRING); // SPRING
    }
}


열거형에 선언된 상수에 접근하는 방법은 열거형이름.상수명 을 통해서 가능합니다. 앞서 배웠던 클래스에서 static 변수를 참조하는 것과 동일하다고 할 수 있습니다.

이제 내가 가장 좋아하는 계절이라는 의미의 참조변수 favoriteSeason 에 Seasons.SPRING 을 담아보도록 하겠습니다.

enum Seasons { SPRING, SUMMER, FALL, WINTER }

public class EnumExample {
    public static void main(String[] args) {
        Seasons favoriteSeason = Seasons.SPRING;
        System.out.println(favoriteSeason); // SPRING
    }
}


보시는 것처럼, Seasons.SPRING 을 Seasons 타입의 참조변수에 할당하고 있습니다.

비슷한 예제를 하나 더 살펴보도록 하겠습니다.

enum Level {
  LOW, // 0
  MEDIUM, // 1
  HIGH // 2
}

public class Main {
  public static void main(String[] args) {
    Level level = Level.MEDIUM;

    switch(level) {
      case LOW:
        System.out.println("낮은 레벨");
        break;
      case MEDIUM:
         System.out.println("중간 레벨");
        break;
      case HIGH:
        System.out.println("높은 레벨");
        break;
    }
  }
}

//출력값
중간 레벨


위의 코드예제를 보면, Level 이라는 열거형을 하나 만들고, 그 안에 세 가지의 열거 상수(LOW, MEDIUM, HIGH)을 선언해주었습니다.

그리고 열거형과 같은 타입의 참조변수 level 에 Level.MEDIUM 값을 할당하고 switch문을 통해 해당 값에 대한 출력값을 얻을 수 있었습니다.

이처럼 enum을 사용하면 변경되지 않는 한정적인 데이터들을 효과적으로 관리할 수 있습니다.

마지막으로 열거형에서 사용할 수 있는 메서드를 살펴보겠습니다.

아래 메서드들은 모든 열거형의 조상인 java.lang.Enum 에 정의되어있는 것으로, 클래스에서 최상위 클래스 Object에 정의된 메서드들을 사용할 수 있었던 것과 동일하다고 할 수 있습니다.

 

리턴
타입
메소드(매개변수) 설명
String name() 열거 객체가 가지고 있는 문자열을 리턴하며, 리턴되는 문자열은 열거타입을 정의할 때 사용한 상수 이름과 동일합니다.
int ordinal() 열거 객체의 순번(0부터 시작)을 리턴합니다.
int              compareTo(비교값) 주어진 매개값과 비교해서 순번 차이를 리턴합니다.
열거
타입
valueOf(String name) 주어진 문자열의 열거 객체를 리턴합니다.
열거
배열
values() 모든 열거 객체들을 배열로 리턴합니다.


위에서 사용했던 코드를 통해서 조금 더 알아보겠습니다.

enum Level {
  LOW, // 0
  MEDIUM, // 1
  HIGH // 2
}

public class EnumTest {
    public static void main(String[] args) {
        Level level = Level.MEDIUM;

        Level[] allLevels = Level.values();
        for(Level x : allLevels) {
            System.out.printf("%s=%d%n", x.name(), x.ordinal());
        }

        Level findLevel = Level.valueOf("LOW");
        System.out.println(findLevel);
        System.out.println(Level.LOW == Level.valueOf("LOW"));

        switch(level) {
            case LOW:
                System.out.println("낮은 레벨");
                break;
            case MEDIUM:
                System.out.println("중간 레벨");
                break;
            case HIGH:
                System.out.println("높은 레벨");
                break;
        }
    }
}

//출력값
LOW=0
MEDIUM=1
HIGH=2
LOW
true
중간 레벨


앞서 봤었던 코드에서 몇 가지 열거형 메서드를 사용한 예제입니다.

먼저 values() 메서드는 컴파일러가 자동적으로 모든 열거형에 추가해주는 메서드로 Level 에 정의된 모든 상수를 배열로 반환했습니다.

이렇게 받은 배열을 앞서 우리가 자바 기초에서 배웠던 향상된 for문과 열거형의 최상위 클래스로부터 확장된 name() 과 ordinal() 을 사용하여 각각 이름과 순서를 출력값으로 반환하고 있습니다.

마지막으로 valueOf() 메서드를 활용하여 지정된 열거형에서 이름과 일치하는 열거형 상수를 반환하고, 반환된 상수가 의도했던 상수와 일치하는 지 여부를 불리언 값으로 확인해주고 있습니다.

제네릭이란?

 

제네릭의 필요성

아래 Basket 클래스는 오로지 String 타입의 데이터만을 저장할 수 있는 인스턴스를 만들 수 있습니다. 그에 따라, 다양한 타입의 데이터를 저장할 수 있는 객체를 만들고자 한다면, 각 타입별로 별도의 클래스를 만들어야 한다

class Basket {
    private String item;

    Basket(String item) {
        this.item = item;
    }

    public String getItem() {
        return item;
    }

    public void setItem(String item) {
        this.item = item;
    }
}

하지만 아래와 같이 제네릭을 사용하면 단 하나의 Basket 클래스만으로 모든 타입의 데이터를 저장할 수 있는 인스턴스를 만들 수 있습니다. 위의 예제와 아래의 예제가 무엇이 다른가요?

class Basket<T> {
    private T item;

    public Basket(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

맞습니다. <T>가 클래스 이름 옆에 추가되었으며, 클래스 몸체 내에 String으로 지정했던 타입들이 T라는 문자 하나로 바뀌었습니다. 이 <T>T가 바로 제네릭의 문법에 해당합니다. 이것이 무엇인지에 대해서는 이어지는 콘텐츠에서 설명하겠습니다.

위의 Basket 클래스는 다음과 같이 인스턴스화 할 수 있습니다.

Basket<String> basket1 = new Basket<String>("기타줄");



예제 코드에서 Basket 클래스가 인스턴스화될 때 클래스 이름 뒤에 <String>이 따라 붙고 있습니다. 이 또한 제네릭의 문법으로, 아래와 같은 의미로 간주할 수 있습니다.

 

“Basket 클래스 내의 T를 String으로 바꿔라”



위 코드를 실행하면 Basket 클래스 내부의 T가 모두 String으로 치환되는 것처럼 동작하게 됩니다.

class Basket {
    private String item;

    Basket(String item) {
        this.item = item;
    }

    public String getItem() {
        return item;
    }

    public void setItem(String item) {
        this.item = item;
    }
}

만약, 아래와 같이 <> 안에 Integer를 넣어 인스턴스화한다면 Basket 클래스 내부의 T는 모두 Integer로 치환됩니다.

이 예제에서 Integer는 int와 같이 정수를 나타내는 타입이라고 생각해주세요. 정확히 말하면, Integer는 int의 래퍼 클래스(wrapper class)입니다. 검색을 통해 래퍼 클래스가 무엇이고, Integer와 int의 차이에 대해 스스로 공부해보세요!

Basket<Integer> basket2 = new Basket<Integer>(1);

// 위와 같이 인스턴스화하면 Basket 클래스는 아래와 같이 변환됩니다. 
class Basket<Integer> {
    private Integer item;

    public Basket(Integer item) {
        this.item = item;
    }

    public Integer getItem() {
        return item;
    }

    public void setItem(Integer item) {
        this.item = item;
    }
}

그렇다면, Basket 클래스를 아래와 같이 인스턴스화한다면 Basket 클래스 내부의 코드가 어떻게 동작할까요? 아래의 상황을 확인하고, Basket 내부의 코드가 결과적으로 어떻게 실행될지 스스로 답해보세요.

  • Basket<Boolean> basket3 = new Basket<Boolean>(true);
  • Basket<Double> basket4 = new Basket<Double>(3.14);

제네릭이란 무엇일까요?

자, 여기까지 제네릭이 왜 필요한지에 대해서 살펴보았으며, 여러분들도 충분히 제네릭의 필요성에 대해서 감을 잡으셨을거라고 생각합니다. 그렇다면, 이제 제네릭이 본질적으로 무엇인지에 대해서 알아봅시다.


제네릭(Generic)은 사전적으로 ‘일반적인’이라는 의미를 가지고 있습니다. 자바에서 제네릭이란, 위에서 살펴본 것처럼 클래스나 메서드의 코드를 작성할 때, 타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화해두는 것을 의미합니다. 즉, 작성한 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 해둔 것을 의미합니다.


제네릭은 클래스와 메서드에 사용할 수 있습니다.

 

제네릭 클래스

 

제네릭 클래스 정의

제네릭이 사용된 클래스를 제네릭 클래스라고 합니다. 앞서 익히 살펴보았던 Basket 클래스가 바로 제네릭 클래스입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Basket<T> {
    private T item;

    public Basket(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}


위의 코드에서, T를 타입 매개변수라고 하며, <T>와 같이 꺽쇠 안에 넣어 클래스 이름 옆에 작성해줌으로써 클래스 내부에서 사용할 타입 매개변수를 선언할 수 있습니다.


즉, 아래와 같이 타입 매개 변수 T를 선언하면,

1
2
3
class Basket<T> {

}


클래스 몸체에서 T를 임의의 타입으로 사용할 수 있습니다.

1
2
3
4
5
class Basket<T> {
    private T item;

    ...
}


만약, 타입 매개변수를 여러 개 사용해야 한다면, 아래와 같이 선언하면 됩니다.

1
class Basket<K, V> { ... }


타입 매개변수는 임의의 문자로 지정할 수 있습니다. 위에서 사용한 T, K, V는 각각 Type, Key, Value의 첫 글자를 따온 것입니다. 이 외에도, Element를 뜻하는 E, Number를 뜻하는 N, 그리고 Result를 뜻하는 R도 자주 사용됩니다.



제네릭 클래스를 정의할 때 주의할 점

제네릭 클래스에서 타입 매개변수를 임의의 타입으로 사용할 수 있다고 하였습니다. 이 때, 아래와 같이 클래스 변수에는 타입 매개변수를 사용할 수 없습니다.

1
2
3
4
class Basket<T> {
	private T item1; // O 
	static  T item2; // X 
}


클래스 변수에 타입 매개변수를 사용할 수 없는 이유는 클래스 변수의 특성을 생각해보면 충분히 이해할 수 있습니다. 클래스 변수는 모든 인스턴스가 공유하는 변수입니다. 만약, 클래스 변수에 타입 매개변수를 사용할 수 있다면 클래스 변수의 타입이 인스턴스 별로 달라지게 됩니다.


즉, 클래스 변수에 타입 매개변수를 사용할 수 있다면, Basket<String>으로 만든 인스턴스와, Basket<Integer>로 만든 인스턴스가 공유하는 클래스 변수의 타입이 서로 달라지게 되어, 클래스 변수를 통해 같은 변수를 공유하는 것이 아니게 됩니다. 따라서 static이 붙은 변수 또는 메서드에는 타입 매개변수를 사용할 수 없습니다.



제네릭 클래스 사용

제네릭 클래스는 멤버를 구성하는 코드에 특정한 타입이 지정되지 않은 클래스이므로, 제네릭 클래스를 인스턴스화할 때에는 의도하고자 하는 타입을 아래와 같이 지정해주어야 합니다.


단, 타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수 없습니다. 만약, int, double과 같은 원시 타입을 지정해야 하는 맥락에서는 Integer, Double과 같은 래퍼 클래스를 활용합니다.

1
2
3
Basket<String>  basket1 = new Basket<String>("Hello");
Basket<Integer> basket2 = new Basket<Integer>(10);
Basket<Double>  basket3 = new Basket<Double>(3.14);


위의 코드에서 new Basket<…>은 아래와 같이 구체적인 타입을 생략하고 작성해도 됩니다. 참조변수의 타입으로부터 유추할 수 있기 때문입니다.

1
2
3
Basket<String>  basket1 = new Basket<>("Hello");
Basket<Integer> basket2 = new Basket<>(10);
Basket<Double>  basket2 = new Basket<>(3.14);


마지막으로, 제네릭 클래스를 사용할 때에도 다형성을 적용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }

class Basket<T> {
    private T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

class Main {
    public static void main(String[] args) {
        Basket<Flower> flowerBasket = new Basket<>();
        flowerBasket.setItem(new Rose());      // 다형성 적용
        flowerBasket.setItem(new RosePasta()); // 에러
    }
}


new Rose()를 통해 생성된 인스턴스는 Rose 타입이며, Rose 클래스는 Flower 클래스를 상속받고 있으므로, Basket<Flower>의 item에 할당될 수 있습니다. Basket<Flower>은 결국 item의 타입을 Flower로 지정하는 것이고, Flower 클래스는 Rose 클래스의 상위 클래스이기 때문입니다.


반면, new RosePasta()를 통해 생성된 인스턴스는 RosePasta 타입이며, RosePasta 클래스는 Flower 클래스와 아무런 관계가 없습니다. 따라서, flowerBasket의 item에 할당될 수 없습니다.

제한된 제네릭 클래스

 

제한된 제네릭 클래스

앞서 살펴본 예제의 Basket 클래스는 인스턴스화 할 때 어떠한 타입도 지정해줄 수 있습니다. 즉, 타입을 지정하는 데에 있어 제한이 없습니다.

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
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }

// 제네릭 클래스 정의 
class Basket<T> {
    private T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

class Main {
    public static void main(String[] args) {

        // 인스턴스화 
        Basket<Rose> roseBasket = new Basket<>();
        Basket<RosePasta> rosePastaBasket = new Basket<>();
    }
}


그러나, 타입 매개변수를 선언할 때 아래와 같이 코드를 작성해주면 Basket 클래스를 인스턴스화할 때 타입으로 Flower 클래스의 하위 클래스만 지정하도록 제한됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }

class Basket<T extends Flower> {
    private T item;
	
		...
}

class Main {
    public static void main(String[] args) {
    
        // 인스턴스화 
        Basket<Rose> roseBasket = new Basket<>();
        Basket<RosePasta> rosePastaBasket = new Basket<>(); // 에러
    }
}


이와 같이 특정 클래스를 상속받은 클래스만 타입으로 지정할 수 있도록 제한하는 것뿐만 아니라, 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한할 수도 있습니다. 이 경우에도 동일하게 extends 키워드를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }

class Basket<T extends Plant> {
    private T item;
	
		...
}

class Main {
    public static void main(String[] args) {

        // 인스턴스화 
        Basket<Flower> flowerBasket = new Basket<>();
        Basket<Rose> roseBasket = new Basket<>();
    }
}


만약, 특정 클래스를 상속받으면서 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려면 아래와 같이 &를 사용하여 코드를 작성해주면 됩니다.

다만, 이러한 경우에는 클래스를 인터페이스보다 앞에 위치시켜야 합니다. 아래 예제의 (1)을 참고하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }

class Basket<T extends Flower & Plant> { // (1)
    private T item;
	
		...
}

class Main {
    public static void main(String[] args) {

        // 인스턴스화 
        Basket<Flower> flowerBasket = new Basket<>();
        Basket<Rose> roseBasket = new Basket<>();
    }
}
 

제네릭 메서드

 

제네릭 메서드

클래스 전체를 제네릭으로 선언할 수도 있지만, 클래스 내부의 특정 메서드만 제네릭으로 선언할 수 있습니다. 이를 제네릭 메서드라고 합니다.


제네릭 메서드의 타입 매개변수 선언은 반환타입 앞에서 이루어지며, 해당 메서드 내에서만 선언한 타입 매개변수를 사용할 수 있습니다.

1
2
3
4
5
6
class Basket {
		...
		public <T> void add(T element) {
				...
		}
}


제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개의 것입니다. 즉, 아래와 같이 동일하게 T라는 타입 매개변수명을 사용한다 하더라도, 같은 알파벳 문자를 이름으로 사용하는 것일 뿐, 서로 다른 타입 매개변수로 간주됩니다.

1
2
3
4
5
6
class Basket<T> {                        // 1 : 여기에서 선언한 타입 매개변수 T와 
		...
		public <T> void add(T element) { // 2 : 여기에서 선언한 타입 매개변수 T는 서로 다른 것입니다.
				...
		}
}


이는 타입이 지정되는 시점이 서로 다르기 때문입니다. 즉, 클래스명 옆에서 선언한 타입 매개변수는 클래스가 인스턴스화될 때 타입이 지정됩니다.


그러나, 제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어집니다. 제네릭 메서드를 호출할 때에는 아래와 같이 호출하며, 이 때 제네릭 메서드에서 선언한 타입 매개변수의 구체적인 타입이 지정됩니다.

1
2
3
Basket<String> basket = new Bakset<>(); // 위 예제의 1의 T가 String으로 지정됩니다. 
basket.<Integer>add(10);                // 위 예제의 2의 T가 Integer로 지정됩니다. 
basket.add(10);                         // 타입 지정을 생략할 수도 있습니다. 


또한, 클래스 타입 매개변수와 달리 메서드 타입 매개변수는 static 메서드에서도 선언하여 사용할 수 있습니다.

1
2
3
4
5
6
class Basket {
		...
		static <T> int setPrice(T element) {
				...
		}
}


제네릭 메서드는 메서드가 호출되는 시점에서 제네릭 타입이 결정되므로, 제네릭 메서드를 정의하는 시점에서는 실제 어떤 타입이 입력 되는지 알 수 없습니다. 따라서 length()와 같은 String 클래스의 메서드는 제네릭 메서드를 정의하는 시점에 사용할 수 없습니다.

1
2
3
4
5
class Basket {
    public <T> void print(T item) {
        System.out.println(item.length()); // 불가
    }
}


하지만 모든 자바 클래스의 최상위 클래스인 Object 클래스의 메서드는 사용가능합니다. 모든 클래스는 Object 클래스를 상속받기 때문입니다. 지금까지 여러분이 사용해본 equals(), toString() 등이 Object 클래스의 메서드에 속합니다.

1
2
3
4
5
class Basket {
    public <T> void getPrint(T item) {
        System.out.println(item.equals("Kim coding")); // 가능
    }
}