Posts 학원 #41일차: 람다(Lambda)
Post
Cancel

학원 #41일차: 람다(Lambda)

인터페이스

프로그래밍을 하다보면 인터페이스가 등장하는데, 이럴 땐 호출 규칙에 따라서 호출하는 쪽, caller 프로그램을 짤 것인지, 호출될 프로그램, callee을 짤 것인지 파악해야 한다.

caller를 만드는 입장

다음 예제에서 Iterator는 인터페이스이다. ArrayList의 iterator() 메서드는 Iterator 인터페이스를 구현한 클래스의 객체를 만들어 리턴한다. main() 메서드에서는 이미 만들어진 Iterator 구현체를 Iterator 규칙(hasNext()next())에 따라 사용하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Exam0110 {
  public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("홍길동");
    list.add("임꺽정");
    list.add("유관순");
    list.add("김구");

    Iterator<String> iterator = list.iterator();

    // 인터페이스를 기준으로 한 개발자 입장:
    // => 인터페이스 호출 규칙에 따라 객체를 사용한다.
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}

callee를 만드는 입장

다음 예제는 File의 list() 메서드가 사용할 필터 객체를 만드는 코드이다. FilenameFilter 인터페이스 규칙에 따라 MyFilter 클래스를(메서드를) 작성한다. 이렇게 작성할 클래스는 list() 메서드 내에서 사용할 것이다. 즉 list() 메서드 내에서 호출할 accept() 메서드를 작성한다.

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
public class Exam0120 {
  public static void main(String[] args) {
    File file = new File(".");


    // 인터페이스를 기준으로 한 개발자 입장:
    // => File에 list() 메서드가 사용할 필터 객체를 만드는 입장이다.
    // => FilenameFilter 인터페이스 규칙에 따라 클래스를 작성한다.
    // => 이렇게 작성한 클래스는 list() 메서드에서 사용할 것이다.

    class MyFilter implements FilenameFilter {
      @Override
      public boolean accept(File dir, String name) {
        return (name.endsWith(".java")) ? true : false;
      }

    }

    String[] filenames = file.list(new MyFilter());

    for (String filename : filenames) {
      System.out.println(filename);
    }
  }
}

Spring을 통해 우리가 하는 일도 위와 같다. caller를 60퍼센트도 만들고 나머지는 callee를 작성할 것이다.

람다(lambda)

com.eomcs.oop.ex12

메서드 한 개짜리 인터페이스를 구현한 익명 클래스를 좀 더 간단히 표현하기 위해 만든 문법이다.

functional interface를 효과적으로 다루는 문법

자바의 코딩 최소 단위는 클래스이다. 메서드를 만들 때도 클래스를 만들고 그 안에 만들어야 한다. 메서드만 따로 만들지는 못한다. C언어나 자바스크립트의 경우 클래스가 없이 함수를 바로 작성할 수 있다. 자바에서 클래스가 도입된 이유는 코드가 더 커지면서 다수의 함수를 적절한 역할과 기능에 따라 그룹으로 묶기 위해서이다. 즉 클래스 문법은 대량의 코드(메서드)를 쉽게 관리하기 위해서 등장하였다.

그러나 때로는 코딩을 할 때 간단한 함수를 다루고 싶을 때가 있다. 자바와 같이 클래스에 묶어주는 객체지향언어가 발전하다가 독립적인 함수로 간단하게 프로그래밍을 하고 싶다는 수요가 늘어났다. 요즘 functional 프로그래밍이 트렌드인 이유이다.

반면 자바는 C언어와 자바스크립트와 다르게 함수 하나를 만들더라도 클래스에 무조건 묶어야 하니 여간 귀찮은 것이 아니다. 그래서 함수형 프로그래밍을 흉내내는 문법이 생겼다. 이것이 바로 람다(Lambda) 문법이다.

람다는 메서드 한 개짜리 인터페이스를 구현한 익명 클래스를 좀 더 간단히 표현한다. 이때 추상 메서드가 한 개만 있는 인터페이스를 functional 인터페이스라고 부른다. 이 인터페이스를 구현하는 경우에만 람다 문법을 사용할 수 있다.

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
public class Exam0110 {

  // 인터페이스는 static을 붙이지 않아도 static 멤버가 사용할 수 있다.
  interface Player {
    void play();
  }
  
