슬기로운 개발생활

온라인 자바 스터디 # 15 - 람다식

by coco3o
반응형

목표

자바의 람다식에 대해 학습하세요.

학습할 것 (필수)

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

람다식이란?

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

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

 

람다식은 FunctionalInterface의 조건을 충족 해야 사용이 가능

FunctionalInterface : 오직 하나의 메소드 선언을 갖는 인터페이스

 

※메소드와 함수

메소드와 함수는 같은 의미지만 용어를 분리해서 사용했다. Java의 메소드는 객체의 행위,동작을 의미하며 특정 클래스에 반드시 속해야 한다는 제약이 있다. 그러나 JDK 1.8이후 람다식의 도입으로 메소드가 하나의 독립적인 기능을 하기 때문에 익명 함수라는 용어를 사용한다.

 

람다식 사용법

 

메소드의 이름과 반환타입을 제거하고 매개변수 선언부와 body{  } 사이에 ->를 추가한다.

ReturnType methodName(Parameter p) { 
	// body
}

(Parameter p) -> {
	// body
}

두 값 중에 큰 값을 반환하는 메소드 max()를 람다식으로 변환하기

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

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

return문 대신 식(expression)으로 대신할 수 있다. 식의 연산 결과가 자동으로 반환값이 된다.

문장이 아닌 식으로 끝에 세미콜론( ; )을 붙이지 않는다.

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

매개변수의 타입은 추론이 가능한 경우(대부분의 경우) 생략 가능하다.

참고로 반환타입을 제거할 수 있는 이유도 항상 추론이 가능하기 때문이다.

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

 

 

람다식 작성 문법 정리

 

1. 기본적인 작성 규칙
- 이름과 반환타입은 작성하지 않는다. (anonymous function)

2. 매개변수
- 추론이 가능한 매개변수의 타입은 생략할 수 있다.
- 단, 매개변수가 두 개 이상일 경우 일부의 타입만 생략하는 것은 허용되지 않는다.
- 선언된 매개변수가 하나인 경우 괄호( )를 생략 할 수 있다.
- 단, 매개변수의 타입을 작성한 경우엔 매개변수가 하나라도 괄호( )를 생략할 수 없다.

3. body { }
- return문(return statement) 대신 식(expression)으로 대체할 수 있다.
- 식(expression)의 끝에 세미콜론(;)은 붙이지 않는다.
- 괄호{ } 안의 문장이 하나일 때는 괄호{ }를 생략할 수 있다.
- 이 때, 문장의 끝에 세미콜론(;)은 붙이지 않는다.
- 그러나 return문은 괄호를 생략할 수 없다.

 

매개변수 규칙
  • 추론이 가능한 매개변수의 타입은 생략할 수 있다.
  • 단, 매개변수가 두 개 이상일 경우 일부의 타입만 생략하는 것은 허용되지 않는다.
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b

 

  • 선언된 매개변수가 하나인 경우 괄호( )를 생략 할 수 있다.
  • 단, 매개변수의 타입이 있으면 괄호( )를 생략할 수 없다.
a -> a * a	// OK

int a -> a * a	// Error

body { } 규칙
  • return문 대신 식으로 대체 할 수 있다.
  • 단, 식의 끝에 세미콜론(;)은 붙이지 않는다.
(int a, int b) -> {
	return a > b ? a : b;
}
(int a, int b) -> a > b ? a : b

 

  • 괄호 { } 안의 문장이 하나일 때 괄호 { }를 생략 할 수 있다.
  • 단, 문장의 끝에 세미콜론(;)은 붙이지 않는다.
(String name, int i) -> { System.out.println(name + "=" + i); }
(String name, int i) -> System.out.println(name + "=" + i)

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

 

함수형 인터페이스는 람다식을 다루는 인터페이스이다. @FunctionalInterface 어노테이션을 사용한다.

 

람다식은 실제로 메소드 그 자체가 아닌 익명 클래스의 객체와 동등하다.

 

익명 객체의 메소드와 람다식의 매개변수, 반환값이 일치하면 익명 객체를 람다식으로 대체 할 수 있다.

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

위 인터페이스를 구현한 익명 클래스의 객체는 아래와 같이 생성 할 수 있다. 

MyFunction f = new MyFunction() {	//MyFunction인터페이스를 구현한 익명 클래스의 객체 생성
	public int max(int a, int b) {
    return a > b ? a : b
    }
}
int big = f.max(5, 3);	// 익명 객체의 메소드 호출    

