관리 메뉴

개발공부기록

람다, 람다 정의, 함수형 인터페이스, 람다와 시그니처, 람다와 생략, 람다의 전달, 고차 함수 본문

자바 로드맵 강의/고급 3 - 람다, 스트림, 함수형 프로그래밍

람다, 람다 정의, 함수형 인터페이스, 람다와 시그니처, 람다와 생략, 람다의 전달, 고차 함수

소소한나구리 2025. 4. 3. 14:47
728x90

람다 정의

람다

자바 8부터 도입된 람다는 자바에서 함수형 프로그래밍을 지원하기 위한 핵심 기능임

람다는 익명 함수이므로 이름 없이 함수를 표현함

 

메서드나 함수는 아래와 같이 표현하며 이름이 있음

반환타입 메서드명(매개변수) {
    본문
}

public int add(int x) {
    return x + 1;
}

 

람다는 아래처럼 이름이 없어서 간결하게 표현함

(매개변수) -> {본문}

(int x) -> {return x + 1;}

 

자바는 독립적인 함수를 지원하지 않으며 메서드는 반드시 클래스나 인터페이스에 속함

 

** 용어 - 람다 vs 람다식(Lambda Expression)

  • 람다: 익명 함수를 지칭하는 일반적인 용어, 개념이라고 보면 됨
  • 람다식: (매개변수) -> {본문} 형태로 람다를 구현하는 구체적인 문법 표준을 지칭함
  • 람다는 개념, 람다식은 그 개념을 구현하는 구체적인 문법을 의미하여 람다가 더 넒은 의미이지만 실무에서 두 용어를 구분해서 부르지는 않음

람다와 익명클래스의 비교

package lambda.lambda1;

public class InstanceMain {
    public static void main(String[] args) {

        Procedure procedure1 = new Procedure() {
            @Override
            public void run() {
                System.out.println("hello! lambda");
            }
        };
        System.out.println("procedure.getClass() = " + procedure1.getClass());
        System.out.println("class.instance = " + procedure1);


        Procedure procedure2 = () -> {
            System.out.println("hello! lambda");
        };
        System.out.println("procedure2.getClass() = " + procedure2.getClass());
        System.out.println("lambda.instance = " + procedure2);
    }
}
/* 실행 결과
procedure.getClass() = class lambda.lambda1.InstanceMain$1
class.instance = lambda.lambda1.InstanceMain$1@6f496d9f
procedure2.getClass() = class lambda.lambda1.InstanceMain$$Lambda/0x0000008801003600
lambda.instance = lambda.lambda1.InstanceMain$$Lambda/0x0000008801003600@10f87f48
*/

 

람다는 표현이 간결함

  • 익명 클래스를 사용하면 new 키워드, 생성할 클래스명, 메서드명, 반환 타입 등을 모두 나열해야함
  • 반면 람다는 이런 부분을 모두 생략하고 매개변수와 본문만 적으면 됨

람다는 변수처럼 다룰 수 있음

  • 람다를 procedure2라는 변수에 담아 해당 변수를 통해 람다를 실행할 수 있음

람다도 클래스가 만들어지고 인스턴스가 생성됨

  • 실행 결과를 보면 익명 클래스의 경우 $로 구분하고 뒤에 숫자가 붙음
  • 람다의 경우에는 $$로 구분하고 뒤에 복잡한 문자가 붙는데 실행 환경에 따라 결과는 다를 수 있음

정리

  • 람다를 사용하면 사용의 보일러플레이트 코드(불편하게 반복하는 코드)를 크게 줄이고 간결한 코드로 생산성과 가독성을 높일 수 있음
  • 대부분의 익명 클래스는 람다로 대체할 수 있으나 완전히 대체할 수 있는 것은 아님
  • 람다를 사용할 때 new 키워드를 사용하지 않지만 람다도 익명 클래스처럼 인스턴스가 생성됨
  • 지금 단계에서의 람다는 익명 클래스의 구현을 간단히 표현할 수 있는 문법 설탕(Syntactic sugar, 코드를 간결하게 만드는 문법적 편의) 역할 정도로 생각하면 되며 람다와 익명 클래스의 차이는 뒤에서 따로 설명함

함수형 인터페이스

함수형 인터페이스

정확히 하나의 추상 메서드를 가지는 인터페이스를 말하며 람다는 추상 메서드가 하나인 함수형 인터페이스에만 할당할 수 있음

단일 추상 메서드를 줄여서 SAM(Single Abstract Method)이라 함

참고로 람다는 클래스, 추상 클래스에는 할당할 수 없으며 오직 단일 추상 메서드를 가지는 인터페이스에만 할당할 수 있음

 

여러 추상 메서드

package lambda.lambda1;

public interface NotSamInterface {
    void run();
    void go();
}

 

인터페이스의 메서드 앞에는 abstract(추상)이 생략되어있음 - 자바 기본

해당 인터페이스는 run(), go() 두 개의 추상 메서드가 선언되어있으므로 단일 추상 메서드(SAM)를 가지고 있지않기 때문에 람다를 할당할 수 없음

 

단일 추상 메서드

package lambda.lambda1;

public interface SamInterface {
    void run();
}

 

해당 인터페이스는 run() 한 개의 추상 메서드만 선언되어있는 단일 추상 메서드(SAM)를 가지고 있는 인터페이스이므로 람다를 할당할 수 있음

 

SamMain

package lambda.lambda1;

public class SamMain {
    public static void main(String[] args) {

        // 함수형 인터페이스에는 람다를 할당 가능
        SamInterface samInterface = () -> {
            System.out.println("sam");
        };
        samInterface.run();

        // SAM이 아닌 인터페이스에는 람다 할당 불가, 컴파일 오류
        NotSamInterface notSamInterface = () -> {
            System.out.println("not sam");

        };
        // 두 개의 메서드 중 어떤걸 실행해야 할지 모름
        notSamInterface.run();
        notSamInterface.go();
    }
}
/* 실행 결과 - 컴파일 오류
java: incompatible types: lambda.lambda1.NotSamInterface is not a functional interface
    multiple non-overriding abstract methods found in interface lambda.lambda1.NotSamInterface
*/

 

