Generics
자바에서 타입(Type)을 일반화(generalize)하여 프로그래밍을 하는 방식
클래스 또는 메서드에서 사용할 데이터 타입을 미리 지정하는 것이 아니라,
인스턴스 생성 시점에서 타입을 결정하는 방식이다.
제네릭을 사용하면 타입 안정성(type safet)을 보장할 수 있고, 코드의 가독성과
재사용성이 높아지는 등의 장점이 있다.
제네릭 타입의 이름 선언
자바에서 정의한 규칙
E | 요소(Element, 자바 컬렉션(Collection)에서 주로 사용) |
K | 키 |
N | 숫자 |
T | 타입 |
V | 값 |
S, U, V | 두 번째, 세 번째, 네 번재에 선언된 타입 |
꼭 위의 규칙을 지키지 않더라도 컴파일은 가능하다.
하지만, 다른 사람이 봤을 때에도 쉽게 이해가 되기 위해 따르는 것이 좋다.
?
(와일드카드)
어떤 타입의 매개변수를 대체할지 미리 지정하지 않고,
다양한 타입을 대입할 수 있도록 하는 방법이다.
"?"기호로 표현한다.
하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능하다.
ex)
<? extends T> : 와일드카드의 상한 제한. T와 그 자손들만 가능
<? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> : 제한 없음. 모든 타입이 가능. <? extends Object>와 동일
제네릭이 없었을 때
Student 등급과 Employee 순위 출력해 보기
class StudentInfo {
public int grade;
StudentInfo(int grade) {
this.grade = grade;
}
}
class StudentPerson {
public StudentInfo info;
StudentPerson(StudentInfo info) {
this.info = info;
}
}
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank) {
this.rank = rank;
}
}
class EmployeePerson {
public EmployeeInfo info;
EmployeePerson(EmployeeInfo info) {
this.info = info;
}
}
//메인 실행
public class GenericDemo {
public static void main(String[] args) {
StudentInfo si = new StudentInfo(2);
StudentPerson sp = new StudentPerson(si);
System.out.println(sp.info.grade);
EmployeeInfo ei = new EmployeeInfo(1);
EmployeePerson ep = new EmployeePerson(ei);
System.out.println(ep.info.rank);
}
}
StudentPerson과 EmployPerson은 똑같은 메커니즘을 가지고 있는 코드라고 할 수 있다.
따라서 중복코드이다. 이를 해결하면 가독성과 유지보수에 이점이 생긴다.
중복을 제거해 보자.
Person을 통합하여 중복코드를 제거
class StudentInfo {
public int grade;
StudentInfo(int grade) {
this.grade = grade;
}
}
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank) {
this.rank = rank;
}
}
class Person {
public Object info;
Person(Object info) {
this.info = info;
}
}
public class GenericDemo {
public static void main(String[] args) {
Person p1 = new Person("부장"); //<<<<<< 컴파일러가 문제가 문제라고 생각하지 않는다.
EmployeeInfo ei = (EmployeeInfo) p1.info;
}
}
메시지
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class EmployeeInfo (java.lang.String is in module java.base of loader 'bootstrap'; EmployeeInfo is in unnamed module of loader 'app')
at GenericDemo.main(GenericDemo.java:22)
ClassCastException -> RunTimeException (컴파일 단계에서의 오류가 아님)
이전 코드에서 Student와 Employee의 중복코드를 제거하기 위해
모든 코드의 부모 격인 Object로 바꾸어 해결하였다.
하지만,
Person p1 = new Person("부장");
이 부분은 학생의 등급, 직원의 순위를
나타내기 위한 의도와는 다르게 "부장"이라는 직책이 들어갔다.
문법적으로도 전혀 틀리지 않았기 때문에 컴파일단계에서 오류를 검출하지 않는다.
이러한 경우는 아주 찾기 어려운 문제가 될 가능성이 높으므로 심각한 문제로 이어질 가능성이 있다.
-> type safety : 타입이 안전하지 않다.
결과적으로 Object로 타입을 했기 때문에 "부장"이라는 엉뚱한 단어가 들어가는 것을 허용했다.
이러한 문제를 해결하기 위해 나온 방법이 "제네릭"이다.
복수의 제네릭 사용해 보기
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank) {
this.rank = rank;
}
}
class Person<T, S> {
public T info;
public S id;
Person(T info, S id) {
this.info = info;
this.id = id;
}
}
public class GenericDemo {
public static void main(String[] args) {
Person<EmployeeInfo, Integer> p1 = new Person<>(new EmployeeInfo(1), 1);
System.out.println(p1.id.intValue());
}
}
복수의 제네릭이 올 경우 ", "로 구분한다.
제네릭에 올 수 있는 데이터는 참조형 데이터만 올 수 있다.
int -> Integer
제네릭 생략
class EmployeeInfo{
public int rank;
EmployeeInfo(int rank) {
this.rank = rank;
}
}
class Person<T, S> {
public T info;
public S id;
Person(T info, S id) {
this.info = info;
this.id = id;
}
public <U> void printInfo(U info) {
System.out.println(info);
}
}
public class GenericDemo {
public static void main(String[] args) {
EmployeeInfo e = new EmployeeInfo(1);
Integer i = 10;
//Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
Person p1 = new Person(e, i);
//p1.<EmployeeInfo>printInfo(e);
p1.printInfo(e);
}
}
주석으로 되어있는 코드로 바꾸어도 문제없다.
메서드에서 제네릭을 사용할 때
제한접근자 뒤에 <U> 이런 식의 타입을 붙여주면 된다.
제네릭의 제한
abstract class Info{
public abstract int getLevel();
}
class EmployeeInfo extends Info {
public int rank;
EmployeeInfo(int rank) {
this.rank = rank;
}
public int getLevel() {
return this.rank;
}
}
class Person <T extends Info> {
public T info;
Person(T info) {
this.info = info;
}
}
public class GenericDemo {
public static void main(String[] args) {
Person<EmployeeInfo> p1 = new Person<>(new EmployeeInfo(1));
Person<String> p2 = new Person<String>("부장");
}
}
<T extends Info>
Info의 자식 클래스만 온다고 제한한다는 의미이다.
따라서
Person<EmployeeInfo> p1 = new Person<>(new EmployeeInfo(1));
는 문제없지만,
Person<String> p2 = new Person<String>("부장");
은 String 타입은 Info의 부모 타입이 아니므로
컴파일 오류가 발생한다.
interface Info{
public abstract int getLevel();
}
class EmployeeInfo implements Info {
public int rank;
EmployeeInfo(int rank) {
this.rank = rank;
}
public int getLevel() {
return this.rank;
}
}
class Person <T extends Info> {
public T info;
Person(T info) {
this.info = info;
}
}
public class GenericDemo {
public static void main(String[] args) {
Person<EmployeeInfo> p1 = new Person<>(new EmployeeInfo(1));
Person<String> p2 = new Person<String>("부장");
}
}
위 코드처럼 추상클래스가 아닌 인터페이스를 쓰더라도
제네릭은 implements 가 아닌 extends를 사용한다.
extends 대신 super도 있다.
super는 부모를 제한한다는 의미이다.
interface Info{
int getLevel();
}
class Person<T> {
public T info;
Person(T info){
this.info = info;
info.getLevel(); //<T> 는 <T extends Object>와 같다.
}
}
class Person<T>
처럼 하게 되면
class Person<T extends Object>
와 같게 된다.
따라서 T는 오브젝트인 상태이기 때문에
인터페이스인 info의 메서드를 호출할 수 없다.
제네릭 사용해 보기
제네릭 클래스
package org.example;
public class MyGenericClass<T> {
private T myFiled;
public void setMyFiled(T value) {
this.myFiled = value;
}
public T getMyFiled() {
return this.myFiled;
}
}
메인 클래스
package org.example;
public class Main {
public static void main(String[] args) {
MyGenericClass<Integer> myInt = new MyGenericClass<>();
myInt.setMyFiled(99);
System.out.println("my_Int : " + myInt.getMyFiled());
MyGenericClass<String> myStr = new MyGenericClass<>();
myStr.setMyFiled("안녕하세요!");
System.out.println("myStr : " + myStr.getMyFiled());
}
}
제네릭을 사용하여 T자리에 어떤 타입이 들어갈지는
객체를 생성하는 시점에서 결정되었다.
따라서 타입이 다르지만, 두 타입 모두 출력이 가능하다.
내용 참고 - 생활코딩 -
'자바 탐구' 카테고리의 다른 글
자바) Program, Process, Thread (0) | 2023.04.27 |
---|---|
인텔리제이) Entity ERD 확인하기 (0) | 2023.04.27 |
자바) 어노테이션 (0) | 2023.04.25 |
스프링) HTTP 상태코드 (0) | 2023.04.25 |
스프링) Bean (0) | 2023.04.23 |