자바 스터디 - 15주차

megaseunghan·2022년 8월 24일
0

자바스터디

목록 보기
15/15
post-thumbnail

목표

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

학습할 것 (필수)

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

람다

JDK 1.8 부터 등장하였다. 람다가 등장하면서 자바는 객체 지향인 동시에 함수형 프로그래밍 언어가 되었다.

  • 람다식은 익명함수라고도 한다 -> 메서드명과 리턴값이 없기 때문이다.
  • 람다식은 메서드를 위해 클래스를 새로 만들거나 메서드를 위한 객체를 생성하는 일련의 과정이 필요하지 않다.
  • 람다식은 메서드의 매개변수로 전달되는 것이 가능하며 메서드의 결과로도 반환될 수 있다.

람다식 사용법

반환 타입, 메서드 이름을 제거하고 매개변수 선언, 구현 사이에 ->를 추가한다.

  • 사용법
(매개변수 선언) -> {
    구현부 문장
}
  • 예제
(int a, int b) -> {
    a > b ? a : b;
}

매개변수가 하나라면

  • ()를 생략할 수 있다. 단, 매개변수에 타입이 있으면 생략이 불가능하다.

  • 구현부에 문장이 한 줄 이면, {}를 생략할 수 있다(;을 붙히면 안된다). 단, 리턴문이 있는 경우에는 생략할 수 없다.

a -> a * a // O
int a -> a * a // X
(int a) -> a * a // O
    
    
// BEFORE
(String name, int i ) -> {
    System.out.println(name + " = " + i)
}    

// AFTER
(String name, int i ) -> System.out.println(name + " = " + i)

// ERROR
(int a, int b) -> return a + b; // X
(int a, int b) -> { return a + b; } // O

함수형 인터페이스

자바에서 모든 메서드는 클래스 내에 포함 되어야 하는데 람다식은 익명 클래스 객체와 동등하다.

  • 람다식
(int a, int b) -> a > b ? a : b;
  • 람다는 다음 객체처럼 취급된다.
new Object() {
    int max(int a, int b) {
        return a > b ? a : b;
    }
}

참조 변수가 있어야 정의된 익명 객체의 메서드를 호출할 수 있다. 참조 변수의 타입은 참조형이기 때문에 클래스, 인터페이스가 되어야 한다.

  • max() 메서드를 정의한 인터페이스
public interface MyFunction() {
    int max(int a, int b);
}
  • max()를 구현한 객체
MyFunction f = new MyFunction() {
    public int max(int a, int b) {
        return a > b ? a : b;
    }
};
  • max()를 람다식으로 구현
MyFunction f = (int a, int b) -> a > b ? a : b; 
int max = f.max(3, 5); // 5

인터페이스를 구현한 익명 객체를 람다식으로 표현 가능한 이유가 람다식도 하나의 익명 객체이고 MyFunction 인터페이스를 구현한 람다식의 매개변수의 타입 개수, 반환값이 일치하기 때문이다.

  • 함수형 인터페이스
@FunctionalInterface
interface MyFunction {
    public abstract int max(int a, int b);
}

함수형 인터페이스는 추상 메서드를 단 하나만 가진다. 그래야 람다식과 메서드가 1:1 관계로 연결된다. 하나의 메서드가 선언된 인터페이스를 정의하여 람다식을 다루면 기존의 자바 규칙을 어기지 않는다. 따라서 인터페이스를 통해 람다식을 다루게 되었고 람다식을 다루기 위한 인터페이스를 함수형 인터페이스라고 부른다. 단, static, default 메서드는 개수의 제한이 없다.

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

메서드의 매개변수가 함수형 인터페이스 타입이면 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다는 뜻이다.

@FuncitonalInterface
interface MyFunction {
    void myMethod();
}
void aMethod(MyFunction f) {
	f.myMethod();
}

MyFunction f = () -> System.out.println("myMethod()");

aMethod(f);
  • 참조변수 없이 직접 람다식을 매개변수로 지정
aMethod(() -> System.out.println("myMethod()"));

aMethod()의 반환타입이 void가 아닌 함수형 인터페이스라면 람다식을 직접 반환할 수 있다.

