관리 메뉴

Rootable의 개발일기

람다식과 함수형 인터페이스 본문

Java

람다식과 함수형 인터페이스

dev-rootable 2024. 5. 22. 14:25

❓ 람다식이란?

 

메서드를 하나의 식(expression)으로 표현한 것

 

메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)'라고도 한다.

 

int method() {
    return (int) (Math.random() * 5) + 1);
}

 

위의 메서드를 람다식으로 바꾸면 다음과 같다.

 

int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int) (Math.random() * 5) + 1);

 

🎯 람다식의 장점

 

✔ 간결한 코드

 

모든 메서드는 클래스에 포함되어야 하므로 클래스를 새로 만들어야 하고, 객체도 생성해야만 비로소 메서드를 호출할 수 있다. 그러나 람다식은 이러한 과정 없이 오직 람다식 자체만으로도 메서드의 역할을 대신할 수 있다.

 

✔ 메서드를 변수처럼 다룰 수 있음

 

람다식은 메서드의 매개변수로 전달이 가능하고, 람다식 자체를 메서드의 결괏값으로 반환할 수도 있다. 이처럼 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.

 

✔ 함수형 인터페이스 활용

 

함수형 인터페이스와 결합하여 코드의 간결성과 가독성을 향상할 수 있다. 자바에서는 기본적으로 사용할 수 있는 다양한 함수형 인터페이스를 제공한다. 대표적으로 Runnable, Callable, Comparator, Predicate, Function, Consumer, Supplier 등이 있다. 이러한 함수형 인터페이스를 통해 대부분의 코드를 처리할 수 있다.

 

✔ 익명 클래스 사용 대체

 

자바 람다식과 함수형 인터페이스를 사용하면 익명 클래스의 선언을 대체할 수 있다. 이를 통해 코드를 더욱 간결하게 만들고, 익명 클래스의 중복 사용에 따른 코드의 중복을 줄일 수 있다. 예를 들어 여러 번 정렬이 필요한 상황에서 Comparator를 사용한다면 코드 중복이 발생하고 코드 가독성이 떨어지게 되는데, 이러한 상황에서 람다식을 사용하여 해결할 수 있다.

 

✔ 스트림 API 활용

 

Java 8부터 도입된 스트림 API는 컬렉션에 저장된 데이터를 간결하게 처리할 수 있는 기능을 제공한다. 특히 람다식과 스트림 API를 결합하여 데이터 필터링, 매핑, 집계 등의 작업을 직관적으로 수행할 수 있다. 이러한 스트림 API를 활용하여 함수형 프로그래밍 스타일로 더욱 직관적이고 간결한 코드를 작성할 수 있다.

 

📝 람다식 작성하기

 

  • 람다식은 메서드의 매개변수 타입을 생략할 수 있다.
    • 단, 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않는다.
  • 반환값이 있는 메서드의 경우, return문 대신 식(expression)으로 대신할 수 있다. 식의 연산 결과가 자동으로 반환값이 된다. (세미콜론 제외)
  • 매개변수가 하나인 경우에는 괄호를 생략할 수 있다.
    • 단, 매개변수의 타입이 있으면 괄호를 생략할 수 없다.
  • {} 안의 명령문이 하나일 때는 {}를 생략할 수 있고, 세미콜론을 붙이지 않는다.

 

int max(int a, int b) {
    return a > b ? a : b;
}

 

(a, b) -> a > b ? a : b

 

📌 함수형 인터페이스(Functional Interface)와 람다식

 

람다식은 익명 클래스의 객체와 동등하다

 

(a, b) -> a > b ? a : b

 

new Object() {
    int max(int a, int b) {
        return a > b ? a : b;
    }
}

 

위는 람다식이고, 아래는 이것을 익명 클래스의 객체로 변환한 것이다.

 

위의 메서드를 호출하려면 익명 클래스의 객체를 받아야 하는데, 참조형이므로 클래스 또는 인터페이스로 받아야 한다.

 

