Home [Java 프로그래밍] 6강 - 제네릭과 람다식
Post
Cancel

[Java 프로그래밍] 6강 - 제네릭과 람다식

💡해당 게시글은 방송통신대학교 김희천 교수님의 'Java 프로그래밍' 강의를 개인 공부 목적으로 메모하였습니다.



학습 목차


  1. 제네릭 타입
  2. 제네릭 메소드와 타입 제한
  3. 람다식

학습 개요


  • 제네릭은 프로그램의 재사용성을 높여주고 소스 코드 컴파일 할 때 자료형 검사를 엄격하게 수행해 에러 최소화하기 위한 기법
  • 람다식은 매개 변수를 갖는 코드 블록으로 익명 클래스의 객체를 생성하는 부분을 수식화한 것
  • 제네릭과 람다식을 활용하는 방법

학습 목표


  1. 제네릭 타입을 이용해 클래스 정의 가능
  2. 제네릭 메소드의 정의와 사용법을 이해
  3. 정의된 제네릭 타입이나 메소드 사용 가능
  4. 람다식의 활용 방법을 설명 가능



1. 제네릭 타입


1-1. 제네릭의 의미

  • 제네릭 클래스, 제네릭 인터페이스, 제네릭 메소드
    • 클래스, 인터페이스, 메소드를 정의할 때 타입 매개변수(타입 파라미터)를 선언하고 사용 가능
  • 장점
    • 여러 유형에 걸쳐 동작하는 일반화된 클래스나 메소드 정의 가능
    • 자료형을 한정함으로써 컴파일 시점에 자료형 검사 가능
      • 실행 오류를 찾아 고치는 것은 어렵기 때문
    • cast(형변환) 연산자의 사용 불필요

1-2. 제네릭의 사용

  • ArrayList 클래스는 List 인터페이스를 구현한 클래스
    1
    2
    3
    4
    5
    
      class ArrayList<E> implements List<E> ... {
          boolean add(E e) { ... }
          E get(int index) { ... }
          E remove(int index) { ... }
      }
    
    • List 인터페이스
      • 순서가 있는 원소들의 집단을 관리하기 위한 컬렉션 인터페이스
        1
        2
        3
        4
        5
        6
        7
        
          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-3. 제네릭 클래스

  • 클래스 정의에서 타입 파라미터를 선언
    • 클래스 사용할 때는 타입 명시
  • 타입 파라미터는 참조형만 가능
    • 필드의 자료형, 메소드 반환형, 인자의 자료형으로 사용 가능
  • 컴파일 할 때, 명확한 타입 검사 수행 가능
    • 메소드 호출 시 인자의 유형이 맞는지
    • 메소드 호출의 결과를 사용할 때 유형이 맞는지
  • 자료형을 매개 변수로 가지는 클래스와 인터페이스를 제네릭 타입이라함

1-4. 제네릭 클래스의 정의

  • 문법
    • class 클래스이름<T1, T2, ... > { }
    • 클래스 정의에서 클래스 이름의 오른편, 각 괄호 <> 안에 타입 파라미터를 표시
    • 콤마(,)로 구분하여 여러 타입 파라미터 지정 가능
    • 타입 파라미터는 타입을 전달 받기 위한 것
    • 타입 파라미터의 이름은 관례적으로 E(element), K(key), V(value), N(number), T(이외) 등을 사용
  • 제네릭 클래스
    1
    2
    3
    4
    5
    6
    
      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
    
      class Data2<T> { // 타입 파라미터 호출
          private T t; // 데이터 필드의 자료형으로 타입 파라미터 사용
    
          public void set(T t) { this.t = t; } // 인자의 자료형으로 타입 파라미터 사용
          public T get( ) { return t; } // 메소드 반환형으로 타입 파라미터 사용
      }
    

1-5. 제네릭 클래스의 필요성

  • 제네릭 타입과 자료형 검사
    • 제네릭 타입 미 사용 시 컴파일 시점에서 오류 검출 불가
  • 의미가 명확하면 생성자 호출 시, 괄호만 사용 가능
    • Data2<String> b3 = new Data2< >( );
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
        public class GenericsTest2 {
            public static void main(String args[ ]) {
                Data2<String> data = new Data2<String>( );
                Integer i = new Integer(20);
                data.set(i); // 컴파일 오류
                String s = (String) data.get( ); 
                // 제네릭 타입 미 사용시 Integer -> String 형 변환 불가능하므로 실행 시 오류 발생
                ...
            }
        }
      

1-6. 제네릭 인터페이스를 구현하는 제네릭 클래스

  • 2개의 타입 매개변수(K,V)를 가지는 클래스
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      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
    10
    
      public class MultipleType {
          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-7. 제네릭 타입을 상속/구현하는 일반 클래스

  • 제네릭 인터페이스를 구현하는 일반 클래스
    • 클래스를 정의할 때 제네릭 인터페이스의 <>안에 자료형 지정
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
        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; }
        }
      
        public class ClassFromGeneric {
            public static void main(String args[ ]) {
                MyPair mp = new MyPair("test", 1);
                ...
            }
        }
      

