일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- NavGraph
- mutableStateOf
- genarics
- snapshotflow
- JCF
- 클린아키텍처
- 안드로이드
- 자바
- Java
- 명령형 ui
- State
- NavHost
- rememberupdatedstate
- 양과 늑대
- BottomSheet
- ModalBottomSheet
- producestate
- apollo3
- 2022 KAKAO BLIND RECRUITMENT
- 명령형ui
- compose
- clean coder
- derivedstateof
- bottomscaffold
- 선언형ui
- 선언형 ui
- mutablestate
- 2022 kakao blind
- remembercoroutinescope
- 2989번
- Today
- Total
버미
자바 제네릭(Generics) 개념 & 문법 본문
제네릭이란?
자바에서 제네릭(Generics)은 클래스 내부에서 사용할 데이터 타입을 외부에서 정하는 것을 말한다.
/*
T는 객체 타입을 말한다.
String, Integer, Double, Long같은 Wrapper Class와
사용자 정의 타입까지 가능하다.
LinkedList<int> list와 같은 primivite type은 불가능하다.
*/
List<String> list = new ArrayList<>();
위 코드에서 클래스 선언 문법에서 꺾쇠 괄호 <>로 되어있는 것이 제네릭이다. 괄호 안에는 타입명을 적는다.
리스트 자료형과 같은 Collection 클래스나 메소드에서 사용할 내부 데이터 타입을 파라미터처럼 외부에서 전달하는 기능이라고 이해하면 된다.
제네릭 타입 매개변수
제네릭은 <>와 같은 꺾쇠 괄호 키워드를 사용하는데 이를 다이아몬드 연산자라고 한다. 이 연산자 안에 식별자 기호를 적어서 전달하는데 마치 메소드가 매개변수를 받아 사용하는 것과 비슷해서 타입 매개변수(Parameter) / 타입 변수라고 부른다.