타입 f = (a, b) -> a > b ? a : b;

 

람다식을 받을 수 있는 클래스 또는 인터페이스는 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다.

 

아래와 같은 인터페이스가 있다고 가정하자.

 

interface MyFunction {
    public abstract int max(int a, int b);
}

 

해당 인터페이스를 구현한 익명 클래스의 객체를 다음과 같이 생성할 수 있다.

 

MyFunction f = new MyFunction() {
                   public int max(int a, int b) {
                       return a > b ? a : b;
                   }
               };
               
int maxVal = f.max(5, 3); //익명 객체의 메서드 호출

 

MyFunction 인터페이스에 정의된 메서드 max()가 람다식과 일치하기 때문에 아래와 같이 대체할 수 있다.

 

MyFunction f = (a, b) -> a > b ? a : b; //익명 객체를 람다식으로 대체
int maxVal = f.max(5, 3); //익명 객체의 메서드 호출

 

위처럼 익명 객체를 람다식으로 교체 가능한 이유는 람다식도 실제로는 익명 객체이기 때문이다. 이렇게 대체하기 위해서는 람다식의 매개변수 타입 및 개수와 반환값이 일치해야 한다.

 

🖍 결론

지금까지 살펴본 것처럼, 하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바 규칙들을 어기지 않으면서도 자연스럽다.

그래서 인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(functional interface)'라고 부르기로 했다.

@FunctionalInterface //컴파일러가 함수형 인터페이스를 올바르게 정의했는지 확인함
interface MyFunction { //함수형 인터페이스
    public abstract int max(int a, int b);
}

 

단, 함수형 인터페이스는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메서드가 일대일로 연결되기 때문이다. static, default 메서드의 개수는 제약이 없다.

 

🔎 함수형 인터페이스 타입과 매개변수의 반환타입

 

메서드의 매개변수가 MyFunction(함수형 인터페이스) 타입이면, MyFunction의 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다. 마찬가지로 람다식은 함수형 인터페이스의 추상 메서드와 동등해야 한다.

 

@FunctionalInterface
interface MyFunction {
    void myMethod();
}

 

void aMethod (MyFunction f) { //매개변수 타입이 함수형 인터페이스
    f.myMethod(); //함수형 인터페이스의 추상 메서드 호출
}

MyFunction f = () -> System.out.println("myMethod()"); //람다식을 참조하는 참조변수
aMethod(f);

 

참조변수 없이 아래와 같이 직접 람다식을 매개변수로 지정할 수 있다.

 

aMethod(() -> System.out.println("myMethod()");

 

메서드의 반환타입이 함수형 인터페이스라면, 해당 함수형 인터페이스의 추상 메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.

 

MyFunction myMethod() {
    MyFunction f = () -> {};
    return f; //return () -> {}; 으로 줄일 수 있음
}

 

🔎 람다식의 타입과 형변환

 

람다식은 오직 함수형 인터페이스로만 형변환이 가능

 

@FunctionalInterface
interface MyFunction {
    void method();
}

 

함수형 인터페이스의 메서드와 동등한 람다식(매개변수 & 반환값 일치)을 함수형 인터페이스로 형변환할 수 있다.

 

MyFunction f = (MyFunction) (() -> System.out.println("Lambda"));
MyFunction f = (Object) (() -> System.out.println("Lambda")); //불가능
Object o = (Object) (MyFunction) (() -> System.out.println("Lambda")); //가능

 

람다식은 익명 객체지만 타입이 존재한다. 하지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다.

 

람다식의 타입: 외부클래스이름$$Lambda$번호

 

일반적인 익명 객체는 객체의 타입이 '외부클래스이름$번호'와 같은 형식으로 타입이 결정되지만 람다식의 타입은 '외부클래스이름$$Lambda$번호'와 같은 형식으로 된다.

 

🔎 람다식에서 외부 변수 참조

 

✔ 지역 변수

 

람다식에서 참조하는 지역 변수는 final이 붙지 않았어도 상수로 간주한다. 따라서, 람다식 내에서 참조하는 지역 변수는 람다식 내에서나 다른 어느 곳에서나 값을 변경할 수 없다.

 

✔ 람다식 밖의 인스턴스 변수

 

람다식 밖 클래스의 인스턴스 변수는 람다식 내에서 참조하더라도 상수로 간주되지 않으므로 값을 변경할 수 있다.

 

@FunctionalInterface
interface MyFunction2 {
    void myMethod();
}

class Outer{
    int val = 10; //Outer.this.val
	
    class Inner{
        int val = 20; //this.val
		
        void method(int i) { //void method(final int i)
            int val = 30; //final int val = 30;
//          i = 10; error -> final val can't change
			
            MyFunction2 f = () -> {
                System.out.println("             i : " + i);
                System.out.println("           val : " + val);
                System.out.println("      this.val : " + this.val);
                System.out.println("Outer.this.val : " + Outer.this.val);
            };
			
            f.myMethod();
        }
    }
}

public class LambdaEx3 {
	public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method(100);
    }
}

 

