C/C++에는 함수포인터라는 개념이 있어 함수를 다른 함수로 전달하고 싶을 때에는 함수 포인터를 사용하면 된다.

그런데 자바는 C/C++보다 더 객체지향적인 언어이기 때문에 메소드(C/C++로 따지면 함수)는 무조건 클래스의 내부에 있어야 한다.

따라서 메소드를 전달하고 싶을 때도 (행위를 전달하고 싶을 때도) 메소드만 전달할 수는 없고 클래스를 만들어 그 안에 메소드를 정의하고 그 클래스의 객체를 생성해서 넘겨줘야 한다.

 

람다식은 객체를 생성하는 방법 중 하나이다. 람다식을 사용하면 객체가 생성된다.

 

자바에는 인터페이스라는 개념이 있다. 인터페이스는 추상클래스의 극단적인 형태라고 보면 된다.

인터페이스는 구현되어 있는 메소드가 없고 모두 추상메소드로만 이루어져 있다. (JDK 8부터 default 메소드와 static 메소드, JDK 9부터 private 메소드는 예외적으로 구현메소드가 가능하다)

인터페이스를 구현하는 클래스는 그 추상메소드를 반드시 구현해야 한다.

 

아래의 코드를 보면 speak()라는 아직 구현되지 않은 추상메소드가 있기 때문에 Person 객체를 생성할 수 없다고 컴파일 에러가 난다.

따라서 Person 타입의 객체를 생성하기 위해서는 speak 메소드를 구현해줄 필요가 있다.

 

그 방법으로 그 인터페이스를 구현하는 MyPerson이라는 클래스를 새로 만들고 그 클래스를 이용해 새로운 객체를 만드는 방법이 있다.

 

아래의 코드가 그 방법을 사용한 것이다.

interface Person{
    void speak();
}

public class PersonTest {
    public static void main(String[] args) {
        Person p = new MyPerson();
        p.speak();
    }
}
class MyPerson implements Person {
    public void speak(){
        System.out.println("어쩌구저쩌구");
    }
}

Person 인터페이스를 구현하는 MyPerson이라는 클래스를 만들어서 speak() 메소드를 구현해주었다.

그러나 이것은 너무 비효율적이고 번거롭다. 딱 한 번만 쓰고 말 객체인데도 그 객체를 만들기 위해서 Person 인터페이스를 구현하는 MyPerson이라는 클래스를 새로 만들었다. 이러한 비효율적인 문제는 무명객체를 만들어 해결할 수 있다.

 

new 인터페이스나 추상클래스(){정의되지 않은 메소드}; 

예를 들어 new Person(){메소드};

이렇게 그 중괄호 안에 구현되지 않은 메소드를 정의해주는 것이 무명객체를 생성하는 방법이다.

interface Person{
    void speak();
}

public class PersonTest {
    public static void main(String[] args) {
        Person p = new Person() {
            @Override
            public void speak() {
                System.out.println("어쩌구저쩌구");
            }
        };
        p.speak();
    }
}

이렇게 말이다.

 

무명객체 안에서 메소드를 오버라이딩하기 위해서는 메소드의 시그니쳐(매개변수 개수, 매개변수 타입, 메소드 이름)와 반환타입이 인터페이스에 정의된 메소드 헤더와 일치해야 한다. 참고로 인터페이스의 추상메소드는 앞에 public이라고 써있지 않아도 무조건 public이다. 따라서 public void speak(){} 하고 중괄호 사이에 speak 메소드가 수행할 코드를 적어주는 것이다.

 

하지만 무명객체를 선언하는 것도 번거롭고 코드가 꽤 길다. 단 한 번만 사용할 객체라 클래스의 이름은 선언하지 않았지만 클래스를 정의하는 것과 똑같이 메소드의 반환타입, 매개변수, 메소드 이름도 적어야 하고, 메소드 내용을 직접 정의해줘야 한다. 정말 꼭 필요한 것은 메소드 내부의 코드인데 말이다.

 

그래서 나온 것이 람다식이다. 람다식을 쓴다는 것은 인터페이스를 구현한 객체를 만드는 것이다. 예를 들어 Person이라는 클래스가 있을 때

new Person(); 한 것과 똑같이 객체를 하나 선언하는 것이다. 다만 그 객체는 이름이 없는 무명객체일 뿐이고 객체를 생성할 때 new를 사용하지 않았을 뿐이다.

 

람다식을 사용할 수 있는 조건은 제한적이다.

일단 람다식은 구현하는 인터페이스에 추상메소드가 딱 1개 있을 때에만 가능하다.