타입 파라미터 정의
다음은 제네릭을 사용하여 클래스를 정의한 코드이다.
class Animals<T> {
List<T> list = new ArrayList<>();
public void add(T animal) {
list.add(animal);
}
}
//제네릭 타입 매개변수에 정수 타입을 할당
Animals<Integer> initAnimals = new Animals<>();
/*
제네릭 타입 매개변수에 문자열 타입을 할당.
JDK 1.7버전부터 new 생성자부분의 제네릭 타입 추론이 가능하다.
*/
Animals<String> initAnimals = new Animals<>();
//제네릭 타입에 클래스를 넣을 수도 있다.
Animals<Tiger> initAnimals = new Animals<>(Tiger);
실행부에서 타입을 받아와 클래스 내부에서 T타입으로 지정한 멤버들에게 타입을 전달하는 것이다. 이를 구체화(Specialization)라고 한다.
복수 타입 & 중첩 타입 파라미터
타입 지정이 여러개 필요한 경우, 여러개의 제네릭을 만들 수 있다.또한 제네릭을 사용하는 객체를 제네릭 타입 파라미터로 받을 수도 있다.
import java.util.ArrayList;
import java.util.List;
class Dog{}
class Cat{}
class Animals<T, U> {
List<T> dogs = new ArratList<>();
List<U> cats = new ArrayList<>();
public void add(T dog, U cat) {
dogs.add(dog);
cats.add(cat);
}
}
public static void main(String[] args) {
// 복수 제네릭 타입
Animals<Dog, Cat> animal = new Animals<>();
animal.add(new Dog(), new Cat());
// LinkedList<String>을 원소로서 저장하는 ArrayList
ArrayList<LinkedList<String>> list = new ArrayList<LinkedList<String>>();
LinkedList<String> node1 = new LinkedList<>();
node1.add("ab");
node1.add("cd");
LinkedList<String> node2 = new LinkedList<>();
node2.add("12");
node2.add("34");
list.add(node1);
list.add(node2);
}
타입 파라미터 기호 네이밍
식별자 기호는 문법적으로 정해지지는 않았다.
하지만 대중적으로 사용하는 암묵적인 규칙이 존재해서 이를 알고 있다면 네이밍을 보거나 사용하는데 용이하다.
타입 | 설명 |
<T> | 타입(Type) |
<E> | 요소(Element) |
<K> | 키(Key). ex) Map<K, V> |
<V> | 리턴 값 or 매핑된 값(Variable) |
<N> | 숫자(Number) |
<S, U, V> | 2번째, 3번째, 4번째에 선언된 타입 |
제네릭을 사용하는 이유와 장점
- 컴파일 타임에 타입 검사를 통한 예외 방지
- JDK 1.5 이전 버전에서는 여러 타입을 다루기 위해 Object 타입을 사용했다. 하지만 Object로 타입을 선언할 경우 반환된 Object객체를 다시 반환된 Object타입으로 일일이 타입 변환을 해야 하며, 런타임 에러도 발생할 수 있다.
- 불필요한 캐스팅을 없애 성능 향상
- Object를 사용해 반환하는 경우 다운 캐스팅을 사용해 가져와야한다. 이는 추가적인 오버헤드가 발생한다.
- 반면 제네릭은 미리 타입을 지정 & 제한해 놓기 때문에 형 변환(Type Casting)의 번거로움을 줄일 수 있으며, 타입 검사에 들어가는 메모리를 줄일 수 있다.
제네릭 사용시 주의사항
1. 제네릭 타입 자체로 타입을 지정해 객체를 생성하는 것은 불가능하다. 즉, new 연산자 뒤에 제네릭 타입 마라미터가 올 수는 없다.
class Sample<T> {
public void Method() {
//불가
T t = new T();
}
}
2. static 멤버에 제네릭 타입이 올 수 없다.
- static 멤버는 제네릭 객체가 생성되기 전에 자료 타입이 정해있어야 하기 때문에.
class Student<T> {
private String name;
private int age = 0;
// static 메서드의 매개변수 타입으로 사용 불가
public static void addAge(T n) { }
}
3. 제네릭 클래스 자체를 배열로 만들 수 없다.
- 다음의 코드는 불가능하다.
class Sample<T> {
}
public class Main {
public static void main(String[] args) {
Sample<Integer>[] arr1 = new Sample<>[5];
}
}
- 아래와 같이 객체 타입 파라미터를 구체화한 경우는 가능하다.
class Sample<T> { }
public class Main {
public static void main(String[] args) {
// new Sample<Integer>() 인스턴스만 저장하는 배열을 나타냄
Sample<Integer>[] sample = new Sample[10];
// 제네릭 타입을 생략해도 위에서 이미 정의했기 때문에 Integer가 추론됨
sample[0] = new Sample<Integer>();
sample[1] = new Sample<>();
// ! Integer가 아닌 타입은 저장 불가능
arr2[2] = new Sample<Double>();
}
}
제네릭 메서드
제네릭 메서드란, 메서드 선언부에 <T>가 선언된 메서드를 말한다.
동적으로 타입을 받아와 사용할 수 있어서 독립적으로 운용 가능하다고 이해하면 된다.
class Animals<T> {
// 클래스의 타입 파라미터를 사용하는 일반 메서드
public T addAnimal(T x, T y) {
// ...
}
// 동적으로 타입 할당 운영되는 제네릭 메서드
public static <T> T addAnimalStatic(T x, T y) {
// ...
}
}
제네릭 메서드 호출 원리
제네릭 타입을 메서드 옆에 지정했으니, 호출도 마찬가지로 메서드 왼쪽에 제네릭 타입이 위치한다.
Animals.<Integer>addAnimalStatic(1, 2);
//제네릭 타입에 들어갈 타입이 추론가능하기 때문에 메서드 제네릭 타입은 생략 가능하다.
Animals.addAnimalStatic("안녕", "잘가");
처음 제네릭 클래스를 인스턴스화하면, 클래스 타입 매개변수에 전달한 타입에 따라 제네릭 메소드도 타입이 정해지게 된다. 그런데 만약 제네릭 메서드를 호출할때 직접 타입 파라미터를 다르게 지정해주거나, 다른 타입의 데이터를 매개변수에 넘기면 독립적인 타입을 가진 제네릭 메서드로 사용한다.
class Animals<T, U> {
// 독립적으로운영되는 제네릭 메서드
public <T, U> void printAnimal(T x, U y) {
// 해당 매개변수의 타입 출력
System.out.println(x.getClass().getSimpleName());
System.out.println(y.getClass().getSimpleName());
}
}
public static void main(String[] args) {
Animals<Long, Integer> animals = new Animals<>();
// 인스턴스화에 지정된 타입 파라미터 <Long, Integer>
animals.printBox(1, 1);
// 하지만 제네릭 메서드에 다른 타입 파라미터를 지정하면 독립적으로 사용한다.
animals.<String, Double>printAnimal("Dog", 5.55);
animals.printAnimal("Dog", 5.55); // 생략 가능
}

