본문 바로가기
☕ Java 웹 프로그래밍/Java

[프로그래머스] Java(자바) 중급 | Part 8. 람다(Lambda)

by 일단연 2023. 5. 19.

람다 표현식 (+함수형 인터페이스/프로그래밍,  일급 객체) 

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

  • 추상 메소드를 딱 하나만 가지고 있는 인터페이스
    • 두 개의 메소드가 있다면 함수형 인터페이스가 아님
  • SAM(Single Abstract Method) 인터페이스
  • 함수형 인터페이스의 어노테이션: @FunctionalInterface
    • 인터페이스의 선언 앞에 붙이면, 컴파일러는 해당 인터페이스를 함수형 인터페이스로 인식
    • Java 컴파일러는 이렇게 명시된 함수형 인터페이스에 두 개 이상의 메소드가 선언되면 오류를 발생시킴
  • Java는 java.util.function 패키지를 통해 여러 상황에서 사용할 수 있는 다양한 함수형 인터페이스를 미리 정의하여 제공
  • 예시 1. @FunctionalInterface 어노테이션을 가지고 있는 인터페이스
    • static 메소드와 default메소드와 상관없이, 추상 메소드가 1개만 있어야 한다는 게 중요
@FunctionalInterface
public interface RunSomething {
  void doIt();
  //void doItAgain(); 

  static void printName(){
    System.out.println("catsbi");
  }
    
  default void printAge(){
    System.out.println("33");
  }
}
  • 예시 2. Runnable 인터페이스
    • 스레드를 만들 때 사용하는 Runnable 인터페이스의 경우, run( )이라는 추상 메소드를 하나만 가지고 있음

 

Runnable 인터페이스를 이용해 스레드를 만드는 코드

  • 스레드를 실행하려면 start( )메소드 내부의 run( )메소드가 실행되어아 함
    • run( )메소드를 실행하려면 run( )메소드를 갖고 있는 Runnable 인터페이스를 객체로 생성해 매개변수에 Runnable 인터페이스를 전달해야 함
  • 1) main 스레드에서 Thread 객체 생성
  • 2) Thread의 생성자에 Runnable 인터페이스를 객체로 생성해 매개변수로 삽입
    • Runnable 인터페이스를 객체로 만들 때 Ctrl+Enter 눌러서 자동완성 시켜주면, 자동으로 run( )메소드가 오버라이딩되어 나옴
  • 3) Runnable 인터페이스의 run( ) 메소드를 구현
  • 4) Thread 생성자를 닫고 Thread가 갖고 있는 start( )메소드로 Thread 실행시키기
    • new Thread(new Runnable( ) {…}).start( );의 의미
      • 스레드 생성자 안에 넣은 Runnable의 run( )메소드가 실행되게 해라
public class LambdaExam1 {

  public static void main(String[] args) {
    new Thread(new Runnable(){
      public void run(){
        for(int i = 0; i < 10; i++){
          System.out.println("hello");
        }          //for
      }            //run()
    }).start();    //Thread
  }                //main 스레드
}                  //class
  • 스레드가 실행되면 스레드 생성자 안에 넣은 run( )메소드가 실행됨
  • 자바는 메소드만 매개변수로 전달할 방법이 없음. 인스턴스만 전달할 수 있음
  • 그렇기 때문에 run( )메소드를 가지고 있는 Runnable 인스턴스를 만들어서 전달

 

람다 표현식은 위처럼 클래스를 작성해 객체를 생성해야만 메소드를 사용할 수 있는 문제점을 해결

  • 람다 표현식 이전에는 클래스를 작성해 객체를 생성해야만 클래스 안에 있는 메소드를 사용할 수 있었음 (메소드만 매개변수로 전달할 수 있는 방법이 없었음. only 인스턴스만 전달 가능)
  • 람다 표현식 이후로, 클래스를 작성해 객체를 생성하지 않아도 메소드를 사용할 수 있기 때문에 좀 더 편리한 프로그래밍이 가능해짐 (메소드를 정의할 필요도 없음)

 

위의 코드를 람다식을 이용해서 수정한 코드

