본문 바로가기
JAVA/이재환의 자바 프로그래밍 입문

[Java] Ch.20 제네릭

by ♡˖GYURI˖♡ 2023. 10. 24.

제네릭의 필요성

다음 코드는 종족 클래스에 종족별 유닛을 생산해서 저장하고 가져오는 것인데, 종족을 표현하는 클래스인 HumanCamp1 클래스와  MachineCamp1 클래스 구조가 똑같음 → 코드가 중복된다는 단점이 있음

class Npc1 {
    public String toString() {
        return "This is a Npc1.";
    }    
}

class HumanCamp1 {
    private Npc1 unit;

    public void set(Npc1 unit) {
        this.unit = unit;
    }

    public Npc1 get() {
        return unit;
    }
}

class Tank1 {
    public String toString() {
        return "This is a Tank1.";
    }    
}

class MachineCamp1 {
    private Tank1 unit;

    public void set(Tank1 unit) {
        this.unit = unit;
    }

    public Tank1 get() {
        return unit;
    }
}

public class Ex01_MyGame1
{
    public static void main(String[] args)
    {
        // 게임 종족 생성
        HumanCamp1 human = new HumanCamp1();
        MachineCamp1 machine = new MachineCamp1();

        // 게임 종족에 유닛을 생성해 담기 
        human.set(new Npc1());
        machine.set(new Tank1());

        // 게임 종족에서 유닛을 가져오기
        Npc1 hUnit = human.get();
        Tank1 mUnit = machine.get();

        System.out.println(hUnit);
        System.out.println(mUnit);
    }
}

 

HumanCamp1 클래스와 MachineCamp1 클래스 구조가 똑같으므로 Camp2로 합치기로 하고 어떤 자식 클래스라도 받아들일 수 있게 매개변수의 자료형을 Object로 만듦

  • 그로 인해 객체를 꺼내올 때 형변환이 필요하게 됨
  • 꺼낼 때 약간 불편하지만 코드는 잘 동작함
  • 하지만 이는 컴파일러의 오류 발견 가능성을 낮추는 결과로 이어짐
class Npc2 {
    public String toString() {
        return "This is a Npc2.";
    }    
}

class Tank2 {
    public String toString() {
        return "This is a Tank2.";
    }    
}

class Camp2 {
    private Object unit;

    public void set(Object unit) {
        this.unit = unit;
    }

    public Object get() {
        return unit;
    }
}

public class Ex02_MyGame2
{
    public static void main(String[] args)
    {
        // 게임 종족 생성
        Camp2 human = new Camp2();
        Camp2 machine = new Camp2();

        // 게임 종족에 유닛을 생성해 담기
        // 자식객체를 부모타입의 변수에 대입
        human.set(new Npc2());
        machine.set(new Tank2());

        // 게임 종족에서 유닛을 가져오기
        // 꺼낼 때 형변환이 필요함
        Npc2 hUnit = (Npc2)human.get();
        Tank2 mUnit = (Tank2)machine.get();

        System.out.println(hUnit);
        System.out.println(mUnit);
    }
}

 

class Npc3 {
    public String toString() {
        return "This is a Npc3.";
    }    
}

class Tank3 {
    public String toString() {
        return "This is a Tank3.";
    }    
}

class Camp3 {
    private Object unit;

    public void set(Object unit) {
        this.unit = unit;
    }

    public Object get() {
        return unit;
    }
}

public class Ex03_MyGame3
{
    public static void main(String[] args)
    {
        // 게임 종족 생성
        Camp3 human = new Camp3();
        Camp3 machine = new Camp3();

        // 게임 종족에 유닛을 생성해 담기
        // 우리가 만든 유닛을 넣어야 하는데....
        human.set("난 공룡");   // <-- human.set(new String("난 공룡");
        machine.set("난 우주인");

        // 게임 종족에서 유닛을 가져오기
        // 꺼낼 때 당연히 게임 유닛을 기대하는데....
        Npc3 hUnit = (Npc3)human.get();
        Tank3 mUnit = (Tank3)machine.get();

        System.out.println(hUnit);
        System.out.println(mUnit);
    }
}

우리가 만든 유닛을 생성해서 넣어줘야 하는데 스트링 객체를 생성해서 넣음

  • 매개변수가 Object 타입이고, 우리도 객체를 생성해 넣어준 것이기 때문에 문법적으로 오류는 나지 않음
  • 하지만 꺼낼 때 에러가 생김
  • 꺼내 쓰는 입장에선 당연히 게임 유닛이 들어 있을 거라고 생각하기 때문에 형변환을 하게 되고, 그 때 에러가 나게 됨

 

