제네릭을 이해하지 못하면 API 도큐먼트를 정획히 이해할 수 없고 컬렉션, 람다식, 스트림, NIO에서 널리 사용되므로 확실히 이해해두어야 한다.
제네릭은 클래스와 인터페이스, 메소드를 정의할 때 타입을 파라미터로 사용할 수 있도록 하는 역할을 한다.
타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다.
컴파일 시 강한 타입 체크를 할 수 있다.
자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제네릭코드에 강한 타입 체크를 한다. 실행 시 타입 에러가 나느 것보다는 컴파일 시에 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋다.
타입변환(casting)을 제거한다.
비제네릭 코드는 불필요한 타입 변환을 하기 때문에 프로그램 성능에 악영향을 미친다(반복적인 캐스팅 때문).//비제네릭 List list = new ArrayList(); list.add("velog"); String str = (String) list.get(0); <br> //제네릭 List<String> list = new ArrayList<>(); list.add("velog"); String str = list.get(0);
제네릭 코드로 작성할 경우 List에 저장되는 요소를 String으로 국한하기 때문에 요소를 꺼낼 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상된다.
또한, 모든 종류의 객체를 저장하면서 타입 변환이 발생하지 않도록 작성할 수 있다.
public class GenericClass<T>{ ... } public interface GenericInterface<T> { ... }
제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 "<>" 부호가 붙고, 사이에 타입 파라미터가 위치한다.
- 타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만, 일반적으로 대문자 알파벳 한 글자로 표현한다.
아래는 제네릭 코드 예시이다.
// Generic.java
public class Generic<T>{
private T t;
public T get(){ return t; }
public void set(T t) { this.t = t; }
}
//GenericExample.java
public class GenericExample{
public static void main(String[] args){
//String으로 타입 지정
Generic<String> generic = new Generic<String>();
generic.set("velog");
String str = generic.get();
//Integer로 타입 지정
Generic<Integer> generic = new Generic<Integer>();
generic.set(100);
int value = generic.get();
}
}
위와 같이 제네릭은 클래스를 설계할 때 구체적인 타입을 명시하지 않고, 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구제적인 타입을 지정함으로써 타입 변환을 최소화한다.
제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있다.
class TestClass<T, K> { ... } interface TestInterface<T, K> { ... }
static class MultiType <T, K> {
private T type;
private K key;
public T getType(){ return type; }
public K getKey(){ return key; }
public void setType( T type ){ this.type = type; }
public void setKey( K key ) { this.key = key; }
}
public static void multiType(){
MultiType<String, Integer> test1 = new MultiType<>();
test1.setType("velog");
test1.setKey(10000);
String type1 = test1.getType();
Integer key1 = test1.getKey();
MultiType<Object, Double> test2 = new MultiType<>();
test2.setType(new Object());
test2.setKey(1000.0);
Object type2 = test2.getType();
Double key2 = test2.getKey();
}
위 코드는 MultiType<T, K> 제네릭 타입을 정의하고 사용하는 간단한 코드이다.
또, 제네릭 타입 변수 선언과 객체 생성을 동시에 할 때, 타입 파라미터 자리에 구체적인 타입을 지정하는 코드가 중복된다. 자바는 이러한 중복코드에 대해서 <>
를 제공한다.
자바 컴파일러는 타입 파라미터 부분에 <>
연산자를 사용하면 타입 파라미터를 유추해서 자동으로 설정해준다.
MultiType<String, Integer> test1 = new MultiType<>();
MultiType<String, Integer> test1 = new MultiType<String, Integer>();
//둘 다 같다. <> 연산자는 타입 파라미터를 유추해서 자동으로 설정해준다.
public <T> T test(T t) { ... }
제네릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.
제네릭 메소드를 선언하는 방법은 리턴 타입 앞에<>
를 추가하고 타입 파라미터를 기술한 다음, 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 된다.
예시 코드
static class Box<T>{
private T box;
public T getBox(){ return box; }
public void setBox(T box){ this.box = box; }
}
static class GenericMethod{
public static <T> Box<T> boxing(T t){
Box<T> box = new Box<T>();
box.setBox(t);
return box;
}
}
public static void genericMethod(){
Box<Integer> box1 = GenericMethod.<Integer>boxing(100);
int intValue = box1.getBox();
Box<String> box2 = GenericMethod.boxing("velog");
String strValue = box2.getBox();
}
다음 boxing() 제네릭 메소드는 <> 기호 안에 타입 파라미터 T를 기술한 뒤, 매개 변수 타입으로 T를 사용했고, 리턴 타입으로 제네릭 타입 Box<T>
를 사용했다.
public static <T> Box<T> boxing(T t)
다음은 genericMethod()
의 코드를 보자.
제네릭 메소드는 두 가지 방식으로 호출할 수 있다.
코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정해도 되고, 컴파일러가 매개 값의 타입을 보고 구체적인 타입을 추정하도록 할 수도 있다.
- 코드에서 타입 파라미터의 구체적인 타입을 명시
- 컴파일러가 매개 값의 타입을 보고 구체적인 타입을 추정
//리턴타입 변수 = <구체적인 타입> 메소드명(매개 값) : 구체적인 타입을 명시
Box<Integer> box1 = GenericMethod.<Integer>boxing(100);
//리턴타입 변수 = 메소드명(매개값) : 컴파일러가 매개 값 추정
Box<String> box1 = GenericMethod.boxing(100);
타입 파라미터에 지정되는 구체적인 타입을 모든 객체가 아닌 특정 객체로 제한해서 사용할 수있다.
public <T extends 상위타입> 리턴타입 메소드(매개변수, ...){ ... }
<T extends 상위타입>
제한된 타입 파라미터를 선언하려면 타입 파라미터 뒤에 extends 키워드를 붙이고 상위 타입을 명시한다.
extends 키워드로 상위 타입을 명시하면 상위타입의 하위 요소들만 타입 파라미터로 사용될 수 있다.
extends 라는 키워드만 봐서는 클래스만 상위 요소로 제한할 수 있겠다고 생각하겠지만, 인터페이스의 경우도 implements 대신 extends 키워드로 제한할 수 있다.
이 경우는 인터페이스의 구현 클래스만 타입 파라미터로 사용될 수 있다.
제한된 타입 파라미터를 사용할 경우 주의할 점이 있다.
바로 상위 타입에 있는 필드와 메소드만 사용할 수 있다.
static class TestParent{
public static void printParent(){
System.out.println("Parent 출력");
}
}
static class Test extends TestParent{
public static void printChild(){
System.out.println("Child 출력");
}
}
public static <T extends TestParent> void genericTest(T t){
t.printParent();
t.printChild();
}
public static void main(String[] args) {
genericMethod(new Test());
}
위 코드는 타입 파라미터의 상위 타입 메소드와 타입 파라미터의 메소드를 호출한 경우이다.
사진처럼 타입 파라미터에만 있는 메서드의 경우는 사용할 수 없고, 상위 타입에 있는 메서드만 사용할 수 있다.
?
코드에서 ?를 일반적으로 와일드카드라고 부른다.
제네릭 타입을 매개 값이나 리턴 타입으로 사용할 때 구체적인 타입 대신 와일드 카드를 다음과 같이 세가지로 사용할 수 있다.
- 제네릭 타입
<?
> : Unbounded Wildcards(제한없음)
- 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
- 제네릭 타입
<? extends 상위타입
> : Upper Bounded Wildcards (상위 클래스 제한)
- 타입 파라미터를 대치하는 구체적인 타입으로 _상위 타입이나 하위 타입-만 올 수 있다.
- 제네릭 타입<? super 하위타입> : Lower Bounded Wildcards(하위 클래스 제한)
- 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위 타입이 올 수 있다.
(super 키워드는 와일드카드에서만 사용가능한 제한 연산자다.)
public class Wildcards {
public static class Person{
private String type;
Person(String type){
this.type = type;
}
}
public static class Worker extends Person{
Worker(String type){
super(type);
}
}
public static class Student extends Person{
Student(String type){
super(type);
}
}
public static class HighStudent extends Student{
HighStudent(String type){
super(type);
}
}
public static 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;
}
}
}
}
public static void registerCourse( Course<?> course ) {
System.out.println(course.getName() + " 수강생");
Arrays.toString(course.getStudents());
}
public static void registerCourseStudent(Course<? extends Student> course){
System.out.println(course.getName() + "수강생");
Arrays.toString(course.getStudents());
}
public static void registerCourseWorker( Course<? super Worker> course ){
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 Person("직장인"));
personCourse.add(new Student("학생"));
personCourse.add(new HighStudent("고등학생"));
Course<Worker> workerCourse = new Course<>("직장인과정", 5);
workerCourse.add(new Worker("직장인"));
Course<Student> studentCourse = new Course<>("학생과정", 5);
studentCourse.add(new Student("학생"));
studentCourse.add(new HighStudent("고등학생"));
Course<HighStudent> highStudentCourse = new Course<>("고등학생", 5);
highStudentCourse.add(new HighStudent("고등학생"));
//<?> 모든 객체를 타입 파라미터로 사용할 수 있음.
registerCourse(personCourse);
registerCourse(workerCourse);
registerCourse(studentCourse);
registerCourse(highStudentCourse);
System.out.println();
//<? extends Student> Student 객체 혹은 Student 하위 객체만 타입 파라미터로 사용할 수 있음.
//registerCourseStudent(personCourse); X
//registerCourseStudent(workerCourse); X
registerCourseStudent(studentCourse);
registerCourseStudent(highStudentCourse);
System.out.println();
//<? super Worker> Worker 객체 혹은 Worker 상위 객체만 파라미터로 사용할 수 있음.
registerCourseWorker(personCourse);
registerCourseWorker(workerCourse);
//registerCourseWorker(studentCourse); X
//registerCourseWorker(highStudentCourse); X
}
}
- 제네릭 타입도 다른 타입과 마찬가지로 부모 클래스가 될 수 있다.
public class ChildProduct<T, M> extends Product<T, M> { ... }
- 또, 자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다.
public class ChildProduct<T, M, C> extends Product<T, M> { ... }
public class Product<T, M> {
private T kind;
private M model;
public void setKind(T kind) { this.kind = kind; }
public void setModel(M model) { this.model = model; }
public T getKind() { return kind; }
public M getModel() { return model; }
}
public class ChildProduct<T, M, C> extends Product<T, M> {
private C company;
public void setCompany(C company) { this.company = company; }
public C getCompany() { return company; }
}
public interface Storage<T> {
public void add(T item, int index);
public T get(int index);
}
public class StorageImpl<T> implements Storage<T> {
private T[] array;
public StorageImpl(int capacity) {
//타입 파라미터로 배열을 생성할 경우 new T[n]으로 생성할 수 없다.
this.array = (T[])(new Object[capacity]);
}
@Override
public void add(T item, int index) {
array[index] = item;
}
@Override
public T get(int index) {
return array[index];
}
}
public class Tv { }
public class GenericExtends {
public static void main(String[] args) {
ChildProduct<Tv, String, String> product = new ChildProduct<>();
product.setKind(new Tv());
product.setModel("Smart TV");
product.setCompany("LG");
Storage<Tv> storage = new StorageImpl<>(100);
storage.add(new Tv(), 0);
Tv tv = storage.get(0);
}
}