java 제네릭

개미Coder
|2024. 5. 3. 11:27
반응형

## 왜 제네릭을 사용해야 하는가?

 

// 제네릭 사용하지 않은 경우
// Collection은 자바의 모든 객체를 저장할 수 있다.
List list = new ArrayList();
// 모든 타입은 Object 타입으로 업캐스팅이 되어서 저장된다.
list.add("안녕");// String
list.add(Integer.valueOf(20));		// Integer
list.add(10.5);	// Double
list.add('A');  	// Character
list.add(true);  // Boolean
list.add(10L);   // Long
list.add(1.0F); // Float

// 문제점 -> 몇번째 인덱스에 어떤 타입인지 일일이 알고 있어야 다운캐스팅이 가능하다.
// 내가 만들면, 알 수도 있다. 하지만, 남이 만든거라면, 어떤 타입인지 어떻게 알것인가..
// 즉, 원하는 데이터를 꺼내올때가 문제란 것이다. 제네릭은 이 안에 무슨 타입만 저장해! 하고 지정해주는 것이다.
if(list.get(0) instanceof String) {
    String msg = (String) list.get(0);
} else if(list.get(0) instanceof Integer) {
    Integer msg = (Integer) list.get(0);
}

 

// 제네릭 사용할 경우 : 저장타입이 결정되는 시점언제
List<String> sido_list = new ArrayList<String>();
sido_list.add("서울");	// 0
sido_list.add("경기");	// 1
sido_list.add("인천");	// 2

System.out.println("요소의 갯수 : " + sido_list.size());
System.out.println(sido_list.get(0));

// 내가만든 ArrayList 사용
MyArrayList myList = new MyArrayList();
myList.add("Hello");
myList.add(10);
myList.add(1.0);

System.out.println("요소의 갯수 : " + myList.size());	// 값 : 3
// 내가 클래스에서 메소드를 만들때 Object로 설정해놨기에 다운캐스팅 해야한다.
// String str = myList.get(0);
String str = (String) myList.get(0);
System.out.println(str.length());	// 값 : 5

 

 

- 기존에 제네릭을 사용하기전 ArrayList와 같이 만든 것

package util;

public class MyArrayList {

	Object [] data = null;
	
	public int size() {
		// data 배열안에 값이 없으면(null) 0, 있으면 그 길이만큼
		return (data == null) ? 0 : data.length;
	}
	
	public void add(Object newObj) {
		// 1. 기존 배열 보다 1개 더 많게끔 임시배열 생성
		Object [] imsi = new Object[this.size()+1];
		// 2. 원래 배열값 -> 임시배열로 옮긴다.
		for(int i = 0; i < this.size(); i++) {
			imsi[i] = data[i];
		}
		// 3. 임시배열의 추가공간에 새데이터(newObj) 넣는다.
		imsi[size()] = newObj;
		// 4. 임시배열 -> data
		data = imsi;
	}// add-end
	
	// 이 메소드를 불러올때 무슨 타입으로 불러올지 모르기에, 최상위 객체인 Object를 사용하는 것이다.
	public Object get(int index) {
		
		return data[index];
		
	}// get-end
	
}

 

 

- 제네릭으로 변경

package util;

public class MyArrayList2<선일> {
	// 일반적으로 제네릭 타입은 E : Element Type (타입이름은 자유)
	선일 [] data = null;
	
	public int size() {
		// data 배열안에 값이 없으면(null) 0, 있으면 그 길이만큼
		return (data == null) ? 0 : data.length;
	}
	
	public void add(선일 newObj) {
		// 1. 기존 배열 보다 1개 더 많게끔 임시배열 생성
		선일 [] imsi = (선일 [])new Object[this.size()+1];
		// 2. 원래 배열값 -> 임시배열로 옮긴다.
		for(int i = 0; i < this.size(); i++) {
			imsi[i] = data[i];
		}
		// 3. 임시배열의 추가공간에 새데이터(newObj) 넣는다.
		imsi[size()] = newObj;
		// 4. 임시배열 -> data
		data = imsi;
	}// add-end
	
	// 이 메소드를 불러올때 무슨 타입으로 불러올지 모르기에, 최상위 객체인 Object를 사용하는 것이다.
	public 선일 get(int index) {
		
		return data[index];
		
	}// get-end
	
}

 


- 제네릭으로 만든 임의의 List 호출

// Integer Type만 저장할 수 있는 객체
MyArrayList2<Integer> lottoList = new MyArrayList2<Integer>();
lottoList.add(1);
lottoList.add(45);
// Generic으로 지정된 Type 외에는 다른 타입은 들어갈 수 없다.
// lottoList.add("안녕~~");

// String Type 저장할 수 있는 객체
MyArrayList2<String> fruitList = new MyArrayList2<String>();
fruitList.add("사과");
fruitList.add("딸기");
System.out.println(fruitList.get(0));	// 값 : 사과
System.out.println(fruitList.get(1));	// 값 : 딸기

