학습 개요
- 제네릭은 프로그램의 재사용성을 높여주고 소스 코드 컴파일 할 때 자료형 검사를 엄격하게 수행해 실행 오류를 최소화하기 위한 기법임
- 람다식은 매개 변수를 갖는 코드 블록으로 익명 클래스의 객체를 생성하는 부분을 수식화한 것임
- 제네릭과 람다식을 활용하는 방법을 익힘
학습 목표
- 제네릭 클래스를 정의할 수 있음
- 제네릭 메소드의 정의와 사용법을 설명할 수 있음
- 정의된 제네릭 타입이나 메소드를 사용할 수 있음
- 람다 식의 활용 방법을 설명할 수 있음
- 표준 함수형 인터페이스를 활용할 수 있음
강의록
제네릭 타입
제네릭의 의미
- 제네릭 클래스, 제네릭 인터페이스, 제네릭 메소드
- 클래스, 인터페이스, 메소드를 정의할 때 타입 매개 변수(타입 파라미터)를 선언하고 사용할 수 있음
- 자바 프로그램의 재사용성을 높이고 오류를 줄이는 방법
- 제네릭의 장점
- 여러 유형에 걸쳐 동작하는 일반화된 클래스나 메소드를 정의할 수 있음
- 자료형을 한정함으로써 컴파일 시점에 자료형 검사가 가능
- 실행 오류를 찾아 고치는 것은 어렵기 때문
- cast(형변환) 연산자의 사용이 불필요
제네릭의 사용
ArrayList
클래스는List
인터페이스를 구현한 클래스1 2 3 4 5 6 7 8 9 10 11 12 13
class ArrayList<E> implements List<E> { public boolean add(E e) { } public E get(int index) { } public E remove(int index) { } }
List
인터페이스- 순서가 있는 원소들의 집단을 관리하기 위한 컬렉션 인터페이스
1 2 3 4 5 6 7 8 9 10 11 12
public class Main { public static void main(String args[]) { List list1 = new ArrayList(); // Object 유형 list1.add("hello"); // hello 문자열이 object 유형으로 형변환 되어 추가 String s1 = (String) list1.get(0); // object 유형으로 반환 되어 강제 형변환(다운캐스팅) 필요 List<String> list2 = new ArrayList<String>(); // 타입 명시 list2.add("hello"); String s2 = list2.get(0); //형변환이 필요 없음 } }
제네릭 클래스
- 클래스 정의에서 타입 파라미터를 선언
- 클래스를 사용할 때는 타입을 명시해야 함
- 타입 파라미터는 참조형만 가능함
- 필드의 자료형, 메소드 반환형, 인자의 자료형으로 사용할 수 있음
- 컴파일 할 때, 명확한 타입 검사 수행할 수 있음
- 메소드 호출 시 인자의 유형이 맞는지
- 메소드 호출의 결과를 사용할 때 유형이 맞는지
- 자료형 매개 변수로 가지는 클래스와 인터페이스를 제네릭 타입이라함
제네릭 클래스의 정의
문법
1 2 3
접근제어자 class 클래스이름<T1, T2, ... > { }
- 클래스 정의에서 클래스 이름의 오른편, 각 괄호 <> 안에 타입 파라미터를 표시함
- 콤마(
,
)로 구분하여 여러 개의 타입 파라미터를 지정할 수 있음 - 타입 파라미터는 타입을 전달 받기 위한 것
- 타입 파라미터의 이름은 관례적으로
E
(element),K
(key),V
(value),N
(number),T
(이외) 등을 사용
제네릭 클래스의 예
일반 클래스
1 2 3 4 5 6 7 8 9 10 11
class Data { private Object object; // java의 클래스 계층 구조에서 가장 상위 구조 public void set(Object object) { this.object = object; // 데이터 필드 세팅 } public Object get() { return object; } }
제네릭 클래스
1 2 3 4 5 6 7 8 9 10 11 12
// Data 클래스의 제너릭 버전 class Data<T> { // 타입 파라미터 호출 private T t; // 데이터 필드의 자료형으로 타입 파라미터 사용 public void set(T t) { this.t = t; // 인자의 자료형으로 타입 파라미터 사용 } public T get() { return t; // 메소드 반환형으로 타입 파라미터 사용 } }
제네릭 클래스의 필요성
- 제네릭 타입과 자료형 검사
- 제네릭 타입을 사용하지 않으면 컴파일 시점에서 오류를 검출하지 못함
의미가 명확하면 생성자 호출 시, 괄호만 사용할 수 있음
1
Data<String> d = new Data<>();
1 2 3 4 5 6 7 8
public class Main { public static void main(String args[]) { Data data = new Data(); Integer i = Integer.valueOf(20); data.set(i); // 컴파일 오류 없음 String s = (String) data.get(); // 실행 오류 } }
1 2 3 4 5 6 7 8
public class Main { public static void main(String args[]) { Data<String> data = new Data<>(); Integer i = Integer.valueOf(20); // data.set(i); // 컴파일 오류 String s = data.get(); } }
제네릭 인터페이스를 구현하는 제네릭 클래스
2개의 타입 매개 변수(K,V)를 가지는 제네릭 인터페이스
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
interface Pair<K, V> { // 타입 매개 변수 2개 존재 public K getKey(); public V getValue(); } class OrderedPair <K, V> implements Pair <K, V> { // 제네릭 클래스 정의 private K key; private V value; public OrderedPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
1 2 3 4 5 6 7 8 9
public class Main { public static void main(String args[]) { Pair<String, Integer> p1; p1 = new OrderedPair<> ("Even", 8); // new OrderedPair <String, Integer> Pair<String, String> p2; p2 = new OrderedPair<> ("hello", "java"); } }
제네릭 타입을 상속/구현하는 일반 클래스
- 제네릭 인터페이스를 구현하는 일반 클래스
- 클래스를 정의할 때 제네릭 인터페이스의 <>안에 실제 자료형을 지정
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
class MyPair implements Pair<String, Integer> { private String key; private Integer value; public MyPair(String key, Integer value) { this.key = key; this.value = value; } public String getKey() { return key; } public Integer getValue() { return value; } }
1 2 3 4 5
public class Main { public static void main(String args[]) { MyPair mp = new MyPair("test", 1); } }
RAW 타입
- 타입 매개 변수 없이 사용되는 제네릭 타입
- 제네릭 타입이지만 일반 타입처럼 사용하는 경우
- 타입 매개 변수를
Object
로 처리함
ex)
1
Data data = new Data("hello");
이때 Data는 제네릭 타입 Data
의 raw 타입 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Data<T> { private T t; public Data(T t) { this.t = t; } public void set(T t) { this.t = t; } public T get() { return t; } }
제네릭 메소드와 타입 제한
제네릭 메소드
- 자료형을 매개 변수로 가지는 메소드
- 하나의 메소드 정의로 여러 유형의 데이터를 처리할 때 유용함
- 메소드 정의에서 반환형 왼편에 각 괄호 <> 안에 타입 매개 변수를 표시
- 타입 매개 변수를 메소드의 반환형이나 메소드 매개 변수의 자료형, 지역 변수의 자료형으로 사용할 수 있음
1 2 3
public static <T> T getLast(T[] a) { return a[a.length-1]; // 배열의 마지막 원소 리턴 }
1 2 3 4 5
public <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; // 배열에서 두 개의 객체를 교환 array[j] = temp; }
- 인스턴스 메소드와
static
메소드 모두 제네릭 메소드로 정의 가능 - 제네릭 메소드를 호출할 때, 타입을 명시하지 않아도 됨
- 전달되는 인자에 의해 타입의 추론이 가능함
1 2 3 4 5 6
class Util { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } }
1 2 3 4 5 6 7 8 9 10
public class Main { public static void main(String args[]) { Pair<Integer, String> p1 = new OrderedPair<>(1, "apple"); Pair<Integer, String> p2 = new OrderedPair<>(2, "pear"); boolean same = Util.<Integer, String>compare(p1, p2); // <Integer, String> 생략 가능 System.out.println(same); } }
제네릭의 타입 제한
- 제네릭 클래스/제네릭 인터페이스/제네릭 메소드를 정의할 때 적용 가능한 자료형에 제한을 두는 것
<T extends Number>
와 같이 하면 T를 상한으로 정할 수 있음T
에 주어지는 자료형은Number
의 서브 클래스여야 함
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class Data<T extends Number> { private T t; public Data() { } public Data(T t) { this.t = t; } public void set(T t) { this.t = t; } public T get() { return t; } }
1 2 3 4 5 6 7
public class Main { public static void main(String args[]) { Data<Integer> data = new Data<>(20); // Integer는 Number의 자식 유형 System.out.println(data.get()); Data<String> data2 = new Data<>("Hello"); //오류 } }
제네릭 타입과 형변환
- 상속 관계가 있어야 상위/하위 자료형의 관계가 존재함
Integer
나Double
은Number
의 자식 클래스- 그러나
Data<Number>
와Data<Integer>
사이는 상속 관계가 없음
1 2 3
class Data<T> { }
1 2
class FormattedData<T> extends Data<T> { // 상속 관계 }
1 2 3 4 5 6 7 8 9 10 11
public class Main { public static void main(String args[]) { Data<Number> data1 = new Data<Number>(); data1.set(Integer.valueOf(10)); // OK data1.set(Double.valueOf(10.1)); // OK // Data<Number> data2 = new Data<Integer>(); // 컴파일 오류(Data<Integer> -> Data<Number> 형변환 불가) Data<Integer> data3 = new FormattedData<Integer>(); // OK } }
제네릭 타입 사용 시 유의 사항
기본 자료형은 타입 매개 변수로 지정 불가
1
Data<int> d = new Data<>(); //오류
타입 매개 변수로 객체 생성 불가
1 2 3
class Data <T> { private t1 = new T(); //오류 }
타입 매개 변수의 타입으로
static
데이터 필드 선언 불가1 2 3
class Data <T> { private static T t2; //오류 }
제네릭 타입의 배열 선언 불가
1
Data<Integer>[] arrayOfData; //오류
람다식
람다식
- 인터페이스를 구현하는 익명 클래스의 객체 생성 부분을 수식화 한 것
- 구현할 것이 1개의 추상 메소드뿐인 경우 간단히 표현할 수 있음
1 2 3 4
Runnable runnable = new Runnable() { // Runnable 인터페이스를 구현하는 익명 클래스 정의 후 객체 생성을 수식화 public void run() { } };
- 람다식 구문
메소드 매개 변수의 괄호, 화살표, 메소드 몸체로 표현
1
인터페이스 객체변수 = (매개 변수목록) → { 실행문 목록 }
1
Runnable runnable = () -> {};
람다식 기본 문법
- 익명 구현 클래스의 객체 생성 부분만 람다식으로 표현함
- 익명 서브 클래스의 객체 생성은 람다식이 될 수 없음
- 이 때 인터페이스에는 추상 메소드가 1개만 존재해야 함
- 2개 이상의 추상 메소드를 포함하는 인터페이스는 사용 불가
- 람다 식의 결과 타입을 타깃 타입이라고 함
- 1개의 추상 메소드를 포함하는 인터페이스를 함수적 인터페이스라 함
- 메소드가 1개 뿐이므로 메소드 이름 생략할 수 있음
- 람다식은 이름 없는 메소드 선언과 유사함
람다식 기본 문법
1
인터페이스 객체 변수 = (매개 변수 목록) → { 실행문 목록 };
매개 변수 목록에서 자료형은 인터페이스(타킷 타입) 정의에서 알 수 있으므로 자료형을 생략하고 변수 이름만 사용 가능
1
(int i ) ->
1
(i) ->
매개 변수가 1개면 괄호도 생략 가능하며 이때 변수 이름 하나만 남음
1
(i) ->
1
i ->
- 매개 변수를 가지지 않으면 괄호만 남음
- 화살표 사용
실행문 목록에서 실행문이 1개이면 중괄호 생략 가능
1
i -> { 실행문1 }
1
i -> 실행문1
1
Runnable runnable = () -> System.out.println("my thread");
단, 실행문이
return
문 뿐이라면return
과 (수식 다음의) 세미콜론, 중괄호를 동시에 생략 해야 하고 1개의 수식만 적어야 함1
i -> { return 1; }
1
i -> 1
1
f1 = (a, b) -> { return a + b; };
1
f2 = (a, b) -> a + b;
람다식의 사용 예
람다식 사용 예제 1
1 2 3
interface Addable { // 인터페이스 int add(int a, int b); // 추상 메소드 }
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
public class Main { public static void main(String[] args) { // 익명 구현 클래스의 객체 생성 Addable ad1 = new Addable() { public int add(int a, int b) { // 추상 메소드 구현 return (a + b); } }; System.out.println(ad1.add(100, 200)); // 매개 변수 목록과 return문을 가진 람다식 Addable ad2 = (int a, int b) -> { // add 메소드 정의 return (a + b); }; System.out.println(ad2.add(10, 20)); //간단한 람다식 Addable ad3 = (a, b) -> (a + b); System.out.println(ad3.add(1, 2)); } } // 출력 결과 // 300 // 30 // 3
람다식 사용 예제 2
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 32 33 34 35 36 37 38 39 40 41
interface MyInterface1 { public void method(int a, int b); } interface MyInterface2 { public void method(int a); } public class Main { public static void main(String args[]) { MyInterface1 f1, f2, f3; MyInterface2 f4, f5; f1 = (int a, int b) -> { System.out.println(a + b); // MyInterface1 인터페이스의 추상 메소드 method 정의 }; f1.method(3, 4); f2 = (a, b) -> { System.out.println(a + b); }; f2.method(5, 6); f3 = (a, b) -> System.out.println(a + b); f3.method(7, 8); f4 = (int a) -> { System.out.println(a); }; f4.method(9); f5 = a -> System.out.println(a); f5.method(10); } } // 7 // 11 // 15 // 9 // 10
람다식의 활용
- 함수형 인터페이스(function interface)
- 1개의 추상 메소드만 가지는 단순한 인터페이스를 함수형 인터페이스라 함
- 패키지
java.util.function
에서 표준 함수형 인터페이스가 제네릭 인터페이스로 제공 됨 - 함수형 인터페이스를 구현하는 클래스를 정의할 때, 익명 클래스 정의를 활용할 수 있으나 람다식이 효율적
- ex) 표준 함수형 인터페이스의 예
Consumer<T>
는void accept(T t)
를 가짐Supplier<T>
는T get()
메소드를 가짐Function<T, R>
은R apply(T t)
를 가짐
함수형 인터페이스와 람다식
함수형 인터페이스와 람다식 사용
1 2 3 4 5 6 7 8
public class Main { public static void main(String args[]) { Thread thd = new Thread(() -> System.out.println("my thread")); thd.start(); } } // my thread
1 2 3 4 5 6 7 8 9 10 11 12 13
import java.util.function.*; public class Main { public static void main(String args[]) { Supplier<Integer> rand = () -> { Integer r = (int) (Math.random() * 10); return r; }; int a = rand.get(); System.out.println(a); } }
학습 정리
- 자료형을 매개 변수로 가지는 클래스와 인터페이스를 제네릭 타입이라고 함
- 제네릭 클래스를 사용할 때 제공되는 타입 파라미터는 필드의 자료형, 메소드의 반환형, 메소드에서 인자의 자료형으로 사용될 수 있음
- 자료형을 매개 변수로 가지는 메소드를 제네릭 메소드라고 함
- 제네릭을 활용하면 컴파일 시점에 명확한 자료형 검사를 수행할 수 있음
- 함수형 인터페이스를 구현하는 클래스의 객체를 생성할 때 람다 식을 사용하는 것이 효율적임
- 람다 식의 결과 타입에 해당하는 인터페이스를 람다 식의 타깃 타입이라고 함
연습 문제
다음과 같은 제네릭 클래스가 있다고 가정하자. 보기에서 문법적으로 오류가 있는 것은?
1 2 3 4 5
class Data<T>{ private T t; public void set(T t) { this.t = t; } public T get() { return t; } }
a.
Data <int> d = new Data <>();
- 제너릭 클래스의 문법적으로 올바른 사용법
Data<Integer>d = new Data<>();
Data<String>d = new Data<String>();
Data d = new Data();
- 제너릭 클래스의 문법적으로 올바른 사용법
다음과 같은 인터페이스가 있다고 가정할 때, 보기에서 람다식 사용이 잘못된 것은?
1 2 3
interface Addable { int add(int a, int b); }
a.
Addable ad = (int a, int b) -> return (a + b);
- 람다식의 올바른 사용법
Addable ad = (int a, int b) -> { return (a + b); };
Addable ad = (int a, int b) -> a + b;
Addable ad = (a, b) -> (a + b);
- 람다식의 올바른 사용법
Java에서 제공되는 표준 함수형 인터페이스 중
Supplier<T>
에서 선언된 추상 메소드의 이름과 형식은 각각 무엇인가?a.
1
T get()