추상 클래스에 추상메소드가 딱 1개 있을 때도 안 된다. 꼭 인터페이스에 추상메소드가 1개 있어야 한다.

추상메소드가 1개만 있는 인터페이스를 함수형 인터페이스라고 한다.

람다식을 사용한다는 것은 구현되지 않은 메소드를 정의해주는 것이다. 그리고 그 결과로 객체가 생성된다.

어느 한 클래스의 인스턴스 메소드가 하나라도 구현되지 않았으면 객체를 생성할 수가 없다.

즉, 인터페이스는 객체 생성이 불가능하다.

그래서 그 메소드 내부를 구현해줘야 객체를 생성할 수 있는데 람다식은 편리성을 위해 어느 메소드를 구현하는지 메소드 이름을 쓰지 않고 그 메소드 내부 코드만 적어주는 방법이다.

그런데 아직 구현되지 않은 메소드가 여러 개라면 이 코드가 어느 메소드를 구현하는 코드인지 알 수가 없다.

따라서 인터페이스에 추상메소드가 딱 1개 있을 때에만 람다식을 쓸 수 있다. 만약 구현되지 않은 메소드가 2개 이상이라면 위의 두 가지 방식(익명 객체 생성, 구현 클래스 만들어서 객체 생성) 중 하나를 사용해야 한다.

 

다음과 같이 Person이라는 인터페이스가 있고 그 안에 speak()라는 추상메소드가 있다. Person 객체를 생성해서 그 객체가 speak라는 행위를 하도록 하고 싶을 때

interface Person{
    void speak();
}

public class PersonTest {
    public static void main(String[] args) {
        Person p1 = () -> System.out.println("어쩌구저쩌구");
        Person p2 = () -> System.out.println("배고프다");
        p1.speak();
        p2.speak();
    }
}

이렇게 람다식을 이용해서 간단히 Person 객체를 생성할 수 있다. 내가 원하는 코드로 speak() 메소드의 내부를 구현해주는 것이다.

 

 

 

따라서 람다식을 이용해서 speak 메소드의 내부를 구현해주는 것이다.

람다식 규칙은

(매개변수)->{메소드 내부 코드;}

인데 speak() 메소드의 매개변수는 없으므로 () 이렇게 빈 괄호를 쳐주는 것이다. 그리고 p1은 speak 메소드를 실행했을 때에 "어쩌구저쩌구"을 출력하고 싶고 p2는 "배고프다"를 출력하고 싶으므로 그렇게 speak 메소드를 정의한 것이다.

그렇게 speak 메소드를 정의한 후에 객체.speak()로 speak 메소드를 실행하는 것이다.

실행한 결과이다.

 

 

이렇게 우리가 speak 메소드를 정의해준대로 실행하는 것을 알 수 있다.

 

람다식에서 메소드 내부 코드를 감싸는 중괄호는 실행문이 한 문장일 때에는 생략 가능하다. 그리고 한 문장일 경우 세미콜론도 생략 가능하다.

위의 코드에서 speak 메소드의 내부 코드는 한 줄이므로 중괄호와 세미콜론을 생략했다. 끝에 있는 세미콜론은 System.out.println("어쩌구저쩌구") 문장의 종결을 의미하는 세미콜론이 아니라 Person p1 = new Person() 뒤에 찍는 그 세미콜론과 같다.

 

또한 메소드의 매개변수가 1개라면 인자를 감싸는 소괄호도 생략 가능하다.

interface ChangeNumber {
    int increase(int n);
}
public class LambdaTest {
    public static void main(String[] args) {
        ChangeNumber c = a -> a + 10;
        printChangeNumber(c, 7);
        printChangeNumber(c, 9);
        printChangeNumber(x -> x + 5, 10); //5를 증가하도록 하는 ChangeNumber 객체를 넘겨줌
    }
    static void printChangeNumber(ChangeNumber changeNumber, int num) {
        System.out.println(changeNumber.increase(num));
    }
}

printChangeNumber() 메소드를 총 3번 호출했고 첫 번째, 두 번째는 10씩 증가시켰고 세 번째 수는 5 증가시켰다.

ChangeNumber 인터페이스의 increase 메소드의 매개변수가 n 한 개이기 때문에 (a) -> a + 10 하지 않고 a -> a + 10 이렇게 괄호를 생략할 수 있다. 그리고 실행문이 한 개이기 때문에 a -> {a + b;} 이렇게 중괄호를 생략할 수 있다.

처음에는 ChangeNumber c = a -> a + 10; 이렇게 인터페이스 객체를 c에 저장해주었고

마지막에는 x -> x + 5라는 ChangeNumber 객체를 바로 printChangeNumber() 메소드의 인자로 넘겨주었다.

