본문 바로가기

국비 교육

2020.9.16 일자 수업 : 람다 문법

 람다 문법 

git/eomcs-java-basic/src/main/java com.eomcs.oop.ex12

람다 문법이 생긴 이유

메서드를 만드려면 클래스를 꼭 만들어줘야하는 객체지향 언어의 번거로움을 해결하기 위한 문법

 

람다 문법은 인터페이스를 구현한 클래스의 선언부와 메서드의 선언부를 모두 생략할 수 있다. 다음 예제에서 익명클래스로 인터페이스를 구현한 코드와 람다 문법으로 인터페이스를 구현한 코드가 결과적으로 같은 작업을 수행한다.

public class Exam0110 {

  interface Player {
    void play();
  }

  public static void main(String[] args) {

    // 익명 클래스로 인터페이스 구현하기
    Player p1 = new Player() {
      @Override
      public void play() {
        System.out.println("익명 클래스");
      }
    };
    p1.play();

    // 람다 문법으로 인터페이스 구현하기
    Player p2 = () -> {
      System.out.println("람다");
    };
    p2.play();
  }
}

 

람다를 사용할 수 있는 조건

다음과 같이 추상 메서드가 한 개 있는 인터페이스"functional interface"라고 부른다. 

  static interface Player {
    void play();
  }

 

이 인터페이스의 구현체를 딱 한번만 생성하고 사용한다면 익명 클래스로 구현할 수 있지만, 안에 있는 추상메서드가 하나 뿐이라면 클래스명도 생략될 뿐만 아니라 메서드명까지 생략되는 람다 형식을 사용할 수가 있다. 추상메서드가 하나밖에 없기 때문에 메서드명이 없어도 특정되기 때문이다. 따라서 functional interface에 대해서만 람다를 사용할 수 있다.

 

functional interface의 조건은 다음과 같다.

 

1) 추상 메서드를 한 개만 갖는다. 추상 메서드가 아닌 것은 여러개 갖고 있어도 된다.

 

2) 추상 메서드를 한 개만 갖는 추상 클래스는 안된다.

 

람다의 사용 위치

람다를 사용할 수 있는 곳 = 익명 클래스를 둘 수 있는 곳

  • 스태틱 필드
  • 인스턴스 필드
  • 로컬 변수
  • 파라미터
public class Exam0160 {
  interface A {
    void print();
  }

  // 스태틱 필드
  static A obj1 = () -> System.out.println("스태틱 필드");

  //인스턴스 필드
  A obj2 = () -> System.out.println("인스턴스 필드");

  public static void main(final String[] args) {

    // 로컬 변수
    A obj3 = () -> System.out.println("로컬 변수!");

    // 파라미터
    m1(() -> System.out.println("파라미터"));
  }

  static void m1(final A obj) {
    obj.print();
  }
}

람다의 내부적인 원리

 

익명 클래스

자바의 nested class 는 모두 별도의 .class 파일을 갖는다.

위의 main()에 정의된 로컬 익명 클래스는 다음과 같은 이름의 .class 파일로 컴파일 된다.

Exam0110$1.class

람다

람다는 해당 클래스의 멤버 메서드로 정의되고 별도의 .class 파일을 생성하지 않는다.

람다 문법이 초기에 등장했을 때는 익명 클래스로 변환되었지만 최근에는 그냥 호출한 클래스의 메서드로 변환된다.

람다를 호출하는 코드는 자동 생성된 메서드를 호출하는 코드로 변환된다.

private static synthetic void lambda$0();
0 getstatic java.lang.System.out : java.io.PrintStream [33]
3 ldc <String "람다"> [39]
5 invokevirtual java.io.PrintStream.println(java.lang.String) : void [41]
8 return
 Line numbers:
 [pc: 0, line: 27]
 [pc: 8, line: 28]

람다의 사용 방법

