Hun's Blog

[JAVA] Generic이란 무엇인가? 본문

Language/Kotlin & Java

[JAVA] Generic이란 무엇인가?

jhk-im 2020. 3. 21. 23:26

Generic


제네릭이란 데이터 타입을 일반화한다는 것을 의미한다.
클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 것이다.

*JDK 1.5 이전에는 여러 타입을 사용하는 클래스나 메소드에서 인수나 반환값으로 Object 타입을 사용했었다. 이 경우 반환된 Object 객체를 다시 원하는 타입으로 변환해야 했으며, 이때 오류가 발생할 가능성이 존재한다.
JDK1.5 부터 도입된 제네릭을 활용하면 컴파일 시 미리 타입이 정해지므로, 타입검사나 타입 변환과 같은 번거로운 작업을 생략할수 있게 된다.

예제

1
2
3
4
5
6
7
8
9
class MyArray<T> {
    T element;
    void setElement(T element){
        this.element = element;
    }
    T getElement() {
        return element;
    }
}
 

예제에 나오는 'T' 를 타입변수라고 하며, 임의의 참조형 타입을 의미한다.
- T 뿐만 아니라 어떠한 문자를 사용해도 상관없음
- 여러개의 타입변수를 쉼표로 구분하여 명시 가능
타입변수는 클래스 뿐만아니라 메소드의 매개변수나 반환값으로도 사용할 수 있다.

 

1
2
3
4
5
//main() 출력
MyArray<Integer> myArray = new MyArray<Integer>();
myArray.setElement(1);
//myArray.setElement("문자"); --> error
System.out.println(myArray.getElement()); // 1
 

제네릭 클래스를 생성할 때에는 타입변수 자리에 실제 타입을 명시해야한다.
이렇게 실제 타입을 명시하면, 내부적으로 정의된 타입 변수가 명시된 실제 타입으로 변환되어 처리된다.

 

1
2
3
4
5
//Java SE7 이상
MyArray<String> myArray1 = new MyArray<>();
myArray1.setElement("hello");
//myArray1.setElement(1); --> error
System.out.println(myArray1.getElement()); // hello
 

new 키워드로 인스턴스 생성 시 타입을 추정할 수 있는 경우 타입 생략이 가능하다.

 

 

제네릭 타입변수 다형성 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AnimalList<T> {
    ArrayList<T> al = new ArrayList<T>();
    void add(T animal) { al.add(animal); }
    T get(int index) { return al.get(index); }
    boolean remove(T animal) { return al.remove(animal); }
    int size() { return al.size(); }
}
class LandAnimal { public void crying() { System.out.println("육지동물"); } }
class Cat extends LandAnimal { public void crying() { System.out.println("냐옹냐옹"); } }
class Dog extends LandAnimal { public void crying() { System.out.println("멍멍"); } }
class Sparrow { public void crying() { System.out.println("짹짹"); } }
 
//main()
AnimalList<LandAnimal> landAnimal = new AnimalList<>();
landAnimal.add(new LandAnimal());
landAnimal.add(new Cat());
landAnimal.add(new Dog());
// 오류가 발생함.
for (int i = 0; i < landAnimal.size(); i++) {
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
cs

 

 

Cat과 Dog는 LandAnimal 클래스를 상속받았으므로 AnimalList<LandAnimal>에 추가할 수 있다. Sparrow의 경우 타입이 다른것이기 때문에 추가할 수 없다.

*자바 코드에 선언되어 사용된 제네릭 타입은 컴파일 시 컴파일러에 의해 자동으로 타입변환된다. 후에 코드내에 제네릭 타입은 제거되어, 컴파일된 class 파일에는 어떠한 제네릭 타입도 포함되지 않게 된다. 이유는 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해서이다.

 

 

 


타입변수의 제한
제네릭은 'T'와같은 타입변수를 사용하여 타입을 제한한다고 했다. 이때 extends 키워드를 사용하면 타입변수에 특정 타입만 사용하도록 제한할 수 있다.
클래스가 아닌 인터페이스를 구현할 경우에도 extends 키워드를 사용해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
//예제
class Test {}
interface TestI{}
class TestList<extends Test>{}
class InterfaceList <extends TestI>{}
//위 처럼 타입 변수에 제한을 걸 수 있다.
 
//main()
TestList<Test> testList = new TestList<>();
//TestList<TestI> --> error
InterfaceList<TestI> interfaceList = new InterfaceList<>();
//InterfaceList<Test> --> error
 


제한을 걸어둔 타입과 일치하지 않으면 error가 뜬다. 제한을 걸지 않아도 문제없이 실행 될 수 있으나 코드의 명확성을 위해 타입의 제한을 명시하는 편이 좋다.

 

 

 

제네릭의 장점 
- 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높인다.
- 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄인다.

 

타입안정성 예제 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//타입안정성 예제 
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; }
}
StudentPerson 과 EmployeePerson이 같은 구조이다.
 
