Java - Lambda (익명함수) 에 대해

제훈·2024년 7월 26일

Java

목록 보기
27/34

Lambda

람다식 (익명함수) : 메소드를 하나의 식으로 표현한 것

f(x, y) = x * y;

위의 식은 x, y의 곱을 표현하는 함수인데 f 이름을 빼고 람다식으로 변경하면 아래처럼 된다.

(x, y) → x * y;

함수 이름을 빼고 → 이후로 식이 바로 오게 되고 "메소드의 이름과 반환값이 없어진다."는 의미는 익명 함수를 의미한다.


람다식이 필요한 이유

  1. 단순하고 편하다.
  2. 람다식을 활용할 수 있게 되면 Collection, stream을 연계해서 쉽게 조작 가능하다.
  3. 불필요하게 반복되는 코드를 제거할 수 있다.

람다 표현식

// 매개 변수 없는 경우
() -> { ... }

// 매개 변수 있는 경우
(타입 매개변수, ...) -> { ... }

람다 표현식에서 매개 변수 타입은 런타임 시에 자동으로 인식되기 때문에 매개변수 타입을 일반적으로 언급하지 않아도 된다.

하나의 매개변수만 존재하는 경우 ( ) 는 생략할 수 있다.
실행문이 하나인 경우에 { } 는 생략 가능하다.


함수적 인터페이스

자바는 메소드를 따로 선언할 수 없다. -> 항상 클래스의 내부에서만 선언되고, 메소드를 가지고 있는 객체를 생성해야 한다.

즉, 람다식은 단독으로 선언 불가능.

람다식은 익명 클래스처럼 인터페이스 변수에 대입된다.

인터페이스는 직접 구현을 할 수 없기 때문에 구현하는 클래스가 필요한데, 람다식을 통해 익명 구현 객체를 생성해 사용할 수 있다.

근데, 인터페이스는 여러 가지 추상 메소드를 가질 수 있는데 람다식이 그게 가능할까?

-> 아니다.

람다식은 하나의 메소드를 정의하기 때문에, 2개 이상에 추상 메소드가 선언된 인터페이스는 람다식으로 객체 생성이 불가능하다.

1개 추상 메소드를 가진 인터페이스만 타깃 타입이 될 수 있는데, 그러한 인터페이스를 함수적 인터페이스라고 한다.

@FunctionalInterface 이 어노테이션을 사용하여 함수적 인터페이스로 사용해보자.

Calculator 인터페이스

@FunctionalInterface
public interface Calculator {
    int sumTwoNumbers(int first, int second);
//    void test();      // 어노테이션 추가 후에는 추상메소드를 2개 이상 가지지 못한다.
}

Calculator의 구현 클래스인 CalculatorImpl

public class CalculatorImpl implements Calculator{
    @Override
    public int sumTwoNumbers(int first, int second) {
        return first + second;
    }
}

위의 것들을 호출할 람다식을 작성할 Application

  1. 인터페이스를 구현한 구현체(Impl 클래스)를 이용한 방식 (동적바인딩을 활용한 메소드 호출)