함수형 인터페이스인 SamInterface는 정상적으로 코드가 수행되지만 함수형 인터페이스가 아닌 NotSamInterface은 컴파일 에러가 발생하며 강제로 실행해보면 위와 같은 오류 메시지가 출력되는 것을 확인할 수 있음

 

람다는 하나의 함수이기 때문에 람다를 인터페이스에 담으려면 하나의 메서드(함수) 선언만 존재해야하기 함

하지만 인터페이스는 여러 메서드를 선언할 수 있으므로 여러 메서드가 선언된 인터페이스에 람다를 적용하면 둘 중 하나에 할당해야하는 문제가 발생함

 

자바는 이런 문제를 해결하기 위해 단 하나의 추상 메서드(SAM)만을 포함하는 함수형 인터페이스에만 람다를 할당할 수 있도록 제한하였음

SamInterface은 run()이라는 단 하나의 추상 메서드만을 포함하기 때문에 문제 없이 람다를 할당하고 실행할 수 있음

 

 

@FunctionalInterface

package lambda.lambda1;

@FunctionalInterface
public interface SamInterface {
    void run();
//    void gogo(); // 컴파일 오류 발생
}

 

@FunctionalInterface 애노테이션은 인터페이스가 단 하나의 추상 메서드가 아니면 컴파일 단계에서 오류를 발생시킴

해당 애노테이션이 붙어있는 인터페이스에 실수로 누군가 추상 메서드를 추가해버리면 그 즉시 오류가 발생되므로 해당 인터페이스를 함수형 인터페이스가 되도록 보장할 수 있음

 

람다를 사용할 함수형 인터페이스라면 @FunctionalInterface를 필수로 추가하는 것이 권장됨


람다와 시그니처

람다를 함수형 인터페이스에 할당할 때는 메서드의 형태를 정의하는 요소인 메서드 시그니처가 일치해야 함

메서드 시그니처의 요소는 아래와 같음

  • 메서드 이름
  • 매개변수의 수와 타입(순서 포함)
  • 반환 타입

예시1

package lambda;

@FunctionalInterface
public interface MyFunction {
    int apply(int a, int b);
}

 

예를 들어 위와 같은 함수형 인터페이스의 메서드가 있다면 해당 메서드의 시그니처는 아래와 같음

  • 메서드 이름: apply
  • 매개변수: int, int
  • 반환 타입 int
MyFunction myfunction = () -> {
    return a + b;
}

 

람다는 익명함수이므로 시그니처에서 이름은 제외하고 매개변수, 반환 타입이 함수형 인터페이스에 선언한 메서드와 맞아야 함

예시의 람다는 매개변수로 int a, int b 그리고 반환 값으로 a + b인 int 타입을 반환하므로 함수형 인터페이스의 메서드와 시그니처가 딱 맞기 때문에 람다를 함수형 인터페이스에 할당할 수 있음

 

참고로 람다의 매개변수 이름은 함수형 인터페이스에 있는 메서드 매개변수의 이름과 상관없이 자유롭게 작성해도 상관없음

 

예시2

package lambda;

@FunctionalInterface
public interface Procedure {
    void run();
}

 

해당 함수형 인터페이스가 가진 메서드의 시그니처는 아래와 같음

  • 메서드 이름: run
  • 매개변수: 없음
  • 반환타입 없음
Procedure procedure = () -> {
    System.out.println("안녕 람다");
}

 

이 람다는 매개변수가 없고 반환 타입이 없으므로 함수형 인터페이스의 메서드와 시그니처가 맞기 때문에 람다를 함수형 인터페이스에 할당할 수 있음


람다와 생략

람다는 간결한 코드 작성을 위해 다양한 문법 생략을 지원함

단일 표현식

LmbdaSimple1 - 매개변수와 반환값이 있는 경우

package lambda.lambda1;

public class LambdaSimple1 {
    public static void main(String[] args) {
        // 기본
        MyFunction function1 = (int a, int b) -> {
            return a + b;
        };
        System.out.println("function1.apply(1, 2) = " + function1.apply(1, 2));
        
        // 단일 표현식인 경우 중괄호와 리턴 생략 가능
        MyFunction function2 = (int a, int b) -> a + b;
        System.out.println("function2.apply(1, 2) = " + function2.apply(1, 2));
        
        // 단일 표현식이 아닐 경우 중괄호와 리턴 모두 필수
        MyFunction function3 = (int a, int b) -> {
            System.out.println("단일 표현식이 아닌 람다 실행");
            return a + b;
        };
        System.out.println("function3.apply(1, 3) = " + function3.apply(1, 3));
    }
}
/* 실행 결과
function1.apply(1, 2) = 3
function2.apply(1, 2) = 3
람다 실행
function3.apply(1, 3) = 4
*/

 

표현식(expression): 하나의 값으로 평가되는 코드 조각을 의미하며 산술 논리 표현식, 메서드 호출, 객체 생성등이 있음

  • 예) x + y, price * quantity, calculateTotal(), age >= 18 

표현식이 아닌 것에는 제어문, 메서드 선언 같은 것이 있음

  • 예) if (condition) {   }

람다 - 단일 표현식(single expression)인 경우

  • 중괄호 { }와 return 키워드를 함께 생략할 수 있으며 표현식의 결과가 자동으로 반환값이 됨
  • 중괄호는 사용하는 경우에는 반드시 return 문을 포함해야 하며, 역으로 말하면 return 문을 명시적으로 포함하고자 할 경우 중괄호를 사용해야 함
  • 반환 타입이 void인 경우에는 return 생략 가능함

단일 표현식이 아닌 경우: 중괄호를 생략할 수 없으므로 return 문도 꼭 포함해야 함

 

