티스토리 뷰

728x90
반응형

람다 표현식(Lambda Expression) 혹은 줄여서 람다식은 함수형 프로그래밍 기법을 지원하는 자바의 문법요소이다.

 

간단하게 말해서 메서드를 하나의 식으로 표현한 것이라고 할 수도 있는데,

 

메서드를 람다식으로 표현하면 클래스와 객체를 생성하지 않고도 메서드를 사용할 수 있다.

 

따라서 람다 함수는 익명 함수와 동의어이며, 더 정확하게는 익명 객체라 볼 수 있다.

 

또한 이 익명 객체를 사용하기 위해 뒤에 살펴볼 함수형 인터페이스(Functional Interface)가 만들어지게 되었다.

 


람다식의 기본 문법

 

람다식의 가장 큰 장점은 코드의 직관성과 간결성이다.

//기존 메서드 표현 방식
void sayhello() {
	System.out.println("HELLO!")
}

//위의 코드를 람다식으로 표현한 식
() -> System.out.println("HELLO!")

기존 메서드 표현 방식에서 반환 타입과 이름을 생략한 것을 볼 수 있다.

 

이것이 람다 함수를 익명 함수라 부르는 이유이다.

 

먼저 몇 가지 예를 보자.

 

//기존 방식
int sum(int num1, int num2) {
	return num1 + num2;
}

//람다식
(int num1, int num2) -> {num1 + num2}
// 기존 방식
void example1() {
	System.out.println(5);
}

// 람다식
() -> {System.out.println(5);}
void example3(String str) {
	System.out.println(str);
}

// 람다식
(String str) -> {System.out.println(str);}

먼저 반환 타입과 메서드 이름을 제거하고 코드 블록 사이에 화살표를 넣는다.

 

추가로 반환 값이 있는 경우 return문과 세미콜론(;)을 생략할 수 있다.

 

이어서 특정한 조건을 만족하는 경우엔 람다식을 더욱 축약할 수 있는데,

 

위에 쓰인 sum 메서드를 보자.

 

먼저, 메서드 바디에 실행문이 하나만 존재할 때 중괄호를 생략할 수 있다.

(int num1, int num2) -> num1 + num2

다음으로, 매개변수의 타입을 쉽게 유추할 수 있는 경우, 타입을 생략할 수 있다.

(num1, num2) -> num1 + num2

처음과 끝을 이어서 보면, 람다식이 얼마나 간결하고 직관적인지 알 수 있다.

//기존 방식
int sum(int num1, int num2) {
	return num1 + num2;
}

//람다식
(num1, num2) -> num1 + num2

추가로, 람다식을 이용한 스레드 생성을 보자.

new Thread(new Runnable() {
    public void run() {
        System.out.println("전통적인 방식의 일회용 스레드 생성");
    }
}).start(); 

new Thread(()->{
    System.out.println("람다 표현식을 사용한 일회용 스레드 생성");
}).start();

함수형 인터페이스(Functional Interface)

 

위에 언급한 대로, 람다식은 정확하게 말하면 익명 객체이다.

 

자바에서는 메서드(함수)는 반드시 클래스 안에 선언되어야 하며, 독립적으로 존재할 수 없기 때문이다.

 

따라서 아래의 두 표현식은 같다.

// sum 메서드 람다식
(num1, num2) -> num1 + num2

// 람다식을 객체로 표현
new Object() {
    int sum(int num1, int num2) {
        return num1 + num1;
    }
}

아래의 객체는 선언과 생성을 동시에 하며 일회용으로 쓰이는 익명 객체이다.

 

람다식이 객체라 한다면, 람다식을 다루기 위한 참조 변수가 필요해진다.

 

그렇다고 아래와 같이 사용하려고 하면 에러가 난다.

Object obj = new Object() {
    int sum(int num1, int num2) {
        return num1 + num1;
    }
}

obj.sum(2,4); // 에러. Object 클래스에 sum()이 없음

이 한계를 극복하기 위해 등장한 것이 바로 함수형 인터페이스(Functional Interface)이다.

 

기존의 인터페이스 문법을 사용하여 람다식을 다루는 것이라고 할 수도 있다.

 

이것이 가능한 이유는 위에 적었듯이 람다식 자체가 하나의 객체이기 때문에 

 

인터페이스에 정의된 추상 메서드를 구현할 수 있기 때문이다.

 

함수형 인터페이스가 단 하나의 추상 메서드만 가질 수 있는 이유도 여기에서 나오는데,

 

람다식과 인터페이스가 1 : 1로 매칭이 되어야 하기 때문이다.

 

위의 예제에 함수형 인터페이스를 적용하면 아래와 같이 된다.

public class LamdaExample1 {
    public static void main(String[] args) {
        ExampleFunction exampleFunction = (num1, num2) -> num1 + num2;
        System.out.println(exampleFunction.sum(10,15));
    }
}