람다는 functional interface를 구현하여 딱 한번만 생성하고 사용할 때, 클래스명과 메서드명을 모두 생략하여 익명 클래스 대신 사용할 수 있는 문법이다. 클래스명과 메서드명이 없기 때문에 람다의 구성 요소는 다음과 같다. 

  • 파라미터 - 메서드의 파라미터
  • 몸체(body) - 추상 메서드를 구현하는 몸체
  • 리턴값 - 메서드가 다 실행되고 리턴되는 값

그러나 분명히 해야하는 것은 람다는 메서드를 정의하는 것이 아니라, 추상메서드를 하나 갖는 인터페이스의 구현체를 정의하고 있다는 사실이다.

람다의 몸체(body)

몸체가 한줄이면 중괄호를 생략할 수가 있다.

public class Exam0120 {

  interface Player {
    void play();
  }

  public static void main(String[] args) {
    Player p1 = () -> System.out.println("테스트1");
    p1.play();
  }
}

 

람다의 파라미터 

  • 파라미터는 괄호() 안에 선언한다.
    Player p1 = (String name) -> System.out.println(name + " 님 환영합니다.");
    p1.play("홍길동");
  • 파라미터 타입을 생략할 수 있다.
    Player p2 = (name) -> System.out.println(name + " 님 환영합니다.");
    p2.play("홍길동");
  • 파라미터가 한 개일 때는 괄호도 생략할 수 있다.(타입을 생략하지 않고 괄호만 생략할 수는 없다.)
    Player p3 = name -> System.out.println(name + " 님 환영합니다.");
    p3.play("홍길동");
    
    // Player p3 = String name -> System.out.println(name + " 님 환영합니다.");
    // p3.play("홍길동");
    // 컴파일 에러
  • 파라미터가 여러 개일 때는 괄호를 생략할 수 없다.
    // Player p3 = name, age -> System.out.printf("%s(%d)님 환영합니다.\n", name, age);
    // p3.play("임꺽정", 30);
    // 컴파일 에러
  • 파라미터가 없을때는 소괄호는 생략할 수 없다.
    // Player p3 = -> System.out.println("테스트3"); 
    // p3.play();
    // 컴파일 오류!

 

람다의 리턴

리턴 값은 return 명령을 사용하여 처리한다.

public class Exam0150 {

  interface Calculator {
    int compute(int a, int b);
  }

  public static void main(String[] args) {
    Calculator c1 = (a, b) -> {
      return a + b;
    };
  }
}

리턴 값을 필요로 하는 메서드의 몸체가 한 문장으로 된 표현식(=값을 리턴하는 한 문장의 코드)인 경우 괄호를 생략할 수 있다. 단 괄호를 생략할 때 return 키워드도 생략해야 한다.

    Calculator c2 = (a, b) -> a - b;
    System.out.println(c2.compute(10, 20));
    
    // Calculator c2 = (a, b) -> return a - b;
    // 컴파일 오류!

값을 리턴해야 하는데 리턴 문장에서 값을 리턴하지 않아도 컴파일 오류이다.

    Calculator c3 = (a, b) -> Math.max(a, b);
    System.out.println(c3.compute(10, 20));
    // ok!

    // Calculator c4 = (a, b) -> System.out.println(a + ",", b); // 컴파일 오류!
    // System.out.println(c4.compute(10, 20));

람다의 활용 - 아규먼트

익명 클래스를 아규먼트에서 바로 정의 / 생성

어제 수업에서 배운 것처럼 익명클래스를 한 메서드의 아규먼트 안에서 바로 정의/생성할 수가 있다.

public class Exam0331 {

  static interface Calculator {
    int compute(int a, int b);
  }
  
  static void test(Calculator c) {
    System.out.println(c.compute(100, 200));
  }
  
  public static void main(String[] args) {
    
    // 메서드 몸체가 한 줄
    test(new Calculator() {
      public int compute(int a, int b) {
        return a + b;
      }
    });
    
    // 메서드 몸체가 여러 줄
    test(new Calculator() {
      public int compute(int a, int b) {
        int sum = 0;
        for (int i = a; i <= b; i++)
          sum += i;
        return sum;
      }
    });
  }
}

