이 포스팅은 신용권님의 이것이 자바다 13장으로 공부한 내용을 정리한 것입니다.
제네릭이란, 클래스와 인터페이스, 메소드를 정의할 때 타입을 파라미터로 사용할 수 있도록 하는 기술이다. 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다. 이런 방식은 여러 장점을 가진다.
제네릭을 사용하지 않는다면 여러 타입을 값으로 받기 위해 변수를 Object 타입으로 선언하게 되는데, 이는 런타임에서 에러가 발생할 확률을 높인다. 아래 코드를 보자.
String str = "aaa";
Object obj = str;
Integer var = (Integer)obj;
이 코드는 컴파일시에는 아무 문제가 없이 컴파일이 된다. 그러나 실행하면 잘못된 타입 변환으로 예외가 발생하며 종료될 것이다.
또한, 제네릭을 사용하면 쓸데없는 형변환 과정을 제거할 수 있다. 아래 코드를 보자.
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0); // 타입 변환이 필요하다.
list에 요소를 저장할땐 Object 타입을 파라미터로 받으므로 String 타입의 문자열을 집어넣어도 되지만, 요소를 찾아올 때에는 Object 타입을 String으로 변환해야 한다. 이런 코드가 많아진다면 성능에 분명 악영향을 줄 것이다. 이를 제네릭을 사용한 코드로 바꾸면
List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 타입 변환이 필요없다.
이렇게 제네릭을 사용하면 list에 저장되는 요소를 String으로 한정하기 때문에 타입변환을 할 필요가 없고 성능이 향상된다.
제네릭 타입 (class<T>, interface<T>)
제네릭 타입이란 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. 선언 문법은 다음과 같다.
접근제한자 class 클래스이름<T> { ... }
접근제한자 interface 인터페이스이름<T> { ... }
이때 T가 타입파라미터이다. 타입 파라미터는 T만 가능한 게 아니라 일반적인 변수명 작성규칙과 동일하게 작성할 수 있지만 일반적으로 대문자 알파벳 한 글자로 표현한다. 제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야하는데, 제네릭을 처음 접하는 사람이라면 이럴거면 처음부터 타입을 지정하지 뭐하러 이러나 싶을지도 모른다. 다음 코드를 보자.
public class Box {
private Object obj;
public void set(Object obj){ this.obj = obj; }
public Object get() { return obj; }
}
Box 클래스는 특정되지 않은 타입의 객체를 저장하기 위한 클래스이다. 따라서 필드를 모든 클래스의 부모인 Object 타입으로 선언해야하고, set,get 메소드 역시 Object 타입을 사용해야했다. 그 후 코드에서 Box 객체에 저장된 객체를 원래타입으로 사용하려면 형변환을 해줘야한다.
Box box = new Box();
box.set("aaa"); // String -> Object 자동 타입 변환
String str = (String)box.get(); // Object -> String 강제 타입 변환
이렇게 코드를 작성하면, Box에 모든 타입의 객체를 저장할 수 있지만, 저장할때와 읽어올 때 모두 타입변환이 발생하게 되고 이는 성능저하로 이어지며 특히 강제 타입 변환에서 프로그래머의 실수로 잘못된 타입 변환이 발생해 런타임에서 프로그램이 종료될 가능성도 생긴다. 이런 문제점을 해결하기 위해 제네릭을 사용한다.
public class Box<T> {
private T t;
public void set(T t){ this.t = t; }
public T get() { return t; }
}
이렇게 Box 클래스를 제네릭을 사용하여 수정하고
Box<String> box = new Box<String>();
box.set("aaaa");
String str = box.get();
이렇게 타입을 지정하여 Box 객체를 사용하면 타입변환이 발생하지 않게 된다. 이유는 String 이라는 타입을 파라미터로 넘기면 아래처럼 T가 String으로 대체되 자동으로 구성되기 때문이다.
public class Box<String> {
private String t;
public void set(String t){ this.t = t; }
public String get() { return t; }
}
String이 아닌 다른 타입을 사용했어도 같은 원리로 타입 변환이 발생하지 않으며 오류발생가능성을 줄인다. 제네릭 타입은 두개 이상의 타입 파라미터를 사용하는 것도 가능하다. 이때 타입 파라미터는 ",(콤마)" 로 구분한다.
접근제한자 class 클래스이름<T,M,N> { ... }
접근제한자 interface 인터페이스이름<T,M,N,K> { ... }
제네릭 타입을 사용할 때 타입파라미터 변수 자리에 어떤 타입이 들어올지 모르므로 Object 클래스에 명시된 메소드만 사용할 수 있다.
제네릭 메소드
제네릭 메소드란 매개타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다. 문법은 다음과 같다.
접근제한자 <타입파라미터,...> 리턴타입 메소드명(매개변수,...) { ... }
구체적으로
public class Util {
public static <T> Box<T> boxing(T t) {
Box<T> box = new Box<T>();
box.set(t);
return box;
}
}
public class Main {
public static void main(String[] args){
Box<Integer> box1 = Util.<Integer>boxing(100); // 명시적으로 타입을 지정
int intValue = box1.get();
Box<String> box2 = Util.boxing("aaa"); // 매개값으로 타입을 추정
String strValue = box2.get();
}
}
제네릭 메소드를 호출하는 방법은 위처럼 2가지가 있다. 하나는 타입을 구체적으로 명시하는 방법이고 다른 하나는 매개값을 통해 타입을 추측하는 방법이다.
리턴타입 변수 = <구체적 타입> 메소드명(매개값); // 명시적으로 구체적 타입을 지정
리턴타입 변수 = 메소드명(매개값); // 매개값을 보고 구체적 타입을 추정
제한된 타입 파라미터 (<T extends 최상위 타입>)
경우에 따라서 제네릭의 타입에 어떤 클래스의 자식이나 어던 인터페이스를 구현한 타입으로 제한할 필요가 있다. 이 경우 기존의 제네릭 문법에 <T> 대신 <T extends 최상위 타입> 과 같이 쓰면 된다. 인터페이스라 해서 implements 를 사용하지 않는다.
이 경우 타입파라미터 변수로 사용가능한 것은 상위타입에 정의된 필드와 메소드뿐이다. 하위타입에 따로 정의된 것은 같은 부모를 상속한 다른 타입에는 정의되어있지 않을 수 있기 때문이다.
와일드카드 타입
코드에서 제네릭타입을 매개값이나 리턴타입으로 사용할 때 구체적인 타입 대신에 와일드카드를 아래 3가지 방식으로 사용가능하다.
제네릭타입<?>
// 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스타입이 올 수 있다.
제네릭타입<? extends 상위타입>
// 타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위타입만 올 수 있다.
제네릭타입<? super 하위타입>
//타입 파라미터를 대치하는 구체적인 타입으로 하위타입이나 상위타입이 올 수 있다.
아래 예제 코드를 보자.
public class Course<T> {
private String name;
private T[] students;
public Course(String name, int capacity) {
this.name = name;
students = (T[]) (new Object[capacity]);
}
public String getName() { return name; }
public T[] getStudents() { return students; }
public void add(T t) {
for(int i=0; i<students.length; i++) {
if(students[i] == null) {
students[i] = t;
break;
}
}
}
}
위 코드에서 T에 들어갈 타입으로 Person, Worker, Student, HighStudent 가 있다고 하자. 이들의 상속관계는 다음과 같다.
Worker extends Person
Student Extends Person
HighStudent extends Student
// 최상위에 Person이 있고 이를 상속하는 Worker과 Student가 있으며, Student를 상속하는
// HighStudent가 있다.
이제 와일드카드를 사용하는 코드를 보자.
import java.util.Arrays;
public class WildCardExample {
public static void registerCourse(Course<?> course) { // 이 메소드의 매개값은 모든 타입이 가능하다.
System.out.println(course.getName() + " 수강생: " +
Arrays.toString(course.getStudents()));
}
public static void registerCourseStudent(Course<? extends Student> course) { // 이 메소드의 매개값은 Student와 이를 상속한 HighStudent가 가능하다.
System.out.println(course.getName() + " 수강생: " +
Arrays.toString(course.getStudents()) );
}
public static void registerCourseWorker(Course<? super Worker> course) { // 이 메소드의 매개값은 Worker과 이의 부모인 Person만 가능하다.
System.out.println(course.getName() + " 수강생: " +
Arrays.toString(course.getStudents()));
}
public static void main(String[] args) {
Course<Person> personCourse = new Course<Person>("일반인과정", 5);
personCourse.add(new Person("일반인"));
personCourse.add(new Worker("직장인"));
personCourse.add(new Student("학생"));
personCourse.add(new HighStudent("고등학생"));
Course<Worker> workerCourse = new Course<Worker>("직장인과정", 5);
workerCourse.add(new Worker("직장인"));
Course<Student> studentCourse = new Course<Student>("학생과정", 5);
studentCourse.add(new Student("학생"));
studentCourse.add(new HighStudent("고등학생"));
Course<HighStudent> highStudentCourse = new Course<HighStudent>("고등학생과정", 5);
highStudentCourse.add(new HighStudent("고등학생"));
registerCourse(personCourse);
registerCourse(workerCourse);
registerCourse(studentCourse);
registerCourse(highStudentCourse);
System.out.println();
//registerCourseStudent(personCourse); (x)
//registerCourseStudent(workerCourse); (x)
registerCourseStudent(studentCourse);
registerCourseStudent(highStudentCourse);
System.out.println();
registerCourseWorker(personCourse);
registerCourseWorker(workerCourse);
//registerCourseWorker(studentCourse); (x)
//registerCourseWorker(highStudentCourse); (x)
}
}
제네릭 타입의 상속과 구현
제네릭 타입도 부모클래스가 될 수 있다. 제네릭 타입을 상속하거나 구현한 클래스도 역시 제네릭타입이 된다. 자식 제네릭 타입은 추가적으로 타입파라미터를 가질 수 있다. 제네릭 인터페이스를 구현하는경우 추가 타입 파라미터를 가질 수 없다.
'Java > 기초문법' 카테고리의 다른 글
[Java 기본 문법] 3. 제어문 (0) | 2021.08.13 |
---|---|
[Java 기본문법] 2. 연산자 (0) | 2021.08.13 |
[Java 기본 문법] 1. 변수와 타입 (0) | 2021.08.13 |
중첩클래스 2 (0) | 2021.03.09 |
중첩클래스 (0) | 2021.02.28 |