[Java] Chap14 람다식

Seunghee Lee·2023년 3월 25일
0

Java

목록 보기
10/12

1. 람다식이란 ?

람다식은 익명 함수를 생성하기 위한 식으로 객체 지향 언어보다 함수지향 언어에 가깝다.

🤔 람다식을 사용하는 이유 ?

  • 자바 코드가 매우 간결해지고,
  • 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있다.

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

람다식 → 매개 변수를 가진 코드 블록 → 익명 구현 객체
(매개변수) -> {실행코드}

원래 코드가 다음과 같다면

Runnalbe runnable = new Runnable() {public void run() { ... }			│ 익명 구현 객체 ( new 이후부터 }; 까지를 말함 )
};

람다식으로 표현하면 다음과 같다.

Runnable runnable = () -> { ... } 		//람다식

2. 람다식 기본 문법

✅ 함수적 스타일의 람다식

(타입 매개변수, ...) -> { 실행문; ... }
  • (타입 매개변수, ...) 는 오른쪽 { 중괄호 } 블록을 실행하기 위해 필요한 값을 제공한다.
  • 매개변수 이름은 개발자가 자유롭게 지정한다.
  • -> 기호는 매개 변수를 이용해서 { 중괄호 } 를 실행한다는 뜻이다.

ex) int 매개 변수 a의 값을 콘솔에 출력하는 람다식

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

💡 매개 변수 타입은 런타임 시에 대입되는 값에 따라 자동으로 인식될 수 있기 때문에 람다식에서는 매개 변수의 타입을 일반적으로 언급하지 않는다.

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

💡 하나의 매개 변수만 있다면 ( 괄호 ) 를 생략할 수 있고, 하나의 실행문만 있다면 { 중괄호 } 도 생략할 수 있다.

a -> System.out.println(a)

💡 만약 매개 변수가 없다면 람다식에서 매개 변수 자리가 없어지므로 ( 빈 괄호 ) 를 반드시 사용해야 한다.

() -> { 실행문; ... }

💡 { 중괄호 } 를 실행하고 결과값을 리턴해야 한다면 다음과 같이 return문을 쓸 수 있다.

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

💡 { 중괄호 } 에 return 문만 있을 경우, return문을 사용하지 않고 다음과 같이 작성하는 것이 정석이다.

(x, y) -> x+y

3. 타킷 타입과 함수적 인터페이스

⚠️ 자바는 메소드를 단독으로 선언할 수 없고 항상 클래스의 구성 멤버로 선언하기 때문에 람다식은 단순히 메소드를 선언하는 것이 아니라 이 메소드를 가지고 있는 객체를 생성해 낸다.

  • 람다식은 익명 구현 클래스를 생성하고 객체화한다.
  • 람다식은 대입될 인터페이스의 종류에 따라 작성 방법이 달라지기 때문에 람다식이 대입될 인터페이스를 람다식의 타겟 타입(target type)이라고 한다.

1) 함수적 인터페이스(@FunctionalInterface)

⚠️ 람다식은 하나의 메소드를 정의하기 때문에, 두 개 이상의 추상 메소드의 구현 객체를 생성할 수 없다 !

하나의 추상 메소드가 선언된 인터페이스는 람다식의 타겟 타입이 될 수 있는데, 이러한 인터페이스를 함수적 인터페이스라고 한다.

두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹해주는 기능
@FunctionalInterface

  • 이 어노테이션은 선택사항이다.
  • 하지만 실수를 방지하고 싶다면 붙여주는 것이 좋다.

2) 매개 변수와 리턴값이 없는 람다식

✅ 코드 형태

클래스/인터페이스 변수명 = () -> { ... }

ex) 매개 변수와 리턴값이 없는 추상 메소드를 가진 함수적 인터페이스가 있다고 가정해보자.

  • [ MyFunctionalInterface.java ] - 함수적 인터페이스
@FunctionalInterface
public interface MyfunctionalInterface {
    public void method();	//매개 변수와 리턴값이 없는 메소드
}
  • [ MyFunctionalInterfaceExample.java ] - 람다식 표현
public class MyfunctionalInterfaceExample {
    public static void main(String... args) {
        MyfunctionalInterface fi;

        fi = () -> {
            String str= "method call1";
            System.out.println(str);
        };
        fi.method();

        fi = () -> {
            System.out.println("method call2");
        };
        fi.method();

        fi = () -> {
            System.out.println("method call3");
        };
        fi.method();
    }
}


3) 매개 변수가 있는 람다식

✅ 코드 형태

클래스/인터페이스 변수명 = (매개변수) -> { ... }

