본문 바로가기

Java/문법

[Java] 제네릭 - 개념, 문법, 타입 범위 제한, 상속, 주의사항

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> 객체를 할당하려고 하면 타입 불일치가 발생함
    }
}

 


참고자료

반응형