본문 바로가기
자바

[Java] 자바의 람다식이란?

by 책 읽는 개발자_테드 2021. 3. 6.
반응형

학습 목표

· 람다식이란? 

   - 함수형 프로그래밍이란?

· 람다식 사용법

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

   - 추상 메소드 선언 형태에 따른 람다식 작성법

· Variable Capture

· 메서드 참조


람다식이란?

 

· 익명 함수(Anonymous function)을 생성하기 위한 식

· 자바8 버전에 도입되어 자바에서 함수형 언어의 장점을 누릴 수 있게 됨

 

자바에서 람다식의 형태는 매개 변수를 가진 코드 블록이지만, 런타임 시에는 익명 구현 객체를 생성한다.

람다식 -> 매개 변수를 가진 코드 블록 -> 익명 구현 객체

 

* 함수형 프로그래밍이란?

객체지향은 동작하는 부분을 캡슐화해서 이해할 수 있게 하고, 함수형 프로그래밍은 동작하는 부분을 최소화해서 코드 이해를 돕는다. - 마이클 페더스‘레거시 코드 활용 전략' 저자 - 
 
함수형 프로그래밍은 순수 함수를 조합하고, 변경 가능한 공유상태를 피하여 부작용을 없애는 것을 기본 원칙으로 소프트웨어를 구성하는 프로그래밍 패러다임이다.

순수 함수란 외부의 상태를 변경하거나 외부에서 함수로 들어온 인자의 상태를 변경하는 부수효과가 없고, 동일한 인자를  주었을 때 항상 같은 값을 리턴하는 함수다.

이러한 특징으로 다음과 같은 장점을 갖는다.
1. 간결한 코드 작성을 할 수 있다. (함수 이름이 없기 때문에)
2. 불변성을 지향하기 때문에 프로그램의 동작을 예측하기 쉬워진다.
3. 부수효과가 없으므로 멀티스레드 환경에서 안전하다.
https://dinfree.com/lecture/language/112_java_9.html

람다식 사용법

(타입 매개변수, ...) -> (실행문; ...)

(int a) -> { System.out.println(a);}

· (타입 매개변수, ...)는 오른쪽 중괄호 {} 블록을 실행하기 위해 필요한 값을 제공하는 역할

· 매개 변수 이름은 개발자의 자유

 

·  -> 기호는 매개 변수를 이용해서 중괄호 {}를 실행한다는 뜻

(int a) -> { System.out.println(a);}

 

·  매개 변수 타입은 런타임 시에 대입되는 값에 따라 자동으로 인식

(a) -> { System.out.println(a);}

 

· 하나의 매개 변수만 있다면 괄호 생략 가능. 하나의 실행문만 있다면 중괄호도 생략 가능.

a -> System.out.println(a)

 

· 매개 변수가 없다면 람다식에서 매개 변수 자리가 없어지므로 빈 괄호를 반드시 사용

() -> {System.out.println("실행");}

 

· return문으로 결과값 지정 가능

(x, y) -> {return x + y; };

 

· 중괄호 {}에 return 문만 있을 경우, 식(expression)으로 대체 가능

(x, y) -> x + y

 

함수형 인터페이스

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

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

// 위 아래 코드는 동등 하다 //

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

 

람다식으로 정의된 익명 객체의 메서드를 호출하기 위해 익명 객체의 주소를 참조변수에 저장해보자.

인터페이스 f = (int a, int b) -> a > b ? a : b;

 

이 처럼 람다식은 인터페이스 변수에 대입할 수 있다. 즉, 람다식은 인터페이스의 익명 구현 객체를 생성한다

람다식은 대입될 인터페이스의 종류에 따라 작성 방법이 달라진다.

람다식이 대입될 인터페이스를 람다식의 타겟 타입이라고 한다.

 

·  하나의 추상 메소드가 선언된 인터페이스만 람다식의 타겟 타입이 될 수 있음

·  이러한 인터페이스를 함수형 인터페이스(functional interface)라고 함

·  함수형 인터페이스를 선언할 때 @FunctionalInterface 어노테이션을 붙이면, 두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹함

@FunctionalInterface
publicinterface MyFunctionalInterface {  
	public void method();   
	public void otherMethod(); //컴파일러가 확인하여 컴파일 오류 발생
}

 

추상 메소드 선언 형태에 따른 람다식 작성법