람다식에서 쓰는 매개변수의 이름은 상관이 없다. a를 써도 되고 x를 써도 되고 number -> number + 10 이렇게 해도 된다.

만약 람다식을 쓰지 않았더라면

interface ChangeNumber {
    int increase(int n);
}
public class LambdaTest {
    public static void main(String[] args) {
        ChangeNumber c = new ChangeNumber() {
            @Override
            public int increase(int n) {
                return n + 10;
            }
        };
        printChangeNumber(c, 7);
        printChangeNumber(c, 9);
        printChangeNumber(new ChangeNumber() {
            @Override
            public int increase(int n) {
                return n + 5;
            }
        }, 10); //10을 5 만큼 증가하도록 하는 ChangeNumber 객체를 넘겨줌
    }
    static void printChangeNumber(ChangeNumber changeNumber, int num) {
        System.out.println(changeNumber.increase(num));
    }
}

코드가 이렇게 길어졌을 것이다. 이 코드는 람다식을 쓴 위의 코드와 정확히 똑같은 코드이다. 그런데 훨씬 길고 복잡하다. 람다식을 쓰면 이렇게 불필요한 코드를 줄일 수 있다.

 

 

그러면 우리가 () -> System.out.println("배고프다")를 쳤을 때 그게 Person 객체를 생성하고 싶은 것인지 어떻게 아는 걸까? 매개변수가 없는 추상메소드를 가지는 또 다른 인터페이스가 있으면 저게 어느 인터페이스를 구현하는 객체인지 어떻게 아는 걸까?

우리가 a -> a + 10 했을 때 그게 ChangeNumber 객체를 생성하고 싶은건지 어떻게 아는 것일까?

() -> System.out.println("배고프다"), a -> a + 10 앞에 각각 Person p, ChangeNumber c 이렇게 타입을 선언했기 때문이다. 따라서 Person 타입의 p라는 변수를 선언함으로 이 람다식이 Person 타입의 객체를 생성한다는 것을 추론할 수 있고 ChangeNumber 타입의 c라는 변수를 선언함으로써 ChangeNumber 타입의 객체를 생성한다는 것을 추론할 수 있다.

만약에 Person p = ()->System.out.println("어쩌구저쩌구");에서 앞에 Person p =을 붙이지 않는다면 다음과 같이 컴파일 에러가 발생한다. 타입을 전혀 추론할 수 없기 때문이다.

Person p = ()->System.out.println("어쩌구저쩌구");로 하지 않았을 때

 

그러면 변수 선언이 아니라 람다식을 메소드의 인자로 쓸 경우에는 타입을 적어주지 않았는데 어떻게 타입을 추론하는 것일까?

메소드는 이미 정의되어 있고 메소드에는 매개변수의 타입까지 정의되어 있으므로 인자로 넣었을 때 그 메소드의 매개변수 타입으로 추론할 수 있다.

personSpeak 메소드를 정의할 때 personSpeak 메소드의 매개변수는 한 개이고 그 타입은 Person 타입이라는 것을 8~10줄에 정의해놓았다. 따라서 메인메소드에서 personSpeak 메소드를 호출해서 사용할 때에 인자로 람다식을 넣어줘도 그 람다식이 Person 타입의 객체라는 것을 추론할 수 있다.

다시 한 번 말하지만 람다식을 사용한다는 것은 객체를 생성하는 것이다. 무명객체를 생성해서 personSpeak 메소드의 인자로 넘기는 것이다. 그러니까 6번째 줄의 ()->System.out.println("안녕)"은  Person 타입의 객체를 생성한 것이다.

 

사실 이렇게 우리가 정의한 인터페이스 객체를 만드는 경우보다는 Java에 원래 있는 인터페이스를 우리가 구현하는 경우가 대부분이다. 정렬을 할 때에 비교기준을 정의해줄 때 사용하는 Comparator, 스트림에서의 인자인 Predicate, Consumer, Supplier, Operator 등은 모두 추상메소드가 1개인 함수형 인터페이스이다. 따라서 람다식을 매우 유용하게 이용할 수 있다. 이는 다음 포스팅에서 자세히 설명하겠다.

 

'Java' 카테고리의 다른 글

[Java] 입출력 스트림  (0) 2020.01.29
[Java] Stream (스트림)  (0) 2020.01.21
[Java] Comparator 인터페이스  (0) 2020.01.21
[Java] String, StringBuilder, StringBuffer의 차이점  (0) 2019.08.30
[Java] 자바(Java) 언어의 특징  (0) 2019.08.28

+ Recent posts