public class Application {
    public static void main(String[] args) {
        Calculator c1 = new CalculatorImpl();
        System.out.println("10과 20의 합은: " + c1.sumTwoNumbers(10, 20));
}
  1. 익명클래스를 활용한 방식(인터페이스의 하위 구현체) - 객체만 만들어서 사용하는 것
    구현 클래스를 지워도 작동한다. 이름이 없이 즉시 1번만 사용하기 위해 사용하는 것은 가능하다.
public class Application {
	public static void main(String[] args) {
        Calculator c2 = new Calculator() {
            @Override
            public int sumTwoNumbers(int first, int second) {
                return first + second;
            }
        };
        System.out.println("10과 20의 합은: " + c2.sumTwoNumbers(10, 20));
    }
}
  1. 람다식을 활용한 방식
public class Application {
	public static void main(String[] args) {
//        Calculator c3 = (x, y) -> {return x + y;};
        Calculator c3 = (x, y) -> x + y;
        System.out.println("10과 20의 합은: " + c3.sumTwoNumbers(10, 20));
    }
}

구문이 하나일 때는 return, return과 관련된 세미콜론(;), 중괄호 삭제 가능하다

익명클래스에 작성한 저 내용이 람다식으로 줄어든 것이다.

실행결과


Java 8에서는 빈번하게 사용되는 함수적 인터페이스를 java.util.function 표준 API 패키지로 제공한다.

크게 Consumer, Supplier, Function, Operator, Predicate 로 구분된다.

Consumer

Consumer 함수적 인터페이스의 특징은 리턴 값이 없는 accept() 메소드를 가지고 있다는 것이다.

accept() : 매개 변수로 넘어온 값을 소비하는 역할만 한다.

소비한다는 것 = return 값이 없다는 말이다.

인터페이스 명추상 메소드설명
Consumervoid accept(T t)객체 T를 받아서 소비한다.
BiConsumer<T, U>void accept(T t, U u)객체 T, U를 받아 소비한다.
intConsumervoid accept(int value)int 값을 받아 소비한다.
DoubleConsumervoid accept(double value)double 값을 받아 소비8한다.
LongConsumervoid accept(long value)long 값을 받아 소비한다.
ObjIntConsumervoid accept(T t, int value)객체 T와 int 값을 받아 소비한다.
ObjDoubleConsumervoid accept(T t, double value)객체 T와 double 값을 받아 소비한다.
ObjLongConsumervoid accept(T t, long value)객체 T와 long 값을 받아 소비한다.

활용

import java.util.function.Consumer;

public class Application {
    public static void main(String[] args) {
        Consumer<String> consumer = (str) ->
            System.out.println(str + "이(가) 입력됨");
        };
    }
}

이런 식으로 반환형이 없는 메소드 관련 람다식을 쓸 수 있다.

BiConsumer<T, U> , ObjIntConsumer<T> 도 추가해보자.

import java.time.LocalTime;
import java.util.Random;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.ObjIntConsumer;

public class Application {
    public static void main(String[] args) {
//    	  Consumer<String> consumer = (str) ->
//            System.out.println(str + "이(가) 입력됨");
//        };
    
    	// 매개변수가 한 개이면 소괄호 생략 가능하며 실행문이 하나인 경우 중괄호도 생략 가능하다.(현재는 return이 없는 구문)
        Consumer<String> consumer = str -> System.out.println(str + "이(가) 입력됨");
        consumer.accept("Hello");

        BiConsumer<String, LocalTime> biConsumer =
                (str, time) -> System.out.println(str + "이(가) " + time + "에 입력됨");
        biConsumer.accept("Hello?", LocalTime.now());

        ObjIntConsumer<Random> objIntConsumer =
                (random, bound) -> System.out.println("0부터 " + bound + "전 까지의 난수 발생: "
                        + random.nextInt(bound));
        objIntConsumer.accept(new Random(), 10);
    }
}

추가)

  • RandomMath.random() 과는 다르다.
    Math.random()메소드는 -> (int)(Math.random() * 10) + 1 을 통해 1~10까지의 난수를 만들었다.
    Random 클래스는 -> new Random().nextInt(10) + 1 으로 할 수 있다.
    Math.randomstatic이지만, Random 클래스는 non-static 이다.

Supplier

Supplier 함수적 인터페이스는 매개변수가 없고 리턴 값이 있는 getXXX() 메소드를 가지고 있다.

Consumer 와 반대 개념인 것이다. 이 메소드는 실행되면 호출한 곳으로 값을 리턴해준다.

인터페이스 명추상 메소드설명
SupplierT get()T 객체를 리턴한다.
BooleanSupplierBoolean getAsBoolean()Boolean 값을 리턴한다.
IntSupplierint getAsInt()int 값을 리턴한다.
DoubleSupplierdouble getAsDouble()double 값을 리턴한다.
LongSuplierlong getAsLong()long 값을 리턴한다.

활용

Application

import java.time.LocalDateTime;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;

public class Application {
    public static void main(String[] args) 
        /* 설명. 추상메소드의 매개변수가 없을 경우나 2개 이상일 경우에는 소괄호()를 생략할 수 없다. */
        Supplier<LocalDateTime> supplier = () -> LocalDateTime.now();
        System.out.println(supplier.get());