LambdaSimple2 - 매개변수와 반환 값이 없는 경우

package lambda.lambda1;

public class LambdaSimple2 {
    public static void main(String[] args) {
        // 매개변수, 반환 값이 없는 경우
        Procedure procedure1 = () -> {
            System.out.println("안녕 람다!");
        };
        procedure1.run();

        // 단일 표현식은 중괄호 생략 가능
        Procedure procedure2 = () -> System.out.println("안녕 람다!");
        procedure2.run();
    }
}
/* 실행 결과
안녕 람다!
안녕 람다!
*/

 

매개변수와 반환 값이 없는 경우에도 동일함

Procedure.run()의 경우 반환 타입이 void이기 때문에 중괄호를 사용해도 return은 생략할 수 있음

타입 추론

LambdaSimple3

package lambda.lambda1;

public class LambdaSimple3 {
    public static void main(String[] args) {
        // 타입 생략 전
        MyFunction function1 = (int a, int b) -> a + b;
        System.out.println("function1.apply(1, 2) = " + function1.apply(1, 2));
        
        // MyFunction 타입을 통해 타입 추론 가능, 람다는 타입 생략 가능
        MyFunction function2 = (a, b) -> a + b;
        System.out.println("function2.apply(2, 3) = " + function2.apply(2, 3));
        
    }
}
/* 실행 결과
function1.apply(1, 2) = 3
function2.apply(2, 3) = 5
*/

 

함수형 인터페이스인 MyFunction의 apply() 메서드에는 이미 int a, int b로 매개변수의 타입이 정의 되어있기 때문에 이 정보를 사용하면 람다의 (int a, int b)에서 타입 정보를 생략할 수 있음

 

자바 컴파일러는 람다가 사용되는 함수형 인터페이스의 메서드 타입을 기반으로 람다의 매개변수와 반환값의 타입을 추론하기 때문에 람다는 타입을 생략할 수 있음

반환 타입은 문법적으로 명시할 수 없는 대신 컴파일러가 자동으로 추론함

매개변수의 괄호 생략

package lambda.lambda1;

public class LambdaSimple4 {
    public static void main(String[] args) {
        
        MyCall myCall1 = (int value) -> value * 2;  // 기본
        MyCall myCall2 = (value) -> value * 2;  // 타입 추론
        MyCall myCall3 =value -> value * 2;     // 매개변수 1개, () 생략 가능
        System.out.println("myCall3.call(5) = " + myCall3.call(5));
    }

    interface MyCall {
        int call(int value);
    }
}
/* 실행 결과
myCall3.call(5) = 10
*/

 

매개변수가 정확하게 하나이면서 타입을 생략하고 이름만 있는 경우 소괄호()를 생략할 수 있음

매개변수가 없거나 둘 이상이라면 ()는 필수적임

 

정리

  • 매개변수 타입: 생략 가능하지만 필요하다면 명시적으로 작성할 수 있음
  • 반환 타입: 문법적으로 명시할 수 없고 식의 결과를 보고 컴파일러가 항상 추론함
  • 람다는 보통 간략하게 사용하는 것이 권장되므로 위에서 살펴본 모든 생략할 수 있는 것을 생략하여 작성해주면됨
  • 보통 IDE에서 람다식을 가장 간략하게 변경해주는 기능이 있음

람다의 전달

람다를 변수에 대입하기

LambdaPassMain1

package lambda.lambda2;

public class LambdaPassMain1 {
    public static void main(String[] args) {
        MyFunction add = (a, b) -> a + b;
        MyFunction sub = (a, b) -> a - b;

        System.out.println("add.apply(1, 2) = " + add.apply(1, 2));
        System.out.println("sub.apply(1, 2) = " + sub.apply(1, 2));
        
        MyFunction cal = add;
        System.out.println("cal(add).apply(1, 2) = " + cal.apply(1, 2));

        cal = sub;
        System.out.println("cal(sub).apply(1, 2) = " + cal.apply(1, 2));
    }
}
/* 실행 결과
add.apply(1, 2) = 3
sub.apply(1, 2) = -1
cal(add).apply(1, 2) = 3
cal(sub).apply(1, 2) = -1
*/

 

변수 add의 타입은 MyFunction 함수형 인터페이스이기 때문에 MyFunction 형식에 맞는 람다를 대입할 수 있음(메서드 시그니처가 일치함)

 

자바에서 기본형과 참조형은 변수에 값(참조형인 경우 인스턴스의 참조값)을 대입할 수 있는데 람다도 마찬가지임

즉, 함수형 인터페이스로 선언한 변수에 람다를 대입하는 것은 람다 인스턴스의 참조값을 대입하는 것임

 

람다도 인터페이스(함수형 인터페이스)를 사용하므로 람다 인스턴스의 참조값을 변수에 전달할 수 있으므로 아래처럼 사용할 수 있음

  • 매개변수를 통해 메서드(함수)에 람다(람다 인스턴스의 참조값)를 전달할 수 있음
  • 메서드가 람다(람다 인스턴스의 참조값)를 반환할 수 있음

람다를 메서드(함수)에 전달하기

LambdaPassMain2

package lambda.lambda2;

public class LambdaPassMain2 {
    public static void main(String[] args) {
        MyFunction add = (a, b) -> a + b;
        MyFunction sub = (a, b) -> a - b;

        System.out.println("변수를 통해 전달");
        calculate(add);
        calculate(sub);

        System.out.println("람다를 직접 전달");
        calculate((a, b) -> a + b);
        calculate((a, b) -> a - b);
    }

    static void calculate(MyFunction function) {
        int a = 1;
        int b = 2;

        System.out.println("계산 시작");
        int result = function.apply(a, b);
        System.out.println("계산 결과: " + result);
    }
}
/* 실행 결과
변수를 통해 전달
계산 시작
계산 결과: 3
계산 시작
계산 결과: -1
람다를 직접 전달
계산 시작
계산 결과: 3
계산 시작
계산 결과: -1
*/

 

