본문 바로가기

Java/기본

[Java] 타입문제예방하기101 - 제네릭(Generic) 이란

개요

앞서 우리는 List, Set, Map 등 컬렉션에 대해 알아보았다. 컬렉션 중 하나인 List에는 add(), get() 메서드를 사용해 값을 추가하거나 꺼내올 수 있다. 이 때 인자값과 리턴값은 Object 형으로 모든 형태가 올 수 있다. 만약에 List에 성적계산을 목적으로 Integer값만 넣으려고 하는데, String값이 포함되어 있다면 어떨까?

for(int i=0; i<list.size(); i++){
	if(list_score.get(i) instance of Integer){
		list_score.get(i) * [성적변환 코드];
	}
}


이렇게 instance of 연산자를 사용하여 요소가 Integer인지 String 형태를 확인하는 코드가 추가된다. 위의 예시에서는 한 줄만 추가되었지만 다른 클래스 혹은 메서드에서 list_score 리스트를 사용하는 경우 이 코드는 추가되고 복잡성이 증가한다. 또한 데이터의 일관성 또한 깨지게 된다. 제네릭은 이러한 문제를 해결하기 위해 등장했다. 

제네릭이란 특정 타입으로 적용될 추상적인 타입을 말한다. 제네릭을 이용하면 요소의 타입을 추가하는 시점부터 형태가 제한되기 때문에, 이후에 객체를 사용할 때는 형태의 일관성을 확인하는 코드없이 사용할 수 있다. 

 

사용목적

  • 컴파일시 강한 타입체크

자바 컴파일러는 잘못된 코드를 작성한 경우(문법에 어긋난 경우) 컴파일 에러 메시지를 내놓는다. 제네릭을 활용하면 자바파일을 실행하기전 컴파일 과정에서 오류를 잡을 수 있다.

 

 

  • 불필요한 타입변환 과정 제거

제네릭 코드를 적용하지 않은 경우 타입변환(casting) 과정을 거치는데 당연히 이 과정을 거칠때마다 프로그램 효율이 떨어지게 된다. 제네릭 코드 적용시 특정 타입만 저장된 것을 컴파일러가 확인할 수 있기 때문에 불필요한 타입변환 과정이 생략된다.

 

 

코드예제

import java.util.ArrayList;
import java.util.List;

public class genericEx {
    public static void main(String[] args) {
        List<?> score_math = new ArrayList<>();
        List<Integer> list_korean = new ArrayList<Integer>();
        List<Integer> list_english = new ArrayList<>();
    }
}
  • list_math : <?> 타입을 지정하여 생성된 List 객체. 모든 타입의 객체를 허용하도록 지정했다.
  • list_korean : Integer타입으로 제한했다. 구현 객체의 요소도 Integer타입 추가되도록 명시적으로 코드를 작성했다.
  • list_english : Integer타입으로 제한했다. 구현 객체의 요소 제한은 생략했다. 자연스럽게 참조클래스인 List의 타입제한, 즉 Integer를 참조한다.

 

허용되지 않는 요소를 넣으려고 하면 빨간줄(경고)이 그어진다.

 


제네릭스는 메서드의 파라미터 및 반환값에 정의할 수 있다.

  • 파라미터에 제네릭스 적용
public class Test01 {
    public static void main(String[] args) {

        Vector<Boolean> yesOrNo = new Vector<>();

        yesOrNo.add(true);
        yesOrNo.add(false);
        yesOrNo.add(true);
        yesOrNo.add(true);
        yesOrNo.add(false);

        printAnswer(yesOrNo, 3);
    }

    static boolean printAnswer(Vector<Boolean> vector, int index){
        boolean result = false;
        if(index > vector.size() -1){
            return false;
        }
        result = vector.get(index);
        return result;
    }
}

 

  • 반환값에 제네릭스 적용
import java.util.ArrayList;
import java.util.List;

public class GenericTest {
    List<String> nameList = new ArrayList<>();

    public static void main(String[] args) {
        GenericTest ge = new GenericTest();

        String[] nameArr = {"나폴레옹", "알렉산드로스", "카이사르", "한니발"};
        for(String name : nameArr){
            ge.makeList(name);
        }
    }

    List<String> makeList(String name){
        nameList.add(name);
        return nameList;
    }
}

 

 


제네릭스는 직접 생성하기

제네릭스는 프로그래머가 직접 생성한 클래스로 지정하는 것도 가능하다.

학생 정보를 담고 있는 Student 클래스

public class Student {
    private String name;
    private int stuNum;
    private int grade;

    Student(String name, int stuNum, int grade){
    }
}

 

Student 클래스를 제네릭스로 하는 attendance List 생성하기

import java.util.ArrayList;
import java.util.List;

public class StudentEnroll {
    public static void main(String[] args) {
        List<Student> attendance = new ArrayList<>();
        Student student01 = new Student("윤동주", 20200104, 3);
        Student student02 = new Student("박목월", 20210618, 2);
        Student student03 = new Student("김소월", 20190204, 4);

        attendance.add(student01);
        attendance.add(student02);
        attendance.add(student03);

        student01.toString();
    }
}