여기서 MyFunction 인터페이스에 정의된 메소드 max( )는 람다식 '(int a, int b) -> a > b ? a : b' 와 일치한다.람다식은 익명 객체와 동등하므로 아래와 같이 대체 할 수 있다.

MyFunction f = (a , b) -> a > b ? a : b;	// 익명 객체를 람다식으로 대체

int big = f.max(5 , 3);	// 익명 객체의 메소드 호출

이렇게 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체 할 수 있는 이유는

람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메소드 max( )와

람다식의 매개변수 타입과 개수 그리고 반환값이 일치하기 때문이다.

 

하나의 메소드가 선언된 인터페이스를 정의해서 

람다식을 이용하는 것은 기존의 자바의 규칙을 어기지 않으면서 자연스럽다.

그렇기 때문에 인터페이스를 통해 람다식을 다루기로 결정되었고,

람다식을 다루기 위한 인터페이스를 함수형 인터페이스(functional interface)라 부르기로 했다.

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

단, 함수형 인터페이스에서는 오직 하나의 추상 메소드만 정의되어 있어야 한다는 제약이 있다.

그래야 람다식과 인터페이스의 메소드가 1 : 1로 연결될 수 있기 때문이다.

다만, static 메소드와 default 메소드의 개수에는 제약이 없다.

함수형 인터페이스로 구현한 인터페이스라면 반드시 '@FunctionalInterface' 어노테이션을 정의하자.

왜냐하면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해주기 때문이다.

 

기본 함수형 인터페이스

자바에서 기본적으로 제공하는 함수형 인터페이스는 다음과 같은 것들이 있다.

  • Runnable
  • Supplier
  • Consumer
  • Function<T, R>
  • Predicate

이 외에도 다양한 것들이 있다.

 

Runnable

Runnable은 인자를 받지 않고 리턴값도 없는 인터페이스이다.

public interface Runnable {
	public abstract void run();
}    

아래 코드처럼 사용할 수 있다.

Runnable runnable = ( ) -> System.out.println("run anything!");
runnable.run();
// 결과
// run anything!

Runnable은 run( )을 호출해야 한다. 함수형 인터페이스마다 run( )과 같은 실행 메소드 이름이 다르다.인터페이스 종류마다 만들어진 목적이 다르고, 그 목적에 맞는 이름을 실행 메소드 이름으로 정하였기 때문이다.

 

Supplier

Supplier<T>는 인자를 받지 않고 T타입의 객체를 리턴한다.

public interface Supplier<T> {
	T get();
}    

아래 코드처럼 사용할 수 있다.

Supplier<String> getString = () -> "Good morning!";
String str = getString.get();
System.out.println(str);

// 결과
// Good morning!

 

Consumer

Consumer<T>는 T 타입의 객체를 인자로 받고 리턴 값은 없다.

public interface Consumer<T> {
	void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
    	Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

아래 코드처럼 사용할 수 있다.

Consumer<String> printString = text -> System.out.println("Help " + text + "!!");
printString.accept("me");
// 결과
// Help me!!

또한, andThen( )을 사용하면 두개 이상의 Consumer를 연속적으로 실행할 수 있다.

Consumer<String> printString = text -> System.out.println("Help " + text + "!!");
Consumer<String> printString2 = text -> System.out.println("--> Yes");
printString.andThen(printString2).accept("me");
// 결과
// Help me!!
// --> Yes

 

Function

Function<T, R>는 T타입의 인자를 받고, R타입의 객체를 리턴한다.

public interface Function<T, R> {
	R apply(T t);
    