public class LambdaExam1 {  
  public static void main(String[] args) {
    new Thread(()->{
      for(int i = 0; i < 10; i++){
        System.out.println("hello");
      }          //for
    }).start();  //람다식으로 만든 스레드
  }              //main 스레드
}                //class
  • new Thread( ( )->{ ..... }).start( ); 에서 ( )->{ ..... } 부분이 람다식(=익명 메소드)임
  • JVM은 Thread생성자를 보고 ( )->{ } 이 무엇인지 대상을 추론
  • Thread생성자 API를 보면, Runnable인터페이스를 받아들이는 것을 알 수 있음
  • JVM은 Thread생성자에 Runnable인터페이스를 구현한 것이 와야 함을 알게 되고 람다식을 Runnable을 구현하는 객체로 자동으로 만들어서 매개변수로 넣어줌

 

람다 표현식(lambda expression)

 개념 

  • 간단히 말해, 메소드를 하나의 식으로 표현한 것
//메소드를 사용하면
int min(int x, int y) {
  return x < y ? x : y;
}

//람다 표현식을 사용하면
(x, y) -> x < y ? x : y;
  • 위의 예제처럼 메소드를 람다 표현식으로 표현하면, 클래스를 작성하고 객체를 생성하지 않아도 메소드를 사용할 수 있음
  • 함수형 인터페이스의 인스턴스를 만드는 방법으로 사용될 수 있음
    • 예: Runnable 인터페이스의 run( )메소드를 사용하는 스레드를 만들기 위해 스레드의 생성자 안에 Runnable 인터페이스의 인스턴스 생성
  • 메소드 매개변수, 리턴타입, 변수로 만들어 사용할 수도 있음 (람다 표현식 = 일급객체)
  • 람다 표현식은 익명 클래스라고도 함
    • Java에서는 클래스의 선언과 동시에 객체를 생성하므로, 단 하나의 객체만을 생성할 수 있는 클래스를 익명 클래스라고 함
    • But, 익명 클래스보다는 코드가 짧음

 문법 

(매개변수목록) -> {함수몸체}
  • 매개변수 목록
    • 매개변수가 없을 때: ( )
    • 매개변수가 한 개일 때: (one) 또는 one > 괄호 생략 가능
    • 매개변수가 여러 개일 때 : (one, two) > 괄호 생략 불가능
    • 매개변수의 타입은 컴파일러가 추론(infer)할 수 있는 경우엔 생략 가능 But, 명시할 수도 있음 (Integer one, Integer two)
  • 함수몸체(실행문)
    • 화살표 오른쪽에 함수 본문을 정의
    • 함수의 몸체가 여러 줄의 명령문으로 이루어진 경우
      • 중괄호 { } 를 사용해서 묶음
    • 함수의 몸체가 한 줄인 경우
      • 중괄호 { } 생략 가능
      • return 키워드도 생략 가능
      • But, return문으로만 구성되어 있는 경우엔 중괄호 { } 생략 불가능
        • <가능> Supplier<Integer> get20 = ( ) -> { return 20; };
        • <불가능> Supplier<Integer> get20 = ( ) -> return 20; //오류 발생
public class Foo {
  public static void main(String[] args) {
    //함수의 몸체가 한 줄인 경우
    Supplier<Integer> get10 = () -> 10;
    Supplier<Integer> get20 = () -> {
      return 20;
    };
    UnaryOperator<Integer> plus10 = i -> i + 10;
    UnaryOperator<Integer> plus20 = (i) -> i + 20;
    BinaryOperator<Integer> plus30 = (i, j) -> i + j + 30;
    BinaryOperator<Integer> plus40 = (Integer i, Integer j) -> i + j + 40;
  }
}

 예시 

        Compare 인터페이스

  • 2개의 값을 비교하여 어떤 값이 더 큰지 구하는 compareTo( )메소드를 가지고 있음
  • 2개의 값을 받아들인 후, 정수를 반환하는 메소드를 선언
public interface Compare{
  public int compareTo(int value1, int value2);
}

        CompareExam 클래스

  • Compare 인터페이스를 이용
  • Compare 인터페이스를 매개변수로 받아들인 후, 해당 인터페이스를 이용하는 exec메소드
  • compareTo메소드가 어떻게 구현되어 있느냐에 따라서 출력되는 값이 다름
public class CompareExam { 
     
  public static void exec(Compare compare){
    int k = 10;
    int m = 20;
    int value = compare.compareTo(k, m);
    System.out.println(value);
  }

