제네릭을 왜 쓰는가?
바나나와 책을 저장하는 객체가 있다고 생각해보자
// Banana클래스와 Banana클래스를 담을 수 있는 Good1클래스
class Banana{}
class Goods1{
private Banana banana = new Banana();
public Banana getBanana(){
return banana;
}
public void setBanana(Banana banana){
this.banana = banana;
}
}
// Book클래스와 Book클래스를 담을 수 있는 Good2클래스
class Book{}
class Goods2{
private Book book = new Book();
public Book getBook(){
return book;
}
public void setBook(Book book){
this.book = book;
}
}
public class ProblemBeforeGeneric {
public static void main(String[] args) {
//Goods1을 이용해 Banana객체를 추가하거나 가져오기
Goods1 goods1 = new Goods1();
goods1.setBanana(new Banana()); //Banana타입만 입력 가능
Banana banana = goods1.getBanana(); //Banana 타입 리턴
//Goods2을 이용해 Book객체를 추가하거나 가져오기
Goods2 goods2 = new Goods2();
goods2.setBook(new Book()); //Book 타입만 입력 가능
Book book = goods2.getBook(); //Book 타입 리턴
}
}
Goods1클래스는 객체를 생성함으로써 바나나를 저장(set)하고 저장된 바나나를 가져오는(get) 기능을 수행할 수 있다.
Goods2클래스도 마찬가지로 책을 저장 및 관리하는 클래스로 필드가 Book타입이라는 것을 제외하면
클래스 Goods1과 동일하다
만약 여기서 사과, 배, 물, 등등.. 여러 물체를 저장한다고 하면 해당 객체에 대해 저장하는 클래스를 전부 생성해 주어야 할 것이다.
1개의 상품 클래스로 모든 타입의 상품을 저장하고 관리할 수 없을까 라고 생각할 수 있다.
모든 클래스의 최상위 객체인 Object 타입을 선언하여 저장 및 읽기 수행이 가능할 것이다.
class Banana{}
class Book{}
// Banan, Book클래스를 모두 담고 꺼낼 수 있는 클래스
class Goods{
private Object object = new Object();
public Object getObject(){
return object;
}
public void setObject(Object object){
this.object = object;
}
}
public class Solution1_UsingObject {
public static void main(String[] args) {
//Goods1을 이용해 Banana객체를 추가하거나 가져오기
Goods goods1 = new Goods();
goods1.setObject(new Banana());
Banana banana = (Banana)goods1.getObject();
//Goods2을 이용해 Book객체를 추가하거나 가져오기
Goods goods2 = new Goods();
goods2.setObject(new Book());
Book book = (Book)goods2.getObject();
//3. 잘못된 캐스팅 (약한 타입체크)
// Goods goods3 = new Goods();
// goods3.setObject(new Apple());
// Pencil pencil2 = (Pencil)goods3.getObject(); //실행예외
}
}
Object 타입으로 다양한 객체를 저장할 때 문제점이 발생하는데, 잘못된 캐스팅으로 실행예외가 발생하여 프로그램이 종료 될 수 있다. (약한체크타입으로 문법 오류를 발생시키지 않는다)
이러한 문제점까지 해결한 개념이 제네릭 이다.
제네릭 문법
제네릭은 잘못된 캐스팅을 할때 문법 오류를 발생시켜 사전에 문제를 예방한다. 이를 강한 체크 타입이라고 한다.
제네릭의 문법 구조는 다음과 같다.
- 클래스: 제네릭 타입 변수명이 1개일 때
접근지정자 class 클래스명<T>{
//타입 T를 사용한 코드
}
- 클래스: 제네릭 타입 변수명이 2개일 때
접근지정자 class 클래스명<K, V>{
//타입 T, V 를 사용한 코드
}
- 인터페이스: 제네릭 타입 변수명이 1개일 때
접근지정자 interface 클래스명<T>{
//타입 T를 사용한 코드
}
- 인터페이스: 제네릭 타입 변수명이 2개일 때
접근지정자 interface 클래스명<K, V>{
// 타입 K,V를 사용한 코드
}
제네릭 타입 변수의 관례적 표기 및 의미
제네릭 타입 변수 | 의미 |
T | 타입(Type) |
K | 키(Key) |
V | 값(Value) |
N | 숫자(Number) |
E | 원소(Element) |
제네릭 클래스의 객체 생성
클래스명<실제 제네릭 타입> 참조변수명 = new 클래스명<실제 제네릭 타입>();
OR
클래스명<실제 제네릭 타입> 참조변수명 = new 클래스명<>();
- 제네릭 클래스는 클래스를 정의하는 시점에 타입을 지정하는 것이 아니라 객체를 생성하는 시점에 타입을 지정함
- 하나의 제네릭 클래스로 다양한 타입의 객체를 저장 및 관리할 수 있는 객체를 생성할 수 있음
제네릭 타입 변수 1개를 가진 제네릭 클래스의 선언 및 활용
class MyClass<T>{
private T t;
public T get(){
return t;
}
public void set(T t){
this.t = t;
}
}
public class SingleGenericArgument {
public static void main(String[] args) {
MyClass<String> mc1 = new MyClass<String>(); //String타입을 저장하거나 꺼내 올 수 있는 객체로 지정
mc1.set("안녕");
System.out.println(mc1.get());
MyClass<Integer> mc2 = new MyClass<>(); //Integer타입을 저장하거나 꺼내올 수 있는 객체로 지정
mc2.set(100);
System.out.println(mc2.get());
// MyClass<Integer> mc3 = new MyClass<>();
// mc3.set("안녕"); // 강한 타입 체크로 문법 오류 발생
}
}
결과
안녕
100
제네릭 타입 변수 2개를 가진 제네릭 클래스의 선언 및 활용
class KeyValue<K,V> {
private K key;
private V value;
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
public class TwoGenericArguments {
public static void main(String[] args) {
//제네릭 타입 변수 K,V가 각각 String, Integer타입으로 결정
KeyValue<String, Integer> kv1 = new KeyValue<>();
kv1.setKey("사과");
kv1.setValue(1000);
String key1 = kv1.getKey();
int value1 = kv1.getValue();
System.out.println("key:"+ key1 + " value:" +value1);
//제네릭 타입 변수 K,V가 각각 String, Integer타입으로 결정
KeyValue<Integer, String> kv2 = new KeyValue<>();
kv2.setKey(404);
kv2.setValue("Not Found(요청한 페이지를 찾을 수 없습니다.)");
int key2 = kv2.getKey();
String value2 = kv2.getValue();
System.out.println("key:"+ key2 + " value:" +value2);
// 해당 제네릭 타입 변수의 필드를 사용하지 않는다는 것을 의미
KeyValue<String, Void> kv3 = new KeyValue<>();
kv3.setKey("키값만 사용");
String key3 = kv3.getKey();
System.out.println("key:" + key3);
}
}
결과
key:사과 value:1000
key:404 value:Not Found(요청한 페이지를 찾을 수 없습니다.)
key:키값만 사용
제네릭의 역할은
- 추가 클래스 생성 없이 어떤 상품도 저장 및 관리할 수 있다.
- setter메서드에 잘못된 객체를 입력했을 때 바로 문법으로 체크할 수 있어야 한다
- getter메서드의 리턴 타입도 다운캐스팅이 필요 없어야 한다.
항목1은 Object타입으로 선언하는 것이 가능하지만 2,3 까지 만족시키는 방법은 제네릭을 이용하는 것이다.
제네릭 클래스를 사용해 다양한 객체를 저장하는 다음 예제를 살펴보자.
class Banana{}
class Book{}
class Goods<T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
public class Solution2_Generic {
public static void main(String[] args) {
//1. Goods을 통해서 Banana 객체 추가 및 가져오기
Goods<Banana> goods1 = new Goods<>();
goods1.set(new Banana());
Banana Banana = goods1.get(); //다운캐스팅 필요 없음
//2. Goods을 통해서 Book 객체 추가 및 가져오기
Goods<Book> goods2 = new Goods<>();
goods2.set(new Book());
Book Book = goods2.get();
//3. 잘못된 타입선언
Goods<Banana> goods3 = new Goods<>();
goods3.set(new Banana());
// Book Book2 = goods3.get(); //강한타입체크
}
}
제네릭 메서드
- 일반 클래스 내부의 특정 메서드만 제네릭으로 선언할 수 도 있다.
- 제네릭 메서드는 호출되는 시점에 실제 제네릭 타입을 지정한다.
제네릭 메서드의 문법구조
- 제네릭 타입 변수명이 1개일 때
접근지정자 <T> T 메서드명 (T t) {
// 타입 T를 사용한 코드
}
- 제네릭 타입 변수명이 2개일 때
접근지정자 <T, V> T 메서드명 (T t, V v) {
// 타입 T, V를 사용한 코드
}
- 매개변수에만 제네릭이 사용됐을 때
접근지정자 <T> void 메서드명 (T t) {
// 타입 T를 사용한 코드
}
- 리턴 타입에만 제네릭이 사용됬을 때
접근지정자 <T> T 메서드명 (int a) {
// 타입 T를 사용한 코드
}
제네릭 메서드 호출의 문법 구조
참조객체.<실제 제네릭 타입>메서드명(입력매개변수);
입력매개변수에 제네릭 타입 변수가 사용돼 입력 매개변수의 타입만으로 실제 제네릭 타입을 예측할 수 있을 때 생략할 수 있다. 다음과 같이 method1(T t)와 같은 제네릭 메서드를 보자
public <T> T method1(T t){
return t;
}
이때 제네릭 타입 변수로 Integer를 대입해 메서드를 호출하고자 할 때 참조객체.<Integer>method1(100)과 같이 사용해야 하지만, 입력매개변수를 보고 제네릭 타입 변수의 실제 타입을 예측할 수 있어 제네릭 타입 변수를 생략할 수 있다.
참조객체.method1(100)으로 표현해도 무방하다.
일반 클래스의 내부에 포함되는 제네릭 메서드 예제
class GenericMethods{
public <T> T method1(T t){
return t;
}
public <T> boolean method2 (T t1, T t2){
return t1.equals(t2);
}
public <K, V> void method3(K k, V v){
System.out.println(k + ":" +v);
}
}
public class GenericMethod {
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
String str1 = gm.<String>method1("안녕"); //제네릭 타입을 String으로 지정
// 입력매개변숫값으로 제네릭 타입을 유추할 수 있을 때 제네릭 타입 지정 생략 가능
String str2 = gm.<String>method1("안녕");
System.out.println(str1);
System.out.println(str2);
boolean bool1 = gm.<Double>method2(2.5, 2.5);
boolean bool2 = gm.method2(2.5,2.5);
System.out.println(str1);
System.out.println(str2);
gm.<String, Integer>method3("국어", 80);
gm.method3("국어", 80);
}
}
결과
안녕
안녕
안녕
안녕
국어:80
국어:80
'Language > Java' 카테고리의 다른 글
[Java] 매개변수의 다형성 (0) | 2022.06.10 |
---|---|
[Java] 쓰레드의 상태 (0) | 2022.04.06 |
[Java] 쓰레드의 동기화 (0) | 2022.04.06 |
[Java] 쓰레드의 속성 (0) | 2022.04.05 |
[Java] 쓰레드, 멀티쓰레드 (0) | 2022.04.05 |