람다를 사용하여 클래스 생성 후, 아규먼트로 주기

이제는 람다를 사용할 수 있으니, 익명클래스 대신 람다로 인터페이스를 구현하고 그 주소를 담은 레퍼런스 변수를 메서드의 아규먼트로 넣어준다.

package com.eomcs.oop.ex12;

public class Exam0331 {

  static interface Calculator {
    int compute(int a, int b);
  }
  
  static void test(Calculator c) {
    System.out.println(c.compute(100, 200));
  }
  
  public static void main(String[] args) {
    // 람다 몸체 한 줄
    Calculator c1 = (a, b) -> a + b;
    test(c1);
    
    // 람다 몸체 여러 줄
    Calculator c2  = (a, b) -> {
      int sum = 0;
      for (int i = a; i <= b; i++)
        sum += i;
      return sum;
    };
    test(c2);
  }
}

아규먼트에서 바로 람다 사용

혹은 레퍼런스 변수에 굳이 담지 않고 아규먼트 안에서 바로 람다로 인터페이스를 구현/생성할 수 있다.

package com.eomcs.oop.ex12;

public class Exam0331 {

  static interface Calculator {
    int compute(int a, int b);
  }
  
  static void test(Calculator c) {
    System.out.println(c.compute(100, 200));
  }
  
  public static void main(String[] args) {
    // 람다 몸체 한 줄
    test((a, b) -> a + b);
    
    // 람다 몸체 여러 줄
    test((a, b) -> {
      int sum = 0;
      for (int i = a; i <= b; i++)
        sum += i;
      return sum;
    });
  }
}

 


중첩 클래스와 람다의 이해

메서드 안에서 인터페이스의 구현체를 로컬 클래스로 생성하여 리턴

getInterest라는 메서드는 파라미터로 받은 변수(rate)를 필드로 갖는 Interest 인터페이스의 구현체를 만들고 그 인스턴스를 생성하여 리턴한다.

package com.eomcs.oop.ex12;

public class Exam0410 {

  static interface Interest {
    double compute(int money);
  }

  static Interest getInterest(final double rate) {
    // 로컬 클래스로 인터페이스 구현한 후 객체 리턴하기
    class InterestImpl implements Interest {
      double rate;

      public InterestImpl(double rate) {
        this.rate = rate;
      }

      @Override
      public double compute(int money) {
        return money + (money * rate / 100);
      }
    }
    return new InterestImpl(rate);
  }

  public static void main(String[] args) {
    Interest i1 = getInterest(1.5);
    System.out.printf("금액: %.2f\n", i1.compute(1_0000_0000));

    Interest i2 = getInterest(2.5);
    System.out.printf("금액: %.2f\n", i2.compute(1_0000_0000));
  }
}

로컬 클래스를 익명 클래스로 변경하고 바로 리턴

이 메서드 안의 정의된 로컬 클래스를 익명 클래스로 변경할 때, 익명 클래스는 생성자를 가질 수 없기 떄문에 기존의 생성자를 인스턴스 블록으로 바꿔준다.

package com.eomcs.oop.ex12;

public class Exam0410 {

  static interface Interest {
    double compute(int money);
  }

  static Interest getInterest(final double rate) {
    // 로컬 클래스로 인터페이스 구현한 후 객체 리턴하기
    return new Interest() {
      double rate;

      {
        this.rate = rate;
      }

      @Override
      public double compute(int money) {
        return money + (money * rate / 100);
      }
    };
  }

  public static void main(String[] args) {
    Interest i1 = getInterest(1.5);
    System.out.printf("금액: %.2f\n", i1.compute(1_0000_0000));

    Interest i2 = getInterest(2.5);
    System.out.printf("금액: %.2f\n", i2.compute(1_0000_0000));
  }
}

