빙응의 공부 블로그

[JAVA]함수형 프로그래밍 - 람다식(Lambda Expression) 본문

JAVA

[JAVA]함수형 프로그래밍 - 람다식(Lambda Expression)

빙응이 2023. 11. 29. 23:38

📝람다식(Lambda Expression)이란?

Stream 연산들은 매개변수로 함수형 인터페이스를 받도록 되어있다.

그리고 람다식은 반환값으로 함수형 인터페이스를 반환하고 있다. 

Stream API를 사용하기 위해서는 람다식과 함수형 인터페이스에 대해 정확히 이해해야 한다. 

 

람다식의 의미

람다식이란 함수를 하나의 식으로 표현한 것이다. 

함수를 람다식으로 표현하면 메소드의 이름이 필요 없기 때문에, 람다식은 익명 함수 중에 하나이다.

  • 익명 함수의 특징
    • 다른 객체들에게 적용 가능한 연산을 모두 지원한다. 
    • 함수를 값으로 사용할 수도 있으며, 파라미터로 전달 및 변수에 대입 하기와 같은 연산도 가능하다. 
//기존 방식
public String hello() {
    return "Hello World!";
}

//람다식 
() -> "Hello World!";

 

위 코드처럼 람다 방식은 메소드 명이 불필요하기 때문에 ()와 ->를 이용해 함수를 선언한다. 

 

이러한 람다식이 등장한 이유는 불필요한 코드를 줄이고, 가독성을 높이기 위함이다.

그렇기 때문에 함수형 인터페이스의 인스턴스를 생성하여 함수를 변수처럼 선언하는 람다식에서는 메소드의 이름이 불필요해 졌다. 

람다식으로 선언한 함수는 1급 객체이기 때문에 Stream API의 매개변수로 전달이 가능하다.

 

📝람다식의 특징 

람다식의 특징
  • 람다식 내에서 사용되는 지역변수는 사실상 final  취급을 받는다. 
      // final 키워드를 사용하지 않은 지역 변수
        int outsideVariable = 10;

        // 람다식 내에서 final 변수로 취급됨
        MyFunctionalInterface myLambda = (x) -> {
            // outsideVariable++;  // 컴파일 에러: 람다식 내에서 final 또는 사실상 final인 변수를 수정할 수 없음
            System.out.println("Lambda Expression: " + (x + outsideVariable));
        };

        // 중복된 변수명 사용 불가
        int outsideVariable = 20;  // 컴파일 에러: 변수명 중복

        myLambda.apply(5);
        
        
        @FunctionalInterface
        interface MyFunctionalInterface {
        void apply(int x);
        }

 

람다식의 장점
  1. 코드를 간결하게 만들 수 있다.
  2. 식에 개발자의 의도가 명확히 드러나 가독성이 높아진다.
  3. 함수를 만드는 과정없이 한번에 처리할 수 있어 생산성이 높다.
  4. 병렬프로그래밍이 용이하다.
  5. 인터페이스의 추상 메서드를 간단히 구현할 수 있어 인터페이스의 의도가 명확해진다. 
public class LambdaExample {
    public static void main(String[] args) {
        // 간단한 계산을 수행하는 람다식
        Calculator adder = (a, b) -> a + b;

        int result = adder.calculate(5, 3);
        System.out.println("덧셈 결과: " + result);
    }

    // 함수형 인터페이스 정의
    interface Calculator {
        int calculate(int a, int b);
    }
}

 

람다식의 단점
  1. 람다를 사용하면서 만든 무명함수는 재사용이 불가능하다. (단, 인터페이스로 정의는 가능)
  2. 디버깅이 어렵다.
  3. 람다를 남발하면 오히려 가독성이 떨어진다. 
  4. 재귀적 함수 만들기에 부적합

 

 

📝함수형 인터페이스

JAVA는 기본적으로 객체지향 언어이다. 그렇기에 순수 함수와 일반 함수를 다르게 취급하고 있다.

JAVA에서는 이를 구분하기 위해 함수형 인터페이스를 만들었다.

함수형 인터페이스란 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션이다.

인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다. 함수형 인터페이스를 사용하는 이유는 

JAVA의 람다식이 함수형 인터페이스를 반환하기 때문이다.

 