  public static void main(String[] args) {    
    exec((i, j)->{
      return i - j;
    }); //결과: -10
  }
}
  • JVM은 exec( )메소드를 찾아보고 매개변수 2개(i, j)를 받아들이는 인터페이스(=Compare)를 받아들이는 메소드(=exec( )메소드)가 뭔지를 찾아봄
    • exec( )메소드가, 해당 Compare 인터페이스를 받아들이는 알맞은 메소드임을 찾게 됨
  • JVM은 람다식을 Compare 인터페이스를 구현하는 익명 객체로 만들어서 exec( )a메소드에 전달함

 장단점 

  • 람다의 장점
    • 코드가 훨씬 간결해지고 가독성도 좋아짐
    • 코드의 간결성 - 람다를 사용하면 불필요한 반복문의 삭제가 가능하며 복잡한 식을 단순하게 표현할 수 있습니다.
    • 지연연산 수행 - 람다는 지연연상을 수행 함으로써 불필요한 연산을 최소화 할 수 있습니다.
    • 병렬처리 가능 - 멀티쓰레디를 활용하여 병렬처리를 사용 할 수 있습니다.
  • 람다의 단점
    • 람다식의 호출이 까다로움
    • 람다 stream 사용 시 단순 for문 혹은 while문 사용 시 성능이 떨어짐
    • 불필요하게 너무 사용하게 되면 오히려 가독성을 떨어뜨릴 수 있음

 

함수형 프로그래밍

  • 함수를 일급 객체(First class object)로 사용할 수 있음
RunSomething runSomething = () -> System.out.println("Hello");
  • Java에서는 이런 형태를 특수한 형태의 Object라고 볼 수 있음
  • 함수형 인터페이스를 인라인 형태로 구현한 Object라 볼 수 있는데, 자바는 객체지향언어(OOP)이기 때문에 이 함수를 메소드 매개변수, 리턴타입, 변수로 만들어서 사용할 수 있음

 

고차 함수(Higher-Order Function) 

  • 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있음
  • Java에서는 함수가 특수한 형태의 오브젝트일 뿐이기에 가능

 

 순수 함수(Pure Function) 

        개념

  • 수학적인 함수에서 가장 중요한 것은 입력받은 값이 동일할 때 결과가 같아야 한다는 것
  • 매개변수로 1을 넣었으면 몇 번을 호출하든 11이 나와야 함. 이런 결과를 보장하지 못하거나 못할 여지가 있다면 함수형 프로그래밍이라고 할 수 없음.
@FunctionalInterface
public interface RunSomething {
    int doIt(int number);
}
public class Foo {
    public static void main(String[] args) {
        RunSomething runSomething = (number) -> {
            return  number + 10;
        };
        System.out.println(runSomething.doIt(1));//11
        System.out.println(runSomething.doIt(1));//11
        System.out.println(runSomething.doIt(1));//11
    }
}

        특징

  • side-effect가 없음 (함수 밖에 있는 값을 변경하지 않음)
  • 상태가 없음 (함수 밖에 있는 값을 사용하지 않음)
public class Foo {
    public static void main(String[] args) {
        //int baseNumber = 10; //---(1)
        RunSomething runSomething = new RunSomething() {
            //int baseNumber = 10; //---(2)
            @Override
            public int doIt(int number) {
                return number + baseNumber;
            }
        };
        
    }
}
  • (1), (2) 위치에 있는 변수는 둘 다 외부값이라 할 수 있고, 접근하거나 변경하려 하면 순수함수라 할 수 없음
  • 물론, 문법적으로 (1) 위치에 있는 지역변수를 참조할 수는 있지만 참조하게 될 경우 다른 곳에서 해당 변수를 변경할 수 없음 (final 변수 취급)

 

일급 객체(First class object)

  • 메소드 매개변수, 리턴타입, 변수로 만들어 사용할 수도 있음 (람다 표현식 = 일급객체)

 1. 변수나 데이터에 담을 수 있음

import java.util.function.Consumer;

public class Main {
  public static void main(String[] args) {
    // 람다식을 인터페이스 타입 변수에 할당
    Consumer<String> c = (t) -> System.out.println(t);
    c.accept("Hello World");
  }
}

2. 함수의 파라미터로 전달할 수 있음

import java.util.function.Consumer;

public class Main {
  // 메소드 매개변수로 람다 함수를 전달
  public static void print(Consumer<String> c, String str) {
    c.accept(str);
  }