1-8. RAW 타입

  • 제네릭 타입이지만 일반 타입처럼 사용하는 경우, 제네릭 타입을 지칭하는 용어
  • 타입 매개변수 없이 사용되는 제네릭 타입
    • 자료형을 Object로 처리함
      1
      
        Data2 data = new Data2("hello");
      
    • 이때 Data2는 제네릭 타입 Data2<T>raw 타입
      • 컴파일 시 타입 검사 명확하지 않아 경고 메세지 발생

2. 제네릭 메소드와 타입 제한


2-1. 제네릭 메소드

  • 자료형을 매개변수로 가지는 메소드
    • 하나의 메소드 정의로 여러 유형의 데이터를 처리할 때 유용함
  • 메소드 정의에서 반환형 왼편, 각 괄호 <> 안에 타입 매개변수를 표시
    1
    2
    3
    
      public static <T> T getLast(T[ ] a) {
          return a[a.length-1]; // 배열의 마지막 원소 리턴
      }
    
    • 타입 매개변수를 메소드의 반환형이나 메소드 매개변수의 자료형, 지역 변수의 자료형으로 사용 가능
  • 인스턴스 메소드와 static 메소드 모두 제네릭 메소드로 정의 가능
  • 제네릭 메소드를 호출할 때, 타입을 명시하지 않아도 인자에 의해 추론이 가능
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      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());
          }
      }
    
      public class GenericsTest5 {
          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);
          }
      }
    

