1. 제네릭이 대체 뭐야?
💡 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법
⇒ 타입을 변수화한 기능 타입 파라미터: Reference 타입만 가능
- 컴파일 타임에 타입 검사를 통한 예외 방지
2. 문법 (클래스, 인터페이스, 메서드)
(1) 제네릭 클래스, 인터페이스 정의
// 클래스
접근 지정자 class 클래스명<T> {
}
접근 지정자 class 클래스명<K, V>{ // 제네릭 타입 변수명이 2개일 때
}
// 인터페이스
접근 지정자 interface 클래스명<T>{
}
접근 지정자 interface 클래스명<K, V>{
}
제네릭 타입 변수 관례적 표기 | 의미 |
T | 타입 Type |
K | 키 Key |
V | 값 Value |
N | 숫자 Number |
E | 원소 Element |
(2) 제네릭 클래스
제네릭 클래스 - 객체 생성
💡 제네릭 클래스는 클래스를 정의하는 시점에 타입을 지정하는 것이 아니라, 객체를 생성하는 시점에 타입을 지정한다.
클래스명<실제 제네릭 타입> 참조변수명 = new 클래스명<실제 제네릭 타입>();
or
클래스명<실제 제네릭 타입> 참조변수명 = new 클래스명<>();
package doit16generics.useGeneric;
public class Main {
public static void main(String[] args) {
Goods<Apple> appleGoods = new Goods<Apple>(); // 객체 생성 시점에 타입 결정
appleGoods.set(new Apple());
Apple apple = appleGoods.get();
Goods<Pencil> pencilGoods = new Goods<>(); // 객체 생성 시점에 타입 결정
pencilGoods.set(new Pencil());
Pencil pencil = pencilGoods.get();
Goods<Pencil> goods3 = new Goods<>();
goods3.set(new Pencil());
// Apple apple2 = goods3.get(); // 잘못된 타입 선언 -> 문법 오류 발생
}
}
MyClass<String> mc1 = new MyClass();
mc1.set("안녕");
System.out.println(mc1.get()); // 안녕
MyClass<Integer> mc2 = new MyClass();
mc2.set(100);
System.out.println(mc2.get()); // 100
<❓> 타입 파라미터 할당 가능 타입
💡 Reference 타입
// 기본 타입 int는 사용 불가
// List<int> intList = new List<>();
// Reference 타입만 사용 가능
List<Integer> integerList = new List<>();
- 할당 가능한 타입: 모든 참조 타입(Reference Type)이 가능하다.
- 예시:
- Example<String>
- Example<Integer> (래퍼 클래스)
- Example<MyClass> (사용자 정의 클래스)
- 참고: 기본 타입(예: int, char, boolean)은 직접 사용할 수 없으며, 이들에 대응하는 래퍼 클래스(Integer, Character, Boolean 등)를 사용해야 한다.
기본타입(primitive type) | 래퍼클래스(wrapper class) |
byte | Byte |
char | Character |
int | Integer |
float | Float |
double | Double |
boolean | Boolean |
long | Long |
short | Short |
(3) 제네릭 메서드
제네릭 메서드 정의, 호출
// 제네릭 타입 변수명이 1개일 때
접근지정자 <T> T 메서드명(T t) {
}
public <T> T printAndReturn(T value) {
System.out.println(value);
return value;
}
// printAndReturn(123) ⇒ T: Integer
// printAndReturn(”Hello”) ⇒ T: String
// 제네릭 타입 변수명이 2개일 때
접근지정자 <T, V> T 메서드명 (T t, V v) {
}
public <T, V> T printFirstAndReturn(T t, V v) {
System.out.println(t);
System.out.println(v);
return t;
}
// printFirstAndReturn(123, “Hello”) → T: Integer, V: String
// 매개변수만 제네릭이 사용되었을 때
접근지정자 <T> void 메서드명(T t) {
}
public <T> void print(T value) {
System.out.println(value);
}
// print(123) → T: Integer
// print(”Hello”) → T: String
// 리턴 타입에만 제네릭이 사용되었을 때
접근지정자 <T> T 메서드명 (int a) {
}
public <T> T getDefaultValue(int a) {
if (a > 0) {
return (T) Integer.valueOf(a); // 여기서 타입 캐스팅이 필요할 수 있습니다.
} else {
return null;
}
}
// 컴파일 시점에서는 T가 어떤 타입인지 알 수 없기 때문에(Strng인지, Integer인지…) 제네릭 타입 T로 강제 캐스팅함
제네릭 메서드 내에서 사용할 수 있는 메서드
💡 Object 메서드
- String.length() 사용불가
→ Object 클래스의 메서드인 equals 사용 가능
3. 제네릭 타입 범위 제한
(1) 제네릭 클래스 - 타입 범위 제한
💡 기본: <T extends 제한타입(클래스/인터페이스명)>
제네릭 클래스에서 extends는 상속의 의미가 아닌, 최상위 클래스 혹은 인터페이스로 지정한다는 의미
- 만약 내가 과일 종류만 저장, 관리하는 제네릭 클래스 생성하고 싶다면?
→ 제네릭 타입 범위 산정이 필요
class Calculator<T extends Number> { // **숫자만** 받아서 계산하는 계산기 클래스
...
}
class A {}
class B extends A {}
class C extends B {}
class D <T extends B> { // **B, C만** 올 수 있음
...
}
(2) 제네릭 메서드 - 타입 범위 제한
💡 기본: 접근지정자 <T extends 클래스/인터페이스명> T 메서드명(T t) { }
class GenericMethods{
public <T extends String> void method2(T t) {
char c = t.charAt(0); // String 메서드 사용 가능
System.out.println(c);
}
}
// 추상 메서드를 호출하기 위해선 구현이 필요함
interface MyInterface{
public abstract void print();
}
class A implements MyInterface{
public void print() {
System.out.println("A's print method");
}
}
class B {
public <T extends MyInterface> void method1(T t) {
t.print();
}
}
public class Main{
public static void main(String[] args) {
B b = new B();
A a = new A();
b.method1(a); // A's print method
}
}
(3) 메서드 매개변수 - 타입 제한
리턴타입 메서드명(제네릭 클래스명<제네릭 타입명> 참조변수명){
}
리턴타입 메서드명(제네릭 클래스명<?> 참조변수명){
}
리턴타입 메서드명(제네릭 클래스명<? extends 상위클래스/인터페이스> 참조변수명){
}
리턴타입 메서드명(제네릭 클래스명<? super 하위클래스/인터페이스> 참조변수명){
}
method(Goods<A> v) // 제네릭 타입: A인 객체만 가능
method(Goods<?> v) // 제네릭 타입: 모든 타입인 객체 가능
method(Goods<? extends B> v) // 제네릭 타입: B 또는 B의 **자식 클래스만** 가능
method(Goods<? super B> v) // 제네릭 타입: B 또는 B의 **부모 클래스만** 가능
4. 제네릭의 상속
종류 | 상속여부 | 예시코드 | 특징 |
클래스 | O | class Parent<K> class Child<K, V> extends Parent<K> |
자식 클래스는 부모의 제네릭 타입 변수를 그대로 물려 받음 따라서 자식 클래스의 제네릭 타입 변수의 개수는 항상 부모보다 같거나 많음 |
메서드 | O | class Parent{ public<T> void print(T t){ System.out.println(t); } } class Child extends Parent{ } |
Parent p = new Parent(); p.print(”안녕”); // 안녕 Child c = new Child(); c.print(”안녕”); // 안녕 |
5. 제네릭 사용시 주의사항
(1) 제네릭 타입의 객체는 생성 불가
- 즉 new 연산자 뒤에 제네릭 타입 파라미터는 올 수 없음
class Sample<T> {
public void someMethod() {
// 제네릭 타입으로 객체 생성 불가
T t = new T(); // 불가
}
}
(2) static 멤버에 제네릭 타입이 올 수 없음
- static 멤버
- 클래스가 동일하게 공유하는 변수
- 따라서 제네릭 객체가 생성되기 전에 이미 자료 타입이 정해져 있어야 함
class Student<T> { private String name; private int age = 0; // static 메서드의 반환 타입으로 사용 불가 public static T addAge(int n) { } }
(3) 제네릭 배열 선언 시 주의할 점
- 기본적으로는 제네릭 클래스 자체에서 배열을 만들 수 없음
class Sample<T> {
}
public class Main {
public static void main(String[] args) {
// new Sample<>[10]: 컴파일 에러 발생
Sample<Integer>[] arr1 = new Sample<>[10]; // 불가
}
}
- 하지만 제네릭 타입의 배열 선언은 허용됨
class Sample<T> {
}
public class Main {
public static void main(String[] args) {
// Sample<Integer>[] arr2를 선언한 후
// 배열 원소 타입을 명시하지 않고 new Sample[10]로 배열 초기화
Sample<Integer>[] arr2 = new Sample[10];
// 제네릭 타입을 생략해도 위에서 이미 정의했기 때문에 Integer 가 자동으로 추론됨
arr2[0] = new Sample<Integer>();
arr2[1] = new Sample<>();
// Integer가 아닌 타입은 저장 불가능
arr2[2] = new Sample<String>();
// 위에서 Sample<Integer>[]로 배열 타입을 선언했기 때문에, Sample<String> 객체를 할당하려고 하면 타입 불일치가 발생함
}
}
참고자료
반응형