· 람다식은 타겟 타입인 함수형 인터페이스가 가지고 있는 추상 메소드의 선언 형태에 따라 작성 방법이 달라짐

 

▶ 예시 - 추상 메서드에 매개 변수가 없고, 리턴 값도 없는 경우

@FunctionalInterface
public interface MyFunctionalInterface {
   public void method();
}

 

· 위 코드 처럼 추상 메소드에 '매개 변수가 없고, 리턴값도 없다면' 람다식은 다음과 같은 형태로 작성하고, 사용 가능

 

MyFunctionalInterface fi = () -> {};
fi.method();

 

▶ 예시 - 추상 메서드에 매개 변수가 있고, 리턴 값은 없는 경우

 

@FunctionalInterface
public interface MyFunctionalInterface {
   public void method(int x);
}

 

· 위 코드 처럼 추상 메소드에 '매개 변수가 있고, 리턴값은 없는' 람다식은 다음과 같은 형태로 작성하고, 사용 가능

 

 

MyFunctionalInterface fi = (x) -> {}; //1
MyFunctionalInterface fi2 = x -> {}; //2
fi.method(5);

 

▶ 예시 - 추상 메서드에 매개 변수가 있고, 리턴 값도 있는 경우

@FunctionalInterface
public interface MyFunctionalInterface {
	public int method(int x, int y);
}

 

· 위 코드 처럼 추상 메소드에 '매개 변수가 있고, 리턴값도 있는' 람다식은 다음과 같은 형태로 작성하고, 사용 가능

 

public class Main {

   public static void main(String[] args){

       MyFunctionalInterface fi = (x,y)-> {
           return x + y;
       }; //1

       MyFunctionalInterface fi2 = (x,y) -> x+y;   //2
       MyFunctionalInterface fi3 = (x,y) -> { return sum(x,y); }; //3
       MyFunctionalInterface fi4 = (x, y) -> sum(x,y); //4

       int result1 = fi.sum(1,2);
       int result2 = fi2.sum(1,2);
       int result3 = fi3.sum(1,2);
       int result4 = fi4.sum(1,2);
   }

   public static int sum(int x, int y){
       return (x + y);
   }
}

 

· 람다식에서 this 키워드를 사용하면, 람다식실행한 객체를 참조

· 익명 객체 내부에서 this익명 객체의 참조지만, 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체라는 점에 주의

@FunctionalInterface
public interface MyFunctionalInterface {
   public void method();
}

 

public class Outter {

   private int outterField = 10;

   class Inner{

       private int innerField =20;

       void method(){

           MyFunctionalInterface fi = () ->{
               System.out.println("outterField: " + outterField);
               System.out.println("outterField: " + Outter.this.outterField);
               System.out.println("innerField: " + innerField);
               System.out.println("innerField: " + this.innerField);
           };

           MyFunctionalInterface fi2 = new MyFunctionalInterface() {
               int num = 30;

               @Override
               public void method() {
                   System.out.println("익명 객체의 this는 익명 객체의 참조 : " + this.num);
               }
           };
           
           fi.method();
           fi2.method();
       }
   }
}

 

public class Main {

   public static void main(String[] args){

       Outter outter = new Outter();
       Outter.Inner inner = outter.new Inner();
       inner.method();
   }
}

 

· 빈번하게 사용되는 함수형 인터페이스는 java.util.function 표준 API 패키지로 제공함

표준 API와 일치하는 형태의 함수형 인터페이스라면, 직접 함수형 인터페이스를 만들지 않고 편하게 가져다 사용할 수 있다.

 

자세한 내용은 아래 링크를 참조하세요!

scshim.tistory.com/287

 

[Java] 표준 API의 함수형 인터페이스, java.util.function 패키지

이 글은 java.util.function 패키지의 함수형 인터페이스 표준 API에 대해서 설명합니다. 학습 목표 · java.util.function 패키지 · Consumer · Supplier · Function · Operator · Predicate java.util.f..

scshim.tistory.com

 

Variable Capture

 

람다식의 실행 블록에서 클래스 멤버(필드와 메소드) 및 지역변수를 사용할 수 있다.

이때, 클래스 멤버는 제약 없이 사용 가능하고, 지역변수는 제약 사항이 따른다.

 

람다식에서 메소드의 매개 변수 또는 지역 변수를 사용하려면 final 특성(final 또는 effectively final)을 가져야한다.

 

왜 이런 특징을 갖고 있을까?

 