  public static void main(String[] args) {
    // 로컬 클래스로 인터페이스 구현하기
    class MyPlayter implements Player {
      @Override
      public void play() {
        System.out.println("로컬 클래스");
      }
    }
    MyPlayer p1 = new MyPlayer();
    p1.play();
    
    // 익명 클래스로 인터페이스 구현하기 
    Player p2 = new Player() {
      @Override
      public void play() {
        System.out.println("익명 클래스");
      }
    };
    p2.play();
    
    // 람다 문법으로 인터페이스 구현하기
    Player 람다1 = () -> {
      System.out.println("람다1");
    }
    람다1.play();
    
    // 람다: 코드 정리
    Player 람다2 = () -> System.out.println("람다2");
    람다2.play();
  }
}

메서드 한 개짜리 인터페이스를 구현한 익명 클래스를 좀 더 간단히 만드는 문법이다. 처음에 람다가 별도 클래스로 정의가 되었다. 그러나 이제 바깥 클래스에서 내부적으로 처리한다. 별도 클래스가 생기지 않는다.

functional Interface?

.class 파일

익명 클래스와 .class 파일

  • 자바의 nested class는 모두 별도의 .class 파일을 갖는다.
  • 위의 main() 메서드에 정의된 로컬 익명 클래스는 다음과 같은 이름의 .class 파일로 컴파일된다. Exam0110$1.class

람다와 .class 파일

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

람다는 해당 클래스 멤버 메서드로 정의된다.별도의 .class 파일을 생성하지 않는다. 람다 문법이 초기에 등장했을 때는 익명 클래스로 변환되었다. 그러나 최근에는 그냥 멤버 메서드로 변환된다.

1
2
3
4
5
6
7
8
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]

Lambda body

람다 body

  • 한 문장일 때는 중괄호를 생략할 수 있다.
  • 파라미터가 없다고 괄호를 생략할 수는 없다.
1
2
3
4
5
6
7
8
9
10
11
public class Exam0120 {
  interface Player {
    void play();
  }
  
  public static void main(String[] args) {
    Player p1 = () -> System.out.println("테스트1");
    Player p2 = () -> { System.out.println("테스트2"); }
    //Player p3 = -> System.out.println("테스트3"); ==> 컴파일 오류!
  }
}

람다 파라미터

  • 파라미터는 괄호() 안에 선언한다.
  • 파라미터 타입을 생략할 수 있다.
  • 파라미터가 한 개일때는 괄호도 생략할 수 있다.
  • 파라미터가 여러 개일때는 괄호를 생략할 수 없다.
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
public class Exam0120 {
  interface Player {
    void play(String name);
  }
  
  interface Runner {
    void run(String name, int age);
  }
  
  public static void main(String[] args) {
    // 파라미터 1개
    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();
    
    // 파라미터 여러개
    Runner r1 = (String name, int age) -> System.out.printf("%s(%d)님이 달립니다.", name, age);
    r1.run();
    Runner r2 = (name, age) -> System.out.printf("%s(%d)님이 달립니다!", name, age);
    r2.run();
    //Runner r3 = name, age -> System.out.printf("%s(%d)님이 달립니다!", name, age); ==> 컴파일 에러! 
  } 
}

람다 리턴

  • 리턴 값은 return 명령을 사용하여 처리한다.
  • 한 문장으로 된 표현식(=값을 리턴하는 한 문장의 코드)인 경우 괄호를 생략할 수 있다. 단 return 키워드도 함께 생략해야 한다. 있으면 컴파일 오류!

표현식(expression)? 문장은 문장인데, 값을 리턴하는 문장.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Exam0150 {
  interface Calculater {
    int compute(int a, int b);
  }
  
  public static void main(String[] args) {
    Calculator c1 = (a, b) -> {
      return a + b;
    };
    System.out.println(c1.compute(10, 20));
    
    Calculator c2 = (a, b) -> a - b;
    System.out.println(c2.compute(10, 20));
    
    // Max.max()는 int 값을 리턴한다. 그래서 이 메서드를 호출하는 문장은 표현식이다.
    Calculator c3 = (a, b) -> Math.max(a, b);
    System.out.println(c3.compute(10, 20));
    
    // 값을 리턴해야 하는데 람다 문장에서 값을 리턴하지 않으면 컴파일 오류!
    //Calculator c4 = (a, b) -> System.out.println(a + ", " + b);
    //System.out.println(c4.compute(10, 20));
  }
}

람다 사용 가능 위치