ex) 매개 변수가 있고 리턴값이 없는 추상 메소드를 가진 함수적 인터페이스가 있다고 보자.

  • [ MyFunctionalInterface.java ] - 함수적 인터페이스
@FunctionalInterface
public interface MyfunctionalInterface {
    public void method(int x);
}
  • [ MyFunctionalInterfaceExample.java ] - 람다식 표현
public class MyfunctionalInterfaceExample {
    public static void main(String... args) {
        MyfunctionalInterface fi;

        fi = (x) -> {
            int result = x*5;
            System.out.println(result);
        };
        fi.method(2);

        fi = (x) -> {
            System.out.println(x*5);
        };
        fi.method(2);
};


4) 리턴값이 있는 람다식

ex) 매개 변수와 리턴값이 있는 추상 메소드를 가진 함수적 인터페이스가 있다고 보자.

  • [ MyFunctionalInterface.java ] - 함수적 인터페이스
@FunctionalInterface
public interface MyfunctionalInterface {
    public int method(int x, int y);
}
  • [ MyFunctionalInterfaceExample.java ] - 람다식 표현
public class MyfunctionalInterfaceExample {
    public static void main(String... args) {
        MyfunctionalInterface fi;

        fi = (x, y) -> {
            int result = x+y;
            return result;
        };
        System.out.println(fi.method(2, 5));

        fi = (x, y) -> {
            return x+y;
        };
        System.out.println(fi.method(2, 5));
}


4. 클래스 멤버와 로컬 변수 사용

람다식의 실행 블록에는 클래스의 멤버 (필드와 메소드) 및 로컬 변수를 사용할 수 있다. 여기서 클래스 멤버는 제약 사항이 없지만, 로컬 변수는 제약 사항이 따른다.


1) 클래스의 멤버 사용

⚠️ 람다식에서 this람다식을 실행한 객체의 참조가 된다.

  • [ MyFunctionalInterface.java ] - 함수적 인터페이스
public interface MyFunctionalInterface {
    public void method();
}
  • [ UsingThis.java ] - this 사용
public class UsingThis {
    public int outterField = 10;

    class Inner {
        int innerField = 20;

        void method() {
            //람다식
            MyFunctionalInterface fi = () -> {
                    System.out.println("outterField = " + outterField);
                    /* 바깥 객체의 참조를 얻기 위해 -> 클래스명.this */
                    System.out.println("UsingThis.this.outterField = " + UsingThis.this.outterField + "\n");

                    System.out.println("innerField = " + innerField);
                    /* 람다식 내부에서 this는 -> inner 객체 참조 */
                    System.out.println("this.innerField = " + this.innerField + "\n");
            };
            fi.method();
        }
    }
}
  • [ UsingThisExample.java ] - 실행 클래스
public interface MyFunctionalInterface {
    public void method();
}


2) 로컬 변수 사용

람다식은 메소드 내부에서 주로 작성되기 때문에 로컬 익명 구현 객체를 생성시킨다고 봐야 한다.

⚠️ 람다식에서 메소드의 매개 변수 또는 로컬 변수를 사용하기 위해선 final을 사용해야 한다.

🤔 왜 final 특성을 가져야 될까 ?

☞ 매개 변수나 로컬 변수는 메소드 실행이 끝나면 스택 메모리에서 사라지기 때문에 익명 객체에서 사용할 수 없게 된다 ! 따라서 final 키워드를 선언하여 메소드 내부에 지역변수로 복사되게 함으로써 익명 객체를 사용할 수 있다.

매개 변수 또는 로컬 변수를 람다식에서 읽는 것은 허용되지만, 람다식 내부 또는 외부에서 변경할 수 없다 !

  • [ MyFunctionalInterface.java ] - 함수적 인터페이스
public interface MyFunctionalInterface {
    public void method();
}
  • [ UsingLocalVariable.java ] - Final 특성을 가지는 로컬 변수
public class UsingLocalVariable {
    public void method(int arg) {
        int localVar = 40;

        /* final 특성 때문에 수정 불가 */
        //arg = 50;
        //localVar = 50

        //람다식
        MyFunctionalInterface fi = () -> {
            System.out.println("arg = " + arg);
            System.out.println("localVar: " + localVar + "\n");
        };
        fi.method();
    }
}
  • [ UsingLocalVariableExample.java ] - 실행 클래스
public class UsingLocalVariableExample {
    public static void main(String... args) {
        UsingLocalVariable ulv = new UsingLocalVariable();
        ulv.method(20);
    }
}


5. 표준 API의 함수적 인터페이스


6. 메소드 참조

