[JAVA] 21. 람다식의 정의와 활용 방법

Re_Go·2024년 6월 8일
0

JAVA

목록 보기
30/37
post-thumbnail

1. 람다식이란?

람다식(lambda expression)이란 함수(함수형 인터페이스의 추상 메서드가 대표적) 를 간결하고 쉽게 표현
할 수 있도록 자바 8부터 구현된 함수 선언 방식인데요.

자바스크립트의 화살표 함수와도 유사한 역할을 하는 이 람다식은 즉 함수형 인터페이스에서 추상 메서드를 오버라이딩 할 때 간결하게 사용
되는데요. 주로 Comparator, Function과 같은 function 페키지의 인터페이스 뿐만 아니라 앞서 살펴본 멀티 스레드에 사용되는 함수형 인터페이스인 RunnableCallable에서도 사용됩니다.

이럴 경우 기존의 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...";
};

또한 익명 구현 객체 (이름이 없는 인터페이스 구현 객체)를 람다식으로 간단하게 변환할 수 있는데요. 사용법은 다음과 같습니다.

  1. 추상 메서드 생성
public interface Calc{
	void calculator(int x, int y)
}
  1. 익명 구현 객체 오버라이딩
new Calc(){
	@Override 
    public void calculator(int x, int y){
		System.out.println(x+y);
	}
}
  1. 해당 메서드를 호출하는 메서드 생성
public static void act(Calc calcMethod){
	int x = 10;
    int y = 20;
    Calc calcMethod(x,y)
}
  1. 함수형 인터페이스를 구현하는 대신 람다식으로 재정의
//변환 전 기본적인 오버라이딩 메서드
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);
  1. 메서드 호출 후 람다식을 매개변수로 전달
act(Calc calculator)
  1. 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 어노테이션을 붙여준다면, 컴파일러는 람다식으로 전달 된 매개변수를 보고 해당 매개변수의 인터페이스가 추상 메서드가 구현된 함수형 인터페이스 라는 것을 더 쉽게 유추할 수 있기 때문에, 가급적 위의 어노테이션을 붙여주는게 좋습니다.

또한 메서드가 두 개 이상 존재하는 함수형 인터페이스는 람다식으로도 표현될 수 없습니다.
(애초에 추상 메서드의 요건 자체를 성립하지 못함.)

2. 매개변수가 존재하지 않는 람다식

여기 클릭 이벤트를 처리하는 인터페이스가 있다고 해보겠습니다.

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 버튼을 클릭했습니다."));

위의 예제는 객체를 생성하고 생성된 객체의 메서드에 바로 람다식을 넘겨주는 예제인데요. 익명 구현 객체가 람다식으로 대체될 때 객체 생성을 제외하고 코드 구현만 넘겨줄 수 있는 것이죠.

3. 매개변수가 존재하는 람다식

다음으로 매개변수가 존재하는 람다식을 살펴보겠습니다. 코드는 위의 코드 예제를 그대로 두고, 인터페이스만 바꿔보겠습니다.

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));

이처럼 람다식은 전달 받는 매개변수 또한 간단하게 적을 수 있습니다.

4. 반환값이 있는 람다식

추상 메서드에서 결과값을 반환하도록 하는 코드가 있다고 가정해 보겠습니다.

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;
});

5. 메서드 참조

이러한 편리한 람다식을 더 축약할 수 있는 버전이 있는데요. 바로 메서드 참조를 사용하는 방식입니다.

다음과 같은 람다식이 있다고 합시다.

() -> System.out.println("Hello World!")

해당 람다식에 메서드 참조를 사용하여 다음과 같이 축약할 수 있는데요.

(System.out::println)

이때 참조 기호 (::)를 중심으로 왼쪽은 해당 클래스, 오른쪽은 해당 클레스의 메서드를 의미하며 메서드 참조에는 정적 메서드 참조, 인스턴스 메서드 참조, 생성자 참조가 대표적이며, 각각의 특징 및 사용법은 다음과 같습니다.

  1. 정적 메서드 참조 (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);
  1. 인스턴스 메서드 참조 (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!");
  1. 생성자 참조 (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());
profile
인생은 본인의 삶을 곱씹어보는 R과 타인의 삶을 배워 나아가는 L의 연속이다.

0개의 댓글