람다가 변수에 전달할 수 있듯이 람다를 매개변수를 통해 메서드(함수)에 전달할 수 있음

calculate() 메서드의 매개변수는 MyFunction 함수형 인터페이스이므로 람다를 전달할 수 있는데, 람다를 변수에 담은 후에 매개변수에 전달할 수도 있고 람다를 직접 전달할 수도 있음

 

일반적인 참조를 매개변수에 전달하는 것과 같은 원리임

람다를 반환하기

LambdaPassMain3

package lambda.lambda2;

public class LambdaPassMain3 {
    public static void main(String[] args) {
        MyFunction add = getOperation("add");
        System.out.println("add.apply(1, 2) = " + add.apply(1, 2));

        MyFunction sub = getOperation("sub");
        System.out.println("sub.apply(3, 4) = " + sub.apply(3, 4));

        MyFunction nonono = getOperation("nonono");
        System.out.println("nonono.apply(1, 2) = " + nonono.apply(1, 2));
    }

    // 람다를 반환하는 메서드
    static MyFunction getOperation(String operator) {
        switch (operator) {
            case "add" :
                return (a, b) -> a + b;
            case "sub" :
                return (a, b) -> a - b;
            default:
                return (a, b) -> 0;
        }
    }
}
/* 실행 결과
add.apply(1, 2) = 3
sub.apply(3, 4) = -1
nonono.apply(1, 2) = 0
*/

 

getOperation() 메서드는 반환 타입이 MyFunction 함수형 인터페이스이므로 람다를 반환할 수 있음

 

분석해보면 getOperation("add") 으로 메서드를 호출하면 해당 메서드 내부에서 (a, b) -> a + b; 람다 인스턴스를 생성하여 해당 참조값을 반환하고 MyFunction add 변수에 람다 인스턴스의 참조값이 대입됨


고차 함수

람다의 전달 정리

람다는 함수형 인터페이스를 구현한 익명 클래스 인스턴스와 같은 개념으로 이해하면 됨

즉, 람다를 변수에 대입한다는 것은 람다 인스턴스의 참조값을 대입하는 것이고 람다를 메서드(함수)의 매개변수나 반환값으로 넘긴다는 것 역시 람다 인스턴스의 참조값을 전달, 반환하는 것임

  • 람다를 변수에 대입: MyFunction add = (a, b) -> a + b; 처럼 함수형 인터페이스 타입의 변수에 람다 인스턴스의 참조를 대입
  • 람다를 메서드 매개변수에 전달: 메서드 호출 시 람다 인스턴스의 참조를 직접 넘기거나, 이미 람다 인스턴스를 담고 있는 변수를 전달
  • 람다를 메서드에서 반환: return (a, b) -> a + b; 처럼 함수형 인터페이스 타입을 반환값으로 지정하여 람다 인스턴스의 참조를 반환

이와 같은 방식으로 람다를 자유롭게 전달하거나 반환할 수 있기 때문에, 코드의 간결성과 유연성이 높아지는 것이며 만약 이를 익명 클래스로 작성했다면 코드가 매우 번잡했을 것임

고차 함수(Higher-Order Function)

고차 함수는 함수를 값처럼 다루는 함수를 뜻하며 일반적으로 다음 두가지 중 하나를 만족하면 고차 함수라 함

  • 함수를 인자로 받는 함수(메서드)
  • 함수를 반환하는 함수(메서드)
// 함수(람다)를 매개변수로 받음
static void calculate(MyFunction function) {
    int a = 1;
    int b = 2;

    System.out.println("계산 시작");
    int result = function.apply(a, b);
    System.out.println("계산 결과: " + result);
}

// 함수(람다)를 반환
static MyFunction getOperation(String operator) {
    switch (operator) {
        case "add" :
            return (a, b) -> a + b;
        case "sub" :
            return (a, b) -> a - b;
        default:
            return (a, b) -> 0;
    }
}

 

앞서 다뤘던 두 메서드 처럼 매개변수나 반환값에 함수(또는 람다)를 활용하는 함수가 고차 함수에 해당함

자바에서 람다(익명 함수)는 함수형 인터페이스를 통해서만 전달할 수 있는데 자바에서 함수를 주고 받는다는 것은 함수형 인터페이스를 구현한 어떤 객체(람다, 익명 클래스)를 주고 받는다는 뜻임

함수형 인터페이스는 인터페이스이므로 익명 클래스, 람다 둘다 대입할 수 있지만 실질적으로 람다를 주로 사용함

 

용어 - 고차 함수

  • 고차 함수(Higher-Order Function)라는 이름은 함수를 다루는 추상화 수준이 더 높다는 데에서 유래했음
  • 보통의 함수는 데이터(값)을 입력으로 받고 값을 반환하는 반면 고차 함수는 함수를 인자로 받거나 함수를 반환
  • 즉, 일반 함수는 값을 다루고 고차 함수는 함수 자체를 다룸
  • 즉, 값을 다루는 것을 넘어 "함수"라는 개념 자체를 값처럼 다룬다는 점에서 추상화의 수준(계층, order)이 한 단계 높아진다고 하여 Higher-Order(더 높은 차원의) 함수라고 부름

문제와 풀이

문제 1. 중복되는 메시지 출력 로직 리팩토링

문제 설명

  • 다음 코드는 화면에 여러 종류의 인삿말 메시지를 출력하지만 모든 메서드마다 "=== 시작 ===" 과 "=== 끝 ==="을 출력하는 로직이 중복되어 있음
  • 이 중복되는 코드를 제거하고 변하는 부분(인삿말 메시지)만 매개변수로 받도록 리팩토링
더보기
package lambda.ex1;

public class M1Before {
    public static void greetMorning() {
        System.out.println("=== 시작 ===");
        System.out.println("Good Morning!");
        System.out.println("=== 끝 ===");
    }

