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<생략>();
제네릭에서 유일하게 할당 받을 수 있는 타입이다.
// 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());
}
}
문법적으로 정해진 식별 기호는 아니지만, 위 기호들은 암묵적인 규칙이다.
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는 숫자 뿐만 아니라 다른 클래스도 대입이 가능하다.
이러한 것들을 제한히ㅏ기 위해 나온 것이 제한된 타입 매개변수라고 한다.
< 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>();
재귀적 타입 한정은 추후 작성예정