익명 클래스를 사용할 수 있는 곳에서는 모두 람다를 사용할 수 있다. 즉 스태틱 필드, 인스턴스 필드, 로컬 변수, 파라미터 자리에 올 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
  }
}

functional interface의 자격

추상 메서드를 한 개만 갖고 있는 인터페이스에 대해 람다 문법으로 익명 클래스를 만들어줄 수 있다.

추상 메서드를 한 개만 갖고 있는 인터페이스에 대해 람다 문법으로 익명 클래스를 만들 수 있다.

1
2
3
4
5
6
7
public class Exam0210 {
  interface Player { void play(); }
  public static void main(String[] args) {
    Player p = () -> System.out.println("player...");
    p.play();
  }
}

추상 메서드가 두 개 이상이면 람다 문법으로 구현할 수 없다.

1
2
3
4
interface Player {
    void play();
    void stop();
}

여러 개의 메서드가 있다 하더라도 추상 메서드가 한 개이면 된다. static 메서드나 default 메서드가 몇 개이든 그 개수는 중요하지 않다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Exam0230 {
  interface Player {
    static String info() { return "Player입니다." }
    default void stop();
    void play();
  }
  
  public static void main(String[] args) {
    Player p = () -> System.out.println("Player...");
    p.play();
    System.out.println(Player.info());
  }
}

인터페이스가 아닌 추상 클래스는 람다 구현의 대상이 아니다.

1
2
3
static abstract class Player {
  public abstract void play();
}

아규먼트에 람다 활용

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 Player {
  void play();
}

static void testPlayer(Player p) {
  p.play();
}

public static void main(String[] args) {
  
  // 로컬 클래스
  class MyPlayer implements Player {
    @Override
    public void play() {
      System.out.println("실행!");
    }
  }
  testPlayer(new MyPlyaer());
  
  // 익명클래스
  testPlayer(new Player() {
    @Override
    public void play() {
      System.out.println("실행~");
    }
  });
  
  // 람다
  testPlayer( () -> System.out.println("실행~") );
}

파라미터와 리턴 값이 있는 람다는 다음과 같이 만들어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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() {
    @Override
    public int compute(int a, int b) {
      return a + b;
    }
  });
  
  // 람다
  test((a, b) -> a + b);
}

여러 문장을 실행하는 경우 블록으로 감싼다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) -> {
  	int sum = 0;
    for (int i = 0; i <= b; i++) {
      sum += i;
    }
    return sum;
  });
}

리턴 문장에 람다 활용

다음과 같은 예제가 있다. getInterest(double rate) 메서드는 Interest 인터페이스의 구현체를 로컬 클래스로 갖고 있고, 해당 클래스의 인스턴스를 리턴한다. InterestImpl은 생성자의 파라미터로 getInterest()의 로컬 변수(파라미터)를 받고 그 값을 가지고 작업한다.

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
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.println("금액: %.2f\n", i2.compute(1_0000_0000));
  }
}

그러나 로컬(또는 익명 로컬) 클래스 안에서 바깥 메서드의 로컬 변수를 사용하면 컴파일러는 자동으로 그 값을 보관하기 위해 필드를 추가한다. 또한 컴파일러는 그 값을 받을 수 있도록 생성자를 변경한다. 따라서 개발자가 직접 필드나 생성자를 정의해줄 필요가 없다. 즉, 개발자는 로컬 변수의 값을 로컬 클래스의 생성자에 전달하기 위해 직접 작성할 필요가 없다. 컴파일러가 자동으로 추가한다. 따라서 다음과 같이 getInterest()를 수정하였다.

단, 이때 로컬 변수는 상수이거나 상수에 준해야 한다. final로 선언되지 않은 변수에 여러번 값이 할당되었다면 해당 로컬 변수는 로컬 클래스에서 접근할 수 없다.

1
2
3
4
5
6
7
8
9
  static Interest getInterest(final double rate) {
    class InterestImpl implements Interest {
      @Override
      public double compute(int money) {
        return money + (money * rate / 100);
      }
    }
    return new InterestImpl(rate);
  }

객체를 한 개만 생성할 것이기 때문에 익명 클래스로 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static Interest getInterest(final double rate) {
  Interest i = new Interest() {
    /*
    상수에 준하는 로컬 변수에 접근할 수 있기 때문에 이러지 않아도 된다!
    
    double rate;
    {
      this.rate = rate;
    }
    */
    @Override
    public double compute(int money) {
      return money + (money * rate / 100);
    }
  }
  return i;
}