멀티타입 파라미터 <K, V>

제네릭은 두 개 이상의 멀티타입 파라미터를 받는 것도 가능하다. 각 타입은 , 로 구분한다.

public class Student<Integer, String>{
	private Integer stuIndex;
	private String stuName;
	
	public Integer getStuIndex(){
		return stuIndex;
	}
	public void setStuIndex(Integer stuIndex){
		this.stuIndex = stuIndex;
	}
	public String getStuName(){
		return stuName;
	}
	public void setStuName(String stuName){
		this.stuName = stuName;
	}	
}

 

public class StudentEx(){
	public static void main(String[] args){
		Student<Integer, String> s01 = new Student<>();
		s01.setStuIndex = 20210703;	
		s01.getStuName = "홍길동";
	}
}

제한된 타입 파라미터 <T extends 최상위타입>

- 타입 파라미터에 지정되는 타입을 제한할 때 사용한다. 최상위 타입을 상속받는 하위 타입만 제네릭으로서 들어올 수 있도록 지정하는 기능이다.
- 최상위 타입에는 클래스, 인터페이스 모두 가능하다. 인터페이스를 지정하는 경우에도 extends 라고 작성한다(implements 라고 적지 않도록 한다)

- T 에는 최상위 타입에 지정한 것과 같은 클래스, 상속받는 하위 클래스, 구현 클래스 등만 가능하다.

 

 

코드 예제

compare() 메서드를 사용하여 두 실수의 크기를 비교하는 예제이다. Number클래스를 상속받는 숫자 타입만 허용된다.

 

public class numberCompare {
    public <T extends Number> int compare(T t1, T t2){
        double value1 = t1.doubleValue();
        double value2 = t2.doubleValue();
        return Double.compare(value1, value2);
    }
}

 

public class Test {
    public static void main(String[] args) {
        numberCompare nc = new numberCompare();
        int result01 = nc.compare(10, 20);
        System.out.println(result01);

        int result02 = nc.compare(5.41, 3.6);
        System.out.println(result02);

        int result03 = nc.compare(3, 3);
        System.out.println(result03);
    }
}

 

혹시 compare(T t1, T t2) 파라미터로 숫자 외의 값을 넣으면 아래와 같은 에러 메시지가 나온다.

(최상위타입 Number에 맞지 않는 타입을 넣어서 컴파일실패했다는 의미)

 

 

실행결과

-1
1
0

와일드 카드 타입

?는 와일드 카드(wildcard)를 의미하는데, 와일드카드는 카드 게임에서 유래된 용어이다. 어떤 용도로든 쓰일 수 있는 카드를 말하는데, 제네릭에서는 임의의 클래스를 의미하는 용어이다.

 

와일드 카드 형태 의미 내용
<?> 제한없음 모든 클래스나 인터페이스 타입 가능
<? extends 상위타입> 상위 클래스 제한 -상위타입 및 상위타입을 상속받는 하위 타입만 가능
-하위타입은 와일드카드인데 상위타입을 제한하므로 상위클래스 제한
<? super 하위타입> 하위 클래스 제한 -하위타입 및 하위타입에 상속해주는 상위 타입만 가능
-상위타입은 와일드카드인데 하위타입을 제한하므로 하위클래스 제한

 

 

코드 예제

  • 토익 수강등록을 하는 코드 예제이다
  • Course 클래스에 타입 제한을 걸어 허용된 Course만 받을 수 있도록 하였다

Person 클래스의 상속구조

 

Course<T> 클래스

제네릭으로 선언하여 제한 가능성을 두었다

public class Course<T> {
    private int level;      //코스 레벨
    private String teacher; //강사 이름

    Course(int level, String teacher){
        this.level = level;
        this.teacher = teacher;
    }    

//강좌내용을 확인하기 위한 오버라이딩
    @Override
    public String toString() {
        return "course : level="+ level+ ", teacher="+ teacher;
    }
}

 

 

Person 클래스 - 최상위타입

//일반인
public class Person {
	private String name;
	private int level;
    
	//getter, setter
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	
	public int getLevel() {
		return level;
	}
	public void setLevel(int level) {
		this.level = level;
	}

	//constructor
	public Person(String name, int level){
		this.name = name;
		this.level = level;
	}
}

 

Worker 클래스 - Person 클래스를 상속받는다

//직장인
public class Worker extends Person {
    public Worker(String name, int level) {
        super(name, level);
    }
}

 

UnivStudent 클래스 - Person클래스를 상속받는다

//대학생
public class UnivStudent extends Person{
    public UnivStudent(String name, int level) {
        super(name, level);
    }
}

 

 

UnivStuGraduating 클래스 - UnivStudent 클래스를 상속받는다

//대학생 졸업예정
public class UnivStuGraduating extends UnivStudent{
    public UnivStuGraduating(String name, int level) {
        super(name, level);
    }
}

 

 

RegisterCourse 클래스 - 강좌를 등록할 수 있는 메서드를 제공

