Generic
💡 제네릭
제네릭이란?
- 클래스 내부에서 사용할 제이터 타입을 외부에서 지정하는 기법
- OPP에서 제네익은 타입을 매개변수(파라미터)화 해서 컴파일 시 구체적인 타입이 결정되도록 하여 여러 자료형이 대체되도록 프로그래밍 하는 것
제네릭을 사용하는 코드의 이점
- 컴파일 시 강한 타입 체크 가능
- 타입 변환 제거 가능
제네릭 타입
- 타입을 파라미터로 가지는 클래스와 인터페이스다. 선언 시 클래스 또는 인터페이스 이름 뒤에 “<>” 다이아몬드 연산자를 사용한다. “<>”사이에 식별자 기호를 지정함으로써 파라미터화 하여 이것을 마치 메서드가 매개변수를 받아 사용하는 것처럼 사용한다.
제네릭 타입의 사용
- 제네릭 타입을 사용하지 않은 경우 빈번한 타입 발생으로 프로그램 성능이 저하된다.
- 제네릭 타입을 사용하면 컴파일 시 타입 파라미터가 구체적인 클래스로 변경되어 형변환이 줄어든다.
제네릭 사용 시 주의 사항
- static 멤버에 제네릭 타입이 올 수 없다.
- 제네릭 클래스 자체를 배열로 만들 수는 없지만 제네릭 타입의 배열 선언은 허용된다.
제네릭 객체
😺 제네릭 클래스
클래스 선언문 옆에 제네릭 타입 매개변수가 쓰이면 이를 제네릭 클래스라고 한다.
class Sample<T> {
private T value; // 멤버 변수 val의 타입은 T 이다.
// T 타입의 값 val을 반환한다.
public T getValue() {
return value;
}
// T 타입의 값을 멤버 변수 val에 대입한다.
public void setValue(T value) {
this.value = value;
}
}
class Main{
public static void main(String[] args) {
// 정수형을 다루는 제네릭 클래스
Sample<Integer> s1 = new Sample<>();
s1.setValue(1);
// 실수형을 다루는 제네릭 클래스
Sample<Double> s2 = new Sample<>();
s2.setValue(1.0);
// 문자열을 다루는 제네릭 클래스
Sample<String> s3 = new Sample<>();
s3.setValue("1");
}
}
😸 제네릭 인터페이스
인터페이스에도 제네릭을 적용할 수 있다. 단, 인터페이스를 implements한 클래스에서도 오버라이딩 한 메서드를 제네릭 타입에 맞춰서 똑같이 구현해 주어야 한다.
interface ISample<T> {
public void addElement(T t, int index);
public T getElement(int index);
}
class Sample<T> implements ISample<T> {
private T[] array;
public Sample() {
array = (T[]) new Object[10];
}
@Override
public void addElement(T element, int index) {
array[index] = element;
}
@Override
public T getElement(int index) {
return array[index];
}
}
public class Main{
public static void main(String[] args) {
Sample<String> sample = new Sample<>();
sample.addElement("This is string", 5);
sample.getElement(5);
}
}
제네릭 함수형 인터페이스
제네릭 인터페이스가 정말 많이 사용되는 곳은 람다 표현식의 함수형 인터페이스다. 제네릭을 활용하여 타입을 동적으로 결정할 수 있다.
// 제네릭으로 타입을 받아, 해당 타입의 두 값을 더하는 인터페이스
interface IAdd<T> {
public T add(T x, T y);
}
public class Main {
public static void main(String[] args) {
// 제네릭을 통해 람다 함수의 타입을 결정
IAdd<Integer> o = (x, y) -> x + y; // 매개변수 x와 y 그리고 반환형 타입이 int형으로 설정된다.
int result = o.add(10, 20);
System.out.println(result); // 30
}
}
😹 제네릭 메서드
- 제네릭 타입 파라미터를 사용하는 메서드와 제네릭 메서드는 다르다.
(그냥 타입 파라미터로 타입을 지정한 메서드일 뿐…진짜가 아니야아…)
class FruitBox<T> {
public T addBox(T x, T y) {
// ...
}
}
제네릭 메서드란, 메서드의 선언부에 <T> 가 선언된 메서드를 말한다.
- 위의 코드에서 클래스의 제네릭 <T> 에서 설정된 타입을 받아와 반환 타입으로 사용할 뿐인 일반 메서드라면, 제네릭 메서드는 직접 메서드에 <T> 제네릭을 설정함으로서 동적으로 타입을 받아와 사용할 수 있는 독립적으로 운용 가능한 제네릭 메서드
class FruitBox<T> {
// 클래스의 타입 파라미터를 받아와 사용하는 일반 메서드
public T addBox(T x, T y) {
// ...
}
// 독립적으로 타입 할당 운영되는 제네릭 메서드
public static <T> T addBoxStatic(T x, T y) {
// ...
}
}
즉, 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 별개인게 되는 것이다. 제네릭 메서드의 제네릭 타입 선언 위치는 메서드 반환 타입 바로 앞이다.
FruitBox.<Integer>addBoxStatic(1, 2);
FruitBox.<String>addBoxStatic("안녕", "잘가");
// 메서드의 제네릭 타입 생략
FruitBox.addBoxStatic(1, 2);
FruitBox.addBoxStatic("안녕", "잘가");
한정된 제네릭 타입 범위
- 제네릭에 타입을 지정해줌으로서 클래스의 타입을 컴파일 타임에서 정하여 타입 예외에 대한 안정성을 확보하는 것은 좋지만 문제는 너무 자유롭다는 점이다.
- 예를 들어 계산기 클래스에 정수, 실수 구분없이 모두 받을 수 있게 하기위해 제네릭으로 클래스를 만들어주었다고 가정해보자. 단순히 <T> 로 지정하게 되면 숫자에 관련된 래퍼 클래스 뿐만 아니라 String이나 다른 클래스들도 대입이 가능하다는 점이 문제이다.
- 그래서 나온 것이 제한된 타입 매개변수 (Bounded Type Parameter) 이다.
😻 타입 한정 키워드 extends
제네릭 타입에 ‘extends’ 키워드를 사용하면 같은 부모 클래스와 부모 클래스를 상속받는 자식 클래스들만 사용 가능하다.
기본적인 용법은 <T extends [제한타입]> 이다. 제네릭 <T> 에 extends 키워드를 붙여줌으로써, <T extends Number> 제네릭을 Number 클래스와 그 하위 타입(Integer, Double)들만 받도록 타입 파라미터 범위를 제한 한 것이다.
// 숫자만 받아 계산하는 계산기 클래스 모듈
class Calculator<T extends Number> {
void add(T a, T b) {}
void min(T a, T b) {}
void mul(T a, T b) {}
void div(T a, T b) {}
}
public class Main {
public static void main(String[] args) {
// 제네릭에 Number 클래스만 받도록 제한
Calculator<Number> cal1 = new Calculator<>();
Calculator<Integer> cal1 = new Calculator<>();
Calculator<Double> cal1 = new Calculator<>();
// Number 이 외의 클래스들은 오류
Calculator<Object> cal2 = new Calculator<>();
Calculator<String> cal3 = new Calculator<>();
Calculator<Main> cal4 = new Calculator<>();
}
}
인터페이스 타입 한정
extends 키워드 다음에 올 타입은 일반 클래스, 추상 클래스, 인터페이스 모두 올 수 있다. 클래스의 상속 관계와 똑같이 생각하면 편하다.
interface Readable {
}
// 인터페이스를 구현하는 클래스
public class Student implements Readable {
}
// 인터페이스를 Readable를 구현한 클래스만 제네릭 가능
public class School <T extends Readable> {
}
public class Main{
public static void main(String[] args) {
// 타입 파라미터에 인터페이스를 구현한 클래스만이 올수 있게 됨
School<Student> a = new School<Student>();
}
}
다중 타입 한정
만일 2개 이상의 타입을 동시에 상속(구현)한 경우로 타입 제한하고 싶다면, & 연산자를 이용하면 된다. 해당 인터페이스들을 동시에 구현한 클래스가 제네릭 타입의 대상이 되게 된다.
단, 자바에서는 다중 상속을 지원하지 않기 때문에 클래스로는 다중 extends는 불가능하고 오로지 인터페이스로만이 가능하다.
interface Readable {}
interface Closeable {}
class BoxType implements Readable, Closeable {}
class Box<T extends Readable & Closeable> {
List<T> list = new ArrayList<>();
public void add(T item) {
list.add(item);
}
}
public class Main {
public static void main(String[] args) {
// Readable 와 Closeable 를 동시에 구현한 클래스만이 타입 할당이 가능하다
Box<BoxType> box = new Box<>();
// 심지어 최상위 Object 클래스여도 할당 불가능하다
Box<Object> box2 = new Box<>(); // ! Error
}
}
제네릭 형변환
제네릭 간의 형변환을 성립되게 하기 위해서는 제네릭에서 제공하는 와일드 카드 ? 문법을 이용하여야 한다.
😼 제네릭 와일드 카드
메소드의 매개변수로 제네릭 클래스의 객체를 받을 때, 타입을 제한하기 위해 사용한다.
- <?> : Unbounded Wildcards (제한 없음)
- 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다
- <? extends 상위타입> : Upper Bounded Wildcards (상위 클래스 제한)
- 타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 상위 타입의 하위 타입(상위타입의 자손들을 구현한 객체들)만 올 수 있다
- <? super 하위타입> : Lower Bounded Wildcards (하위 클래스 제한)
- 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 하위 타입의 상위 타입(하위타입의 조상들을 구현한 객체들)만 올 수 있다
- 와일드 카드에는 “&”을 사용할 수 없어서 여러개의 클래스 나열이 불가능 하다.
import java.util.ArrayList;
import java.List;
class Family {}
class Parent extends Family {
String name;
Parent(String name) {
super(name);
this.name = name;
}
}
class Child extends Parent {
String name;
Child(String name) {
super(name);
this.name = name;
}
}
public class WildCardExample {
public static void main(String[] args) {
List<Family> familyList = new ArrayList<>();
List<Parent> parentList = new ArrayList<>();
List<Child> childList = new ArrayList<>();
// Super class Family of Parent class
addFamilyTree(familyList, "Alex");
// Parent class
addFamilyTree(parentList, "Alex");
// 컴파일 오류
// subclass Child of Parent class
addFamilyTree(childList, "Alex");
}
// Parent와 Family만 가능
public static void addFamilyTree(List<? super Parent> familyList, String name) {
familyList.add(new Parent(name));
}
}
'공log > [JAVA]' 카테고리의 다른 글
[JAVA] 자바 #5 - 정규식 (1) | 2023.10.23 |
---|---|
[JAVA] 자바 #4 - 투 포인터 (0) | 2023.09.25 |
[JAVA] 자바 #2 - 진법 변환하기 (0) | 2023.09.04 |
[JAVA] 자바 #1 - 최대 공약수와 최소 공배수 (0) | 2023.09.04 |