레퍼런스 변수에 담을 필요 없이 바로 리턴할 수 있다.

1
2
3
4
5
6
7
8
static Interest getInterest(final double rate) {
  return new Interest() {
    @Override
    public double compute(int money) {
      return money + (money * rate / 10);
    };
  };
}

익명 클래스를 람다로 바꾼다.

1
2
3
static Interest getInterest(final double rate) {
  return money -> money + (money * rate / 10);
}

메서드 레퍼런스(참조)

가능한 중복코드를 제거하고 기존 코드(메서드)를 재활용하기 위해 메서드 레퍼런스 문법을 사용한다. 메서드 한 개 짜리 인터페이스의 구현체를 만들 때 기존 메서드를 람다 구현체로 사용할 수 있다. 단 인터페이스에 선언된 메서드의 규격과 일치해야 한다.

  • 규격: 파라미터 타입 및 개수, 리턴 타입
  • 문법: 클래스명::메서드명

스태틱 메서드 레퍼런스

메서드 한 개짜리 인터페이스의 구현체를 만들 때 기존 스태틱 메서드를 람다 구현체로 사용할 수 있다. 단 인터페이스에 선언된 메서드의 규격과 일치해야 한다.

직접 람다 문법을 만드는 대신 이미 똑같은 규칙을 갖고 있는 어떤 클래스에 스태틱으로 갖고 있다면, 메서드 이름 빼고 시그너처가 일치하는 기존에 작성된 스태틱 메서드가 있다면 이렇게 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 x, int y);
  }

  public static void main(String[] args) {
    Calculator c1 = MyCalculater::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()
  }
}

위의 Calculator c1 = MyCalulator::plus는 내부적으로 다음 코드와 같다.

단, 내부적으로 람다에 관련된 것은 static 메서드로 처리가 된다. .class 파일이 만들어지지 않는다.

1
2
3
4
5
6
Calculator c1 = new Calculator() {
  @Override
  public int compute(int a, int b) {
    return MyCalculator.plus(a, b);
  }
}

왜 메서드 레퍼런스가 가리키는 실제 메서드는 인터페이스에 선언된 메서드의 규격과 일치해야 할까?

인터페이스에 정의된 메서드(compute())가 호출되었을 때, 그 파라미터 값은 메서드 레퍼런스로 지정된 스태틱 메서드(plus())에게 전달될 것이다. 그래서 스태틱 메서드의 파라미터는 항상 인터페이스 메서드에 정의된 파라미터 값을 받을 수 있어야 한다.

스태틱 메서드의 리턴 값은 인스턴스 메서드에 정의된 대로 리턴할 수 있어야 한다. 그래서 스태틱 메서드의 리턴 타입은 인터페이스 리턴 타입과 일치하거나 그 타입으로 바꿀 수 있어야 한다.