        BooleanSupplier booleanSupplier = () -> {
            int random = (int)(Math.random() * 2);
            return random == 0? false: true;
        };
        System.out.println("랜덤 true or false 생성기: " + booleanSupplier.getAsBoolean());
    }
}

실행결과


Function

이번에는 매개변수와 리턴값도 반환하는 함수적 인터페이스를 알아보자.

Function 함수적 인터페이스는 매개변수와 리턴값이 있는 applyXXX() 를 가지고 있다.
이 메소드들은 매개 변수 타입과 리턴 타입에 따라서 다양한 메소드들이 있다.

인터페이스 명추상 메소드설명
Function <T, R>R apply(T t)객체 T를 객체 R로 매핑한다.
BiFunction <T, U, R>R apply(T t, U u)객체 T와 U를 객체 R로 매핑한다.
IntFunctionR apply(int value)int를 객체 R로 매핑한다.
IntToDoubleFunctiondouble applyAsDouble(int value)int를 double로 매핑한다.
IntToLongFunctionlong applyAsLong(int value)int를 long으로 매핑한다.
DoubleFunctionR apply(double value)double을 객체 R로 매핑한다.
LongToDoubleFunctiondouble applyAsDouble(long value)long을 double로 매핑한다.
LongToIntFunctionint applyAsInt(long value)long을 int로 매핑한다.
ToDoubleBiFunction<T, U>double applyAsDouble(T t, U u)객체 T와 U를 double로 매핑한다.
ToDoubleFunctiondouble applyAsDouble(T value)객체 T를 double로 매핑한다.
ToIntBiFunction<T, U>int applyAsInt(T t, U u)객체 T와 U를 int로 매핑한다.
ToIntFunctionint applyAsInt(T t)객체 T를 int로 매핑한다.
ToLongBiFunction<T, U>long applyAsLong(T t, U u)객체 T와 U를 long으로 매핑한다.
ToLongFunctionlong applyAsLong(T t)객체 T를 long으로 매핑한다.

활용

Application

import java.util.function.BiFunction;
import java.util.function.Function;

public class Application {
    public static void main(String[] args) {
        /* 매개변수 및 반환형이 있는 메소드 관련 람다식 */
        Function<String, Integer> function = str -> Integer.valueOf(str);
        String strValue = "12345";
        System.out.println(function.apply(strValue) instanceof Integer);

		/* 매개변수 2개에 반환형까지 작성해서 람다식 작성 가능하다. */
        BiFunction<String, String, Integer> biFunction =
                (str1, str2) -> Integer.valueOf(str1) + Integer.valueOf(str2);
        System.out.println(biFunction.apply("12345", "11111"));
    }
}

실행결과


Operator

Operator 함수적 인터페이스는 Function 과 똑같이 작동한다.
매개변수와 리턴값이 있는 applyXXX() 메소드를 가지고 있다.

다른 점은 뭐가 있을까?

-> 매개변수와 반환값이 모두 같은 타입인 메소드 관련 람다식이라는 것이다.

인터페이스 명추상 메소드설명
BinaryOperatorBiFunction<T, U, R>의 하위 인터페이스T와 U를 연산하여 R로 리턴한다.
UnaryOperatorFunction<T, R> 의 하위 인터페이스T를 연산한 후 R로 리턴한다.
DoubleBinaryOperatordouble applyAsDouble(double, double)매개변수 두 개를 활용하여 double 타입으로 리턴한다.
DoubleUnaryOperatordouble applyAsDouble(double)매개변수 한 개를 활용하여 double 타입으로 리턴한다.
IntBinaryOperatorint applyAsInt(int, int)두 개의 int를 연산하여 int 타입으로 리턴한다
IntUnaryOperatorint applyAsInt(int)한 개의 int를 연산하여 int 타입으로 리턴한다.
LongBinaryOperatorlong applyAsLong(long, long)두 개의 long을 연산하여 long 타입으로 리턴한다.
LongUnaryOperatorlong applyAsLong(long)한 개의 long을 연산하여 long 타입으로 리턴한다.

활용

Application

import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;