익명 구현 객체를 포함해 객체를 생성하면, jvm의 동적 메모리 할당 영역인 heap 영역에 객체가 생성된다.

이렇게 생성된 객체는 자신을 감싸고 있는 멤버 메서드의 실행이 끝난 후에도 heap 영역에 존재하므로 사용할 수 있다.

 

하지만 이 멤버 메서드에 정의된 매개변수나 지역변수는 jvm의 런타임 스택 영역에 할당되고, 메서드 실행이 끝나면 사라져 더 이상 사용할 수 없다.

 

따라서 멤버 메서드 내부에서 생성된 객체가 자신을 감싸고 있는 메서드의 매개변수나 지역변수를 사용하려 할 때 문제가 생길 수 있다.

 

자바에서는 이 문제를 variable capture라고 하는 값 복사를 사용해 해결한다. 컴파일 시점에 매개변수나 지역 변수를 멤버 메서드 내부에서 생성한 객체가 사용할 경우, 객체 내부로 값을 복사해 사용한다. 

 

단, final 키워드로 작성되거나 final 성격을 가져야 한다는 제약이 있다.

 

▶ 예시 - Variable Capture 사용

@FunctionalInterface
public interface MyFunctionalInterface {
   public void method();
}

public class UsingLocalVariable {

   void method(int arg){
       int localVar = 40;
       // localVar = 100; final 특성으로 수정 불가
       // arg = 100; final 특성으로 수정 불가

       MyFunctionalInterface fi = () -> {
           System.out.println("arg: " + arg);
           System.out.println("localVar: " + localVar);
       };
       fi.method();
   }
}

 

public class Main {

   public static void main(String[] args){

       UsingLocalVariable usingLocalVariable = new UsingLocalVariable();
       usingLocalVariable.method(123);
   }
}

 

메서드 참조

· 자바 8부터 더블 클론(::)을 사용하여 메서드 참조 가능

 

▶ 예시 - System.out.println 메서드 참조

        IntStream.range(0,100)
                .forEach(System.out::println);

· 메서드 참조의 종류

종료
static 메서드 참조 ContainingClass::staticMethodName
특정 객체의 인스턴스 메서드 참조 containingObject:: nstanceMethodName
특정 유형의 임의의 객체에 대한 인스턴스 메서드 참조 ContainingType::methodName
생성자 참조 ClassName::new

 

static 메서드 참조

▶ 예시 

public class MethodReferenceDemo {
    public static void main(String[] args){
        MethodReferenceDemo demo = new MethodReferenceDemo();
        String[] strArr = {"str1","str2","str3"};
        demo.staticReference(strArr);
    }

    private static void printResult(String value){
        System.out.println(value);
    }

    private void staticReference(String[] stringArray){
        Stream.of(stringArray).forEach(MethodReferenceDemo::printResult);
    }
}

결과

· 위 코드는 MethodReferenceDemo 클래스르 printResult()라는 static 메서드를 참조함

 

특정 객체의 인스턴스 메서드 참조

▶ 예시 

        IntStream.range(0,100)
                .forEach(System.out::println);

· 위 코드는 System.out::println과 같이 System 클래스에 선언된 out 변수의 println() 메서드를 참조함

· 즉, 변수에 선언된 메서드 참조를 의미

 

특정 유형의 임의의 객체에 대한 인스턴스 메서드 참조

▶ 예시 

    private void objectReference(String[] strArr){
        Arrays.sort(strArr, String::compareToIgnoreCase); //임의 객체 참조
        Arrays.asList(strArr).stream().forEach(System.out::println); //인스턴스 메서드 참조
    }

 

· 위 코드에서 String::compareToIgnoreCase는 static 참조처럼 사용한다. 하지만 String 클래스에 compareToIgnoreCase는 아래와 같이 정의되어 있다. 이와 같이 메서드 참조를 사용할 수도 있다.

 

    public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }

 

생성자 참조

▶ 예시 

    interface MakeString{
   	String fromBytes(char[] chars);
    }

    private void createInstance(){
        MakeString makeString = String::new;
        char[] chars = {'a','b','c'};
        String madeString = makeString.fromBytes(chars);
        System.out.println(madeString);
    }

· 위 코드와 같이 '생성자::new'의 형태로 생성자 참조가 가능함

 

출처이것이 자바다

자바의 정석

yadon079.github.io/2021/java%20study%20halle/week-15

 

반응형

댓글