메서드 레퍼런스를 지정할 때 리턴 타입의 규칙

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

  • 같은 리턴 타입
  • 암시적 형변환 가능한 타입
  • auto-boxing 가능한 타입
  • void
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class Exam0530 {

  static class MyCalculator {
    public static int plus(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!
    /*
    위 문장은 다음 문장과 같다.
    Calculator1 c1 = new Calculator1() {
      @Override
      public double compute(int a, int b) {
        return MyCalculator.plus(a, b);//int
      }
    };
    */
    System.out.println(c1.compute(100, 200));

    // 리턴 타입 int ===> float
    Calculator2 c2 = MyCalculator::plus; // OK!
    /*
    위 문장은 다음 문장과 같다.
    Calculator2 c2 = new Calculator2() {
    	@Override
    	public float compute(int a, int b) {
    		return MyCalculator(a, b);//int
    	}
    };
    */
    
    // 리턴 타입 int ===> short
    // Calculator3 c3 = MyCalculator::plus; // 컴파일 오류!
    /*
    위 문장은 다음 문장과 같다.
    Calculator3 c3 = new Calculator3() {
      @Override
      public short compute(int a, int b) {
        return MyCalculator.plus(a, b);//int
      }
    };
    */

    // 리턴 타입 int ===> void
    Calculator4 c4 = MyCalculator::plus; // OK!
    c4.compute(100, 200); // "plus() 메서드의 리턴 값은 무시한다!"
    //  위 문장은 다음과 같다.
    /*
    Calculator4 c4 = new Calculator(int a, int b) {
      @Override
      public void compute(int a, int b) {
        MyCalculator.plus(a, b);//int
      }
    };
    */

    // 리턴 타입 int ===> Object
    Calculator5 c5 = MyCalculator::plus; // OK!
    // 위 문장은 다음과 같다.
    /*
    Calculator5 xx = new Calculator(int a, int b) {
      @Override
      public void compute(int a, int b) {
        //오토박싱
        return Integer.valueOf(MyCalculator.plus(a, b));
      }
    };
    */
    System.out.println(c5.compute(100, 200));

    // 리턴 타입 int ===> String
    // Calculator6 c6 = MyCalculator::plus; // 컴파일 오류!
    // 위 문장은 다음과 같다.
    /*
    Calculator c6 = new Calculator(int a, int b) {
      @Override
      public String compute(int a, int b) {
        return MyCalculator.plus(a, 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 메서드 레퍼런스 - 스태틱 메서드 레퍼런스
package com.eomcs.oop.ex12;


public class Exam0540 {

  static class MyCalculator {
    public static int plus(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
    /*
    오토 언박싱
    Calculaotor7 c7 = new Calculcator() {
      @Override
      public int compute(Integer a, Integer b) {
        return MyCalculator(a.intValue(), b.intValue());
      }
    };
    */

    // 파라미터 타입: int ===> int, int
    // Calculator8 c8 = MyCalculator::plus; // 컴파일 오류!
    /*
    Calculator 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, c); // 컴파일 오류!
        // compute()는 int 값 세 개를 받아서 plus()에 세 개 모두 전달한다.
        // 그러나 plus()는 int 파라미터가 두 개만 있다.
      }
    };
    */
  }
}



메서드 레퍼런스 - 인스턴스 메서드 레퍼런스

메서드 한 개짜리 인터페이스의 구현체를 만들 때 기존 인스턴스 메서드를 람다 구현체로 사용할 수 있다. 단 인터페이스에 선언된 메서드의 규격과 일치해야 한다. 보통 특정 인스턴스 값을 가지고 작업해야 할 경우에 이 방식을 사용한다. 위의 스태틱 메서드 레퍼런스 예제에서는 메서드를 값 없이 지정했기 때문에 메서드가 사용해야 하는 외부 값을 파라미터로 넘겨줘야 했다. 그러나 인스턴스 메서드를 지정하면 값과 함께 메서드를 지정하는 것이다. 다음 코드에서 보통예금 인스턴스의 메서드를 메서드 레퍼런스로 가리키면 인스턴스 필드의 값을 메서드에서 마음대로 다룰 수 있다.

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
42
43
44
45
46
47
48
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("년 이자: %s\n", i1.compute(10_0000_0000));
    /*
    위의 코드는 내부적으로 다음과 같다.
    Interest i1 = new Interest() {
      @Override
      public double compute(int money) {
        // 인터페이스 메서드 레퍼런스는 실제
        // 인터페이스 구현체에서 다음과 같이 메서드로 호출된다.
        return 보통예금.year(money);
      }
    };
    */
    
    i1 = 보통예금::month;
    System.out.printf("월 이자: %s\n", i1.compute(10_0000_0000));
    
    i1 = 보통예금::day;
    System.out.printf("일 이자: %s\n", i1.compute(10_0000_0000));
    
    System.out.println("[정기예금]");
    Interest i2 = 정기예금::year;
    System.out.printf("년 이자: %s\n", i1.compute(10_0000_0000));
    
    i2 = 정기예금::month;
    System.out.printf("월 이자: %s\n", i1.compute(10_0000_0000));
    
    i3 = 정기예금::day;
    System.out.printf("일 이자: %s\n", i1.compute(10_0000_0000));
  }
}

다음과 같이 활용할 수 있다. 다음 예제에서 java.util.function.Predicate 인터페이스를 String 클래스의 isEmpty() 메서드를 참조하여 구현하고 있다.

java.util.function

  • 표준 API Functional Interface

  • 람다로 많이 쓸만한 인터페이스를 자바에서 정의해주었다.
  • 오로직 람다식만을 지원하기 위해 만들어진 인터페이스 모음이다.
  • 람다식 타겟 타입은 표준 API 함수적 인터페이스와 사용자 정의 인터페이스로 나뉘어진다.

Predicate<String> p = String::isEmpty

  • predicate 인터페이스를 String의 isEmpty()메서드로 구현한 것이다.

