람다식(lambda expression)
이란 함수(함수형 인터페이스의 추상 메서드가 대표적) 를 간결하고 쉽게 표현
할 수 있도록 자바 8부터 구현된 함수 선언 방식인데요.
자바스크립트의 화살표 함수와도 유사한 역할을 하는 이 람다식은 즉 함수형 인터페이스에서 추상 메서드를 오버라이딩 할 때 간결하게 사용
되는데요. 주로 Comparator
, Function
과 같은 function 페키지의 인터페이스 뿐만 아니라 앞서 살펴본 멀티 스레드에 사용되는 함수형 인터페이스인 Runnable
과 Callable
에서도 사용됩니다.
이럴 경우 기존의 Runnable과 Callable 객체를 생성한 후 오버라이딩 어노테이션으로 run과 call을 각각 재정의 하는 코드를 구현해야 했으나, 람다식을 사용해 해당 객체 생성과 어노테이션 재정의가 축약할 수 있게 되었음을 의미하죠. 이는 각각의 클래스가 run과 call 추상 메서드 딱 하나씩만 갖고 있기 때문에 가능한 일입니다.
// Runnable에 적용된 람다식 예제 public class Main { public static void main(String[] args) { // Runnable에 람다식 적용 (원래 형태는 new Runnable() 객체를 할당한 뒤 run 메서드를 오버라이드 하여 코드문을 작성 Runnable task = () -> System.out.println("Runnable task is running..."); // 스레드 생성 및 실행 Thread thread = new Thread(task); thread.start(); } } // Callable에 적용된 람다식 예제 // 원래는 new Callable<String> 객체를 생성 후 call 메서드를 오버라이드 해서 코드문을 작성. Callable<String> task = () -> { // 작업을 수행하고 결과를 반환 return "Callable task is running..."; };
또한 익명 구현 객체 (이름이 없는 인터페이스 구현 객체)를 람다식으로 간단하게 변환할 수 있는데요. 사용법은 다음과 같습니다.
- 추상 메서드 생성
public interface Calc{ void calculator(int x, int y) }
- 익명 구현 객체 오버라이딩
new Calc(){ @Override public void calculator(int x, int y){ System.out.println(x+y); } }
- 해당 메서드를 호출하는 메서드 생성
public static void act(Calc calcMethod){ int x = 10; int y = 20; Calc calcMethod(x,y) }
- 함수형 인터페이스를 구현하는 대신 람다식으로 재정의
//변환 전 기본적인 오버라이딩 메서드 Calc calculator = new Calc() { @Override public void calculator(int x, int y) { System.out.println(x + y); } }; //람다식으로 변환 한 오버라이딩 메서드 Calc calculator = (x, y) -> System.out.println(x + y);
- 메서드 호출 후 람다식을 매개변수로 전달
act(Calc calculator)
- act 메서드를 생성할 때 익명 구현 객체를 바로 생성하여 매개변수로 전달
// act 메서드를 호출하면서 람다식 (인터페이스를 오버라이딩 해서 재정의 한 코드문)을 매개변수로 전달. act((x, y) -> System.out.println(x + y));
또한 람다식은 인터페이스의 익명 구현 객체이므로 인터페이스 타입의 매개변수에 대입될 수 있습니다.
@FunctionalInterface public interface Calc { void calculator(int x, int y); } public class Main { public static void main(String[] args) { // 메서드 호출괃 동시에 람다식으로 인터페이스의 추상 메서드를 오버라이딩으로 재정의 한 후 바로 전달 add((x, y) -> { int result = x + y; System.out.println(result); } > add((x, y) -> { int result = x - y; System.out.println(result); } // 인터페이스 타입의 매개변수를 받는 메서드가 람다식 (Calc calc)으로 구성된 메서드를 받아 그 안에서 로컬 변수들을 실제 메서드에 반환하는데, 참고로 익명 구현 인터페이스는 add 메서드 호출 시 생성된 람다식 객체를 의미함. public void add(Calc calc) { int x = 10; int y = 20; calc.calculator(10, 20); } }
이때 함수형 인터페이스에 @FunctionalInterface
어노테이션을 붙여준다면, 컴파일러는 람다식으로 전달 된 매개변수를 보고 해당 매개변수의 인터페이스가 추상 메서드가 구현된 함수형 인터페이스 라는 것을 더 쉽게 유추할 수 있기 때문에, 가급적 위의 어노테이션을 붙여주는게 좋습니다.
또한 메서드가 두 개 이상 존재하는 함수형 인터페이스는 람다식으로도 표현될 수 없습니다.
(애초에 추상 메서드의 요건 자체를 성립하지 못함.)
여기 클릭 이벤트를 처리하는 인터페이스가 있다고 해보겠습니다.
public class Button { @FunctionalInterface public static interface ClickListener { void onClick(); } private ClickListener clickListener; public void setClickListener(ClickListener clickListener) { this.clickListener = clickListener; } public void click() { this.clickListener.onClick(); } }
해당 인터페이스의 메서드를 구현한 클래스 객체를 넘겨주면 그 코드문을 내용으로 한 메서드를 필드에 셋팅하고, 이후 click 메서드가 호출되면 넘겨 받은 코드문(재정의 된 onclick 메서드)이 실행됩니다.
이때 재정의 하게 되는 인터페이스가 함수형 인터페이스 일 때 객체에 익명 구현 함수를 넘겨줄 수 있는데요.
우선 평범한 방법을 생각해 보겠습니다.
// Button.ClickListener를 구현하는 클래스 정의 class MyClickListener implements Button.ClickListener { @Override public void onClick() { System.out.println("Cancel 버튼을 클릭했습니다."); } } // Button btnCancel 생성 Button btnCancel = new Button(); // MyClickListener 인스턴스 생성 MyClickListener listener = new MyClickListener(); // btnCancel에 ClickListener 설정 btnCancel.setClickListener(listener); btnCancel.click(10,20)
위 코드는 익명 구현 객체를 생성하지 않고 평범하게 메서드를 상속 받아 재정의 한 클래스와, Button 클래스와 메서드를 재정의 한 클래스의 객체를 각각 생성한 뒤 해당 객체를 btnCalcel의 setClickListener에 넘겨주는 예제인데요. 물론 넘겨줄 때 new MyClickListener() 객체를 생성하여 넘겨줄 수도 있습니다.
아무튼 이를 좀 익명 구현 객체로 좀 더 간단하게 축약한 코드는 다음과 같습니다.
public class ButtonExample { public static void main(String[] args) { // Button btnCancel 생성과 동시에 익명 객체 전달 Button btnCancel = new Button(); btnCancel.setClickListener(new Button.ClickListener() { @Override public void onClick() { System.out.println("Cancel 버튼을 클릭했습니다."); } }); btnCancel.click(10,20)
위의 코드는 해당 메서드를 상속 받는 클래스를 생성하지 않고, 바로 해당 Button 클래스의 익명 구현 객체를 생성한 뒤 메서드를 재정의 하는 코드인데요.
어느정도 축약되었긴 하지만, 위 코드를 람다식으로 더 축약할 수 있습니다.
Button btnCancel = new Button(); btnCancel.setClickListener(() -> System.out.println("Cancel 버튼을 클릭했습니다."));
위의 예제는 객체를 생성하고 생성된 객체의 메서드에 바로 람다식을 넘겨주는 예제인데요. 익명 구현 객체가 람다식으로 대체될 때 객체 생성을 제외하고 코드 구현만 넘겨줄 수 있는 것이죠.
다음으로 매개변수가 존재하는 람다식을 살펴보겠습니다. 코드는 위의 코드 예제를 그대로 두고, 인터페이스만 바꿔보겠습니다.
public class Button { @FunctionalInterface public interface ClickListener { void onClick(int x, int y); } private ClickListener clickListener; public void setClickListener(ClickListener clickListener) { this.clickListener = clickListener; } public void click(int x, int y) { if (clickListener != null) { this.clickListener.onClick(x, y); } } }
위의 코드를 보면 onClic에 매개변수 x와 y가 선언되어 있고, 해당 메서드를 실행하는 메서드 click에도 마찬가지로 간단한 조건에 의해 x와 y 매개변수를 넘겨주도록 되어있는데요.
위의 함수형 인터페이스를 상속 받아 메서드를 재정의 하고 호출하는 작업을 했을때는 아래와 같습니다.
// Button.ClickListener를 구현하는 클래스 정의 class MyClickListener implements Button.ClickListener { @Override public void onClick(int x, int y) { System.out.println("결과는" + x + y); } } // Button btnCancel 생성 Button btnCancel = new Button(); // MyClickListener 인스턴스 생성 MyClickListener listener = new MyClickListener(); // btnCancel에 ClickListener 설정 btnCancel.setClickListener(listener); btnCancel.click(10,20)
위의 코드를 다시 익명 구현 객체로 전달하는 예제 코드로 바꾸면 다음과 같고요.
public class ButtonExample { public static void main(String[] args) { // Button btnCancel 생성과 동시에 익명 객체 전달 Button btnCancel = new Button(); btnCancel.setClickListener(new Button.ClickListener() { @Override public void onClick(int x, int y) { System.out.println("결과는" + x + y); } }); btnCancel.click(10,20)
위 코드를 다시 람다식으로 재정의 해보겠습니다.
Button btnCancel = new Button(); btnCancel.setClickListener((x,y) -> System.out.println("결과는" + x + y));
이처럼 람다식은 전달 받는 매개변수 또한 간단하게 적을 수 있습니다.
추상 메서드에서 결과값을 반환하도록 하는 코드가 있다고 가정해 보겠습니다.
public class Button { @FunctionalInterface public interface ClickListener { int onClick(int x, int y); } private ClickListener clickListener; public void setClickListener(ClickListener clickListener) { this.clickListener = clickListener; } public void click(int x, int y) { if (clickListener != null) { this.clickListener.onClick(x, y); } } }
이때 해당 메서드를 정의하는 람다식은 다음과 같이 작성할 수 있는데요.
Button btnCancel = new Button(); btnCancel.setClickListener((x,y) -> x+y);
위에서 살펴본 코드와 마찬가지로 동일한 코드이긴 하지만, 해당 재정의 된 코드를 람다식으로 전달할 때 실행되는 코드문이 하나라면 중괄호와 return문을 무시할 수 있습니다.
그 말인 즉, 두 줄 이상의 코드문이라면 당연히 중괄호와 return문을 작성해야 하겠죠.
Button btnCancel = new Button(); btnCancel.setClickListener((x,y) -> { int result1 = x+y; int result2 = x-y; return result1 + result2; });
이러한 편리한 람다식을 더 축약할 수 있는 버전이 있는데요. 바로 메서드 참조
를 사용하는 방식입니다.
다음과 같은 람다식이 있다고 합시다.
() -> System.out.println("Hello World!")
해당 람다식에 메서드 참조를 사용하여 다음과 같이 축약할 수 있는데요.
(System.out::println)
이때 참조 기호 (::)를 중심으로 왼쪽은 해당 클래스, 오른쪽은 해당 클레스의 메서드를 의미하며 메서드 참조에는 정적 메서드 참조, 인스턴스 메서드 참조, 생성자 참조가 대표적이며, 각각의 특징 및 사용법은 다음과 같습니다.
- 정적 메서드 참조 (Static Method Reference) : 클래스에 선언된 정적 메서드를 참조할 때 사용됩니다. 이때 참조하는 메서드와 호출하는 메서드의 매개변수 타입과 개수가 일치해야 합니다. (이를 메서드 시그니쳐라고도 부릅니다.)
// 정적 메서드 정의 class Utils { static int doubleValue(int num) { return num * 2; } } // 정적 메서드 참조 Function<Integer, Integer> doubler = Utils::doubleValue; // Function 타입의 apply를 호출하는데, apply 메서드와 doubleValue 간에 메서드 시그니쳐가 같기 때문에 결과적으로 doubleValue를 참조하여 해당 코드를 실행합니다. int result = doubler.apply(5); // 결과: 10 System.out.println(result);
- 인스턴스 메서드 참조 (Instance Method Reference) : 객체의 인스턴스 메서드를 참조하며, 이를 위해 인스턴스를 생성해야 합니다.
// 인스턴스 메서드 정의 class Printer { void print(String message) { System.out.println(message); } } // 인스턴스 생성 및 메서드 참조 Printer printer = new Printer(); Consumer<String> messagePrinter = printer::print; // Consumer 메서드의 accept 메서드를 호출하여 매개변수를 참조 메서드에 전달후 처리합니다. messagePrinter.accept("Hello, world!");
- 생성자 참조 (Constructor Reference) : 클래스의 생성자를 참조하는 것입니다. 이때 함수형 인터페이스의 매개변수 개수에 따라 실행되는 생성자가 다를 수 있습니다. (물론 이 경우 호출하는 인터페이스에서 여러 매개변수를 입력 받는 메서드를 구현해 줘야 합니다.)
class Person { private String name; private int age; // 매개변수가 없는 기본 생성자 Person() { this.name = "Unknown"; this.age = 0; } // 이름과 나이를 매개변수로 받는 생성자 Person(String name, int age) { this.name = name; this.age = age; } // Getter 및 Setter 생략 } // 매개변수 없는 기본 생성자 참조 Supplier<Person> personSupplier = Person::new; // 매개변수가 있는 기본 생성자 참조 BiFunction<String, Integer, Person> personConstructor = Person::new; // 생성된 객체 가져오기 Person person1 = personSupplier.get(); Person person2 = personConstructor.apply("Re_Go", 30); System.out.println("Person 1:"); System.out.println("Name: " + person1.getName()); System.out.println("Age: " + person1.getAge()); // person2의 속성 출력 System.out.println("\nPerson 2:"); System.out.println("Name: " + person2.getName()); System.out.println("Age: " + person2.getAge());