class Npc4 {
    public String toString() {
        return "This is a Npc4.";
    }    
}

class Tank4 {
    public String toString() {
        return "This is a Tank4.";
    }    
}

class Camp4 {
    private Object unit;

    public void set(Object unit) {
        this.unit = unit;
    }

    public Object get() {
        return unit;
    }
}

public class Ex04_MyGame4
{
    public static void main(String[] args)
    {
        // 게임 종족 생성
        Camp4 human = new Camp4();
        Camp4 machine = new Camp4();

        // 게임 종족에 유닛을 생성해 담기
        // 우리가 만든 유닛을 넣어야 하는데....
        human.set("난 공룡");
        machine.set("난 우주인");

        System.out.println(human.get());
        System.out.println(machine.get());
    }
}

에러는 발생하지 않았지만 원하는 결과가 아님

이처럼 실행을 할 때 에러가 발생하지 않으면 프로그래머는 코드에 이상이 없다고 생각할 수 있음

 

이처럼 제네릭을 적용하기 이전의 코드는 객체를 돌려받을 때 형변환을 잊지 말고 해야 한다는 불편함이 있고, 코드 진행상 프로그래머가 실수를 해도 그 실수가 드러나지 않을 수도 있다는 잠재적 위험이 존재함

 

 

제네릭 기반의 클래스 정의하기

제네릭은 클래스, 메서드에서 사용할 자료형을 나중에 확정하는 기법

클래스나 메서드를 선언할 때가 아닌 사용할 때, 즉 객체를 생성할 때나 메서드를 호출할 때 정한다는 의미

 

객체 생성 시 결정이 되는 자료형의 정보를 T로 대체

다이아몬드 연산자 <>를 통해 자료형을 전달함

 

// 제네릭을 사용하지 않는 코드
class Camp {
    private Object unit;
    
    public void set(Object unit) {
    	this.unit = unit;
    }
    
    public Object get() {
    	return unit;
    }
}
// 제네릭을 사용하는 코드
class Camp<T> {
    private T unit;
    
    public void set (T unit) {
    	this.unit = unit;
    }
    
    public T get() {
    	return unit;
    }
}

 

Camp<Npc> human = new Camp<Npc>();
Camp<Npc> human = new Camp<>();	// 뒤쪽은 추론 가능하므로 자바 7부터 생략

T를 Npc로 결정하여 인스턴스 생성

따라서 Npc 또는 Npc를 상속하는 하위 클래스의 인스턴스를 저장할 수 있음

 

Camp<Tank> machine = new Camp<>();

T를 Tank로 결정하여 인스턴스 생성

따라서 Tank 또는 Tank를 상속하는 하위 클래스의 인스턴스를 저장할 수 있음

 

▼제네릭 관련 변수 용어

용어 대상
타입 매개변수(type parameter) Camp<T>에서 T
타입 인수(type argument) Camp<Npc>에서 Npc
매개변수화 타입(parameterized type) Camp<Npc>

 

타입 매개변수 이름 규칙

▼일반적인 관례

  • 보통 한 문자
  • 대문자

▼보편적인 선택

E Element
K Key
N Number
T Type
V Value

 

 

제네릭 기반의 코드로 개선한 결과

class Npc5 {
    public String toString() {
        return "This is a Npc4.";
    }    
}

class Tank5 {
    public String toString() {
        return "This is a Tank4.";
    }    
}

class Camp5<T> {
    private T unit;

    public void set(T unit) {
        this.unit = unit;
    }

    public T get() {
        return unit;
    }
}

public class Ex05_MyGameGeneric1
{
    public static void main(String[] args)
    {
        // 게임 종족 생성
        Camp5<Npc5> human = new Camp5<>();
        Camp5<Tank5> machine = new Camp5<>();

        // 게임 종족에 유닛을 생성해 담기
        human.set(new Npc5());
        machine.set(new Tank5());

        // 게임 종족에서 유닛을 가져오기
        Npc5 hUnit = human.get();
        Tank5 mUnit = machine.get();

        System.out.println(hUnit);
        System.out.println(mUnit);
    }
}

 

class Npc6 {
    public String toString() {
        return "This is a Npc4.";
    }    
}

class Tank6 {
    public String toString() {
        return "This is a Tank4.";
    }    
}

class Camp6<T> {
    private T unit;