Predicate 인터페이스? 매개변수와 boolean 리턴값이 있는 testXXX() 메서드를 가지고 있다. 매개값을 조사해서 boolean값을 리턴하는 역할을 한다.

표준 API Functional Interface? 함수적 인터페이스 집합 패키지로 java.util.function 패키지에 있다. 오로지 람다식만을 지원하기 위해 만들어진 인터페이스 모음이다. 람다식 타겟 타입은 표준 API 함수적 인터페이스와 사용자 정의 인터페이스로 나뉘어진다.

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
import java.util.function.Predicate;

public class Exam0630 {
  public static void main(String[] args) {
    // Predicate<String> 인터페이스 구현체 준비하기
    
    // 1) 로컬 클래스로 인터페이스 구현체 만들기
    class MyPredicate<T> implements Predicate<T> {
      @Override
      public boolean test(T value) {
        return ((String)value).isEmpty();
      }
    }
    
    // 2) 익명 클래스로 인터페이스 구현체 만들기
    Predicate<String> p1 = new Predicate<>() {
      @Override
      public boolean test(String value) {
        return value.isEmpty();
      }
    };
    
    // 3) 람다로 인터페이스 구현체 만들기
    Predicate<String> p1 = value -> value.isEmpty();
    
    // 4) 메서드 레퍼런스를 사용하여 기존 클래스의 메서드를 인터페이스 구현체로 사용한다.
    Predicate<String> p4 = String::isEmpty;
  }
}

생성자 레퍼런스

인터페이스에 정의된 메서드가 생성자의 형식과 일치한다면 메서드 레퍼런스로 생성자를 지정할 수 있다. 그러면 인터페이스의 메서드를 호출하면 지정된 클래스의 인스턴스를 만든 후 생성자를 호출한다.

다시 한번! 메서드 레퍼런스는? 인터페이스를 직접 구현하지 않고 이미 구현된 메서드를 인터페이스 구현체로 재활용하는 문법이다. 인터페이스의 메서드를 호출하면 지정된 클래스의 인스턴스를 만든 후 생성자를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Exam0710 {
  static interface ListFactory {
    List create();
  }
  
  public static void main(String[] args) {
    ListFactory f1 = ArrayList::new;
    /*
    생성자 레퍼런스를 지정하는 것은
    다음과 같이 익명 클래스를 만드는 것과 같다.
    ListFactory f1 = new ListFactory() {
      @Override
      public List create() {
        return new ArrayList();
      }
    }
    */
    List list = f1.create(); // new ArrayList();
    
    System.out.println(list instanceof ArrayList); //true
    System.out.println(list.getClass().getName()); //java.util.ArrayList
  }
}

생성자 레퍼런스를 지정할 때, 인터페이스 메서드에 따라 호출할 생성자가 결정된다. 만약 클래스가 인터페이스의 메서드의 규격과 같은 생성자를 가지고 있지 않다면 해당 클래스로 생성자 레퍼런스를 지정할 수 없다.

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
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); }
  static interface Factory3 { Message get(String name, int age); }
  
  public static void main(String[] args) {
    Factory1 f1 = Message::new; //Factory() 생성자를 가리킨다.
    // 컴파일러는 Message의 기본 생성자를 호출하는 Factory 구현체를 만들어 리턴한다.
    
    Factory2 f2 = Message::new; //Factory(String name) 생성자를 가리킨다.
    // 컴파일러는 Message의 생성자 중에서
    // String을 파라미터로 받는 생성자를 호출하는 Factory 구현체를 만들어 리턴한다.
    
    //Factory3 f3 = Message::new; 
    // Message(String, int) 생성자가 없기 때문에 컴파일 오류!
    // 즉 컴파일러는 Message의 생성자 중에서
    // String과 int를 파라미터로 받는 생성자를 호출하는 Factory 구현체를 만들어야 하는데,
    // Message 클래스에는 String과 int를 파라미터로 받는 생성자가 없기 때문에
    // Factory 구현체를 만들 수 없다.
    // 그래서 컴파일 오류가 발생한다.
    
    Message msg = f1.get(); // ==> new Message()
    msg.print(); //이름없음님 반갑습니다!
    
    msg = f2.get("김하연"); // ==> new Message("홍길동")
    msg.print(); //김하연님 반갑습니다!
    
  }
}