  public static void main(String[] args) {
    print((t) -> System.out.println(t) ,"Hello World");
  }
}

3. 함수의 리턴값으로 사용할 수 있음

import java.util.function.Consumer;

public class Main {
  public static Consumer<String> hello() {
    // 람다 함수 자체를 리턴함
    return (t) -> {
      System.out.println(t);
    };
  }

  public static void main(String[] args) {
    Consumer<String> c = hello();
    c.accept("Hello World");
  }
}

 

출처 1: https://catsbi.oopy.io/e980e4b7-fde3-4ceb-91f9-181ce2e7b507#11c9ff0d-0077-4abe-aac2-b209b72dfefa

출처 2: https://inpa.tistory.com/entry/CS-👨‍💻-일급-객체first-class-object#자바의_람다_함수의_일급_객체


 

 람다, 내부클래스, 익명클래스 실습 

 

* 실습 이전에 내부클래스 복습하기 *

 

실습 1

  • 문제 설명
    • 내부클래스, 익명클래스, 람다를 왜 사용하는지 자바를 처음 시작할 때는 이해하기 어려울 수 있습니다. 지금은 예제를 보면서 "저렇게도 쓸 수 있구나!" 정도로 이해하면 됩니다. 내부클래스, 익명클래스, 람다를 이용해서 같은 작업을 어떻게 다르게 할 수 있는지 살펴보면서 각각이 어떻게 쓰이는지 눈여겨보세요. 다음 코드는 앞으로 예제에서 사용할 Car클래스입니다.
  • Car 클래스
public class Car{
  //이어지는 예제에서 사용할 Car클래스입니다.
  //이름, 탑승인원, 가격, 사용년수를 필드로 가집니다.
  public String name;
  public int capacity;  
  public int price;
  public int age;
    
  //각각의 필드를 생성자에서 받아서 초기화합니다.
  public Car(String name, int capacity, int price, int age){
    this.name = name;
    this.capacity = capacity;
    this.price = price;
    this.age = age;
  }
    
  //Car 객체를 문자열로 출력하면 이름을 출력합니다.
  public String toString(){
    return name;
  }
    
  public static void main(String args[]){
    Car car = new Car("new model", 4, 3000, 0);
  }
}

 

 

실습 2 - List 생성

  • 문제 설명
    • main에서는 다양한 조건의 Car 객체를 만들어서 cars라는 리스트에 넣습니다. 이 cars라는 리스트에 있는 차를 검색해서 조건에 맞는 차를 출력하는 예제들을 살펴볼 텐데요. 첫 번째로 가격이 2000보다 싼 차량을 검색해서 이름을 출력하는 printCarCheaperThan 이라는 함수가 있습니다.
  • Car 클래스 (실습 2 ~ 5 동안 변경 없이 계속 쓰이므로 이하의 실습에선 생략)
public class Car{
  public String name;
  public int capacity;  
  public int price;
  public int age;
    
  public Car(String name, int capacity, int price, int age){
    this.name = name;
    this.capacity = capacity;
    this.price = price;
    this.age = age;
  }
    
  public String toString(){
    return name;
  }
}
  • CarExam 클래스
import java.util.*;

public class CarExam{
  public static void main(String[] args){
    //Car객체를 만들어서 cars에 넣습니다.
    List<Car> cars = new ArrayList<>();
    cars.add( new Car("작은차",2,800,3) );
    cars.add( new Car("봉고차",12,1500,8) );
    cars.add( new Car("중간차",5,2200,0) );
    cars.add( new Car("비싼차",5,3500,1) );
        
    printCarCheaperThan(cars, 2000);
  }
    
  public static void printCarCheaperThan(List<Car> cars, int price){
    for(Car car : cars){
      if(car.price < price){
        System.out.println(car);
      }
    }
  }
}
  • 결과
    • 작은차
      봉고차

 

실습 3 - 내부 클래스 활용

  • 문제 설명
    • 이번에는 조건이 더 복잡한 경우입니다. 내부클래스를 이용해서 CheckCar라는 인터페이스를 만들고, 그걸 구현하는 CheckCarForBigAndNotExpensive클래스를 만들어서 4명 이상이 탈 수 있고, 가격이 2500 이하인 차를 검색합니다.
  • CarExam 클래스