    public static void greetAfternoon() {
        System.out.println("=== 시작 ===");
        System.out.println("Good Afternoon!");
        System.out.println("=== 끝 ===");
    }

    public static void greetEvening() {
        System.out.println("=== 시작 ===");
        System.out.println("Good Evening!");
        System.out.println("=== 끝 ===");
    }

    public static void main(String[] args) {
        greetMorning();
        greetAfternoon();
        greetEvening();
    }
}

 

정답

더보기

하나의 메서드로 합치고 매개변수(문자열)만 다르게 받아 처리하면 됨

package lambda.ex1;

public class M1After {
    public static void hello(String hello) {
        System.out.println("=== 시작 ===");
        System.out.println(hello);
        System.out.println("=== 끝 ===");
    }

    public static void main(String[] args) {
        hello("Good Morning!");
        hello("Good Afternoon!");
        hello("Good Evening!");
    }
}

 

실행 결과는 동일

문제 2. 값 매개변수화 - 다양한 단위를 매개변수로 받기

문제 설명

  • 주어진 숫자(예: 10)를 특정 단위(예: "kg")로 출력하는 간단한 메서드를 작성한 예시코드를 숫자와 단위를 나누고 재사용 가능한 메서드를 사용하도록 코드를 수정
더보기
package lambda.ex1;

public class M2Before {
    public static void print1() {
        System.out.println("무게: 10kg");
    }

    public static void print2() {
        System.out.println("무게: 50kg");
    }

    public static void print3() {
        System.out.println("무게: 200g");
    }

    public static void print4() {
        System.out.println("무게: 40g");
    }

    public static void main(String[] args) {
        print1();
        print2();
        print3();
        print4();
    }
}

 

정답

더보기
package lambda.ex1;

public class M2After {

    public static void print(int num, String unit) {
        System.out.println("무게: " + num + unit);
    }

    public static void main(String[] args) {
        print(10, "kg");
        print(50, "kg");
        print(200, "g");
        print(40, "g");
    }
}

 

실행 결과 동일

문제 3. 동작 매개변수화 - 익명 클래스로 다른 로직 전달

문제 설명

  • 1부터 N까지 더하는 로직과 배열을 정렬하는 Arrays.sort() 로직을 각각 실행하고 이 두 가지 로직 모두 실행에 걸린 시간을 측정하고자 할때 람다를 사용하지 않고 익명 클래스를 사용하여 문제를 해결
    • "실행 시간 측정" 로직은 변하지 않는 부분, "실행할 로직"은 바뀌는 부분
    • 앞서 정의한 Procedure 함수형 인터페이스를 사용
    • measure(Procedure p) 메서드 안에서 동작할 로직은 아래와 같음
      • 실행 전 시간 기록
      • p.run() 실행
      • 실행 후 시간 기록
      • 걸린 시간 출력
  • main() 에서 익명 클래스 두 가지를 만들어 각각 실행 시간을 측정
    • 1부터 N까지 합을 구하는 로직(measure 메서드 호출)
    • 배열을 정렬하는 로직(measure 메서드 호출)
    • 즉, measure 메서드가 총 2번 호출됨
더보기

예시 출력

[1부터 100까지 ] 결과: 5050

실행 시간: 4592542ns

 

원본 배열: [4, 3, 2, 1]

배열 정렬: [1, 2, 3, 4]

실행 시간: 301083ns

 

1. 1부터 N까지 합을 구하는 로직 (measure 메서드 호출)

int N = 100;
long sum = 0;
for (int i = 1; i <= N; i++) {
    sum += i;
}

 

2. 배열을 정렬하는 로직 (measure 메서드 호출)

int[] arr = { 4, 3, 2, 1 };
System.out.println("원본 배열: " + Arrays.toString(arr));
Arrays.sort(arr);
System.out.println("배열 정렬: " + Arrays.toString(arr));

정답

더보기
package lambda.ex1;

public class M3MeasureTime {
    public static void main(String[] args) {

        Procedure sum = new Procedure() {

            @Override
            public void run() {
                int N = 100;
                long sum = 0;
                for (int i = 1; i <= N; i++) {
                    sum += i;
                }
                System.out.println("[1부터 " + N + "까지 합] 결과: " + sum);
            }
        };

        Procedure sort = new Procedure() {

            @Override
            public void run() {
                int[] arr = {4, 3, 2, 1};
                System.out.println("원본 배열: " + Arrays.toString(arr));
                Arrays.sort(arr);
                System.out.println("배열 정렬: " + Arrays.toString(arr));
            }
        };

        measure(sum);
        System.out.println();
        measure(sort);

    }

    static void measure(Procedure p) {
        long startTime = System.nanoTime();

        p.run();

        long endTime = System.nanoTime();
        System.out.println("실행 시간: " + (endTime - startTime) + "ns");
    }
}

 

실행 결과

[1부터 100까지 합] 결과: 5050
실행 시간: 5956250ns

원본 배열: [4, 3, 2, 1]
배열 정렬: [1, 2, 3, 4]
실행 시간: 374084ns

문제 4. 람다로 변경 - 간결하게 코드 작성하기

문제 설명

  • 이번 문제에서 익명 클래스로 작성한 부분을 람다로 변경
  • measure() 메서드와 Procedure 인터페이스는 그대로 두고 main()에서 익명클래스를 람다로 변경하여 코드를 간결하게 만들기

정답

더보기

 

package lambda.ex1;

public class M4MeasureTime {
    public static void main(String[] args) {
		
        // 람다로 1부터 N까지 합 구하기
        measure(() -> {
            int N = 100;
            long sum1 = 0;
            for (int i = 1; i <= N; i++) {
                sum1 += i;
            }
            System.out.println("[1부터 " + N + "까지 합] 결과: " + sum1);
        });
        
        System.out.println();
        
        // 람다로 배열 정렬
        measure(() -> {
            int[] arr = {4, 3, 2, 1};
            System.out.println("원본 배열: " + Arrays.toString(arr));
            Arrays.sort(arr);
            System.out.println("배열 정렬: " + Arrays.toString(arr));
        });

    }