    public void set(T unit) {
        this.unit = unit;
    }

    public T get() {
        return unit;
    }
}

public class Ex06_MyGameGeneric2
{
    public static void main(String[] args)
    {
        // 게임 종족 생성
        Camp6<Npc6> human = new Camp6<>();
        Camp6<Tank6> machine = new Camp6<>();

        // 게임 종족에 유닛을 생성해 담기
        human.set(new Npc6());
        machine.set("난 공룡");

        // 게임 종족에서 유닛을 가져오기
        Npc6 hUnit = human.get();
        Tank6 mUnit = machine.get();

        System.out.println(hUnit);
        System.out.println(mUnit);
    }
}

이제 타입 인수로 지정한 클래스형 외에 다른 형의 객체는 대입할 수 없음

 

제네릭의 장점

  • 중복된 코드의 결합 & 간소화
  • 데이터를 가져올 때 형변환 없이 가져올 수 있음
  • 데이터 대입 시 다른 자료형이 대입되는 것 방지 → 강한 자료형 체크

 

 

매개변수가 여러 개일 때 제네릭 클래스의 정의

class Camp7<T1, T2> 
{
    private T1 param1;
    private T2 param2;

    public void set(T1 o1, T2 o2) 
    {
        param1 = o1;
        param2 = o2;
    } 
    
    public String toString() 
    {
        return param1 + " & " + param2;
    }
}

public class Ex07_MultiParameter
{
    public static void main(String[] args)
    {
        Camp7<String, Integer> camp = new Camp7<>();
        camp.set("Apple", 25);
        System.out.println(camp);
    }
}

 

 

제네릭 클래스의 매개변수 타입 제한하기

상속 관계를 표시하여 매개변수의 타입을 제한할 수 있음

class Box<T extends Number> {...}

인스턴스 생성 시 타입 인수로 Number 또는 이를 상속하는 클래스만 올 수 있게 설정한 것

이렇게 하면 Number에서 상속받은 메서드를 안전하게 사용할 수 있음

 

// 매개변수 타입을 제한하지 않은 경우
class Camp<T> {
	private T ob;
    ......
    public int toIntValue() {
    	return ob.intValue();	// Error!
    }
}
// 매개변수 타입을 제한하는 경우
class Camp<T extends Number> {
	private T ob;
    ......
    public int toIntValue() {
    	return ob.intValue();	// Ok
    }
}

위의 코드는 아무 자료형이나 들어올 수 있기에 래퍼 클래스의 메서드를 호출하면 에러가 발생함

그러나 아래처럼 제네릭에 지정할 수 있는 자료형을 Number를 상속받은 래퍼 타입만으로 한정한다면 intValue() 메서드를 사용할 때 에러 걱정을 할 필요가 없게 됨

 

class Camp8<T extends Number> 
{
    private T ob;

    public void set(T o) {
        ob = o;
    }
    public T get() {
        return ob;
    }
}

public class Ex08_BoundedCamp
{
    public static void main(String[] args)
    {
        Camp8<Integer> iBox = new Camp8<>();
        iBox.set(24);

        Camp8<Double> dBox = new Camp8<>();
        dBox.set(5.97);

        System.out.println(iBox.get());
        System.out.println(dBox.get());
    }
}

 

 

제네릭 메서드의 정의

클래스 전부가 아닌 메서드 하나에 대해서도 제네릭으로 정의할 수 있음

class MyData
{
	public static <T> T showData(T data)
    {
    	if (data instanceof String)
        	System.out.println("String");
        else if (data instanceof Integer)
        	System.out.println("Integer);
        else if (data instanceof Double)
         	System.out.println("Double");
        return data;
    }
}

 

제네릭 메서드의 T는 메서드 호출 시점에 결정됨

MyData.<String>showData("Hello World!");

 

다음과 같이 타입 인수 생략이 가능함

생략된 인수는 매개변수로 들어온 데이터의 자료형으로 추론하게 됨

MyData.showData(1);

 

class MyData 
{
    public static <T> T showData(T data)
    {
        if (data instanceof String)
            System.out.println("String");
        else if (data instanceof Integer)
            System.out.println("Integer");
        else if (data instanceof Double)
            System.out.println("Double");

        return data;
    }
}

public class Ex09_GenericMethod
{
    public static void main(String[] args)
    {
        MyData.<String>showData("Hello World");
        MyData.showData(1);		// <Integer> 생략
        MyData.showData(1.0);	// <Double> 생략
    }
}