//중복제거
class Person{
    public Object info;
    Person(Object info){ this.info = info; }
}
중복되는 클래스를 다음과 같이 하나로 만들었다.
 
//main()
Person p1 = new Person("부장");
EmployeeInfo ei = (EmployeeInfo)p1.info;
System.out.println(ei.rank);
 

실행 시 런타임 오류가 발생한다.
클래스 Person의 생성자는 매개변수 info의 데이터 타입이 Object이다. 따라서 모든 객체가 될 수 있다. EmployeedInfo나 StudentInfo가 아닌 String이 와도 컴파일 에러가 발생하지 않는다. 컴파일은 문제없는데 런타임에서 에러가 발생하는 것은 심각한 문제를 초래할 수 있다.
*이러한 상황을 타입에 대해 안전하지 않다고 하는 것이다.
*모든 타입이 올 수 있기 때문에 타입을 제한할 수 없게 되는 것이다. 제네릭이 필요하다.

 

제네릭화 예제

1
2
3
4
5
6
7
8
9
10
11
제네릭화 예제
class Person<T>{
    public T info;
    Person(T info){ this.info = info; }
}
 
//main()
Person<EmployeeInfo> p1 = new Person<EmployeeInfo>(new EmployeeInfo(1));
System.out.println(p1.info.rank); // 컴파일 성공
Person<String> p2 = new Person<String>("부장");
System.out.println(p2.info.rank); // 컴파일 실패
 

p1을 보면 제네릭이 EmployeeInfo가 들어가있다.
p1의 타입은 EmployeeInfo 라는 뜻이고 p1.info = EmployeeInfo 가 된다.
EmployeeInfo 에는 rank라는 필드가 있기 때문에 p1.info.rank가 컴파일에 성공한다.

p2를 보면 제네릭에 String이 들어가있다.
p2의 타입은 String이라는 뜻이고 p2.info = String 이 된다.
String에는 rank라는 필드가 없기 때문에 컴파일 에러가 발생한다.

 

 


형변환 예제

1
2
3
4
5
ArrayList aList = new ArrayList();
aList.add("hello");
aList.add("java");
String hello = (StringaList.get(0);
String java = (StringaList.get(1);
http://colorscripter.com/info#e" target="_blank" style="text-decoration:none;color:white">cs

제네릭스를 사용하는 대표적인 예가 ArrayList이다. 제네릭스 없이 사용가능하다.
위처럼 제네릭스를 사용하지 않을 경우 ArrayList 안에 추가되는 객체는 Object 자료형으로 인식된다. Object 자료형은 모든 객체가 상속하고 있는 가장 기본적인 자료형이다. 따라서 값을 가져올 경우 위처럼 String 자료형으로 형변환을 해주어야 한다. 이 과정에서 List안에 String 객체 이외에 다른 객체가 들어갈 수도 있기 때문에 형 변환 과정에서 잘못된 형 변환으로인한 오류가 발생할 수 있다.

 

 

제네릭 활용

1
2
3
4
5
6
ArrayList<String> aList = new ArrayList<String>();
aList.add("hello");
aList.add("java");
String hello = aList.get(0);
String java = aList.get(1);
 

제네릭을 활용하면 형변환을 따로 하지 않아도 되어 불필요한 코딩을 줄일 수 있고 다른 객체가 들어왔을때 미리 컴파일 에러를 발생시켜서 알려주기 때문에 용이하다.


* 컴파일 단계에서 오류 검출
* 중복 제거와 타입 안정성 동시에 추구

 

 


복수의 제네릭 

 

1
2
3
4
5
6
7
8
9
10
11
12
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;
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

복수의 제네릭을 사용할 때 <T,S> 와 같은 형식을 사용한다.

1
2
3
4
EmployeeInfo e = new EmployeeInfo(1);
Integer i = new Integer(10);
Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
System.out.println(p1.id.intValue()); //10 
 

제네릭은 기본 데이터 타입을 사용할 수 없고 참조 데이터 타입에 대해서만 사용할 수 있다.
Person<EmployeeInfo, int> 는 컴파일 에러를 발생시킨다.
Person<EmployeeInfo, Integer> 처럼 해야한다.

 

 


제네릭 생략 예제
제네릭은 생략 가능하다.

1
2
3
4
EmployeeInfo e = new EmployeeInfo(1);
Integer i = new Integer(10);
Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
Person p2 = new Person(e, i);
 
 

p1과 p2는 동일하게 동작한다.
하지만 제네릭이 있는 것이 가독성에 더 좋은 것같다.

 

 

 

제네릭 메소드
제네릭은 메소드에 적용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
    }
}
 