외부 메서드의 변수(rate)를 그대로 사용

이너클래스에서 번거롭게 필드를 선언하지 말고 직접 외부 변수에 접근하여 사용할 수 있도록 하면, 이너 클래스 안의 인스턴스 블록은 따로 필요 없다. 단 로컬 클래스에서 사용하는 외부 클래스의 변수는 상수이거나 상수에 준하므로 rate는 final이어야만 한다.

package com.eomcs.oop.ex12;

public class Exam0410 {

  static interface Interest {
    double compute(int money);
  }

  static Interest getInterest(final double rate) {
    // 로컬 클래스로 인터페이스 구현한 후 객체 리턴하기
    return new Interest() {
      @Override
      public double compute(int money) {
        return money + (money * rate / 100);
      }
    };
  }

  public static void main(String[] args) {
    Interest i1 = getInterest(1.5);
    System.out.printf("금액: %.2f\n", i1.compute(1_0000_0000));

    Interest i2 = getInterest(2.5);
    System.out.printf("금액: %.2f\n", i2.compute(1_0000_0000));
  }
}

 

익명 클래스를 람다로 변경

이렇게 하면 정의된 익명 클래스 안에 메서드 하나 밖에 없으므로 람다를 사용할 수 있다. 그러면 다음과 같이 getInterest()의 몸체를 한 줄로 줄일 수 있다.

package com.eomcs.oop.ex12;

public class Exam0410 {

  static interface Interest {
    double compute(int money);
  }

  static Interest getInterest(final double rate) {
    // 로컬 클래스로 인터페이스 구현한 후 객체 리턴하기
    return (money) -> money + (money * rate / 100);
  }

  public static void main(String[] args) {
    Interest i1 = getInterest(1.5);
    System.out.printf("금액: %.2f\n", i1.compute(1_0000_0000));

    Interest i2 = getInterest(2.5);
    System.out.printf("금액: %.2f\n", i2.compute(1_0000_0000));
  }
}

메서드 레퍼런스 

 

람다로 인터페이스의 추상 메서드를 구현할 때, 직접 구현하지 말고 다른 클래스에 있는 메서드를 그대로 호출한다면 메서드 레퍼런스를 사용할 수 있다. 이를 사용하기 위해서는 인터페이스에 선언된 메서드의 규격과 호출할 메서드의 규격이 일치해야 한다.

규격 :  파라미터 타입 및 개수, 리턴 타입

 메서드의 레퍼런스을 사용하는 문법은 다음과 같다. 좌항에 구현할 인터페이스타입은 레퍼런스 타입으로 두고, 우항에 메서드를 구현할 때 호출하게 될 메서드가 있는 클래스명 혹은 인스턴스의 레퍼런스 변수명::호출할 메서드명을 작성하면 된다.

인터페이스 타입 변수명 = 사용할 클래스 / 인스턴스::호출할 메서드명

메서드의 레퍼런스의 내부적 원리

메서드 레퍼런스를 통해 인터페이스를 구현하고 인터페이스의 메서드를 호출할 때 지정한 파라미터 값은 메서드 레퍼런스로 지정된 메서드에게 전달된다. 그리고 지정된 메서드가 리턴하는 값을 구현된 메서드가 그대로 리턴한다.

