[Java] 람다 표현식(Lambda Expression)

2023. 10. 18. 14:21프로그래밍/Java

람다라는 표현은 프로그램을 처음 배울 때 얼핏 들은 기억이 있다. 

그러나 척 보기에도 사용하기 어려워보이고 그 당시의 내 수준에서는 공부할 부분이 아니라 생각해 넘기곤 했다.

그러나 생각해보면 JDK별로 다른 버전을 제공하고 내가 왜 그 버전을 사용해야 하는지에 대한 고민은 거의 해보지 않았던 것 같다.

그냥 라이브러리와 여러 프레임워크간의 호환성만을 위해 언어의 버전을 반 강제적으로 좇았기 때문이다.

 

- 람다 표현식이란 무엇인가?

JDK8버전부터 추가된 Lambda는 Stream과도 관련이 있는 듯하다.

Oracle 공식 문서를 통해 알게 된 간단한 정보로는 익명클래스(인터페이스 포함)의 사용을 더욱 더 간결하게 해준다는 장점이 있었다.

JDK8버전부터는 람다 표현식과 스트림 기능을 추가하게 됐다. 즉 인터페이스를 익명클래스로 사용할 때 간단한 표현식으로 나타내는 것이 '람다 표현식' 인 것이다. 

 

Oracle에서 제공한 예제를 통해 살펴보자.

package lambda;


import java.time.LocalDate;
import java.time.chrono.IsoChronology;
import java.util.ArrayList;
import java.util.List;


interface CheckPerson2 {
    boolean test(Person p);
}

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    Person(String nameArg, LocalDate birthdayArg,
           Sex genderArg, String emailArg) {
        name = nameArg;
        birthday = birthdayArg;
        gender = genderArg;
        emailAddress = emailArg;
    }

    interface CheckPerson {
        boolean test(Person p);
    }

    public int getAge() {
        return birthday
                .until(IsoChronology.INSTANCE.dateNow())
                .getYears();
    }

    public void printPerson() {
        System.out.println(name + ", " + this.getAge());
    }

    public Sex getGender() {
        return gender;
    }

    public String getName() {
        return name;
    }

    public String getEmailAddress() {
        return emailAddress;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }

    public static List<Person> createRoster() {

        List<Person> roster = new ArrayList<>();
        roster.add(
                new Person(
                        "Fred",
                        IsoChronology.INSTANCE.date(1980, 6, 20),
                        Person.Sex.MALE,
                        "fred@example.com"));
        roster.add(
                new Person(
                        "Jane",
                        IsoChronology.INSTANCE.date(1990, 7, 15),
                        Person.Sex.FEMALE, "jane@example.com"));
        roster.add(
                new Person(
                        "George",
                        IsoChronology.INSTANCE.date(1991, 8, 13),
                        Person.Sex.MALE, "george@example.com"));
        roster.add(
                new Person(
                        "Bob",
                        IsoChronology.INSTANCE.date(2000, 9, 12),
                        Person.Sex.MALE, "bob@example.com"));

        return roster;
    }


    public static void printPersons(
            List<Person> roster, CheckPerson tester) {

        System.out.println(tester);
        System.out.println(tester instanceof CheckPerson);
        for (Person p : roster) {
            if (tester.test(p)) {
                p.printPerson();
            }
        }
    }


    public static void main(String[] args) {

        List<Person> roster = Person.createRoster();



        // 1. 비교하는 메서드를 가진 클래스를 하나 새로 생성하고 그 클래스의 객체를 매개변수로 넘겨줌.
        printPersons(
                roster, new CheckPersonEligibleForSelectiveService());





        // 2. Person 클래스의 내부 인터페이스를 구현했을 때. 이는 인터페이스를 함수화 시켜줌.
        printPersons(
                roster,
                new CheckPerson() {
                    public boolean test(Person p) {
                        return p.getGender() == Person.Sex.MALE
                                && p.getAge() >= 18
                                && p.getAge() <= 25;
                    }
                }
        );



        // 3. 2번의 예제에서 람다 표현식을 썼을떄의 예제
        printPersons(
                roster,
                p -> p.getGender() == Person.Sex.MALE
                        && p.getAge() >= 18
                        && p.getAge() <= 25
        );

        // 람다 표현식에서 body부분에 return값을 설정하는 예제
        printPersons(
                roster,
                p -> {
                    return p.getGender() == Person.Sex.MALE
                            && p.getAge() >= 18
                            && p.getAge() <= 25;
                }
        );


    }


}


class CheckPersonEligibleForSelectiveService implements Person.CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
                p.getAge() >= 18 &&
                p.getAge() <= 25;
    }

}

우선 전체코드이다. Person이라는 클래스에서는 여러개의 필드가 명시돼있다. 

눈여겨봐야할 곳은 Person클래스 내부에 선언한 CheckPerson 인터페이스와 CheckPerson을 매개변수로 받는 printPerson메서드이다. 

* 추가로 명시하자면 내부 인터페이스로 익명클래스로 부를 수 있다고 한다. 또한 실제로 컴파일러는 내부 클래스에 관한 처리도 익명클래스로 인식하고 동일하게 한다고 한다. 

인터페이스를 구현할 때 가끔 인터페이스의 객체를 실수로 생성하게 되면 아래와 같이 생성된 경험이 있다.

CheckPerson checkPerson = new CheckPerson() {
    @Override
    public boolean test(Person p) {
        return false;
    }
};

이는 익명클래스와 인터페이스가 객체 생성이 불가능하기 때문에 익명클래스로서 동작하기 위해 컴파일러가 자동으로 생성해주는 것이다.

 

여기서 printPersons 메서드를 총 3번을 호출하게 되는데 이 때의 방법은 제 각각 다르다.

 

1. CheckPerson으로부터 구현받은 CheckPersonEligibleForSelectiveService클래스를 매개변수로 넘겨준다.

2. Person 클래스의 내부 인터페이스인 CheckPerson을 익명클래스로 생성한다.

3. 2번을 람다표현식으로 나타냈을 때

 

printPersons메서드 내부적으로 출력문을 찍어보면 다음과 같다.

lambda.CheckPersonEligibleForSelectiveService@49e4cb85
Bob, 23
lambda.Person$1@5ae9a829
Bob, 23
lambda.Person$$Lambda$20/0x00000008000b1040@548b7f67
Bob, 23
lambda.Person$$Lambda$21/0x00000008000b1440@6d78f375
Bob, 23

 

 

그렇다면 또 한가지 의문점은 PrintPersons의 메서드는 두번째 매개변수로 CheckPerson의 객체만 받아야 하는데 arrow(->)를 이용한 람다 표현식에는 객체에 대한 아무런 타입도 명시돼있지 않음에도 Person클래스라고 인식된다. 

이는 내부적으로 구현된 인터페이스의 타입에 맞게 알아서 매핑되기 때문에 람다표현식을 통해 표현한 p는 곧 CheckPerson객체에 속하는 객체라고 인식하고 동작되는 듯하다.