2-2. 제네릭의 타입 제한

  • 자료형을 매개 변수화하여 클래스 / 인터페이스 / 메소드를 정의할 때, 적용 가능한 자료형에 제한을 두는 것
    1
    2
    3
    4
    5
    6
    
      class Data<T extends Number> {
          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
    4
    5
    6
    
      public class BoundedType {
          public static void main(String args[]) {
              Data<Integer> data = new Data<>(20); // Integer는 Number의 자식 유형
              System.out.println(data.get());
          }
      }
    
    • <T extends Number>와 같이 하면 T를 상한으로 지정 가능
      • T에 주어지는 자료형은 Number의 서브 클래스

2-3. 제네릭 타입과 형변환

  • 상속 관계가 있어야 상위/하위 자료형의 관계가 존재
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      class Data<T> { }
      class FormattedData<T> extends Data<T> { // 상속 관계
      }
    
      public class GenericTypeConversion1 {
          public static void main(String args[]) {
              Data<Number> data = new Data<Number>();
    
              data.set(new Integer(10)); // OK
              data.set(new Double(10.1)); // OK
    
              Data<Number> data1 = new Data<Integer>(); // 컴파일 오류(Data<Integer> -> Data<Number> 형변환 불가)
              Data<Integer> data = new FormattedData<Integer>();
          }
      }
    
    • IntegerDoubleNumber의 자식 클래스
    • Data<Number>Data<Integer>는 상하위 관계가 없음

2-4. 제네릭 타입 사용 시 유의 사항

  • 기본 자료형은 타입 매개변수로 지정 불가
    1
    
      Data<int> d = new Data<>( ); //오류
    
  • 타입 매개 변수로 객체 생성 불가
    1
    
      class Data <T> { private T t1 = new T( ); } //오류
    
  • 타입 매개변수의 타입으로 static 데이터 필드 선언 불가
    1
    
      class Data <T> { private static T t2; } //오류
    
  • 제네릭 타입의 배열 선언 불가
    1
    
      Data <Integer>[ ] arrayOfData; //오류
    



3. 람다식


3-1. 람다식

  • 인터페이스를 구현하는 익명 클래스의 객체 생성 부분을 수식화 한 것
    • 구현할 것이 1개의 추상 메소드 뿐일 때 간단히 표현 가능
      1
      2
      
        Runnable runnable = new Runnable( ) { // Runnable 인터페이스를 구현하는 익명 클래스 정의 후 객체 생성을 수식화
        	public void run( ) { ... } }; 
      
  • 람다식 구문
    • 메소드 매개변수의 괄호, 화살표, 메소드 몸체로 표현
      1
      
        Runnable runnable = () -> { };
      
      • 인터페이스 객체변수 = (매개변수목록){실행문 목록}

3-2. 람다식 기본 문법

  • 인터페이스를 구현하는 익명 구현 클래스의 객체 생성 부분만 람다식으로 표현
    • 익명 서브 클래스의 객체 생성은 람다식이 될 수 없음
  • 이 때 인터페이스에는 추상 메소드가 1개만 존재해야함
    • 2개 이상의 추상 메소드를 포함하는 인터페이스는 사용 불가
  • 람다식의 결과가 타입을 타깃 타입이라함
  • 1개의 추상 메소드를 포함하는 인터페이스를 함수적 인터페이스라함
    • 메소드가 1개 뿐이므로 메소드 이름 생략 가능
    • 람다식은 이름 없는 메소드 선언과 유사
  • 인터페이스 객체 변수 = (매개변수 목록) → {실행문 목록};
    • 매개변수 목록에서 자료형은 인터페이스(타킷 타입) 정의에서 알 수 있으므로 자료형을 생략하고 변수 이름만 사용 가능
      1
      
        (int i ) ->
      
      1
      
        (i) ->
      
    • 매개변수가 1개면 괄호도 생략 가능하며 변수 이름 하나만 남음
      1
      
        (i ) ->
      
      1
      
        i ->
      
    • 매개 변수를 가지지 않으면 괄호만 남음
    • 화살표 사용
    • 실행문 목록에서 실행문이 1개면 중괄호 생략 가능
      1
      
        i  -> { 실행문1 }
      
      1
      
        i -> 실행문1
      
    • 실행문이 return문 뿐이라면 return과 세미콜론, 중괄호를 동시 생략하고 1개의 수식만 남게 됨
      1
      
        i  -> { return 1; }
      
      1
      
        i -> 1
      

3-3. 람다식의 사용

  • 람다식 사용 예제 1
    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
    
      interface Addable { // 인터페이스
          int add(int a, int b); // 추상 메소드
      }
    
      public class LambdaExpressionTest {
          public static void main(String[] args) {
                
              // 람다식 사용하지 않은 객체 생성 코드
              Addable ad1 = new Addable() { // 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
    
      interface MyInterface1 { public void method(int a, int b); }
      interface MyInterface2 { public void method(int a); }
    
      public class LambdaTest1 {
          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
    

3-4. 람다식의 활용

  • 함수적 인터페이스(function interface)
    • 1개의 추상 메소드만 가지는 단순한 인터페이스를 함수적 인터페이스라함
    • 패키지 java.util.function에서 표준 함수적 인터페이스가 제네릭 인터페이스로 제공
    • 함수적 인터페이스를 구현하는 클래스를 정의할 때, 익명 클래스 정의를 활용할 수 있으나 람다식이 효율적
  • 표준 함수적 인터페이스의 예
    • Consumer<T>void accept(T t)를 가짐
    • Supplier<T>T get( ) 메소드를 가짐
    • Function<T, R>R apply(T t)를 가짐

3-5. 함수적 인터페이스와 람다식

  • 함수적 인터페이스와 람다식 사용
    1
    2
    3
    4
    5
    6
    
      public class RunnableTest4 {
          public static void main(String args[]) {
              Thread thd = new Thread(( ) -> System.out.println("my thread"));
              thd.start( );
          }
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    
      import java.util.function.*;
    
      public class ConsumerTest{
          public static void main(String args[]) {
              Consumer<String>con = t -> System.out.println("Hello " + t); // void accept(T t)
              con.accept("Java");
          }
      }
    



학습 정리


  • 자료형을 매개변수로 가지는 클래스와 인터페이스를 제네릭 타입이라 함
  • 제네릭 클래스를 사용할 때 제공되는 타입 파라미터는 필드의 자료형, 메소드의 반환형, 인자의 자료형으로 사용 가능
  • 자료형을 매개 변수로 가지는 메소드를 제네릭 메소드라함
  • 제네릭을 활용하면 컴파일 시점에 명확한 자료형 검사 수행 가능
  • 함수적 인터페이스를 구현하는 클래스의 객체를 생성할 때 람다식을 사용하는 것이 효율적
  • 람다식의 실행 결과가 대입되는 인터페이스를 람다식의 타깃 타입이라함



연습 문제


Q1.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Data<T>{
  private T t;
  public void set(T t) { this.t = t; }
  public T get( ) { return t; }
}

// 다음과 같은 제네릭 클래스가 있다고 가정하자. 보기에서 문법적으로 오류가 있는 것은?
// 1. Data <int> d = new Data <> ( );
// 2. Data <Integer> d = new Data <> ( );
// 3. Data <String> d = new Data <String> ( );
// 4. Data d = new Data( );

// 1. Data <int> d = new Data <> ( );

Q2.

1
2
3
4
5
6
7
8
9
10
interface Addable {
	int add(int a, int b);
}
// 아래와 같은 인터페이스가 있다고 가정할 때, 보기에서 람다식 사용이 잘못된 것은?
// 1. Addable ad = (int a, int b) -> { return (a + b); };
// 2. Addable ad = (int a, int b) -> return (a + b);
// 3. Addable ad = (a, b) -> { return (a + b); };
// 4. Addable ad = (a, b) -> (a + b);

// 2. Addable ad = (int a, int b) -> return (a + b);

Q3.

1
2
// Java에서 제공되는 표준 함수적 인터페이스 중 Consumer<T>에서 선언된 추상 메소드의 이름과 형식은 무엇인가?
// void accept(T t)

Alicloud AnalyticDB MySQL Query

[Java 프로그래밍] 7강 - 패키지와 예외 처리