▩ 목 차 ▩
1. 실수를 방지할 수 있도록 도와주는 제네릭
1-1. 제네릭이 뭐지?
1-2. 제네릭 타입의 이름 정하기
1-3. 제네릭에 ?가 있는 것은 뭐야?
1-4. 제네릭 선언에 사용하는 타입의 범위도 지정할 수 있다.
1-5. 메소드를 제네릭하게 선언하기
2. 제네릭 정리
■ 1. 실수를 방지할 수 있도록 도와주는 제네릭 ■
개발을 하면서 점(.) 하나 잘목 찍거나 세미콜론(;)을 잘못 찍어서 컴파일이 안된 경우가 많을 것이다. 이클립스를 사용하면 코딩 단계에서 매우 쉽게 걸러낼 수 있지만, 자잘한 실수가 아니라 실행시에 개발자가 미처 생각하지 못한 부분에서 프로그램이 예외를 발생시키는 경우가 있다.
==> 위의 경우를 위해서 메소드 개발과 JUnit과 같은 테스트 코드를 작성하는 것이 좋다.
[ *JUnit : 메소드나 클래스 같은 작은 단위를 쉽게 테스트 할 수 있도록 도와주는 프레임워크 이다. ]
[EX] - DTO 클래스
package part21;
import java.io.Serializable;
public class CastingDTO implements Serializable{
private Object object;
public void setObject(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
}
DTO(Data Transfer Object) 는 계층 간 데이터 교환을 하기 위해 사용하는 객체로 위와 같이 private 변수, getter, setter, Serializable 구현을 해야만 제대로된 DTO 클래스라고 할 수 있다.
[EX] - 위에서 정의한 CastingDTO를 사용하는 GenericSample 클래스
package part21;
public class GenericSample {
public static void main(String[] args) {
GenericSample sample = new GenericSample();
sample.checkCastingDTO();
}
public void checkCastingDTO() {
CastingDTO dto1 = new CastingDTO();
dto1.setObject(new String());
CastingDTO dto2 = new CastingDTO();
dto2.setObject(new StringBuffer());
CastingDTO dto3 = new CastingDTO();
dto3.setObject(new StringBuilder());
}
}
ObjectCheckSample 클래스의 checkCastingDTO() 메소드를 살펴보면 dto1~dto3까지의 CastringDTO 클래스의 객체를 만들었다. dto1에는 String을, dto2에는 StringBuffer, dto에는 StringBuilder 객체를 각각 지정하였다. 이 코드는 컴파일 및 실행이 문제없다.
==> 왜냐하면 Object 클래스는 모든 클래스의 부모 클래스이므로, 매개변수로 어떤 참조 자료형을 넘겨도 상관없기 때문이다.
문제는 저장되어 있는 값을 꺼낼 때 발생한다. 즉, 각 객체의 getObject()메소드를 호출했을 때 리턴 값으로 넘어오는 타입은 Object다. 그래서 String, StringBuffer, StringBuilder 타입으로 형 변환을 해야만 한다.
String temp1=(String)dto1.getObject();
StringBuffer temp2=(StringBuffer)dto2.getObject();
StringBuilder temp3=(StringBuilder)dto.getObject();
[ 위의 코드를 자세히 말해보자면 오른쪽에 있는 ??.getObject(); 구문은 Object 이기때문에 부모 클래스이다. 하지만 왼쪽에 있는 String, StringBuffer, StringBuilder 들은 자식 클래스이다. 즉 ,부모 클래스가 자식 클래스로 형변환을 했기 때문에 ()를 이용하여 형변환 하려는 타입을 명시해준다. ]
근데 만약 dto2의 인스턴스 변수의 타입이 StringBuilder인지, StringBuffer인지가 혼동될 경우는 어떻게 할 까?
==> instanceof라는 예약어를 사용하여 타입을 점검하면 된다.
[EX] - instanceof 예약어를 이용한 타입 점검
public void checkDTO(CastingDTO dto) {
Object tempObject = dto.getObject();
if(tempObject instanceof StringBuilder) {
System.out.println("StringBuilder");
}
else if(tempObject instanceof StringBuffer) {
System.out.println("StringBuffer");
}
}
위의 코드처럼 이렇게 점검을 하는것은 매우 귀찮다.
==> 이러한 단점을 보완하기 위해서 제네릭 이란것이 있다.
■ 1-1. 제네릭이 뭐지?
제네릭은 타입 형 변환에서 변환에서 발생할 수 있는 문제점을 "사전"에 없애기 위해서 만들어졌다.
[ "사전"이라고 하는 것은 예외가 발생하는 것을 처리하는 것이 아니라, 컴파일 할 때 점검할 수 있도록 한 것을 말한다. ]
앞에서 만든 CastingDTO 클래스를 보게 된다면 타입을 Object로 처리를 하기 떄문에 어떤 타입이든 사용할 수 있다.
이 클래스를 제네릭으로 선언하면 다음과 같다.
[EX] - CastingDTO 제네릭선언
package part21;
import java.io.Serializable;
public class CastingGenericDTO<T> implements Serializable {
private T object;
public void setObject(T obj) {
this.object = obj;
}
public T getObject() {
return object;
}
}
기존 클래스와 차이점은 클래스 선언문에 꺽쇠가 열리고 알파벳 T가 적히고 닫힌것을 볼 수 있다. 그리고 또 앞의 소스에서는 Object라는 타입을 선언했지만, 제네릭 소스코드를 보면 타입 부분이 전부 T로 바뀐 것을 알 수 있다.
[ 여기에 있는 T는 아무런 이름이나 지정해도 컴파일 하는데 전혀 상관 없다. ]
꺽쇠 안에 선언한 그 이름은 가상의 타입이라고 생각하면 된다. [ 꺽쇠 안에는 현재 존재하는 클래스 이름을 사용해도 되고, 존재하지 않은 것을 사용해도 된다. 되도록이면 클래스 이름의 명명 규칙과 동일하게 지정하는 것이 좋다. ]
[EX] - 제네릭 클래스를 이용하여 각 타입을 명시해줘 객체를 생성
public void checkGenericDTO() {
CastingGenericDTO<String> dto1 = new CastingGenericDTO<String>();
dto1.setObject(new String());
CastingGenericDTO<StringBuffer> dto2 = new CastingGenericDTO<StringBuffer>();
dto2.setObject(new StringBuffer());
CastingGenericDTO<StringBuilder> dto3 = new CastingGenericDTO<StringBuilder>();
dto3.setObject(new StringBuilder());
}
위의 코드를 한번 봐보자.
객체를 선언할 때 꺽쇠안에 각 타입을 명시해줘서 귀찮게만 된 것 같을 수도 있다. 하지만 명시해준 타입으로 저장이 되는 로직을 갖기 때문에(set) dto1~dto3까지의 객체의 getObject() 메소드를 사용하여 객체를 가져올 때는 다음과 같이 단단해진다.
[EX] - 제네릭 클래스를 이용한 명시적으로 타입을 지정
String temp1 = dto1.getObject();
StringBuffer temp2 = dto2.getObject();
StringBuilder temp3 = dto3.getObject();
전에 처럼 instanceof()로 변수 타입을 검사한 후 형 변환을 할 필요가 없어진 것을 볼 수 있다.
==> 왜냐하면 dto1~dto3의 제네릭 타입은 각각 String, StringBuffer, StringBuilder이기 때문이다.
[ 만약 잘못된 타입으로 치환하면 컴파일 자체가 안되기 떄문에 실행시에 잘못 형 변환하여 예외가 발생하는 일은 없다. ]
위에 예시와 같이 명시적으로 타입을 지정할 때 사용하는 것이 제네릭이라는 것이다.
■ 1-2. 제네릭 타입의 이름 정하기
제네릭 타입을 선언할 때에는 클래스 선언시 꺾쇠 안에 어떤 단어가 들어가더라도 상관이 없다고 했다. 그런데 자바에서 정의한 기본 규칙은 있다.
- E : 요소 (Element, 자바 컬렉션에서 주로 사용됨)
- K : 키
- N : 숫자
- T : 타입
- V : 값
- S,U,V : 두 번째, 세 번째, 네 번째에 선언된 타입
위와 같은 규칙을 지켜야 컴파일이 되는것은 아니다. 하지만 다른 어떤 사람이 보더라도 쉽게 이해할 수 있도록 하려면 위 규칙을 따르는게 좋다.
■ 1-3. 제네릭에 ?가 있는 것은 뭐야?
제네릭을 사용할 때 <>안에 들어가는 타입은 기본적으로 어떤 타입이라도 상관 없다.
이번절에는 메소드의 매개변수로 넘어가는 제네릭에 대해서 알아보자.
[EX] - 제네릭 클래스
package part21;
public class WildcardGeneric<W> {
W wildcard;
public void setWildcard(W wildcard) {
this.wildcard = wildcard;
}
public W getWildcard() {
return wildcard;
}
}
package part21;
public class WildcardSample {
public static void main(String[] args) {
// TODO Auto-generated method stub
WildcardSample sample = new WildcardSample();
sample.callWildcardMetod();
}
public void callWildcardMetod() {
WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
wildcard.setWildcard("A");
wildcardStringMethod(wildcard);
}
public void wildcardStringMethod(WildcardGeneric<String> c) {
String value = c.getWildcard();
System.out.println(value);
}
}
위의 2개의 소스코드중 위에 있는 소스코드는 제네릭 클래스의 소스코드이고, 아래의 소스코드는 제네릭 클래스를 이용하여 명시적으로 타입을 지정하여 객체를 생성하여 이용하는 메소드(callWildcardMethod())를 보여주고 매개변수로 제네릭타입을 받는 메소드(wildcardStringMethod())를 보여주는 소스코드이다. 아래의 자세한 설명을 보자.
- callWildcardMethod()메소드에서는 WildcardGeneric이라는 클래스에 String을 사용하는 제네릭한 객체를 생성한다.
- 생성한 객체로 wildcardStringMethod()를 호출할 때 매개변수로 넘겨준다.
- wildcardStringMethod()에서는 해당 매개변수를 받아서 결과를 호출한다.
wildcardStringMethod()를 한번 보자.
이 메소드의 매겨 변수는 반드시 String을 사용하는 WildcardGeneric 객체만 받을 수 있다.
만약 다른 타입으로 선언된 WildcardGeneric 객체를 받으려면 어떻게 해야 할까? 예를 들면 WildcardGeneric<Integer>와 같이 선언된 객체를 받는 경우이다.
==> 답이 없다. 왜냐하면 제네릭한 클래스의 타입만 바꾼다고 Overloading이 불가능하기 떄문이다. 그래서 이런 경우에는 다음과 같이 선언하는 것이 가능하다.
[EX] - 제네릭 클래스의 객체를 매개변수로 Object 타입으로 받는 방법(Object로 받으면 모든것을 다 사용 가능하다. 왜냐하면 Object 클래스는 모든 클래스의 아버지이기 떄문이다.)
public void wildcardStringMethod(WildcardGeneric<?> c) {
Object value = c.getWildcard();
System.out.println(value);
}
위와 같이 String 대신에 ?를 적어주면 어떤 타입이 제네릭 타입이 되더라도 상관없다.
하지만, 메소드 내부에서는 해당 타입을 정확히 모르기 때문에 Object로 처리해야만 한다.( Object는 모든 클래스의 아버지 이기때문에 모든 것을 다 수용가능하기 때문)
여기서 ?로 명시한 타입을 영어로는 wildcard 타입이라고 부른다.
만약 넘어오는 타입이 두 세가지로 정해져 있다면, 다음과 같이 메소드 내에서 instanceof 예약어를 사용하여 해당 타입을 확인하면 된다.
[EX] - wildcard 타입을 이용하고 instanceof 예약어를 사용하여 타입 확인
public void wildcardStringMethod(WildcardGeneric<?> c) {
Object value = c.getWildcard();
if(value instanceof String) {
System.out.println(value);
}
}
중요한 것은 wildcard(?)는 메소드의 매개변수로만 사용하는 것이 좋다.
==> 왜냐하면 어떤 객체를 wildcard로 선언하고, 그 객체의 값은 가져올 수는 있지만(get), wildcard로 객체를 선언했을때에는 특정 타입으로 값을 지정하는것(set)은 불가능 하기 때문이다.
■ 1-4. 제네릭 선언에 사용하는 타입의 범위도 지정할 수 있다.
제네릭을 사용할 때 <>안에는 어떤 타입이라도 상관 없다고 했지만, wildcard를 이용해서 사용하는 타입을 제한 할 수 는 있다.
사용방법으로는 "?" 대신에 "? extends 타입"으로 선택하는 것이다.
[EX] - wildcard를 이용하여 제네릭 선언에 사용하는 타입의 범위를 지정하기
package part21;
public class Car {
protected String name;
public Car(String name) {
this.name = name;
}
public String toString() {
return "Car name="+name;
}
}
package part21;
import java.util.jar.Attributes.Name;
public class Bus extends Car{
public Bus(String name) {
super(name);
}
public String toString() {
return "Bus name="+ name;
}
}
package part21;
public class CarWildcardSample {
public static void main(String[] args) {
CarWildcardSample sample = new CarWildcardSample();
sample.callBoundeWildcardMethod();
}
public void callBoundeWildcardMethod() {
WildcardGeneric<Car> wildcard = new WildcardGeneric<Car>();
wildcard.setWildcard(new Car("Mustang"));
boundedWildcardMethod(wildcard);
}
public void boundedWildcardMethod(WildcardGeneric<? extends Car> c ) {
Car value = c.getWildcard();
System.out.println(value);
}
}
Car name=Mustang
위의 소소 코드가 3개 있다. 첫 번쨰 소스코드는 Car 클래스이고, 두 번째 소스코드는 Car클래스를 상속받은 Bus 클래스이다.
세번째 소스코드는 callBoundeWildcardMethod() 메소드에서는 제네릭 클래스의 객체를 생성하는데 타입을 Car 클래스인 객체를 생성하는 것이다. Car 클래스 타입의 객체를 생성한 후 setWildcard()메소드를 이용하여 Car 클래스의 생성자를 이용해 "Mustang"을 넣고 객체를 생성했다. 그리고 boundedWildcardMethod()메소드를 호출하는데 매개변수를 제네릭 클래스인 wildcardGenric 클래스로 받는데 여기서 타입은 ?인 wildcard 타입이다. 근데 여기서 다른점은 extends를 이용하여 Car 타입만을 매개변수 타입으로 허용한다는 점이다. 만약 Car 타입이 아닌 다른 타입을 매개변수로 넘겨준다면 컴파일 오류가 뜬다. 하지만 Car타입의 상속을 받은 클래스 타입은 가능하다. 즉 Bus 클래스 타입도 가능한 것이다.
[EX] - Car 타입의 상속을 받은 Bus 타입도 제네릭 타입의 매개변수로 가능한지 여부
public void callBoundeWildcardMethod() {
WildcardGeneric<Bus> wildcard = new WildcardGeneric<Bus>();
wildcard.setWildcard(new Bus("Mustang"));
boundedWildcardMethod(wildcard);
}
public void boundedWildcardMethod(WildcardGeneric<? extends Car> c ) {
Car value = c.getWildcard();
System.out.println(value);
}
Bus name=Mustang
위의 코드를 보게 되면 <? extends Car>를 이용하여 Car 타입만을 매개변수 타입으로 허용을 하였지만, Car를 상속받은 Bus 타입도 정상적으로 출력이 된다는 점이다.
여기서 <? extneds Car>는 "Bounded Wildcars"라고 부른다. 즉, 매개 변수로 넘어오는 제네릭 타입의 경계를 지정하는데 사용한다는 의미로 해석하면 된다. 또한 "?로 사용하는 wildcard와 마찬가지로, Bounded wildcards로 선언한 타입에는 값을 할당할 수 는 없다.
==> 조회용 매개변수로 사용해야만 한다.
■ 1-5. 메소드를 제네릭하게 선언하기
앞에서 wildcard로 메소드를 선언하는 방법에는 매개변수로 사용된 객체에 값을 추가할 수가 없다는 것의 단점이 있었다.
방법이 있다. 아래 예제를 보며 방법을 익히자.
[EX] - wildcard로 메소드를 선언하는 방법에 매개변수로 사용된 객체에 값을 추가하는 메소드
package part21;
public class GenericWildcardSample {
public static void main(String[] args) {
GenericWildcardSample sample = new GenericWildcardSample();
}
public <T> void genericMethod(WildcardGeneric<T> c, T addValue) {
c.setWildcard(addValue);
T value = c.getWildcard();
System.out.println(value);
}
}
위의 코드에서 genericMethod() 메소드를 보게 되면 리턴 타인 앞에 <>로 제네릭 타입을 선언해 놓았다. 그리고 매개변수에서는 그 제네릭 타입이 포함된 객체를 받아서 처리한 것을 볼 수 있다. 게다가 값도 할당을 하였다. 값이 제대로 할당이 되는지 아래 예시로 값을 할당을 해보자.
[EX] - wildcard로 메소드를 선언하는 방법에 매개변수로 사용된 객체에 값을 추가하는 메소드에 값 넣어서 확인
public void callGenricMethod() {
WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
genericMethod(wildcard, "Data");
}
Data
위의 코드를 살펴보자. 제네릭 클래스를 이용하여 타입(String)을 지정하여 wildcard라는 객체를 생성했다. 그리고 genericMethod()메소드에 wildcard의 객체와 "Data"라는 String 문자열을 매개변수로 보냈다. 그러면 generiMethod()메소드가 호출되어 wildcard 객체가 c 객체에 복사(heap영역에 같은 값을 가르키는것)되고 "Data" Straing 문자열은 addValue에 복사가 된다. [ 메모리 관점에서 보게되면 wildcard객체와 c객체는 Stack에서는 서로 다른 영역에 있지만 heap영역에서 같은 값을 참조한다. 즉, c객체를 바꾸고 메소드를 종료하더라도 wildcard객체는 c객체와 heap 영역에 있는 같은 실제값을 참조하기 때문에 똑같이 변하게 되는것이다.(pass by reference) ] 여기서 c변수(wildcard객체)에 addvalue("Data")의 값을 넣고(setWildcard) T 타입의 value변수에 c변수에 값을 불러와(getWildcard) 할당한다. 그리고 나서 출력문을 이용해서 value를 찍게된다.
그렇다면 wildcard로 메소드를 선언하는 방법에는 매개변수로 사용된 객체에 값을 추가하는데, 그 중 원하는 타입만을 정해줄 수는 없을까?
즉, Bounded Wildcards 처럼 원하는 타입만 객체에 값을 추가한다는 말이다.
public <T extends Car> void boundedGenricMethod(WildcardGeneric<T> c, T addValue) {
c.setWildcard(addValue);
T value = c.getWildcard();
System.out.println(value);
}
public void callGenricMethod_bounded() {
WildcardGeneric<Car> wildcard = new WildcardGeneric<Car>();
boundedGenricMethod(wildcard, new Car("aaa"));
}
위와 같이 사용하면 Bounded Wilcards처럼 원하는 타입만 객체에 값을 추가할 수 있다.
그러면 내가 만들 메소드는 제네릭 타입이 두 개 인데, 이럴땐 어떻게 할까?
public <S,T extends Car> void multiGenericMethod(WildcardGeneric<S> c, S addValue, WildcardGeneric<T> e,T another) {
c.setWildcard(addValue);
S value = c.getWildcard();
e.setWildcard(another);
T value2 = e.getWildcard();
}
public void callGenricMethod_multi() {
WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
WildcardGeneric<Car> wildcard2 = new WildcardGeneric<Car>();
multiGenericMethod(wildcard,"Data", wildcard2, new Car("aaa"));
}
위와 같이 한 개 이상의 제네릭 타입 선언 콤마로 구분하여 나열하여 주면 된다.
이렇게 하면 S와 T라는 제네릭 타입을 메소드에서 사용할 수 있다.
■ 2. 제네릭 정리 ■
이 장에서 설명한 대로 제네릭 클래스를 선언할 수는 없어도, 사용하는 방법은 꼭 이해하고 넘어가야만 한다. 왜냐하면 엄청나게 많이 사용되기 때문이다.
사용법만 잘 알아도 자바 개발하는 데는 큰 무리가 없다.
따라서 선언하는 부분이 이해 안된다고 낙심하고 이 책을 덮어버리면 절대 안된다.
'JAVA > 자바의신 2' 카테고리의 다른 글
24장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part3(Map) (1) | 2022.09.19 |
---|---|
23장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part2(Set과 Queue) (0) | 2022.09.18 |
22장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part1(List) (0) | 2022.09.18 |
20장 가장 많이 쓰는 패키지는 자바랭 (0) | 2022.09.17 |
19장 자바에 대해서 더 알아보자 (1) | 2022.09.15 |