Test 결과

 

외부 지역 변수와 동일 이름의 람다식 매개변수는 허용되지 않는다.

 

@FunctionalInterface
interface MyFunction2 {
    void myMethod();
}

class Outer{
    int val = 10; //Outer.this.val
	
    class Inner{
        int val = 20; //this.val
		
        void method(int i) { //void method(final int i)
            int val = 30; //final int val = 30;
//          i = 10; error -> final val can't change
			
            MyFunction2 f = (i) -> { //error -> i는 변수 이름 중복
                System.out.println("             i : " + i);
                System.out.println("           val : " + val);
                System.out.println("      this.val : " + this.val);
                System.out.println("Outer.this.val : " + Outer.this.val);
            };
			
            f.myMethod();
        }
    }
}

 

❓ 익명 객체(익명 클래스)란?

 

프로그램에서 일시적으로 한 번만 사용되고 버려지는 객체

 

📢 익명 객체(익명 클래스)를 사용하는 이유

 

익명 객체는 나중에 재사용이 되지 않는다. 이 말은 확장성이 그렇게 좋지 못하다는 것을 의미한다. 자바에서 확장성을 고려한 설계는 매우 중요하지만 이로 인해 유지보수에서 더 불리한 경우도 있다. 그래서 다음과 같은 경우에 사용하는 것이 좋다.

 

  • 프로그램 내에서 일회용으로 사용되어야 하는 객체 (ex. UI 이벤트 처리, 스레드 객체 등 단발성 이벤트 처리)
  • 재사용성이 없고, 확장성을 활용하는 것이 유지보수에서 더 불리할 때
    • 비즈니스 로직이 재각각이며, 재사용성이 전혀 없어 매번 클래스를 생성해야 하는 비용이 큰 상황

 

References:

 

https://limkydev.tistory.com/226

 

[Java] 익명객체(익명클래스)란? (이 글 하나로 한방에 정리!)

익명객체(익명클래스) 란? 이번시간에는 자바 익명객체(익명클래스)에 대해서 알아보도록 하겠습니다. 익명객체(익명클래스) 말그대로.. 이름이 없는 객체? 클래스?,,,그래서 무명클래스라고도

limkydev.tistory.com

 

https://yozm.wishket.com/magazine/detail/2023/

 

코드 가독성 높이는 자바 람다식과 함수형 인터페이스 | 요즘IT

자바(Java)는 시간이 지남에 따라 발전하면서 더욱 효율적이고 간결한 코드를 작성할 수 있도록 다양한 기능을 제공하고 있습니다. 특히 자바 8부터 도입된 람다식과 함수형 인터페이스는 자바에

yozm.wishket.com

 

'Java' 카테고리의 다른 글

Java의 static 키워드  (0) 2024.05.17
상속과 합성  (0) 2024.05.13
Map  (2) 2024.05.11
Set  (0) 2024.05.11
LIFO 구조와 ArrayDeque  (0) 2024.05.11