@FunctionalInterface // 함수형 인터페이스를 올바로 작성했는지 컴파일러가 체크
interface ExampleFunction {
    public abstract int sum(int num1, int num2);
}

// 출력 결과
25

함수형 인터페이스의 추상 메서드를 람다식과 엮어서 구현하고 있는 것을 볼 수 있다.

 

이어서 매개변수와 리턴 값의 유무에 따른 예를 보자.

 

 

매개변수와 리턴 값이 없는 람다식

@FunctionalInterface
public interface MyFunctionalInterface {
    public void accept();
}

public class MyFunctionalInterfaceExample {
    public static void main(String[] args) throws Exception {
        MyFunctionalInterface example;
        example = () -> {
            String str = "첫 번째 메서드 호출!";
            System.out.println(str);
        };
        example.accept();

        example = () -> System.out.println("두 번째 메서드 호출!");
        //실행문이 하나라면 중괄호 { }는 생략 가능
        example.accept();
    }
}

// 출력 결과
첫 번째 메서드 호출!
두 번째 메서드 호출!

함수형 인터페이스의 추상 메서드 accept()가 매개변수와 리턴 값을 가지지 않기 때문에

 

example()에 대한 람다식도 위와 같이 적혀야 한다.

 

 

매개변수가 있는 람다식

@FunctionalInterface
public interface MyFunctionalInterface {
    public void accept(int x);
}

public class MyFunctionalInterfaceExample {
    public static void main(String[] args) throws Exception {
        MyFunctionalInterface example;
        example = (x) -> {
            int result = x * 5;
            System.out.println(result);
        };
        example.accept(2);

        example = (x) -> System.out.println(x * 5);
        example.accept(2);
    }
}

// 출력 결과
10
10

매개변수가 하나 존재하는 accept() 메서드를 타깃으로 갖는 람다식 작성에 관한 예이다.

 

이번엔 람다식에도 매개변수가 하나 존재함을 알 수 있다.

 

 

매개변수와 리턴 값이 있는 람다식

@FunctionalInterface
public interface MyFunctionalInterface {
    public int accept(int x, int y);
}

public class MyFunctionalInterfaceExample {
    public static void main(String[] args) throws Exception {
        MyFunctionalInterface example;
        example = (x, y) -> {
            int result = x + y;
            return result;
        };
        int result1 = example.accept(2, 5);
        System.out.println(result1);


        example = (x, y) -> { return x + y; };
        int result2 = example.accept(2, 5);
        System.out.println(result2);


        example = (x, y) ->  x + y;
        // 실행문이 한 줄인 경우, 중괄호 {}와 return문 생략가능
        int result3 = example.accept(2, 5);
        System.out.println(result3);


        example = (x, y) -> sum(x, y);
        // 실행문이 한 줄인 경우, 중괄호 {}와 return문 생략가능
        int result4 = example.accept(2, 5);
        System.out.println(result4);

    }

    public static int sum(int x, int y){
        return x + y;
    }
}

//출력 결과
7
7
7
7

두 개의 매개변수와 리턴 값을 가진 accept()에 대한 람다식과 호출 방법이다.

 


메서드 레퍼런스

 

메서드 레퍼런스, 또는 메서드 참조는 람다 표현식을 더욱 간단하게 표현하기 위해 사용되는 방법이다.

 

예를 들어, 다음 메서드는 두 개의 값 중 큰 수를 리턴하는 람다식이다.

(left, right) -> Math.max(left, right);

이 람다식처럼 람다 표현식이 단 하나의 메서드만을 호출하는 경우, 아래와 같이 간략화할 수 있다.

// 클래스이름::메서드이름

Math :: max; // 메서드 레퍼런스

불필요한 매개변수를 제거하고 '::' 기호를 이용해 표현한 것을 볼 수 있다.

 

메서드 레퍼런스도 함수형 인터페이스의 익명 구현 객체로 생성되므로 람다식과 동일하게 인터페이스의 추상 메서드가

 

어떤 매개변수를 가지는지, 리턴 값이 존재하는지에 따라 사용법이 달라진다.

 

예를 들어 아래의 IntBinaryOperator 인터페이스는 두 개의 int 값을 받아 int 값을 리턴한다.

IntBinaryOperator operator = Math :: max; //메서드 레퍼런스

메서드 레퍼런스는 사용하는 패턴에 따라 다음의 세 가지로 분류할 수 있는데,

 

  • 정적(static) 메서드 레퍼런스
  • 인스턴스 메서드 레퍼런스
  • 생성자 레퍼런스

아래에서 간단히 예를 보자.

 

 

정적(static) 메서드 레퍼런스와 인스턴스 메서드 레퍼런스

 

정적 메서드를 참조할 경우는 아래와 같고,

클래스명::메서드명

인스턴스 메서드를 참조하는 경우는 다음과 같다.

참조변수명::메서드명