import java.util.*;

public class CarExam{
  public static void main(String[] args){
    List<Car> cars = new ArrayList<>();
    cars.add( new Car("작은차",2,800,3) );
    cars.add( new Car("봉고차",12,1500,8) );
    cars.add( new Car("중간차",5,2200,0) );
    cars.add( new Car("비싼차",5,3500,1) );
        
    printCar(cars, new CheckCarForBigAndNotExpensive());
  }
    
  public static void printCar(List<Car> cars, CheckCar tester){
    for(Car car : cars){
      if (tester.test(car)) {
        System.out.println(car);
      }
    }
  }
    
  interface CheckCar{
    boolean test(Car car);
  }
    
  //내부클래스를 만들어서 사용합니다.
  static class CheckCarForBigAndNotExpensive implements CheckCar{
    public boolean test(Car car){
      return car.capacity >= 4 && car.price < 2500;
    }
  }
}
  • 결과
    • 봉고차
      중간차

 

  • 중첩 인터페이스
    • 클래스의 멤버로 선언된 인터페이스
    • 해당 클래스와 긴밀한 관계를 맺는 구현 클래스를 만들기 위해 사용
    • 인스턴스 멤버 인스턴스와 정적 멤버 인터페이스 모두 가능
      • 인스턴스 멤버 인터페이스는 외부 클래스의 객체가 있어야 사용 가능하고 정적 멤버 인터페이스는 외부 클래스의 객체 없이 외부 클래스만으로 바로 접근할 수 있음
      • 주로 정적 멤버 인터페이스를 많이 사용: UI 프로그래밍에서 이벤트를 처리할 목적으로 많이 활용됨

 

실습 4 - 익명 클래스 활용

  • 문제 설명
    • 같은 검색조건에 대해 익명 클래스를 이용하면 별도 클래스를 만들 필요가 없으므로 코드가 조금 더 짧아집니다.
import java.util.*;

public class CarExam{
  public static void main(String[] args){
    List<Car> cars = new ArrayList<>();
    cars.add( new Car("작은차",2,800,3) );
    cars.add( new Car("봉고차",12,1500,8) );
    cars.add( new Car("중간차",5,2200,0) );
    cars.add( new Car("비싼차",5,3500,1) );
        
    printCar(cars, 
      //인터페이스 CheckCar를 구현하는 익명클래스를 만듭니다.
      new CheckCar(){
        public boolean test(Car car){
          return car.capacity >= 4 && car.price < 2500;
        }
      }
    );
  } //main 메소드
    
  public static void printCar(List<Car> cars, CheckCar tester){
    for(Car car : cars){
      if (tester.test(car)) {
        System.out.println(car);
      }
    }
  }
    
  interface CheckCar{
    boolean test(Car car);
  }
  
}
  • 결과
    • 봉고차
      중간차

 

실습 5 - 람다 활용

  • 문제 설명
    • 같은 검색조건에 대해 람다를 이용하면 메소드 지정도 필요 없으므로 더 간결하게 표현할 수 있습니다.
  • CarExam 클래스
import java.util.*;

public class CarExam{
  public static void main(String[] args){
    //Car객체를 만들어서 cars에 넣습니다.
    List<Car> cars = new ArrayList<>();
    cars.add( new Car("작은차",2,800,3) );
    cars.add( new Car("봉고차",12,1500,8) );
    cars.add( new Car("중간차",5,2200,0) );
    cars.add( new Car("비싼차",5,3500,1) );
        
    CarExam carExam = new CarExam();
    carExam.printCar(cars, 
      //인터페이스 CheckCar의 test메소드에 대응하는 람다를 만듭니다.
      (Car car) -> { return car.capacity >= 4 && car.price < 2500; }
    );
  }
    
  public void printCar(List<Car> cars, CheckCar tester){
    for(Car car : cars){
      if (tester.test(car)) {
        System.out.println(car);
      }
    }
  }
    
  interface CheckCar{
    boolean test(Car car);
  }  
}
  • 결과
    • 봉고차
      중간차
      출력: 내부클래스, 익명클래스, 람다가 불필요하게 복잡해 보일 수 있지만 다양한 기능을 구현하다 보면 람다를 쓰는 게 더 효율적일 수 있습니다.
      이것으로 모든 자바 강의를 마쳤습니다. 수고하셨습니다.