메소드 참조(Method Referenece)는 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내어, 람다식에서 불필요한 매개 변수를 제거하는 것이 목적이다.

💡 람다식은 종종 기존 메소드를 단순히 호출하는 경우가 많다.

예를 들어, 두 개의 값을 받아 큰 수를 리턴하는 Math 클래스의 max() 정적 메소드를 호출하는 람다식은 다음과 같다.

(left, right) -> Math.max(left, right);

람다식은 단순히 두 개의 값을 Math.max() 메소드의 매개값으로 전달하는 역할만 하기 때문에, 다음과 같이 메소드 참조를 이용하면 깔끔하게 처리할 수 있다.

Math :: max;	//메소드 참조

메소드 참조는 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 타겟 타입인 인터페이스의 추상 메소드가 어떤 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라진다.

💡 메소드 참조는 정적 또는 인스턴스 메소드를 참조할 수 있고, 생성자 참조도 가능하다.

1) 정적 메소드와 인스턴스 메소드 참조

✅ 코드형태 - 정적(static) 메소드를 참조할 경우

클래스 :: 메소드

✅ 코드형태 - 인스턴스 메소드를 참조할 경우

참조변수 :: 메소드

ex) Calculator의 정적 및 인스턴스 메소드를 참조해보자

  • [ Calculator.java ] - 정적 및 인스턴스 메소드
public class Calculator {
    public static int staticMethod(int x, int y) {
        return x+y;
    }

    public int instanceMethod(int x, int y) {
        return x+y;
    }
}
  • [ MethodReferenceExample.java ] - 정적 및 인스턴스 메소드
public class MethodReferenceExample {
    public static void main(String... args) {
        IntBinaryOperator operator;

        //정적 메소드 참조
        operator = (x,y) -> Calculator.staticMethod(x,y);
        System.out.println("Result1 = " + operator.applyAsInt(1, 2));

        operator = Calculator :: staticMethod;
        System.out.println("Result2 = " + operator.applyAsInt(3, 4));

        //인스턴스 메소드 참조
        Calculator obj = new Calculator();
        operator = (x,y) -> obj.instanceMethod(x, y);
        System.out.println("Result3 = " + operator.applyAsInt(5, 6));

        operator = obj :: instanceMethod;
        System.out.println("Result4 = " + operator.applyAsInt(7, 8));
    }
}

2) 매개 변수의 메소드 참조

✅ 코드 형태

클래스 :: 메소드		//정적 메소드 참조와 동일하지만 전혀 다른 코드가 실행된다.

ex) 두 문자열이 대소문자 구분없이 동일한 알파벳으로 구성되어 있는지 비교해보자.

  • [ ArgumentMethodReferenceExample.java ] - 매개 변수의 메소드 참조
public class ArgumentMethodReferenceExample {
    public static void main(String... args) {
        ToIntBiFunction<String, String> function;

        function = (a,b) -> a.compareToIgnoreCase(b);
        print(function.applyAsInt("Java8", "JAVA8"));

		/* 메소드 참조 표현 */
        function = String::compareToIgnoreCase;
        print(function.applyAsInt("Java8", "JAVA8"));
    }

    private static void print(int order) {
        if(order < 0) {
            System.out.println("사전 순으로 먼저 옵니다.");
        }
        else if (order == 0) {
            System.out.println("동일한 문자열입니다.");
        }
        else {
            System.out.println("사전순으로 나중에 옵니다.");
        }
    }
}

3) 생성자 참조

생성자를 참조한다는 것은 객체 생성이 가능하다는 것을 의미한다.

단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치할 수 있다.

✅ 코드 형태

클래스 :: new

ex) 생성자 참조를 이용해 객체를 생성해 보자.

  • [ ConstructorReferenceExample.java ] - 생성자 참조
public class ConstructorReferenceExample {
    public static void main(String... args) {
        /* 생성자 참조 - 매개변수 1개 */
        Function<String, Member> function1 = Member::new;
        Member member1 = function1.apply("angel");

        /* 생성자 참조 - 매개변수 2개 */
        BiFunction<String, String, Member> function2 = Member::new;
        Member member2 = function2.apply("신천사", "angel");
    }
}
  • [ Member.java ] - 생성자 오버라이딩
public class Member {
    private String name;
    private String id;

    public Member() {
        System.out.println("Member() 실행");
    }

    public Member(String id) {
        System.out.println("Member(String id) 실행");
    }

    public Member(String name, String id) {
        System.out.println("Member(String name, String id) 실행");
        this.name = name;
        this.id = id;
    }
}

profile
자라나라 개발개발 ~..₩

0개의 댓글