제네릭(Generics)

조용근·2024년 1월 25일

자바 정리

목록 보기
15/21

제네릭

  • 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 방법
  • 객체별로 다른 타입의 자료가 저장
  • <> 사용
ArrayList<String> list = new ArrayList<>();

list 클래스의 자료형 타입은 String으로 지정되어 문자열 데이터만 리스트에 적을 수 있다.

String[]array = new String[10];

//String은 타입이고 []은 배열 자료형이다.

ArrayList<String> list = new ArrayList<>();

//ArrayList는 리스트 자료형이고 <>은 타입이다.

제네릭은 객체(Object)에 타입을 지정해주는 것이다.

제네릭 타입 매개변수(타입 변수)

<>안에 식별 기호를 넣어줌으로써 메서드가 매개변수를 받아 사용하는 것과 비슷함.

List<String>stringList = new ArrayList<String>();
class FruitBox<T>{
	List<T> fruits = new ArrayList<>();
    
    public void add(T fruit){
    	fruits.add(fruit);
    }
}

이를 인스턴스화 해주면,


//제네릭 타입 매개변수에 정수 타입 할당
FruitBox<Integer>intBox = new FruitBox<>();
//제네릭 타입 매개변수에 실수 타입 할당
FruitBox<Double>intBox = new FruitBox<>();
//제네릭 타입 매개변수에 문자열 타입 할당
FruitBox<String>intBox = new FruitBox<>();
//클래스 할당
FruitBox<클래스>intBox = new FruitBox<클래스>();

클래스의 경우, new 생성자 부분의 타입 매개변수를 생략할 수 있다.

FruitBox<클래스>intBox = new FruitBox<생략>();

Reference 타입

제네릭에서 유일하게 할당 받을 수 있는 타입이다.
// int,double형 등을 제네릭 타입 파라미터로 넣을 수 없다.

//int(기본 타입)은 사용이 불가하다. 
List<int>intList = new List<>();
//Wrapper 클래스를 사용해야만 한다.
List<Interger>intergerList = new List<>();

Wrapper 클래스에 대해서는 뒤에 다룰 것이다.

  • 제네릭 타입 매개변수에 클래스 타입이 온다는 뜻은 다형성원리를 적용한다는 것!
package javaplus.generic;

import java.util.ArrayList;
import java.util.List;

class Fruit {}
class Apple extends Fruit{}
class Banana extends Fruit{}

class FruitBox<T>{
    List<T> fruits = new ArrayList<>();

    public void add(T fruit){
        fruits.add(fruit);
    }
}

public class GenericMain {
    public static void main(String[] args) {
        FruitBox<Fruit>box = new FruitBox<>();

        //다형성 원리 적용
        box.add(new Fruit());
        box.add(new Apple());
        box.add(new Banana());
    }
}

타입 매개변수 이름

  • T = 타입
  • E = 요소(리스트)
  • K = 키//ex)Map<k,v>
  • V = 리턴값
  • N = 숫자
  • S,U,V = 2,3,4번째에 선언된 타입

문법적으로 정해진 식별 기호는 아니지만, 위 기호들은 암묵적인 규칙이다.
for문을 사용할 때, 반복변수를 i,j,k로 사용한 것처럼 S,U는 i,j와 같은 기능으로써 사용하는 것이다.
위 기호들을 다른말로 타입변수라고 한다.

사용이유

Apple[] arr = { new Apple(), new Apple(), new Apple() };
FruitBox box = new FruitBox(arr);

// 가져온 타입이 Object 타입이기 때문에 일일히 다운캐스팅을 해야함 - 쓸데없는 성능 낭비
Apple apple1 = (Apple) box.getFruit(0);
Apple apple2 = (Apple) box.getFruit(1);
Apple apple3 = (Apple) box.getFruit(2);


// 미리 제네릭 타입 파라미터를 통해 형(type)을 지정해놓았기 때문에 별도의 형변환은 필요없다.
FruitBox<Apple> box = new FruitBox<>(arr);

Apple apple = box.getFruit(0);
Apple apple = box.getFruit(1);
Apple apple = box.getFruit(2);

주의사항

1. 객체 그 자체로 생성 불가

class Sample<T> {
    public void someMethod() {
        T t = new T();
    }
}

제네릭 타입 자체로 객체 생성 불가
// new 연산자 뒤에 제네릭 타입 매개변수가 못온다.

2. static의 멤버로 제네릭 타입이 올 수 없다.

class Student<T> {
    private String name;
    private int age = 0;

    // static 메서드의 반환 타입으로 사용 불가
    public static T addAge(int n) {

    }
}

static 멤버는 클래스를 공유하는 변수로, 자료 타입이 정해지게 된다. 따라서 제네릭 객체가 올 수 없다.(논리적 오류)

3. 배열 선언은 가능하다.

class Sample<T> { 
}

public class Main {
    public static void main(String[] args) {
        Sample<Integer>[] arr1 = new Sample<>[10];
    }
}

위 코드처럼 제네릭 클래스 자체가 배열을 만들 수 없다.

하지만, 배열 선언은 가능하다.

class Sample<T> { 
}

public class Main {
    public static void main(String[] args) {
    	// new Sample<Integer>() 인스턴스만 저장하는 배열을 나타냄
        Sample<Integer>[] arr2 = new Sample[10]; 
        
        // 제네릭 타입을 생략해도 위에서 이미 정의했기 때문에 Integer 가 자동으로 추론됨
        arr2[0] = new Sample<Integer>(); 
        arr2[1] = new Sample<>();
        
        // ! Integer가 아닌 타입은 저장 불가능
        arr2[2] = new Sample<String>();
    }
}

