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

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

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



학습 개요


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



학습 목표


  • 제네릭 클래스를 정의할 수 있음
  • 제네릭 메소드의 정의와 사용법을 설명할 수 있음
  • 정의된 제네릭 타입이나 메소드를 사용할 수 있음
  • 람다 식의 활용 방법을 설명할 수 있음
  • 표준 함수형 인터페이스를 활용할 수 있음



강의록


제네릭 타입

제네릭의 의미

  • 제네릭 클래스, 제네릭 인터페이스, 제네릭 메소드
    • 클래스, 인터페이스, 메소드를 정의할 때 타입 매개 변수(타입 파라미터)를 선언하고 사용할 수 있음
    • 자바 프로그램의 재사용성을 높이고 오류를 줄이는 방법
  • 제네릭의 장점
    • 여러 유형에 걸쳐 동작하는 일반화된 클래스나 메소드를 정의할 수 있음
    • 자료형을 한정함으로써 컴파일 시점에 자료형 검사가 가능
      • 실행 오류를 찾아 고치는 것은 어렵기 때문
    • 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"); //오류
          }
      }
    

제네릭 타입과 형변환

  • 상속 관계가 있어야 상위/하위 자료형의 관계가 존재함
    • IntegerDoubleNumber의 자식 클래스
    • 그러나 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. 다음과 같은 제네릭 클래스가 있다고 가정하자. 보기에서 문법적으로 오류가 있는 것은?

    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();
  2. 다음과 같은 인터페이스가 있다고 가정할 때, 보기에서 람다식 사용이 잘못된 것은?

    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);
  3. Java에서 제공되는 표준 함수형 인터페이스 중 Supplier<T>에서 선언된 추상 메소드의 이름과 형식은 각각 무엇인가?

    a.

    1
    
     T get()
    

[유비쿼터스 컴퓨팅 개론] 5강 - 디바이스 기술 - 나노 기술 및 차세대 전지

[파이썬 프로그래밍 기초] 6강 - 선택 구조