public class RegisterCourse {
	//모든 Course 를 받을 수 있다
	public void toeicGeneral(Course<?> course) {
		String desc = "일반과정 | "+ course.toString();
		System.out.println(desc);
	}	
	
	//하위타입제한 - Worker의 상위클래스만 가능하다
	public void toeicWorker(Course<? super Worker> course) {
		String desc = "직장인과정 | "+ course.toString();
		System.out.println(desc);
	}
	
	//상위타입제한 - UnivStudent를 상속받는 하위클래스만 가능하다
	public void toeicUniv(Course<? extends UnivStudent> course) {
		String desc = "대학생과정 | "+ course.toString();
		System.out.println(desc);
	}
	
	//상위타입제한 - UnivStuGraduating를 상속받는 하위클래스만 가능하다
	public void toeicUnivGraduating(Course<? extends UnivStuGraduating> course) {
		String desc = "대학생(졸업예정)과정 | "+ course.toString();
		System.out.println(desc);
	}	
}

 

 

위에서 코딩한 내용을 확인해보자

public class Test {
    public static void main(String[] args) {
    	
    	//코스마련
    	Course<Person> coursePerson		= new Course<>(1, "김길동");
    	Course<Worker> courseWorker		= new Course<>(2, "조길동");
    	Course<UnivStudent> courseUniv		= new Course<>(3, "최길동");
    	Course<UnivStuGraduating> courseGraduating	= new Course<>(4, "엄길동");
    	
    	RegisterCourse register = new RegisterCourse();
    	
    	//모든 강좌에 신청 가능
    	register.toeicGeneral(coursePerson);
    	register.toeicGeneral(courseWorker);
    	register.toeicGeneral(courseUniv);
    	register.toeicGeneral(courseGraduating);
    	
    	//대학생을 위한 강좌만 신청가능
//    	register.toeicUniv(coursePerson);	//불가
//    	register.toeicUniv(courseWorker);	//불가
    	register.toeicUniv(courseUniv);
    	register.toeicUniv(courseGraduating);
    	
    	//대학생(졸업예정)을 위한 강좌만 신청가능
//    	register.toeicUnivGraduating(coursePerson);	//불가
//    	register.toeicUnivGraduating(courseWorker);	//불가
//    	register.toeicUnivGraduating(courseUniv);	//불가
    	register.toeicUnivGraduating(courseGraduating);
    	
    	//직장인 및 상위타입만 가능
    	register.toeicWorker(coursePerson);
//    	register.toeicWorker(courseUniv);		//불가
//    	register.toeicWorker(courseGraduating);	//불가
    	register.toeicWorker(courseWorker);
    }
}

 

실행 결과

일반과정 | course : level=1, teacher=김길동
일반과정 | course : level=2, teacher=조길동
일반과정 | course : level=3, teacher=최길동
일반과정 | course : level=4, teacher=엄길동
대학생과정 | course : level=3, teacher=최길동
대학생과정 | course : level=4, teacher=엄길동
대학생(졸업예정)과정 | course : level=4, teacher=엄길동
직장인과정 | course : level=1, teacher=김길동
직장인과정 | course : level=2, teacher=조길동

 

 

 


제네릭 타입의 상속

제네릭 타입도 상속을 해줄 수 있다. 하위 클래스는 상위클래스에서 정의한 타입 파라미터에 새로 정의한 파라미터를 추가할 수 있다.

 

코드 예제

Prototype 클래스를 상속받는 Product01 과 Product02 클래스, Product02를 상속받는 Product03 클래스

 

public class Prototype<K, F>{
    private K Kind;
    private F function;
}

 

Product01 클래스는 제네릭을 정의하지 않았다.

public class Product01 extends Prototype {	}

 

Product02 클래스는 Prototype 클래스의 파라미터 K, F 를 구체적으로 명시했다.

public class Product02<Integer, String> extends Prototype{
    private Integer Kind;
    private String function;
}

 

Product03 클래스는 Product02 클래스의 파라미터에 파라미터를 추가했다.

public class Product03<String, Integer, Boolean> extends Product02{
    private Integer Kind;
    private String function;
    private Boolean complete;
}

 


제네릭 타입의 구현

제네릭 인터페이스를 구현한 클래스도 제네릭 타입이 된다

 

 

코드 예제

Swim 인터페이스

public interface Swim<T> {
    void setWhere(T t);
    void swim();
}

 

SwimImple 클래스

-Swim 인터페이스를 구현하는 클래스이다.

-구현하려는 인터페이스가 제네릭 타입이므로 SwimImple  클래스는 제네릭 타입이 된다

-Swim 인터페이스의 메서드 구현한다.

public class SwimImple<T> implements Swim<T> {

    T t;

    @Override
    public void setWhere(T t) {
        this.t = t;
    }

    @Override
    public void swim() {
        System.out.println(t+ "에서 수영");
    }
}

 

SwimOcean 클래스

-메인함수가 위치해있다.

public class SwimOcean {
    public static void main(String[] args) {
        SwimImple<String> play = new SwimImple<>();
        play.setWhere("바다");
        play.swim();
    }
}

 

실행 결과

바다에서 수영