제네릭 타입으로 Integer로 지정을 해놨기 때문에, 다른 타입은 저장할 수 없다.

제네릭 객체

1. 제네릭 클래스

class 옆에 제네릭 타입 매개변수가 쓰이면, 제네릭 클래스이다.

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;
    }
}

public static void main(String[] args) {
        Sample<Interger> s1 = new Sample<>();
        s1.setValue(1);
        
        Sample<Interger> s2 = new Sample<>();
        s2.setValue(1.0);
        
        Sample<Interger> s3 = new Sample<>();
        s3.setValue("1");
    }

2. 제네릭 인터페이스

인터페이스를 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 static void main(String[] args) {
        Sample<String> sample = new Sample<>();
        sample.addElement("This is string", 5);
        sample.getElement(5);
    }
}

3. 제네릭 함수형 인터페이스
람다를 활용한 함수형 인터페이스이다.

// 제네릭으로 타입을 받아, 해당 타입의 두 값을 더하는 인터페이스
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
    }
}

4. 제네릭 메서드
메서드 선언부에 T가 선언된 메서드이다.

class FruitBox<T> {

    public T addBox(T x, T y) {
        // ...
    }
}

위 코드는 클래스 타입 파라미터를 받아서 사용하는 일반 메서드이다.
T라는 타입 파라미터를 가져와 사용한다.

class FruitBox<T> {
	
    // 독립적으로 타입 할당 운영되는 제네릭 메서드
    public static <T> T addBoxStatic(T x, T y) {
        // ...
    }
}

< T >라는 독립적인 메서드의 타입 파라미터를 가져와 사용한다.

제네릭 메서드 호출 방법

public class Test{
	public static void main(String[] args){
    	FruitBox.<Integer>addBoxStatic(1,2);
		FruitBox.<String>addBoxStatic("a","b");
		
	}        
}

<>안에 들어갈 데이터 타입은 메서드 매개변수를 보고 추정할 수 있기 때문에 생략할 수 있다.

FruitBox.addBoxStatic(1,2);
FruitBox.addBoxStatic("a","b");

아래 box1.< String, Double > printBox("hello", 5.55)처럼 다른 타입 파라미터를 지정하면, 독립적으로 운용된다.

class FruitBox<T, U> {
    // 독립적으로운영되는 제네릭 메서드
    public <T, U> void printBox(T x, U y) {
        // 해당 매개변수의 타입 출력
        System.out.println(x.getClass().getSimpleName());
        System.out.println(y.getClass().getSimpleName());
    }
}

public static void main(String[] args) {
    FruitBox<Integer, Long> box1 = new FruitBox<>();

    // 인스턴스화에 지정된 타입 파라미터 <Integer, Long>
    box1.printBox(1, 1);

    // 하지만 제네릭 메서드에 다른 타입 파라미터를 지정하면 독립적으로 운용 된다.
    box1.<String, Double>printBox("hello", 5.55);
    box1.printBox("hello", 5.55); // 생략 가능
}

문제점 - 자율성

// 숫자만 받아 계산하는 계산기 클래스 모듈
class Calculator<T> {
    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) {
        // 제네릭에 아무 타입이나 모두 할당이 가능
        Calculator<Number> cal1 = new Calculator<>();
        Calculator<Object> cal2 = new Calculator<>();
        Calculator<String> cal3 = new Calculator<>();
        Calculator<Main> cal4 = new Calculator<>();
    }
}
}

메서드에 모든 타입을 넣어줄 수 있도록 제너릭을 설정했지만, T는 숫자 뿐만 아니라 다른 클래스도 대입이 가능하다.
이러한 것들을 제한히ㅏ기 위해 나온 것이 제한된 타입 매개변수라고 한다.

타입 한정 키워드(extends)

< T extends 제한타입 > 이다. 예를 들어, < T extends Number >이라고 한다면, 제레릭을 Number 클래스와 그 하위 타입(Integer, Double)만 받도록 타입 매개변수를 제한한다.

클래스의 상속 extends와 제네릭 타입 한정 키워드인 extends는 완전히 다른 것이다.
<>안에 extends가 있으면 제네릭, <>바깥에 있으면 상속이다.

인터페이스 타입 한정

interface Readable {
}

// 인터페이스를 구현하는 클래스
public class Student implements Readable {
}
 
// 인터페이스를 Readable를 구현한 클래스만 제네릭 가능
public class School <T extends Readable> {
}

public static void main(String[] args) {
    // 타입 파라미터에 인터페이스를 구현한 클래스만이 올수 있게 됨	
    School<Student> a = new School<Student>();
}

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 static void main(String[] args) {
    // Readable 와 Closeable 를 동시에 구현한 클래스만이 타입 할당이 가능하다
    Box<BoxType> box = new Box<>();

    // 심지어 최상위 Object 클래스여도 할당 불가능하다
    Box<Object> box2 = new Box<>(); // ! Error
}

제네릭은 똑같은 타입만 받기 떄문에, 다형성을 이용할 수 없다!!

// 배열은 OK 
Object[] arr = new Integer[1];

// 제네릭은 ERROR 
List<Object> list = new ArrayList<Integer>();

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%A0%9C%EB%84%A4%EB%A6%ADGenerics-%EA%B0%9C%EB%85%90-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0#%ED%83%80%EC%9E%85_%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0_%EA%B8%B0%ED%98%B8_%EB%84%A4%EC%9D%B4%EB%B0%8D

profile
Today I Learn

1개의 댓글

comment-user-thumbnail
2024년 1월 25일

재귀적 타입 한정은 추후 작성예정

답글 달기