System.out.println("---[과일목록]---");
for(int i = 0; i < fruitList.size(); i++) {
    System.out.println(fruitList.get(i));
}


 
제네릭 타입을 이용함으로써 잘못된 타입이 새로 추가되었는데, 제네릭 타입을 이용함으로써 
잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있게 되었다. 
제네릭은 컬렉션, 람다식, 스트림. NIO에서 널리 사용된다.
API 도큐먼트를 보면 제네릭 표현이 많기 때문에 제네력을 이해하지 못하면 API 도큐먼트를 정확히 이해할 수 없다. 
제네릭은 클래스와 인터페이스, 그리고 메소드를 정의할 때 타입(type)을 파라미터(parameter) 로 시용할 수 있도록 한다. 
타입 파라미터는 코드 작성시 구체적인 타입으로 대체되어 다양한 묘드를 생성하도록 해준다.  
제네릭을 시용하는 코드는 비제네릭 코드에 비해 다음과 같은 이점을 가지고 있다.

컴파일 시 강한 타입 체크를 할 수 있다.
자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 
제네릭 코드에 대해 강한타입 체크를한다.   
실행 시 타입 에러가나는 것보다는 컴파일 시에 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋다.

타입 변환(casting)을 제거한다.

비제네릭 코드는 불필요한 타입 변환을 하기 때문에 프로그램 성능에 악영향을 미친다. 
다음 코드를 보면 List에 문자열 요소를 저장했지만, 요소를 찾아올 때는 반드시 String으로 타입 변환을 해야한다.


List list = new ArrayList(); 
list.add ("hello");
String str = (String)list.get (0);  // 타입 변환을 해야 한다




다음과 같이 제네릭 코드로 수정하면 List에 저장되는 요소를 String 타입으로 국한하기 때문에 
요소를 찾아올 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상된다.


List<String> list = new ArrayList<String>(); 
list.add ("hello" );
String str = list.get (0);    // 타입 변환을 하지 않는다.






## 제네릭 타입(class(T), interface(T))

 

 


제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. 
제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 " < > " 부호가 붙고 사이에 타입 파라미터가 위치한다. 
아래 코드에서 타입파라미터의 이름은 T이다.


public class 클래스명<T> { ... }
public interface 인터페이스명<T> { ... }




타입 파라미터는 변수명과 통일한 규칙에 따라 작성할 수 있지만 일반적으로 대문자 알파벳 한글자로 표현한다. 
제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 한다. 
그렇다면 왜 이런 타입 파라미터를 시용해야 할까? 그 이유를 알기 위해 다음 Box 클래스를 살펴보자.

public class Box { 
	private Object object;

public void set(Object object) { 
	this.object object;
} 

public Object get() { 
	return object; 
	}
}




Box 클래스의 필드 타입이 Object인데, Object 타입으로 선언한 이유는 
필드에 모든 종류의 객체를 저장하고 싶어서이다.
Object 클래스는 모든 자바 클래스의 최상위 조상(부모) 클래스이다. 
따라서 자식 객체는 부모 타입에 대입할 수 있다는 성질 때문에 
모든 자바 객체는 Object 타입으로 자동 타입 변환되어 저장된다.


Object object = 자바의 모든 객체;




set() 메소드는 매개 변수 타입으로 Object를 사용함으로써 매개값으로 자바의 모든 객체를 받을 수 있게 했고,
받은 매개값을 Object 필드에 저장시킨다. 
반대로 get() 메소드는 Object 필드에 저장된 객체를 Object 타입으로 리턴한다. 
만약 필드에 저장된 원래 타입의 객체를 얻으려면 다음과 같이 강제 타입 변환을 해야 한다.

Box box = new Box() ;
box.set("hello");     //String 타입을 Object 타입으로 자동 타입 변환해서 저장
String str = (String) box.get();    //Object 타입을 String 타입으로 강제 타입 변환해서 얻음



 

## 제네릭 타입(class(T), interface(T))

 

 

-타입을 매개변수로 바꿔서 하나의 타입을 정해주면 통일되게 사용한다.



제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. 
제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 " < > " 부호가 붙고 사이에 타입 파라미터가 위치한다. 
아래 코드에서 타입파라미터의 이름은 T이다.


public class 클래스명<T> { ... }
public interface 인터페이스명<T> { ... }




타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만 일반적으로 대문자 알파벳 한글자로 표현한다. 
제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 한다. 
그렇다면 왜 이런 타입 파라미터를 시용해야 할까? 그 이유를 알기 위해 다음 Box 클래스를 살펴보자.

public class Box { 
private Object object;

public void set(Object object) { 
this.object = object;
} 

public Object get() { 
return object; 
}
}




Box 클래스의 필드 타입이 Object인데, Object 타입으로 선언한 이유는 
필드에 모든 종류의 객체를 저장하고 싶어서이다.
Object 클래스는 모든 자바 클래스의 최상위 조상(부모) 클래스이다. 
따라서 자식 객체는 부모 타입에 대입할 수 있다는 성질 때문에 
모든 자바 객체는 Object 타입으로 자동 타입 변환되어 저장된다.