    // measure() 메서드는 기존과 동일
}

 

실행 결과 동일

문제 5. 고차 함수 - 함수를 반환하기

문제 설명

  • 두 정수를 받아서 연산하는 MyFunction을 사용하여 함수를 반환
  • static MyFunction getOperation(String operator)라는 정적 메서드를 작성
  • 매개변수인 operator에 따라 아래의 내용을 전달하고 반환
    • operator가 "add"면, (a, b)를 받아 a + b를 리턴하는 람다를 반환
    • "sub"면 a - b를 리턴하는 람다를 반환
    • 그 외의 경우는 항상 0을 리턴하는 람다를 반환
  • main() 메서드에서 getOperation("add"), getOperation("sub"), getOperation("xxx")를 각각 호출해서 반환된 람다를 실행
더보기

예시 출력

add(1, 2) = 3

sub(1, 2) = -1

xxx(1, 2) = 0 // 그 외의 경우

 

정답

더보기
package lambda.ex1;

public class M5Return {
    public static void main(String[] args) {
        MyFunction add = getOperation("add");
        MyFunction sub = getOperation("sub");
        MyFunction xxx = getOperation("xxx");
        System.out.println("add(1, 2) = " + add.apply(1, 2));
        System.out.println("sub(1, 2) = " + sub.apply(1, 2));
        System.out.println("xxx(1, 2) = " + xxx.apply(1, 2));
    }

    static MyFunction getOperation(String operator) {
        switch (operator) {
            case "add":
                return (a, b) -> a + b;
            case "sub":
                return (a, b) -> a - b;
            default:
                return (a, b) -> 0;
        }
    }
}

 

실행 결과

add(1, 2) = 3
sub(1, 2) = -1
xxx(1, 2) = 0

문제 6. filter 함수 구현하기

이번 문제들은 이후에 설명할 스트림은 물론, 함수형 프로그래밍의 개념을 이해하기 위해 반드시 반복해서 풀어보고 이해해야 함

고차 함수 개념을 직접 실습해볼 수 있도록 구성되어있으며 각 문제에서 요구하는 핵심 사항은 "함수를 매개변수로 받거나, 함수를 반환" 하는 구조를 구현하는 것임

 

요구 사항

  • 정수 리스트가 주어졌을 때, 특정 조건에 맞는 요소들만 뽑아내는 filter 함수를 직접 만들어보기
  • filter(List<Integer> list, MyPredicate predicate) 형식의 정적 메서드를 하나 작성
    • MyPredicate는 함수형 인터페이스이며 boolean test(int value); 같은 메서드를 가짐
  • main()에서 예시로 다음과 같은 상황을 실습
    • 리스트: [-3, -2, -1, 1, 2, 3, 5]
    • 조건 1: 음수(negative)만 골라내기
    • 조건 2: 짝수(even)만 골라내기
더보기

예시 실행

원본 리스트: [-3, -2, -1, 1, 2, 3, 5]
음수만: [-3, -2, -1]
짝수만: [-2, 2]

 

함수형 인터페이스 예시

@FunctionalInterface
public interface MyPredicate {
    boolean test(int value);
}

 

기본 코드 예시

package lambda.ex2;

public class FilterExample {