public class Application {
    public static void main(String[] args) {
        /* 설명. 매개변수 및 반환형이 있지만 모두 같은 타입인 메소드 관련 람다식(feat.제네릭 타입 동일) */
        UnaryOperator<String> unaryOperator = str -> str + "World!!";
        System.out.println(unaryOperator.apply("Hello, "));

        BinaryOperator<String> binaryOperator = (str1, str2) -> str1 + str2;
        System.out.println(binaryOperator.apply("Hello2, ", "World!!2"));
    }
}

실행결과


Predicate

Predicate 함수적 인터페이스는 매개 변수와 boolean 리턴 값이 있는 testXXX() 를 가지고 있다.

testXXX() : 매개변수 값을 이용해 true 또는 false 를 리턴하는 역할

인터페이스 명추상 메소드설명
PredicateBoolean test(T t)객체 T를 조사하여 true, false를 리턴한다.
BiPredicate<T, U>Boolean test(T t, U u)객체 T와 U를 조사하여 true, false를 리턴한다.
DoublePredicateBoolean test(double value)double 값을 조사하여 true, false를 리턴한다.
IntPredicateBoolean test(int value)int 값을 조사하여 true, false를 리턴한다.
LongPredicateBoolean test(long value)long 값을 조사하여 true, false를 리턴한다.

활용

Application

import java.util.function.Predicate;

public class Application {
    public static void main(String[] args) {
        /* 설명. boolean 반환형을 가지는 메소드 관련 람다식 */
        Predicate<Object> predicate = value -> value instanceof String;
        System.out.println("문자열인지 확인: " + predicate.test("123"));
        System.out.println("문자열인지 확인: " + predicate.test(123));
    }
}

실행결과


메소드 참조

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

이미 존재하는 메소드를 마치 오버라이딩하는 것처럼 참조해서 적용하는 방법이다.

메소드 참조를 하기 위해서는 3가지 조건을 만족해야 하는데

함수형 인터페이스매개변수 타입/개수/반환 형이랑 메소드의 매개변수 타입/개수/반환 형이 같아야 한다.


매개변수의 메소드 참조

위에서 나온 3가지 조건이 만족하면 가능하다.

메소드 참조 표현식

클래스이름::메소드이름 (static)일 경우
참조변수이름::메소드이름 (non-static)일 경우

  • 활용
import java.util.function.BiFunction;

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

        /* 수업목표. 기존에 존재하는 메소드를 참조해 람다식을 적용할 수 있다. */
        BiFunction<String, String, Boolean> biFunction;

        String str1 = "METHOD";
        String str2 = "METHOD";

        boolean result = false;

        biFunction = String::equals;				// 1. 기존에 있는 메소드를 참조한 것
//        biFunction = (x, y) -> x.equals(y);		// 2. 직접 람다식을 활용한 것

        result = biFunction.apply(str1, str2);      // str1.equals(str2);

        System.out.println("result = " + result);
    }
}

둘다 실행해보면 알겠지만 1, 2번 둘다 동일하다.

실행결과


생성자 참조

생성자는 new로 생성하기 때문에 참조 표현식이 조금 다르긴 하지만 가능하다.

메소드 참조 표현식

클래스이름::new

  • 활용
    Member 클래스
public class Member {
    private String memId;

    public Member(String memId) {
        this.memId = memId;
    }

    public void setMemId(String memId) {
        this.memId = memId;
    }

    @Override
    public String toString() {
        return "Member{" +
                "memId='" + memId + '\'' +
                '}';
    }
}

생성자 메소드 참조를 할 겸 Member 클래스를 만들어봤다.

Application

import java.util.function.Function;

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

        /* 수업목표. 기존에 존재하는 생성자를 참조한 람다식을 활용할 수 있다. */
//        Function<String, Member> constMember = (str) -> {return new Member(str);};
        Function<String, Member> constMember = Member::new;

        Member member1 = constMember.apply("Lambda A");
        System.out.println("member1 = " + member1);

        Member member2 = constMember.apply("Lambda B");
        System.out.println("member2 = " + member2);
    }
}

profile
백엔드 개발자 꿈나무

0개의 댓글