MyFunction myMethod() {
    MyFunction f = () -> {};
    return f;
}
// 위 코드를 다음과 같이 반환할 수 있다.
MyFunction myMethod() {
    return () -> {};
}

예제

  • 함수형 인터페이스 MyFunction
@FunctionalInterface
interface MyFunction {
    void run();
}
  • 예제
public class Example {
    static void execute(MyFunction f) {
        f.run();
    }
    
    static MyFunction getMyFunction() {
        return () -> System.out.println("f3.run()");
    }
    
    public static void main(String[] args) {
        MyFunction f1 = () -> System.out.println("f1.run()");
        
        MyFucntion f2 = new MyFunction() {
            @Override
            public void run() {
                System.out.pritnln("f2.run()");
            }
        }
        
        MyFunction f3 = getMyFunction();
        
        f1.run();
        f2.run();
        f3.run();
    }
}
  • 실행 결과
f1.run()
f2.run()
f3.run()

Variable Capture

람다식의 바디 내에서 외부에 정의된 변수를 사용할 수 있는 것을 말한다. 이렇게 람다식의 함수 파라미터로 전달되는 변수가 아닌 외부 정의 변수를 자유 변수(Free Variable)라고 한다.

람다식 내부에서 자유변수를 참조하는 것을 람다 캡처링이라고 한다.

람다 캡처링의 제약 조건

람다 캡처링이 일어나기 위해서는 외부에 정의된 지역 변수가 final 키워드로 선언되어야 한다는 조건이 있다.

final로 선언되지 않은 지역 변수는 final처럼 동작하게 해야한다(값이 변경되면 안된다.)

조건 정리

  1. 외부 지역 변수가 final 키워드로 선언
  2. 외부 지역 변수가 final로 선언되지 않았다면 final처럼 동작하게 해야함

외부 지역 변수가 값이 바뀌거나 다시 할당되면 안된다.

지역 변수 캡처(Local Variable Capture)

지역변수 캡쳐는 람다식 바디 외부에 선언된 지역변수에 접근할 수 있다.

String localVariable = "hello";

new Thread(() -> {
   System.out.println(localVariable + ", new Thread"); 
});

람다식 내부에서 지역변수를 참조할 수 있는 이유는 변수에 대한 참조가 effectively final이기 때문이다.

람다식에서 지역변수를 참조하려면 위에서 언급한 것처럼 final이거나 final이 아니지만 값이 변경되지 않아야 한다.(만약 람다식 이전, 이후로 변수가 변경된다면 컴파일러가 람다식 내부에 에러를 띄운다.)

인스턴스 변수 캡처(Instance Variable Capture)

람다식은 람다를 생성하는 객체의 인스턴스 변수를 캡쳐할 수 있다. 박스를 생성하는 BoxFactory를 통해 알아보자.

  • BoxFactory
@FunctionalInterface
public interface BoxFactory {
    public void createBox();
}
  • Creator
public class Creator {
    private int boxCount = 0;
    
    void createOneBox() {
        BoxFactory factory = () -> this.boxCount++;
        factory.createBox();
    }
    public static void main(String[] args) {
        Creator creator = new Creator();
        creator.createOneBox();
    	System.out.println(creator.boxCount);   
    }
}

Creator 클래스의 createOneBox() 메서드에서 람다식으로 BoxFactory의 추상메서드를 구현하며 메소드를 실행한다. 메소드는 Creator의 boxCount를 1씩 추가한다.

예제의 결과로 boxCount는 1이 되는 것을 확인할 수 있다.

하지만 지역변수 캡쳐와는 다르게 외부 변수의 값을 변경할 수 있다.

정적 변수 캡쳐(Static Variable Capture)

람다식은 정적 변수를 캡처할 수 있는데, 정적 변수는 원래 자바 애플리케이션 내부 어디에서든지 접근이 가능하다. 인스턴스 변수와 마찬가지로 정적 변수의 값이 변경되어도 계속 캡쳐할 수 있다.

변수 종류별로 캡처의 제약조건이 생기는 이유

출처 : bugoverdose님 블로그

람다식 내부에서 클래스의 static 필드 또는 인스턴스 필드를 캡처하는 데에는 아무런 제약이 존재하지 않았다.

하지만 지역변수를 람다식에서 참조하려고 하는 경우는 값이 변경되지 않는 final || effectively final인 경우에만 참조가 가능했다.