예시로 우리는 둘 중 더 큰 값을 리턴하는 것을 보면 된다.

public class LambdaExample{
	public static void main(String[] args) {
    	//기존의 익명함수
		System.out.println(new MyLambdaFunction(){
    			public int sum(int a, int b){
        			return a+b;
        		}
    		}.sum(3,5));

	}
}

 

하지만 우리는 함수형 인터페이스를 이용해서 함수를 변수처럼 선언할 수 있게 되었다. 

우리는 단 하나의 추상메소드를 넣어 인터페이스를 만들고 @FunctionalInterface 어노테이션을 추가해주면 된다.

@FunctionalInterface
interface LambdaFunction {
    int sum(int a, int b);
}

public class LambdaExample {

    public static void main(String[] args) {

        // 람다식을 이용한 익명함수
        LambdaFunction lambdaFunction = (int a, int b) -> a+b;
        System.out.println(lambdaFunction.sum(3, 5));
    }

}

 

 

이제 우리는 함수형 인터페이스로 인해  람다식으로 생성된 순수 함수를 재사용이 가능해졌다. 

📝주요 함수형 인터페이스 

Supplier (공급자)
  • Supplier<T> 인터페이스는 파라미터 없이 값을 제공하는데 사용된다. 
  • T get() 추상 메소드를 가지고 있다.
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

           
Supplier<String> supplier = () -> "Hello, Supplier!";
System.out.println(supplier.get()); // 출력: Hello, Supplier!

 

Consumer (소비자)
  • Consumer<T> 인터페이스는 값을 받아서 어떤 동작을 수행하는데 사용한다.
  • void accep(T t) 추상 메소드를 가지고 있다. 
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}


Consumer<String> consumer = s -> System.out.println("Consumed: " + s);
consumer.accept("Hello, Consumer!"); // 출력: Consumed: Hello, Consumer!

 

Predicate (조건자) 
  • Predicate<T> 인터페이스는 주어진 조건을 만족하는지 여부를 확인하는데 사용한다.
  • boolean test(T t) 추상 메소드를 가지고 있다.
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // 출력: true
System.out.println(isEven.test(5)); // 출력: false

 

Function (함수)
  • Function<T, R> 인터페이스는 하나의 값을 받아서 다른 타입으로 변환하는 데 사용한다. 
  • R apply(T t) 추상 메소드를 가지고 있다.
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}


Function<String, Integer> strLength = s -> s.length();
System.out.println(strLength.apply("Hello, Function!")); // 출력: 17

📝메소드 참조

메소드 참조란 함수형 인터페이스를 람다식이 아닌 일반 메소드를 참조시켜 선언하는 방법이다. 

일반 메소드를 참조하기 위해서는 다음 3가지 조건을 만족해야 한다. 

  • 함수형 인터페이스의 매개변수 타입 = 메소드의 매개변수 타입
  • 함수형 인터페이스의 매개변수 개수 = 메소드의 매개변수 개수
  • 함수형 인터페이스의 반환형 = 메소드의 반환형

참조가능한 메소드는 일반 메소드, Static 메소드, 생성자가 있으며 클래스이름::메소드이름으로 참조할 수 있다.

 

일반 메소드 참조
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// 람다 표현식을 사용한 경우
names.forEach(name -> System.out.println(name));

// 일반 메서드 참조를 사용한 경우
names.forEach(System.out::println);
// 기존의 람다식
Function<String, Integer> function = (str) -> str.length();
function.apply("Hello World");

// 메소드 참조로 변경
Function<String, Integer> function = String::length;
function.apply("Hello World");

위 코드의 Function은 Function<T,R>로 받고 R apply<T t>로 리턴하기 때문에 가능하다. 

 

Static 메소드 참조
    public static void main(String[] args) {
        List<Double> numbers = Arrays.asList(1.0, 2.0, 3.0, 4.0);

        // 람다 표현식을 사용한 경우
        numbers.forEach(number -> System.out.println(squareRoot(number)));

        // Math 클래스의 정적 메서드 참조를 사용한 경우
        numbers.forEach(Math::sqrt);
    }

    // 다른 클래스의 정적 메서드
    public static double squareRoot(double number) {
        return Math.sqrt(number);
    }

 

 

 

 


✔참조 사이트