    default <V> FUnction<V, R> compose(Function<? Super V, ? extends T> before) {
    	Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
        default <V> FUnction<T, V> compose(Function<? Super R, ? extends V> after) {
    	Objects.requireNonNull(after);
        return (T t) -> after.apply(after.apply(t));
    }
    
    static <T> Function<T, T> identity() {
    	return t -> t;
    }
}

다음과 같이 사용할 수 있다. apply() 메소드를 사용한다.

Function<Integer, Integer> multiply = (value) -> value *2;
Integer result = multiply.apply(3);
System.out.println(result);
// 결과
// 6

 

compose( ) 는 두개의 Function을 조합하여 새로운 Function 객체를 만들어주는 메소드이다.

주의할 점은 andThen() 과는 실행 순서가 반대이다.

compose( ) 인자로 전달되는 Function이 먼저 수행되고 그 이후에 호출하는 객체의 Function이 수행된다.

 

예를들어, 다음과 같이 compose를 사용하여 새로운 Function을 만들 수 있다. 

apply를 호출하면 add 먼저 수행되고 그 이후에 multiply가 수행된다.

Function<Integer, Integer> multiply = (value) -> value * 2;
Function<Integer, Integer> add      = (value) -> value + 3;

Function<Integer, Integer> addThenMultiply = multiply.compose(add);

Integer result1 = addThenMultiply.apply(3);
System.out.println(result1);
// 결과
// 12

 

Predicate

Predicate<T> 는 T타입 인자를 받고 결과로 boolean을 리턴한다.

public interface Predicate<T> {
    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

다음과 같이 사용할 수 있다. test( ) 메소드를 사용한다.

Predicate<Integer> isBiggerThanFive = num -> num > 5;
System.out.println("10 is bigger than 5? -> " + isBiggerThanFive.test(10));
// 결과
// 10 is bigger than 5? -> true

 

and( )와 or( )는 다른 Predicate와 함께 사용된다.

and( )는 두 개의 Predicate가 true일 때 true를 리턴하며, or( )는 두 개중에 하나만 true면 true를 리턴한다.

Predicate<Integer> isBiggerThanFive = num -> num > 5;
Predicate<Integer> isLowerThanSix = num -> num < 6;
System.out.println(isBiggerThanFive.and(isLowerThanSix).test(10));
System.out.println(isBiggerThanFive.or(isLowerThanSix).test(10));
// 결과
// false
// true

 

isEqual( )은 static 메소드로, 인자로 전달되는 객체와 같은지 체크하는 Predicate 객체를 만들어 준다.

Predicate<String> isEquals = Predicate.isEqual("Google");
isEquals.test("Google");
// 결과
// true

Variable Capture

람다식에서 외부 지역변수를 참조하는 행위를  Lambda Capturing(람다 캡쳐링)  이라고 한다.

 

람다에서 접근가능한 변수는 아래와 같이 세가지 종류가 있다.

 

1. 지역 변수

2. static 변수

3. 인스턴스 변수

 

지역변수만 변경이 불가능하고 나머지 변수들은 읽기 및 쓰기가 가능하다.

람다는 지역 변수가 존재하는 스택에 직접 접근하지 않고, 지역 변수를 자신(람다가 동작하는 쓰레드)의 스택에 복사한다.

각각의 쓰레드마다 고유한 스택을 갖고 있기 때문에  지역 변수가 존재하는 쓰레드가 사라져도

람다는 복사된 값을 참조하면서 에러가 발생하지 않는다.

 

그런데 멀티 쓰레드 환경이라면, 여러 개의 쓰레드에서 람다식을 사용하면서 람다 캡쳐링이 계속 발생하는데

이 때 외부 변수 값의 불변성을 보장하지 못하면서 동기화 문제가 발생한다.

이러한 문제로 지역변수 final,  Effectively Final  제약조건을 갖게된다.

Effectively Final
람다식 내부에서 외부 지역변수를 참조하였을 때 지역 변수는 재할당을 하지 않아야 하는 것을 의미

 

인스턴스 변수나 static 변수는 스택 영역이 아닌 힙 영역에 위치하고,

힙 영역은 모든 쓰레드가 공유하고 있는 메모리 영역이기 때문에, 값의 쓰기가 발생하여도 별 문제가 없는 것이다.

@FunctionalInterface
interface MyFunction {
    void myMethod();
}

class Outer {
    int val = 10;

    class Inner {
        int val = 20;

        void method(int i) {    // void method(final int i)
            int val = 30;   // final int val = 30;
//            i = 10;       // ERROR. 상수의 값은 변경할 수 없다.

            MyFunction 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();
        }
    } // End Of Inner
}   // End Of Outer

public class LambdaEx {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method(100);
    }
}
// 결과
             i : 100
           val : 30
      this.val : 21
Outer.this.val : 11

 