‘람다’는 새로 메서드를 구현해야 하지만, ‘메서드 레퍼런스’는 기존 클래스의 메서드를 재활용할 수 있다. 인터페이스 구현체를 직접 만들지 않고 기존에 존재하는 메서드를 구현체로 사용하는 문법이 메서드 레퍼런스이다. 메서드 레퍼런스에는 스태틱 메서드 레퍼런스, 인스턴스 메서드 레퍼런스, 생성자 레퍼런스가 있다.

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
public class Exam0750 {
  @SuppressWarnings("unchecked")
  static <T> Collection<T> prepareNames(Supplier<Collection<T>> factory, T... values) {
    for (T value : values) {
      list.add(value);
    }
    return list;
  }
  
  static <T> void print(Iterator<T> i) {
    while (i.hasNext()) {
      System.out.println(i.next() + ",");
    }
    System.out.println();
  }
  
  public static void main(String[] args) {
    Collection<String> c1 = prepareNames(ArrayList<String>::new, "monica", "phoebe", "rachel", "monica");
    print(c1.iterator());
    System.out.println("----------------");
    Collection<String> c2 = prepareNames(HashSet<String>::new, "monica", "phoebe", "rachel", "monica");
    print(c2.iterator());
  }
}

/*
monica, phoebe, rachel, monica,
----------------
monica, rachel, phoebe,
*/

prepareNames()는 T 타입 객체를 다루는 목록인 Collection 객체를 리턴한다. 단, prepareNames() 안에 Collection 객체를 만드는 코드는 없다. 대신 T를 담는 컬렉션 객체를 만들어주는 Supplier를 파라미터로 담는다. 메인 메서드에서 Supplier<Collection<T>> factory 자리에 ArrayList를 만들어주는 생성자를 참조하게 하였다. 그러면 내부적으로 Supplier<Collction<T>>를 구현하는 익명클래스를 작성하고, 익명클래스 안에서 Supplier 인터페이스의 T get() 메서드를 오버라이딩하는데 그 return 자리에 new ArrayList<T>()를 호출하게 된다.

Collection <T> list = factory.get();: => new ArrayList<String>()

위의 코드가 어떤 과정의 결과인지 다음 코드를 통해 알아보자.

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class Exam0751 {
  @SuppressWarnings ("unchecked")
  static <T> Collection<T> prepareNames(Supplier<Collection<T>> factory, T... values) {
    Collection<T> list = factory.get(); // => new ArrayList<String>()
    for (T value : values) {
      list.add(value);
    }
  }
  
  static <T> void print(Iterator<T> i) {
    while (i.hasNext()) {
      System.out.print(i.next() + ",");
    }
    System.out.println();
  }
  
  public static void main(String[] args) {
    // 1) 로컬 클래스를 사용하여 Supplier 구현체 만들기
    class MySupplier<T> implements Supplier<T> {
      @Override
      public T get() {
        return (T) new ArrayList<>();
      }
    };
    Supplier<Collection<String>> supplier1 = new MySupplier<Collection<String>>();
    Collection<String> list1 = prepareNames(supplier1, "monica", "phoebe", "rachel", "monica");
    
    // 2) 익명 클래스를 사용하여 Supplier 구현체 만들기    
    Supplier<Collection<String>> supplier2 = new Supplier<Collection<String>() {
      @Override
      public Collection<String> get() {
        return new ArrayList<>();
      }
    };    
    Collection<String> list2 = prepareNames(supplier2, "monica", "phoebe", "rachel", "monica");
    
    
    // 3) 익명 클래스를 사용하여 Supplier 구현체 만들기2  
    Collection<String> list3 = prepareNames(
      new Supplier<Collection<String>() {
      	@Override
      	public Collection<String> get() {
        	return new ArrayList<>();
      	}
    	}, 
      "monica", "phoebe", "rachel", "monica");
    
    // 4) 람다 문법으로 Supplier 구현체 만들기
    Collection<String> list4 = prepareNames(() -> new ArrayList<String>(), "monica", "phoebe", "rachel", "monica");
    
    // 5) 메서드 레퍼런스 문법으로 Supplier 구현체 만들기
    Collection<String> list5 = prepareNames(() -> ArrayList::new), "monica", "phoebe", "rachel", "monica");
    
    print(list.iterator());
  }
}
This post is licensed under CC BY 4.0 by the author.

학원 #40일차: 파일 입출력

학원 #43일차: 파일 입출력 API

Loading comments from Disqus ...