main()
EmployeeInfo e = new EmployeeInfo(1);
Integer i = new Integer(10);
Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
p1.<String>printInfo("hi"); // hi
p1.printInfo("hello"); // hello
p1.<Integer>printInfo(13); // 13
p1.printInfo(13.5); // 13.5
 
 

printInfo는 제네릭 메소드이기 때문에 입력값이 특정 타입으로 정해져있지 않다.
메소드앞에 <>은 생략 가능하다.


정리 -
ArrayList를 예로들어 이해를 시작하는 것이 가장 좋은 것 같다. 
ArrayList<String> list ... 처럼  리스트에 저장될 데이터 타입을 <String> 으로 미리 지정하는 것을 제네릭 이라고 한다. 
미리 지정하지 않으면 어떤 불편함이 있는것일까? 

ArrayList list = new ArrayList(); 와 같이 제네릭 없이도 사용 가능하다. 
이때 리스트에 들어갈 데이터 타입이 미리 지정이 되어있지 않기 때문에 
ist.add("hello");
list.add(1);
String과 int 형태의 데이터를 둘다 넣을 수 있다. 
모든 타입을 넣을 수 있다는 것은 모든 타입의 가장 상위에 있는 Object로 들어간다는 뜻이다.
그리고 데이터를 꺼내올 때 다음과 같이 형변환이 있어야 한다는 뜻이다.  
String s = (String) list.get(0);
int i = (int) list.get(1); 
list에 데이터가 2개 뿐이라 큰 문제가 없어 보인다. 

극단적인 예를 들어보자. 
데이터가 10만개고 계속 추가/삭제가 된다고 생각해보자. 타입은 int, String, float 가리지 않고 마구 들어온다. List 인터페이스를 떠올려보면 추가/삭제시 index 번호도 바뀐다. 그것에 맞춰서 일일이 형변환을 한다는 것은 불필요한 코딩이 될 수 있다. 
더 큰 문제는 위의 상황은 컴파일시에 나오는 것이 아니라 실행할때 발생하는 런타임 에러를 발생할 가능성이 높다. 

* 컴파일 단계에서 미리 오류 검출 타입 안정성을 추구 한다.   

ArrayList 처럼 자바에서 제공하는 클래스 이외에도 직접 제네릭 클래스를 만들어서 활용할수도 있다. 사용법 들을 일일이 외우기 보다는 제네릭 클래스를 활용하는 방법과 앞으로 만나게 될 예제에서 '아! 이것은 제네릭을 활용하고 있구나' 를 캐치하여 해석할 수 있게 되는것이 중요한 것 같다. 제네릭을 활용하는 예제를 만나게 되면 다시한번 제네릭에 대해서 다뤄보도록 하겠다.



https://opentutorials.org/course/1223/6237
http://tcpschool.com/java/java_generic_concept

 

제네릭 - 생활코딩

제네릭이란? 제네릭(Generic)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 말이 어렵다. 아래 그림을 보자. 위의 그림은 아래의 코드를 간략화한 것이다. package org.opentutorials.javatutorials.generic; class Person { public T info; } public class GenericDemo { public static void main(String[] args) { P

opentutorials.org

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

 

'Language > Kotlin & Java' 카테고리의 다른 글

Parcelable in Kotlin  (0) 2020.11.07
[JAVA] Hash란 무엇인가?  (2) 2020.03.21
[JAVA] MAP의 자료구조  (0) 2020.03.21
[JAVA] Call by value와 Call by reference  (0) 2020.03.21
[JAVA] abstract와 interface의 차이점  (1) 2020.03.21