본문 바로가기
Back-end/Java 개념

자바에서 람다를 사용하는 방법

by devraphy 2023. 6. 26.

0. 개요

- 자바에서 람다를 잘 써보고 싶은 마음에 본 포스팅을 준비했습니다.

- 우선 람다를 이해하기 위해서는 기본적인 인터페이스에 대한 이해도가 필요합니다.

- 인터페이스를 시작으로 람다에 대해 배워봅시다! 

 

1. 인터페이스를 매개변수로 받는 메서드

- 인터페이스를 매개변수로 받으면 어떻게 될까? 

- 한번도 의식적으로 사용해 본 적도, 생각해 본 적도, 고민해 본 적도 없는 질문이다.

- 다음 코드를 보자.

예시코드 

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print();
    }
}

public interface PrintThing {

    void print();
}

- 위의 코드에서 main() 메서드에 있는 sound() 메서드는 PrintThing 인터페이스를 매개변수로 받는다.

- 그렇다면 저 자리에는 어떤 객체가 들어가야 하는 것일까? 

- PrintThing 인터페이스를 implements 받는 클래스의 객체가 들어가면 된다!

- 신기하지 않은가?!?! 이걸 알게되고 나서 코드 작성에 있어 확장성이 한단계 업그레이드된 기분이 들었다.

- 모두가 알고 있었겠지만, 나는 몰랐다. 

 

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print();
    }
    
    Dog doggy = new Dog();
    
    sound(doggy);
}

public interface PrintThing {
    void print();
}

public class Dog implements PrintThing {

    public String name;
    public Dog(){};
    
    @Override
    public void print() { // 인터페이스 상속 메서드
    	System.out.println("BOW WOW");
    }
}

 

2. 그래서 이게 람다랑 무슨 상관인데...?

- 라고 생각했다면 당신은 똑똑하다! 생각할 줄 아는 사람! 배운 사람!

- 자, 이제부터는 람다를 왜 사용하는지 그 본질에 대해서 알아보자. 

- 위에서 아래와 같은 코드를 보았을 것이다. 

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print();
    }
    
    Dog doggy = new Dog();
    
    sound(doggy);
}

public interface PrintThing {
    void print();
}

public class Dog implements PrintThing {

    public String name;
    public Dog(){};
    
    @Override
    public void print() { // 인터페이스 상속 메서드
    	System.out.println("BOW WOW");
    }
}

- 위의 로직의 흐름을 살펴보자.

- 우선 sound() 메서드를 사용하기 위해서는 PrintThing 인터페이스를 상속받은 클래스의 객체가 필요하다.

- 그러면 PrintThing 인터페이스를 상속받은 클래스를 만들어줘야 한다.

- 결국 Dog라는 클래스를 만들고 해당 클래스의 객체를 생성하여 사용하므로써 sound() 메서드를 사용할 수 있게 된다.

 

- 귀찮다. 이 과정 자체가 귀찮다.

- 만약 sound() 메서드를 사용하는 것이 지속적으로, 재사용성이 있는 메서드가 아니라 일회성으로 사용하는 것이라면?

- 그저 비효율의 끝이다. 

 

그래서 람다를 쓴다.

- 이 귀찮음과 이 비효율적인 상황이 어쩌면 람다의 탄생 목적이지 않을까? 생각해본다. 

- 즉, 람다는 메서드 그 자체를 매개변수로 넘기기 위해서 사용하는 것이다.

- 그렇다면 위에서 봤던 코드를 어떻게 람다로 바꿀 수 있는지 하나하나 차근차근 보자.

 

3. 람다로 교체

- 위에서 귀찮음을 강조했다.

- 인터페이스 상속받는 것도 귀찮고, 오버라이드 하는 것도 귀찮다고 말했다.

- sound() 메서드를 그냥 바로 쓸 수는 없을까?

- 아래의 코드처럼 말이다. 

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print();
    }
    
    sound(
    	public void print() {
    	   System.out.println("BOW WOW");
    	}
    );
}

public interface PrintThing {
    void print();
}

- 놀라지 마시라. 위의 코드처럼 사용하려는 것이 람다의 컨셉 그 자체다.

- 그럼 위의 코드를 람다의 문법에 맞게 바꿔보자. 

 

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print();
    }
    
    sound(() -> {
        System.out.println("BOW WOW");
        }
    );
}

public interface PrintThing {
    void print();
}

- 람다는 접근지정자, 반환형, 메서드명을 필요로 하지 않는다. 자바가 알아서 확인하고 체크한다. 

- 그러므로 저 3가지 요소를 제거하면 괄호만 남게된다.

- 여기에서 괄호와 중괄호 사이에 화살표만 넣어주면 람다식이 완성된다.

 

- 추가로 만약 람다 표현식으로 전달하려는 것이 단일 표현식이라면, 중괄호도 필요없다.

- 아래의 최종형태를 살펴보자.

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print();
    }
    
    sound(() -> System.out.println("BOW WOW"));
}

public interface PrintThing {
    void print();
}

 

4. 람다를 객체로 만들 수 있지 않을까?

- 위의 코드를 보면 람다식을 이용하여 메서드를 매개변수화 시켜서 넣었다.

- 그럼 람다식 자체를 객체화 시킬 수 있지 않을까?

- 그렇다. 아래의 코드를 살펴보자.

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print();
    }
    
    PrintThing printThing = () -> System.out.println("BOW WOW");
    sound(printThing);
}

public interface PrintThing {
    void print();
}

- 지금까지는 매개변수가 없는 인터페이스의 메서드를 사용하여 구현해보았다.

- 그렇다면 만약 인터페이스의 추상메서드에 매개변수가 존재한다면 어떻게 해야할까?

 