제네릭 타입 범위 한정하기
용법은 <T extends [제한타입]> 이다. 제네릭 <T> 에 extends 키워드를 붙여줌으로써, <T extends Number> 제네릭을 Number 클래스와 그 하위 타입(Integer, Double)들만 받도록 타입 파라미터 범위를 제한 한 것이다.
클래스의 상속 키워드와 제네릭의 타입 한정 키워드가 둘다 똑같이 extends 라 혼동할 수 있다.
꺾쇠 괄호 안에 extends가 있으면 이건 제한을 의미, 괄호 바깥에 있으면 상속을 의미한다.
인터페이스도 타입을 한정지을 수 있으며 2개 이상의 타입을 동시에 상속(구현)하는 경우로 제한하고 싶다면 '&' 연산자를 이용하면 된다.
단, 자바에서는 다중 상속을 지원하지 않기 때문에 클래스로는 다중 extends는 불가능하고 오로지 인터페이스로만이 가능하다.
제네릭 캐스팅 문제
처음에 소개한 타입 파라미터의 다형성은 포함 원소로서 가능하다는거지, 형변환은 객체 대 객체를 말하는 것이다.
예를 들어 배열 같은 경우 반목문의 변수로 Object 타입으로 받아 사용해도 문제가 없다.
public static void main(String[] args) {
Dog[] integers = new Dog[]{
new Dog(),
new Dog(),
new Dog(),
};
print(integers);
}
public static void print(Animals[] arr) {
for (Object e : arr) {
System.out.println(e);
}
}
하지만 위의 코드에서 배열을 리스트의 제네릭으로 바꾸면 컴파일 에러가 발생한다.
public static void main(String[] args) {
List<Integer> lists = new ArrayList<>(Arrays.asList(1, 2, 3));
print(lists); // 컴파일 에러 발생
}
public static void print(List<Object> list) {
for (Object e : list) {
System.out.println(e);
}
}
배열 같은 경우 print 메서드의 매개변수로 아규먼트가 넘어갈때 Integer[] 배열 타입이 Object[] 배열 타입으로 업캐스팅되어 문제가 없지만, 제네릭 같은 경우 타입 파라미터가 같은 타입만 받기 때문에 다형성을 사용할 수 없어서 그런 것이다
제네릭 와일드 카드
제네릭 간의 형변환을 성립시키려면 제네릭에서 제공하는 와일드 카드 '?' 문법을 이용해야 한다.
- <?> : Unbounded Wildcards (제한 없음)
- 모든 클래스나 인터페이스가 타입으로 올 수 있다.
- <? extends 상위 타입> : Upper Bounded Wildcards (상위 클래스 제한)
- 상위 타입이나 상위 타입의 하위 타입만 올 수 있다.
- <? super 하위 타입> : Lower Bounded Wildcards (하위 클래스 제한)
- 하위 타입이나 하위 타입의 상위 타입만 올 수 있다.
제네릭 타입 소거
제네릭은 JDK 1.5부터 도입된 문법이기 때문에, 그 이전 자바 버전으로 컴파일 시 제네릭 타입을 사자리게 된다.
즉, 클래스 파일(.class)에는 제네릭 타입에 대한 정보는 없는 것이다.
제네릭은 실제 바이트 코드에는 없는 반쪽짜리 언어 문법이라고 할 수 있다. 그래서 제네릭을 개발자가 잘못된 방향으로 설계하면 잠재적인 힙 오염 문제에 빠질 수 있다. 따라서 우리가 바이트 코드를 보고 코딩하지는 않지만, 올바르게 제네릭을 설계하기 위해 제네릭의 컴파일 과정을 한번 쯤 알아볼 필요가 있다.
- 참고 사이트
☕ 자바 제네릭(Generics) 개념 & 문법 정복하기
제네릭 (Generics) 이란 자바에서 제네릭(Generics)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 객체별로 다른 타입의 자료가 저장될 수 있도록 한다. 자바에서 배
inpa.tistory.com
'Language > Java' 카테고리의 다른 글
자바 인터페이스와 추상 클래스 (0) | 2025.04.17 |
---|---|
자바 컬렉션 프레임 워크 (Java Collections Framework) (0) | 2024.06.24 |