public static void main(String[] args) {

    // Calculator c1 = MyCalculator::plus;

    // 위의 코드는 내부적으로 다음과 같다.
    //
    Calculator c1 = new Calculator() {
      @Override
      public int compute(int a, int b) {

        return MyCalculator.plus(a, b);
      }
    };

스태틱 메서드 레퍼런스

인터페이스를 구현할 때 호출할 메서드가 스태틱 메서드일 때 스태틱 메서드 레퍼런스를 사용할 수 있다. 단, "::" 앞에 오게 되는 것은 사용되는 스태틱 메서드를 갖는 클래스명이다.

public class Exam0510 {
  
  static class MyCalculator {
    public static int plus(int a, int b) {return a + b;}
    public static int minus(int a, int b) {return a - b;}
    public static int multiple(int a, int b) {return a * b;}
    public static int divide(int a, int b) {return a / b;}
  }

  static interface Calculator {
    int compute(int a, int b);
  }

  public static void main(String[] args) {

    Calculator c1 = MyCalculator::plus;
    Calculator c2 = MyCalculator::minus;
    Calculator c3 = MyCalculator::multiple;
    Calculator c4 = MyCalculator::divide;
    
    System.out.println(c1.compute(200, 17)); // compute() ==> plus()
    System.out.println(c2.compute(200, 17)); // compute() ==> minus()
    System.out.println(c3.compute(200, 17)); // compute() ==> multiple()
    System.out.println(c4.compute(200, 17)); // compute() ==> divide()
  }
}

 

인스턴스 메서드 레퍼런스

사용할 메서드가 인스턴스 메서드라면, 이 메서드를 가진 클래스의 인스턴스를 미리 생성하여 메서드 레퍼런스를 사용할 수가 있다. "::" 앞에는 인스턴스의 레퍼런스 변수명이 와야한다. 인스턴스 메서드를 호출하면 인스턴스가 생성되면서 미리 특정값으로 지정된 변수를 사용하는 메서드를 사용할 수가 있다.

public class Exam0610 {

  static class Calculator {
    double rate;

    public Calculator(double rate) {
      this.rate = rate;
    }

    public double year(int money) {
      return money * rate / 100;
    }

    public double month(int money) {
      return money * rate / 100 / 12;
    }

    public double day(int money) {
      return money * rate / 100 / 365;
    }
  }

  static interface Interest {
    double compute(int money);
  }

  public static void main(String[] args) {

    Calculator 보통예금 = new Calculator(0.5);
    Calculator 정기예금 = new Calculator(1.5);
    Calculator 청년행복예금 = new Calculator(10);

    System.out.println("[보통예금]");
    Interest i1 = 보통예금::year;
    System.out.printf("년 이자: %.1f\n", i1.compute(10_0000_0000));

    i1 = 보통예금::month;
    System.out.printf("월 이자: %.1f\n", i1.compute(10_0000_0000));

    i1 = 보통예금::day;
    System.out.printf("일 이자: %.1f\n", i1.compute(10_0000_0000));

    System.out.println("--------------------------");

    System.out.println("[정기예금]");
    Interest i2 = 정기예금::year;
    System.out.printf("년 이자: %.1f\n", i2.compute(10_0000_0000));

    i2 = 정기예금::month;
    System.out.printf("월 이자: %.1f\n", i2.compute(10_0000_0000));

    i2 = 정기예금::day;
    System.out.printf("일 이자: %.1f\n", i2.compute(10_0000_0000));
  }
}

메서드 레퍼런스의 리턴값과 파라미터

인터페이스로부터 구현된 메서드의 리턴값과 파라미터는 메서드 레퍼런스로 지정된 메서드의 리턴값과 파라미터와 호환이 가능해야한다. 인터페이스의 구현체에서 정의된 메서드를 호출할 때 지정된 파라미터가 문제없이 그 안에서 호출된 메서드의 파라미터로 전달되어야한다. 또한 호출된 메서드가 리턴하는 값도 온전히 구현체에서 정의된 메서드의 리턴값이 되어야한다. 

 

메서드 레퍼런스의 리턴 타입

메서드 레퍼런스의 리턴 타입은 다음과 같아야한다.

  • 구현체에서 정의된 메서드와 같은 리턴 타입
  • 구현체에서 정의된 메서드의 리턴 타입으로 암시적 형변환 가능한 타입(서브 클래스 타입)
  • auto-unboxing / boxing 가능한 타입
  • void

결론 : 메서드 레퍼런스가 가리키는 실제 메서드를 호출한 후 그 메서드가 리턴한 값이 인터페이스에 정의된 메서드의 리턴 값으로 사용될 수 있다면 문제가 없다.

public class Exam0530 {

  static class MyCalculator {
    public static int plus(int a, int b) {
      return a + b;
    }

    public static int minus(int a, int b) {
      return a - b;
    }

    public static int multiple(int a, int b) {
      return a * b;
    }

    public static int divide(int a, int b) {
      return a / b;
    }
  }

  static interface Calculator1 {
    double compute(int a, int b);
  }

  static interface Calculator2 {
    float compute(int a, int b);
  }

  static interface Calculator3 {
    short compute(int a, int b);
  }

  static interface Calculator4 {
    void compute(int a, int b);
  }

  static interface Calculator5 {
    Object compute(int a, int b);
  }

  static interface Calculator6 {
    String compute(int a, int b);
  }

  public static void main(String[] args) {

    // 리턴 타입 int ===> double
    Calculator1 c1 = MyCalculator::plus; // OK!
    System.out.println(c1.compute(100, 200));

    // 리턴 타입 int ===> float
    Calculator2 c2 = MyCalculator::plus; // OK!
    System.out.println(c2.compute(100, 200));

    // 리턴 타입 int ===> short
    // Calculator3 c3 = MyCalculator::plus; // 컴파일 오류!

    // 리턴 타입 int ===> void
    Calculator4 c4 = MyCalculator::plus; // OK!
    c4.compute(100, 200); // plus() 메서드의 리턴 값은 무시한다.

    // 리턴 타입 int ===> Object
    Calculator5 c5 = MyCalculator::plus; // OK!
    System.out.println(c5.compute(100, 200));

    // 리턴 타입 int ===> String
    // Calculator6 c6 = MyCalculator::plus; // 컴파일 오류!

  }
}

 

메서드 레퍼런스의 파라미터 타입과 개수

메서드 레퍼런스의 파라미터 타입은 다음과 같아야한다. 

  • 구현체에서 정의된 메서드와 같은 파라미터 타입
  • 구현체에서 정의된 메서드에서 지정된 파라미터가 암시적 형변환될 수 있는 타입(수퍼 클래스 타입)
  • auto-unboxing / boxing 가능한 타입

결론 : 인터페이스 규칙에 따라 받은 값을 실제 메서드에 그대로 전달할 수 있다면 가능하다.

// 메서드 레퍼런스 - 스태틱 메서드 레퍼런스
package com.eomcs.oop.ex12;


public class Exam0540 {

  static class MyCalculator {
    public static int plus(int a, int b) {
      return a + b;
    }

    public static int minus(int a, int b) {
      return a - b;
    }

    public static int multiple(int a, int b) {
      return a * b;
    }

    public static int divide(int a, int b) {
      return a / b;
    }
  }

  static interface Calculator1 {
    int compute(byte a, byte b);
  }

  static interface Calculator2 {
    int compute(short a, short b);
  }

  static interface Calculator3 {
    int compute(long a, long b);
  }

  static interface Calculator4 {
    int compute(float a, float b);
  }

  static interface Calculator5 {
    int compute(Object a, Object b);
  }

  static interface Calculator6 {
    int compute(String a, String b);
  }

  static interface Calculator7 {
    int compute(Integer a, Integer b);
  }

  static interface Calculator8 {
    int compute(int a);
  }

  static interface Calculator9 {
    int compute(int a, int b, int c);
  }

  public static void main(String[] args) {

    // 파라미터 타입: byte ===> int
    Calculator1 c1 = MyCalculator::plus; // OK!

    // 파라미터 타입: short ===> int
    Calculator2 c2 = MyCalculator::plus; // OK!

    // 파라미터 타입: long ===> int
    // Calculator3 c3 = MyCalculator::plus; // 컴파일 오류!

    // 파라미터 타입: float ===> int
    // Calculator4 c4 = MyCalculator::plus; // 컴파일 오류!

    // 파라미터 타입: Object ===> int
    // Calculator5 c5 = MyCalculator::plus; // 컴파일 오류!

    // 파라미터 타입: String ===> int
    // Calculator6 c6 = MyCalculator::plus; // 컴파일 오류!

    // 파라미터 타입: Integer ===> int
    Calculator7 c7 = MyCalculator::plus; // OK

    // 파라미터 타입: int, int ===> int
    // Calculator8 c8 = MyCalculator::plus; // 컴파일 오류!

    // 파라미터 타입: int, int ===> int
    // Calculator9 c9 = MyCalculator::plus; // 컴파일 오류!

  }
}

메서드 레퍼런스의 파라미터 개수는 구현체에서 정의된 메서드의 파라미터의 개수와 일치해야한다. 

  • 메서드 레퍼런스의 파라미터 개수가 더 많으면? 남은 파라미터에 들어갈 아규먼트가 지정되지 않는다.
    // 파라미터 타입: int ===> int, int
       Calculator8 c8 = MyCalculator::plus; // 컴파일 오류!
    
       Calculator8 c8 = new Calculator8() {
         @Override
         public int compute(int a) {
           return MyCalculator.plus(a, ?); // 컴파일 오류!
           // compute()는 int 값 한 개만 받는데, plus()는 int 값 두 개를 요구한다.
         }
       };
  • 메서드 레퍼런스의 파라미터 개수가 더 적으면? 구현체에서 정의된 메서드를 통해 받은 아규먼트 중 무엇을 골라 메서드의 아규먼트로 전할지 알 수 없다.
    // 파라미터 타입: int, int, int ===> int, int
    Calculator9 c9 = MyCalculator::plus; // 컴파일 오류!
    
       Calculator9 c9 = new Calculator9() {
         @Override
         public int compute(int a, int b, int c) {
           return MyCalculator.plus(a, b); // 컴파일 오류!
           // compute()는 int 값 세 개를 받아서 plus()에 세 개 모두 전달한다. 
           // 그러나 plus()는 int 파라미터가 두 개만 있다.
         }
       };

 

메서드 레퍼런스 활용 - 생성자 레퍼런스

 

어떤 객체를 생성하는 create메서드를 가진 functional Interface를 만들고 어떤 객체의 new 연산자를 메서드 레퍼런스로 지정하여 인터페이스 구현체를 정의하면 이 구현체만으로 다른 클래스의 객체를 생성할 수 있다. 

package com.eomcs.oop.ex12;

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

public class Exam0710 {

  static interface ListFactory {
    List create();
  }

  public static void main(String[] args) {

    ListFactory f1 = ArrayList::new;

    List list = f1.create(); // new ArrayList();

    System.out.println(list instanceof ArrayList);
    System.out.println(list.getClass().getName());
  }
}

내부적으로는 new 연산자를 통해 인스턴스를 생성하고, 원하는 생성자까지 호출하여 객체를 초기화한 후 리턴할 것이다.

    ListFactory f1 = ArrayList::new;
    
    // ListFactory f1 = new ListFactory() {
    //   @Override
    //   public List create() {
    //     return new ArrayList();
    //   }
    // };

단, 호출하고자하는 생성자와 구현한 메서드의 파라미터 타입과 개수가 일치해야한다. 원하는 생성자를 파라미터 타입과 개수로 지정하면 해당 클래스에서 그에 맞는 생성자를 찾을 것이다.

public class Exam0730 {

  static class Message {
    String name;

    public Message() {
      this.name = "이름없음";
    }

    public Message(String name) {
      this.name = name;
    }

    public void print() {
      System.out.printf("%s님 반갑습니다!\n", name);
    }
  }

  static interface Factory1 {
    Message get();
  }

  static interface Factory2 {
    Message get(String name);
  }

  public static void main(String[] args) {

    Factory1 f1 = Message::new; // Message() 생성자를 가리킨다.
    Factory2 f2 = Message::new; // Message(String) 생성자를 가리킨다.

    Message msg = f1.get(); // ==> new Message()
    msg.print();

    msg = f2.get("홍길동"); // ==> new Message("홍길동")
    msg.print();
  }
}

메서드 레퍼런스 활용 - 생성자 메서드 레퍼런스

Supplier<T>

Supplier<T> 인터페이스는 람다를 활용하기 위해 자바에서 제공된 API이다. Supplier<T> 인터페이스는 T 타입의 객체를 리턴하는 get() 추상 메서드 하나만을 갖는 functional interface이다. 따라서 우리는 이것을 통해 원하는 타입의 객체를 얼마든지 생성하여 리턴받을 수 있다.

 

다음은 <T> 제네릭 메서드인 prepareNames 를 정의하여 원하는 요소들을 원하는 타입의 컬렉션에 담은 결과물을 리턴할 수 있도록 한 프로그램이다. 이 메서드의 특징은 다음과 같다.

  • Collection<T> 객체를 생성할 Supplier 인터페이스의 구현체와, 컬렉션에 담을 T 요소들을 모두 파라미터로 받는다.
  • 파라미터로 받은 Supplier 인터페이스의 get() 메서드를 호출하여 원하는 컬렉션의 인스턴스를 생성한다.
  • 생성한 컬렉션 객체에 파라미터로 받은 T 객체를 모두 담는다.
  • 컬렉션에 요소들을 모두 담은 Collection<T>타입의 결과물을 리턴한다. 

이렇게 메서드를 정의하고, 메서드를 호출할 때에는 아규먼트로 Supplier 구현체를 람다를 통해 생성할 수 있는데, 이 구현체의 get 메서드원하는 컬렉션 클래스의 생성자를 호출할 것이므로, 메서드 레퍼런스를 사용하면 더 간편해진다.

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.function.Supplier;

public class Exam0750 {

  static <T> Collection<T> prepareNames(Supplier<Collection<T>> factory, T... names) {
    Collection<T> list = factory.get();
    for (T name : names) {
      list.add(name);
    }
    return list;
  }

  static <T> void print(Iterator<T> i) {
    while (i.hasNext()) {
      System.out.print(i.next() + ",");
    }
    System.out.println();
  }

  public static void main(String[] args) {

    Collection<String> c1 = prepareNames(ArrayList<String>::new, "홍길동", "임꺽정", "유관순", "임꺽정");
    print(c1.iterator());

    System.out.println("------------------------");

    Collection<String> c2 = prepareNames(HashSet<String>::new, "홍길동", "임꺽정", "유관순", "임꺽정");
    print(c2.iterator());
  }
}

람다 활용 - FileFilter 구현체 생성

git/eomcs-java-basic/src/main/java com.eomcs.io.ex01.Exam0640
git/eomcs-java-basic/src/main/java com.eomcs.io.ex01.Exam0650

저번 수업에서 FileFilter 인터페이스의 구현체를 통해서 특정 폴더의 파일과 하위 디렉토리 중 java 파일인 것만을 뽑아 배열에 담는 연습을 했다. 이 FileFilter 인터페이스는 추상 메서드를 하나만 가진 functional interface이므로 무리 없이 람다로 바꿔줄 수 있다.

import java.io.File;
import java.io.FileFilter;

public class Exam0640 {


  public static void main(String[] args) throws Exception {

    File dir = new File(".");

    File[] files = dir.listFiles(new FileFilter() {
      @Override
      public boolean accept(File file) {
        if (file.isFile() && file.getName().endsWith(".java"))
          return true;
        return false;
      }
    });
    
    // 위의 코드와 아래 코드는 같다.
    File[] files = dir.listFiles(file -> file.isFile() && file.getName().endsWith(".java"));

    for (File file : files) {
      System.out.printf("%s %12d %s\n", file.isDirectory() ? "d" : "-", file.length(),
          file.getName());
    }
  }
}