    // 고차 함수, 함수를 인자로 받아서 조건에 맞는 요소만 뽑아내는 filter
    public static List<Integer> filter(List<Integer> list, MyPredicate predicate) {
        List<Integer> result = new ArrayList<>();
        for (int val : list) {
            if (predicate.test(val)) {
                result.add(val);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> numbers = List.of(-3, -2, -1, 1, 2, 3, 5);
        System.out.println("원본 리스트: " + numbers);
        
        // 1. 음수(negative)만 뽑아내기
        // 코드 작성
        
        // 2. 짝수(even)만 뽑아내기
        // 코드 작성
    }
}

 

 

정답

더보기

익명 클래스 정답은 생략

 

람다식으로 구현

package lambda.ex2;

public class FilterExampleEx2 {

    // 고차 함수, 함수를 인자로 받아서 조건에 맞는 요소만 뽑아내는 filter
    public static List<Integer> filter(List<Integer> list, MyPredicate predicate) {
        List<Integer> result = new ArrayList<>();
        for (int val : list) {
            if (predicate.test(val)) {
                result.add(val);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> numbers = List.of(-3, -2, -1, 1, 2, 3, 5);
        System.out.println("원본 리스트: " + numbers);

        // 1. 음수(negative)만 뽑아내기
        List<Integer> negative = filter(numbers, val1 -> val1 < 0);
        System.out.println("음수만: " + negative);

        // 2. 짝수(even)만 뽑아내기
        List<Integer> even = filter(numbers, val -> val % 2 == 0);
        System.out.println("짝수만: " + even);
    }
}

 

filter() 메서드가 MyPredicate라는 "조건 함수"를 받아서 test()가 true일 때만 결과 리스트에 추가

이렇게 함수를 인자로 받아서 로직을 결정하는 형태가 전형적인 고차 함수임

문제 7. map 함수 구현하기

요구사항

  • 문자열 리스트를 입력받아 각 문자열을 어떤 방식으로 변환(map, mapping)할지 결정하는 함수(map)를 만들기
  • map(List<String> list, StringFunction func) 형태로 구현함
    • StringFunction은 함수형 인터페이스이며 String apply(String s); 메서드를 가짐
  • main()에서 다음 변환 로직들을 테스트해보기
    • 변환 1: 모든 문자열을 대문자로 변경
    • 변환 2: 문자열 앞, 뒤에 ***를 붙여서 반환(ex: "hello" -> "***hello***)
더보기

예시 실행

원본 리스트: [hello, java, lambda]
대문자 변환 결과: [HELLO, JAVA, LAMBDA]
특수문자 데코 결과: [***hello***, ***java***, ***lambda***]

 

함수형 인터페이스

package lambda;

@FunctionalInterface
public interface StringFunction {
    String apply(String s);
}

 

코드 예시

package lambda.ex2;

public class MapExample {

    // 고차 함수, 함수를 인자로 받아, 리스트의 각 요소를 변환
    public static List<String> map(List<String> list, StringFunction func) {
        // 코드 작성
        return null;
    }

    public static void main(String[] args) {
        List<String> words = List.of("hello", "java", "lambda");
        System.out.println("원본 리스트: " + words);
        
        // 1. 대문자 변환
        // 코드 작성
        
        // 2. 앞,뒤에 *** 붙이기(람다로 작성)
        // 코드 작성
    }
}

 

정답

더보기
package lambda.ex2;

public class MapExampleEx {

    // 고차 함수, 함수를 인자로 받아, 리스트의 각 요소를 변환
    public static List<String> map(List<String> list, StringFunction func) {
        List<String> newList = new ArrayList<>();
        for (String str : list) {
            newList.add(func.apply(str));
        }

        return newList;
    }

    public static void main(String[] args) {
        List<String> words = List.of("hello", "java", "lambda");
        System.out.println("원본 리스트: " + words);

        // 1. 대문자 변환
        System.out.println("대문자 변환 결과: " + map(words, a -> a.toUpperCase()));

        // 2. 앞,뒤에 *** 붙이기(람다로 작성)
        System.out.println("특수문자 데코 결과: " + map(words, a -> "***" + a + "***"));
    }
}

문제 8. reduce(또는 fold)함수 구현하기

요구사항

  • 정수 리스트를 받아서 모든 값을 하나로 누적(reduce)하는 함수를 만들기
  • reduce(List<Integer> list, int initial, MyReducer reducer) 형태로 구현됨
    • MyReducer는 int reduce(int a, int b); 메서드를 제공하는 함수형 인터페이스임
    • initial은 누적 계산의 초깃값(ex: 0 또는 1 등)을 지정함
  • main() 에서 다음 연선을 테스트해보기
    • 연산 1: 리스트 [1, 2, 3, 4]를 모두 더하기
    • 연산 2: 리스트 [1, 2, 3, 4]를 모두 더하기
더보기

예시 실행

리스트: [1, 2, 3, 4]

(누적 +): 10

(누적 *): 24

 

함수형 인터페이스

package lambda.ex2;

@FunctionalInterface
public interface MyReducer {
    int reduce(int a, int b);
}

 

코드 예시

package lambda.ex2;

public class ReduceExample {

    // 함수를 인자로 받아 리스트 요소를 하나로 축약(reduce)하는 고차 함수
    public static int reduce(List<Integer> list, int initial, MyReducer reducer) {
        // 코드 작성
        return 0;
    }
    
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4);
        System.out.println("리스트: " + numbers);
        
        // 1. 합 구하기 (초깃값 0, 덧셈 로직)
        // 코드 작성
        
        // 2. 곱 구하기 (초깃값 1, 곱셈 로직)
        // 코드 작성
    }
}
  • 고차 함수: MyReducer.reduce() 메서드가 함수를 인자로 받아서 내부 로직(합산, 곱셈 등)을 다르게 수행함
  • 곱은 초기값을 1로 한 것에 주의, 0으로 하면 결과는 무조건 0이됨

 

정답

더보기
package lambda.ex2;

public class ReduceExampleEx {

    // 함수를 인자로 받아 리스트 요소를 하나로 축약(reduce)하는 고차 함수
    public static int reduce(List<Integer> list, int initial, MyReducer reducer) {
        for (Integer num : list) {
            initial = reducer.reduce(initial, num);
        }
        // 코드 작성
        return initial;
    }

    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4);
        System.out.println("리스트: " + numbers);

        // 1. 합 구하기 (초깃값 0, 덧셈 로직)
        System.out.println("합(누적 +): " + reduce(numbers, 0, (a, b) -> a + b));

        // 2. 곱 구하기 (초깃값 1, 곱셈 로직)
        System.out.println("곱(누적 *): " + reduce(numbers, 1, (a, b) -> a * b));
    }
}

 

** 용어 - reduce, fold

  • 정답과 같은 방식으로 여러 값을 계산하여 하나의 최종 값을 반환하는 경우 reduce(축약하다), fold(접는다) 같은 단어를 사용함
  • reduce: 1, 2, 3, 4 라는 숫자를 하나씩 계산하면서 축약하기 때문에 축약하다는 의미의 reduce를 사용
  • fold: 마치 종이를 여러 번 접어서 하나의 작은 뭉치로 만드는 것처럼 초깃값과 연산을 통해 리스트의 요소를 하나씩 접어서 최종적으로 하나의 값으로 축약한다는 의미임

문제 9. 함수를 반환하는 buildGreeter 만들기

요구사항

  • 문자열을 입력받아 새로운 함수를 반환해주는 buildGreeter(String greeting)라는 메서드를 작성
    • ex: buildGreeter("Hello") -> 새로운 함수 반환
    • 새로운 함수는 입력받은 문자열에 대해 greeting + ", " + (입력 문자열) 형태로 결과를 반환
  • 함수를 반환받은 뒤에, 실제로 그 함수를 호출해 결과를 확인
  • 함수형 인터페이스는 이전에 사용한 StringFunction을 사용
더보기

문제 예시

package lambda.ex2;

public class BuildGreeterExample {

    // 고차 함수, greeting 문자열을 받아, 새로운 함수를 반환
    public static StringFunction buildGreeter(String greeting) {
        // 코드 작성
        return  null;   // 람다 반환
    }

    public static void main(String[] args) {
        // 코드 작성
    }
}

 

실행 결과

Hello, Java
Hi, Lambda

 

정답

더보기
package lambda.ex2;

public class BuildGreeterExampleEx {