  • 람다식 내에서 참조하는 지역변수는 final이 붙지 않아도 상수로 간주된다.
  • 람다식 내에서 지역변수 i와 val을 참조하고 있으므로 람다식 내에서나 다른 곳에서도 이 변수들의 값을 변경할 수 없다.
  • 반면, Inner 클래스와 Outer 클래스의 인스턴스 변수인 this.val과 Outer.this.val은 상수로 간주되지
  • 않아서 값을 변경해도 된다.
  • 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조다.
  • 따라서 위의 코드에서 this는 중첩 객체인 Inner이다.

 


메소드, 생성자 레퍼런스

 

메소드 참조

람다식이 하나의 메소드만 호출하는 경우에는 '메소드 참조(method reference)' 를 통해 람다식을 간략하게 작성할 수 있다.

 

메소드 참조를 작성하는 방법은 아래와 같다.

클래스이름::메소드이름
or
참조변수::메소드이름
Function<String, Integer> f = s -> Integer.parseInt(s);

보통 위처럼 람다식을 작성하는 데, 이 람다식을 메소드로 표현하면 아래와 같다.

// Test Method
Integer exam(String s) {
	return Integer.parseInt(s)
}

이 wrapper 메소드는 별로 하는 일이 없다. 값을 받아서 Integer.parseInt()에게 넘겨줄 뿐이다.

 

Function<String, Integer> f = s -> Integer.parseInt(s);

// 메소드 참조
Function<String, Integer> f = Integer::parseInt;

메소드 참조를 이용하면 더 간략하게 작성할 수 있다.

위 메소드 참조에서 람다식 일부가 생략되었지만, 컴파일러는 생략된 부분을 우변의 parseInt 메소드의 선언부로부터,

또는 좌변의 Function 인터페이스에 지정된 제네릭 타입으로부터 쉽게 알아낸다.

 

또 다른 예를 보자.

BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equlas(s2);

위 람다식에서 어떤 부분을 변경할 수 있을까?

 

  • 참조변수 f의 타입으로 유추하면 람다식이 두 개의 String 타입의 매개변수를 받는다. 따라서, 매개변수 생략가능.
  • 매개변수를 생락하면, equals만 남는데, 두 개의 String을 받아서 Boolean을 반환하는 equals라는 메소드 이므로
  • String::equals로 변경한다.
BiFunction<String, String, Boolean> f = String::equals;

 

메소드 참조로 변경한 결과이다.

MyClass obj = new MyClass();
Function<String, Boolean> f = x -> obj.equals(x);   //  람다식
Function<String, Boolean> f2 = obj::equals;         //  메소드 참조

메소드 참조를 사용하는 경우가 한 가지 더 있는데, 이미 생성된 객체의 메소드를 람다식에서 사용한 경우에는

클래스 이름 대신 그 객체의 참조변수 를 적어야 한다.

종류 람다 메소드 참조
static 메소드 참조 x -> ClassName.method(x) ClassName::method
인스턴스 메소드 참조 (obj, x) -> obj.method(x) ClassName:method
특정 객체 인스턴스 메소드 참조 x -> obj.method(x) obj::method

생성자의 메소드 참조

생성자를 호출하는 람다식도 메소드 참조로 변환할 수 있다.

Supplier<MyClass> s = () -> new MyClass();      //  람다식
Supplier<MyClass> s = MyClass:new;              //  메소드 참조

매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.

Function<Integer, MyClass> f = i -> new MyClasS(i);     //  람다식
Function<Integer, MyClass> f = MyClass:new;             //  메소드 참조

BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);  //  람다식
BiFunction<Integer, String, MyClass> bf = MyClass::new;                 //  메소드 참조

만약 배열을 생성할 때는 다음과 같이 사용한다.

Function<Integer, int[]> f = x -> new int[x];   //  람다
Function<Integer, int[]> f2 = int[]::new;       //  메소드 참조

 

예제

 

public class MethodReferences {
    public static void main(String[] args) {
//        Function<String, Integer> f = s -> Integer.parseInt(s);
        Function<String, Integer> f = Integer::parseInt;

        System.out.println(f.apply("100") + 200);

        // Supplier 입력 X, 출력 O
//        Supplier<MyClass> s = () -> new MyClass();
        Supplier<MyClass> s = MyClass::new;
        System.out.println(s.get());

        Function<Integer, MyClass> f2 = MyClass::new;
        MyClass m = f2.apply(100);
        System.out.println(m.iv);
        System.out.println(f2.apply(200).iv);

        Function<Integer, int[]> f3 = int[]::new;
        System.out.println(f3.apply(10).length);

    }
}

class MyClass {
    int iv;

    MyClass () {}

    MyClass (int iv) {
        this.iv = iv;
    }
}

 

 

references : atoz-develop.tistory.com/entry/JAVA-%EB%9E%8C%EB%8B%A4%EC%8B%9DLambda-Expression www.notion.so/758e363f9fb04872a604999f8af6a1ae codechacha.com/ko/java8-functional-interface/ watrv41.gitbook.io/devbook/java/java-live-study/15_week

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기