Object object = 자바의 모든 객체;


set() 메소드는 매개 변수 타입으로 Object를 사용함으로써 매개값으로 자바의 모든 객체를 받을 수 있게 했고,
받은 매개값을 Object 필드에 저장시킨다. 
반대로 get() 메소드는 Object 필드에 저장된 객체를 Object 타입으로 리턴한다. 
만약 필드에 저장된 원래 타입의 객체를 얻으려면 다음과 같이 강제 타입 변환을 해야 한다.

Box box = new Box() ;
box.set("hello");    //String 타입을 Object 타입으로 자동 타입 변환해서 저장
String str = (String) box.get();    //Object 타입을 String 타입으로 강제 타입 변환해서 얻음

 

 

-<T>를 제네릭 타입에서 매개변수로 사용해서 String으로 바꿔먹는 예시

 

-물론, Integer 정수 타입으로도 되겠지요??



 

이와 같이 Object 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있다는 장점은 있지만, 
저장할 때 타입 변환이 발생하고, 읽어올 때에도 타입 변환이 발생한다.   
이러한 타입 변환이 빈번해지면 전체 프로그램 성능에 좋지 못한 결괴를 가져올 수 있다.   
그렇다면 모든 종류의 객체를 저장하면서 타입 변환이 발생하지 않도록 하는 방법이 없을까? 
해결책은 제네릭에 있다. 다음은 제네릭을 이용해서 Box 클래스를 수정한 것이다.


public class Box <T> {
private T t;
public void  setGeneri(T t) { 
this.t = t; 
}
public T getGeneric() {  
return t; 
}
}




타입 파라미터 T를 사용해서 Object 타입을 모두 T로 대체했다.    
T는 Box 클래스로 객체를 생성할 때 구체적인 타입으로 변경된다.   
예를 들어 다음과 같이 Box 객체를 생성했다고 가정해보자.


Box<String> box = new Box<String>();


타입 파라미터 T는 String 타입으로 변경되어 Box 클래스의 내부는 다음과 같이 자동으로 재구성된다.

public class Box <String> {
private String t;
public void  setGeneric(String t) { 
this.t = t; 
}
public String getGeneric() {  
return t; 
}
}



필드 타입이 String으로 변경되었고, setGeneric() 메소드도 String 타입만 매개값으로 받을 수 있게 변경 되었다. 
그리고 getGeneric() 메소드역시 String 타입으로 리턴하도록 변경되었다. 
그래서 다음코드를 보면 저장할 때와 읽어올 때 전혀 타입 변환이 발생하지 않는다.


Box<String> box = new Box<String>(); 
box.setGeneric("hello");
String str =  box.getGeneric();




이번에는 다음과 같이 Box 객체를 생성했다고 가정해보자. 
Integer는 int 값에 대한 객체 타입으로 자바에서 제공하는 표준 API이다.

Box<Integer> box = new Box<Integer>();


타입 파라미터 T는 Integer 타입으로 변경되어 Box 클래스는 내부적으로 다음과 같이 자동으로 재구성된다.

public class Box <Integer> {
private Integer t;
public void setGeneric(Integer t) { 
this.t = t; 
}
public Integer getGeneric() {  
return t; 
}
}




필드 타입이 Integer로 변경되었고, setsetGeneric() 메소드도 Integer 타입만 매개값으로 받을 수 있게 변경 되었다. 
그리고 getGeneric() 메소드 역시 Integer 타입으로 리턴하도록 변경되었다. 
그래서 다음 코드를 보면 저장할 때와 읽어올 때 전혀 타입 변환이 발생하지 않는다.


Box<Integer> box = new Box<Integer>();
box.setGeneric(6); // 자동 Boxing
int value = box.getGeneric(); // 자동 UnBoxing



이와 같이 제네릭은 클래스를 설계할 때 구체적인 타입을 명시하지 않고 
타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구체적인 타입을 지정함으로써 타입 변환을 최소화시킨다.







## 멀티 타입 파라미터(class(K, V, .. .), inte야ace(K, V, .. .)}

제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있는데, 이 경우 각 타입 파라미터를 콤마로 구분한다. 

다음 예제는 Product <T , M) 제네릭 타입을 정의하고 ProductExample 클래스에서 
Product<Tv, String) 객체와 Product<Car, String) 객체를 생성한다. 
그리고 Getter와 Setter를 호출하는 방법을 보여준다.

 

반응형

'java(2)↗' 카테고리의 다른 글

java 컬렉션 프레임워크 (Set, Map)  (0) 2024.05.03
java 컬렉션 프레임워크(List)  (0) 2024.05.03
java 멀티 스레드  (0) 2024.05.02
java 중첩 클래스  (0) 2024.05.02
java Arrays 클래스, Boxing 박싱, Date, Format  (0) 2024.05.01