    // 고차 함수, greeting 문자열을 받아, 새로운 함수를 반환
    public static StringFunction buildGreeter(String greeting) {
        return (str) -> greeting + ", " + str;
    }

    public static void main(String[] args) {

        StringFunction helloGreeter = buildGreeter("Hello");
        System.out.println(helloGreeter.apply("Java"));

        System.out.println(buildGreeter("Hi").apply("Lambda"));
    }
}

문제 10. 함수 합성하기 (compose)

람다를 전달하고 람다를 반환까지하는 문제

 

요구사항

  • 문자열을 변환하는 함수 두 개(MyTransformer 타입)를 받아서 f1을 먼저 적용하고 그 결과에 f2를 적용하는 새로운 함수를 반환하는 compose 메서드를 만들기
  • 예시 상황
    • f1: 대문자로 바꿈
    • f2: 문자 앞 뒤에 "**"을 붙임
    • 합성 함수(compose())를 "hello"에 적용하면 -> "**HELLO**"
더보기

함수형 인터페이스

package lambda.ex2;

@FunctionalInterface
public interface MyTransformer {
    String transform(String s);
}

 

예시 코드

package lambda.ex2;

public class ComposeExample {
    // 고차 함수, f1, f2라는 두 함수를 인자로 받아 f1을 먼저 f2를 나중에 적용하는 새 함수를 반환
    public static MyTransformer compose(MyTransformer f1, MyTransformer f2) {
        // 코드 작성
        return null; // 람다 반환
    }

    public static void main(String[] args) {
        // f1: 대문자로 변환
        MyTransformer toUpper = s -> s.toUpperCase();
        
        // f2: 앞 뒤에 ** 붙이기
        MyTransformer addDeco = s -> "**" + s + "**";
        
        // 합성: f1 -> f2  순서로 적용하는 함수
        MyTransformer composeFunc = compose(toUpper, addDeco);
        
        // 실행
        String result = composeFunc.transform("hello");
        System.out.println("result = " + result);
    }
}

 

실행 결과

**HELLO**

 

정답 - 익명클래스로 구현

더보기
package lambda.ex2;

public class ComposeExampleEx1 {

    public static MyTransformer compose(MyTransformer f1, MyTransformer f2) {

        return new MyTransformer() {
            @Override
            public String transform(String s) {
                return f2.transform(f1.transform(s));
            }
        };
    }


    // main 메서드 생략
}

정답 - 람다로 구현

더보기
package lambda.ex2;

public class ComposeExampleEx2 {

    public static MyTransformer compose(MyTransformer f1, MyTransformer f2) {
        return s -> f2.transform(f1.transform(s));
    }

    // main 메서드 생략
}

정리

지금까지 진행한 5가지 문제는 자바에서 고차 함수를 구현할 때 자주 등장하는 패턴으로 구성되어 있음

  • filter: 조건(함수)을 인자로 받아 리스트에서 필요한 요소만 추려내기
  • map: 변환 로직(함수)을 인자로 받아 리스트의 각 요소를 다른 형태로 바꾸기
  • reduce: 누적 로직(함수)을 인자로 받아 리스트의 모든 요소를 하나의 값으로 축약하기
  • 함수를 반환: 어떤 문자열/정수 등을 받아서 그에 맞는 새로운 "함수"를 만들어 돌려주기
  • 함수 합성: 두 함수를 이어 붙여서 한 번에 변환 로직을 적용할 수 있는 새 함수를 만들기

이런 문제들을 통해서 다음 내용들을 깊이 있게 이해할 수 있음

  • 자바에서 함수형 인터페이스를 이용해 함수를 표현하고 이를 매개변수/반환값으로 활용하는 방식
  • 익명 클래스 또는 람다를 활용해 간결하게 고차 함수를 구현하는 방법
  • filter-map-reduce등, 컬렉션/스트림 라이브러리에서도 흔히 볼 수 있는 고차 함수 패턴(뒤에서 다룸)

람다란?

  • 자바 8에서 도입된 익명 함수로 이름 없이 간결하게 함수를 표현함
  • 예시: (x) -> x + 1
  • 익명 클래스보다 보일러플레이트 코드를 줄여 생산성과 가독성을 높이는 문법 설탕(Syntactic sugar) 역할을 함

함수형 인터페이스

  • 람다를 사용할 수 있는 기반으로 단일 추상 메서드(SAM)만 포함하는 인터페이스
  • 예시: @FunctionalInterface를 적용하여 하나의 메서드만 정의된 인터페이스
  • 여러 메서드가 있으면 람다 할당 불가(모호성 방지)

람다 문법

  • 기본 형태: (매개변수) -> {본문}
  • 생략 가능
    • 단일 표현식(본문, 반환 생략): x -> x + 1
    • 타입 추론: (int x) -> x를  x -> x로 타입 생략 가능
    • 매개변수 괄호(단일 매개변수일 때): x -> x
  • 시그니처(매개변수 수/타입, 반환 타입)이 함수형 인터페이스와 일치해야 함

람다 활용

  • 변수 대입: MyFunction f = (a, b) -> a + b; 처럼 람다 인스턴스를 변수에 저장할 수 있음
  • 메서드 전달: calculate((a, b) -> a + b) 로 함수처럼 전달 가능
  • 반환: return (a, b) -> a + b;로 메서드에서 람다를 반환할 수 있음

고차 함수

  • 함수를 인자나 반환값으로 다루느 함수(filter, map, reduce 등)
  • 자바에서는 함수형 인터페이스와 람다로 구현하며 코드의 유연성과 추상화 수준을 높임
  • 예시: List<Integer> filter(List<Integer> list, MyPredicate p)는 조건 함수를 받아서 동작하는 고차 함수임

기타

  • 람다는 익명 클래스를 간소화한 도구지만 내부적으로 인스턴스가 생성됨
  • 반복 연습으로 문법과 활용법을 익히는 것이 중요함

출처 : 인프런 - 김영한의 실전 자바 - 고급3편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용함

728x90