5. 매개변수가 존재하는 추상메서드를 람다로 표현해보기

- 다음 예시코드를 보자.

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print();
    }
    
    PrintThing printThing = () -> System.out.println("BOW WOW");
    sound(printThing);
}

public interface PrintThing {
    void print(String prefix);
}

- 위의 예시처럼 인터페이스 내부의 추상메서드에 매개변수가 할당 된다면 람다식을 어떻게 작성해야할까?

- 두려워할 것 없다! 다음과 같이 변경하면 그만이다!

 

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print("myDog: "); // 여기에 반드시 String 매개변수가 들어간다!
    }
    
    PrintThing printThing = (prefix) -> System.out.println(prefix + "BOW WOW");
    sound(printThing);
}

public interface PrintThing {
    void print(String prefix);
}

- 앞서 public void print()에서 접근지정자, 반환형, 메서드명을 제거하고 괄호만 남겨두었던 것을 기억할 것이다.

- 그렇다면 이번에는 public void print(String prefix)에서 동일하게 접근지정자, 반환형, 메서드명을 제거하고 괄호만 남긴다.

- 그러면 (String prefix)만 남게 된다.

- 이를 토대로 람다식을 작성하면 된다. 위의 예시코드처럼 말이다.

 

- 사실 위의 예시코드처럼 String 타입의 매개변수를 반드시 사용해야만 하는 것은 아니다.

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print("myDog: ");
    }
    
    PrintThing printThing = (prefix) -> System.out.println(prefix + "BOW WOW");
    sound(printThing);
}

public interface PrintThing {
    void print(String prefix);
}

- 다만, 위의 코드처럼 람다식에 명시적으로 String 타입의 매개변수를 입력해야한다는 차이가 있을 뿐이다.

 

6. 매개변수가 한개 이상이라면?

- 매개변수가 한개 이상일 때에도 마찬가지다.

- 다음의 예시코드처럼 작성하면 된다.

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print("myDog: ", "!!!");
    }
    
    PrintThing printThing = (prefix, suffix) 
    	-> System.out.println(prefix + "BOW WOW" + suffix);
    
    sound(printThing);
}

public interface PrintThing {
    void print(String prefix, String suffix);
}

 

7. 반환형이 존재하는 경우

- 반환형이 존재한다면, 해당 반환형에 맞는 값을 return 해주면 된다.

- 다음 예시코드를 보자.

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print("myDog: ", "!!!");
    }
    
    PrintThing printThing = (prefix, suffix) -> {
        return prefix + "BOW WOW" + suffix;
    };
    
    sound(printThing);
}

public interface PrintThing {
    String print(String prefix, String suffix);
}

- 한가지 더 편의기능이 존재한다.

- 반환형이 존재하면 자바에서 알아서 반환형이 있다는 것을 인식한다.

- 그러므로 굳이 위의 코드처럼 작성하는 것이 아니라 다음의 예시코드처럼 작성할 수 있다.

- 다만, 이 또한 하나의 표현식으로 작성하는 경우에만 해당한다.

 

예시코드

public static void main(String[] args) {

    static void sound(PrintThing printThing) {
    	printThing.print("myDog: ", "!!!");
    }
    
    PrintThing printThing = (prefix, suffix) -> prefix + "BOW WOW" + suffix;
    
    sound(printThing);
}

public interface PrintThing {
    String print(String prefix, String suffix);
}

 

8. Functional Interface와 람다의 상관관계

- 아래의 예시코드를 보자.

예시코드

@FunctionalInterface
public interface PrintThing {
    String print(String prefix, String suffix);
}

- 위의 예시코드처럼 인터페이스에 1개의 추상 메서드만 존재하는 구조를 우리는 Functional Interface라고 부른다.

- 존재하는 하나의 추상 메서드가 static일수도, default일수도 있다.

- 중요한 것은 인터페이스에 하나의 추상 메서드만이 존재하는 것이다. 

- 그리고 Functional Interface의 경우 어노테이션을 사용해서 명시적으로 표시할 수 있다.

- 이는 다른 이름으로 SAM(Single Abstract Method)이라고 불리기도 한다.

 

9. Functional Interface와 람다의 상관관계

- 이번 포스팅의 핵심이기도 한 부분이다.

- 람다를 사용하고 싶다면 반드시 Functional Interface를 대상으로 해야한다는 것이다.

 

- 이에 대해서는 어쩌면 당연한 것일 수도 있다.

- 왜냐면 람다를 사용하는 경우 메서드의 접근지정자, 반환형, 메서드명을 명시하지 않기 때문이다.

- 이는 자바에서 알아서 람다식으로 표현된 해당 메서드를 찾아 사용한다는 것인데, 결국 인터페이스에 하나의 추상메서드만 존재했을 때 람다로 표현된 메서드가 무엇인지 자바가 인식할 수 있다는 이야기이기도 하다.

 

- 즉, 인터페이스에 추상 메서드가 2개라면 람다식으로 표현한 메서드가 어떤 추상 메서드를 가리키는 것인지 알 수 없다는 것이다.

- 이와 같은 이유로 람다를 함수형 표현식이라고 부르기도 하는 것이다.

 

10. Anonymous Inner Class

-  람다를 사용하기 위해서 반드시 Functional Interface를 정의하여 사용할 필요는 없다.

- Anonymous Inner Class를 사용하면 Functional Interface처럼 람다를 사용할 수 있기 때문이다.

- 이에 대한 내용은 추후에 포스팅으로 정리해보겠다.

 

- 여기까지 기본적인 람다의 개념에 대해서 알아보았다. 

 

댓글