지역변수에 이런 제약이 생기는 이유는 JVM의 메모리 구조와 관련이 있다. 인스턴스나 클래스 변수는 JVM의 힙, 메서드 영역에 저장되지만 지역변수는 스택에 저장되기 때문이다.

만약 지역변수의 값을 캡처한 람다를 반환하는 메서드가 있다면 해당 메서드가 종료될 때 메서드에서 선언되고 사용된 모든 지역변수는 할당이 해제된다.

그럼에도 불구하고 람다는 계속 지역변수를 아무런 문제없이 참조하여 사용할 수 있는데, 이는 람다 내부에서 사용되는 지역변수는 원본 지역변수를 복제한 데이터이기 때문이다. 그렇기 때문에 지역변수의 할당이 해제되어도 람다에선 유지가 되는 것이며 값의 변경이 이뤄지지 않아야 한다는 제약이 생겨난 것이다.

주의 사항

static, instance 필드의 경우는 제약이 존재하지 않는다. 값의 변경이 자유롭게 이뤄진다는 것이다. 위에서 언급했듯 이런 필드는 스택이 아닌 힙, 메서드(데이터) 영역에 생성되기 때문이다.

그런데 이처럼 값을 자유자재로 바꿀수 있게 되면 무분별한 참조로 인해 애플리케이션 전체에서 공유되는 가변적 자원의 값이 결국 특정 시점에서 에상치 못했던 값으로 나오는 등 여러 문제가 생기기 마련이다.

메소드, 생성자 레퍼런스

람다식을 사용하면 메서드를 아주 간결하게 표현할 수 있다. 하지만 람다식이 하나의 메서드만 호출하는 경우 더 간결하게 표현할 수 있는 방법인 메서드 참조(레퍼런스)를 사용할 수 있다.

예제 1

  • 람다식을 사용하는 경우
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
  • 메서드 레퍼런스
Function<String, Integer> f = Integer::parseInt;

람다식의 일부는 생략되었지만 컴파일러는 parseInt()메서드의 선언부에서, 또는 좌변의 지네릭타입으로부터 쉽게 알아낼 수 있다.

예제 2

  • 람다식을 사용하는 경우
BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);
  • 메서드 레퍼런스
BiFunction<String, String, Boolean> f = String::equals;

f의 타입인 BiFunction을 봐도 String 타입이 2개오는 것을 알 수 있기 때문에 s1, s2는 생략되어도 된다. 그럼 두 개의 String 값을 서로 equals()로 비교하고 그 값을 Boolean으로 반환하면 좋겠지만 같은 메서드가 다른 클래스에 존재할 수도 있기 때문에 equals앞에는 클래스 이름이 반드시 필요하다.

예제 3

이미 생성된 객체의 메서드를 람다식에서 사용한 경우, 클래스 이름대신 참조 변수를 적어줘야 한다.

  • 람다식
MyClass obj = new MyClass();

Function<String, Boolean> f = (x) -> obj.equals(x);
  • 메서드 레퍼런스
MyClass obj = new MyClass();

Function<String, Boolean> f = MyClass::equals;

생성자 레퍼런스

생성자를 호출하는 람다 표현식도 앞서 정리한 메서드 레퍼런스와 다를 것 없다. 단순히 객체를 생성하고 반환하는 람다 표현식은 생성자 레퍼런스로 변환할 수 있다. 당연한 얘기지만 생성자가 없으면 컴파일 에러가 발생한다.

  • 람다식
Supplier<MyClass> s = () -> new MyClass();
  • 생성자 레퍼런스
Supplier<MyClass> s = MyClass::new;

정리

메서드 레퍼런스를 만드는 유형은 세가지가 있다.

  1. 정적 메서드 참조 - Integer의 parseInt()를 Integer::parseInt로 사용가능
  2. 다양한 형식의 인스턴스 메서드 참조 - String의 length(), equals() 등 메서드를 String::length, String::equals로 사용가능
  3. 기존 객체의 인스턴스 메서드 참조 - MyClass의 equals()메서드를 MyClass::equals로 사용 가능

생성자 레퍼런스를 만들 수 있다. 단, 생성자가 있어야 한다.

0개의 댓글