정적 메서드와 달리 인스턴스 메서드의 경우는 당연하게도 객체 생성이 선행되어야 한다.

 

이어지는 예제는 Calculator 클래스의 정적, 인스턴스 메서드를 참조한다.

import java.util.function.IntBinaryOperator;

class Calculator {
    public static int staticMethod(int x, int y) {
        return x + y;
    }

    public int instanceMethod(int x, int y) {
        return x * y;
    }
}

public class MethodReferences {
    public static void main(String[] args) throws Exception {
        IntBinaryOperator operator;

    /*정적 메서드
	클래스명::메서드명
	*/
        operator = Calculator::staticMethod;
        System.out.println("정적메서드 결과 : " + operator.applyAsInt(3, 5));

    /*인스턴스 메서드
	인스턴스명::메서드명
	*/
        Calculator calculator = new Calculator();
        operator = calculator::instanceMethod;
        System.out.println("인스턴스 메서드 결과 : "+ operator.applyAsInt(3, 5));
    }
}

//출력 결과
정적메서드 결과 : 8
인스턴스 메서드 결과 : 15

위에 설명한 바와 같이 정적 메서드는 클래스명::메서드명으로, 인스턴스 메서드는 인스턴스명::메서드명으로 참조한 것을 볼 수 있다.

 

 

생성자 레퍼런스

 

생성자 레퍼런스는 말 그대로 생성자를 참조하는 코드이다.

 

람다 표현식이 단 하나의 메서드만을 호출하는 경우와 비슷하게,

 

단순히 객체를 생성하고 반환하는 람다 표현식은 생성자 레퍼런스로 변환할 수 있다.

(a,b) -> {return new Company(a,b);};

예를 들면 Company 클래스의 인스턴스를 생성하는 위와 같은 람다식은

Company::new

와 같이 변환하여 사용할 수 있다.

 

생성자가 오버로딩 되어있는 경우, 컴파일러가 함수형 인터페이스의 추상 메서드와

 

동일한 매개변수 타입과 개수를 가지고 있는 생성자를 찾아 실행하게 된다.

 

다음 예를 보자.

//Member.java
public class Member {
  private String name;
  private String id;

  public Member() {
    System.out.println("Member() 실행");
  }

  public Member(String id) {
    System.out.println("Member(String id) 실행");
    this.id = id;
  }

  public Member(String name, String id) {
    System.out.println("Member(String name, String id) 실행");
    this.id = id;
    this.name = name;
  }

  public String getName() {
    return name;
  }

public String getId() {
    return id;
  }
}
import java.util.function.BiFunction;
import java.util.function.Function;

public class ConstructorRef {
  public static void main(String[] args) throws Exception {
    Function<String, Member> function1 = Member::new;
    Member member1 = function1.apply("kimcoding");

    BiFunction<String, String, Member> function2 = Member::new;
    Member member2 = function2.apply("kimcoding", "김코딩");
  }
}

//출력 결과
Member(String id) 실행
Member(String name, String id) 실행

위에 사용된 함수형 인터페이스 Function <String, Member>는 String 타입을 받아 Member 타입을 반환하는 역할을 한다.

 

apply는 Function 함수형 인터페이스의 추상 메서드 이름이다.

 

비슷하게 BiFunction <String, String, Member>의 경우는 두 개의 String 타입을 받아 Member 타입을 반환한다.

 

여기서도 동일하게 apply가 BiFunction 함수형 인터페이스의 추상 메서드 이름이다.

 

매개변수의 타입과 개수에 따라 다른 생성자가 호출되었음을 확인할 수 있다.

 

아래는 자주 사용하는 함수형 인터페이스의 예이다.

 

함수형 인터페이스 파라미터 타입 반환 타입 추상 메서드 이름 설명 다른 메서드
Runnable 없음 void run 인자나 반환 값 없이 액션을 수행한다. 없음
Supplier 없음 T get T 타입 값을 공급한다. 없음
Consumer T void accept T 타입 값을 소비한다. andThen
BiConsumer<T, U> T, U void accept T와 U타입 값을 소비한다. andThen
Function<T, R> T R apply T 타입 인자를 받는 함수다. compose
andThen
identity
BiFunction<T, U, R> T, U R apply T와 U타입 인자를 받는 함수다. andThen
UnaryOperator T T apply T 타입에 적용하는 단항 연산자다. compose
andThen
identity
BinaryOperator T, T T apply T 타입에 적용하는 이항 연산자다. andThen
maxBy
minBy
Predicate T boolean test Boolean 값을 반환하는 함수다. and
or
negate
isEqual
BiPredicate<T, U> T, U boolean test 두 가지 인자를 받고 boolean 값을 반환하는 함수다. and
or
negate

출처: https://imcts.github.io/java-method-reference/

 

JAVA8 메소드 레퍼런스(Method Reference)

JAVA8 Method Reference

imcts.github.io

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
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
글 보관함