람다식 사용법

람다식 사용법은 다음과 같다.

() -> {}
인자   바디
  • 인자가 없을때
    • () → {바디}
  • 인자가 한개일때
    • (a) → {바디}
    • a → {바디}
  • 인자가 여러개일때
    • (a, b) → {바디}

인자의 타입은 컴파일러가 추론하기때문에 생략이 가능하지만 명시할 수도 있다.

  • (Integer a, String b) → {바디}

바디

  • 화살표 오른쪽에 함수 본문을 정의한다.
  • 여러 줄인 경우에 {} 를 사용하여 묶는다.
    • 한 줄인 경우에 {} 생략가능하며, return도 생략 가능하다.
      () -> System.out.println("한 줄일 경우 중괄호 생략");
      () -> 10;             // return 생략 가능
    

함수형 인터페이스

👉 추상 메소드를 하나만 가지고 있는 인터페이스
SAM (Single Abstract Method)
👉 @FuncationInterface 애노테이션을 가지고 있는 인터페이스

함수형 인터페이스란?

함수형 인터페이스는 추상 메소드를 하나만 가지고 있는 인터페이스로,

public interface RunSomething {
    void doIt();
}

다음과 같이 두개이상의 추상 메소드를 가지고 있으면 함수형 인터페이스가 아니다.

public interface RunSomething {
    void doIt();      // abstract 생략됨.

    void doItAgain();
}

그러나, java 8부터 도입된 static 메소드, default 메소드가 있어도 추상메소드가 하나라면 함수형 인터페이스가 맞다.

public interface RunSomething {
    void doIt();      

    static void printName() {
        System.out.println("yesol");
    }
    
    default void printAge() {
        System.out.println("50");
    }
}

함수형 인터페이스를 정의할 때 자바에서 기본으로 제공되는 @FunctionalInterface 애노테이션을 사용하면 실수로 추상메소드를 한개 이상 정의했을 때 컴파일 에러를 발생시켜 안전하게 코딩이 가능하다.

@FunctionalInterface                 // 컴파일 에러
public interface RunSomething {
    void doIt();
    void doItAgain();                
}

함수형인터페이스 사용

  • java 8 이전
public class App {
    public static void main(String[] args) {
         // java 8 이전 익명 내부 클래스 (anonymous inner class)
        RunSomething runSomething = new RunSomething() {
            @Override
            public void doIt() {
                System.out.println("Hello, Java");
            }
        };
        runSomething.doIt();
    }
}
  • java 8 이후
public class App {
    public static void main(String[] args) {
        RunSomething runSomething = () -> System.out.println("Hello, Java");
        runSomething.doIt();
    }
}

위 식은 람다 형태로 작성한 것으로 java 8이전의 식과 동작은 동일하다. 람다식은 실질적으로 자바에서는 함수형 인터페이스를 인라인으로 구현한 object로 볼 수 있다. 또한, 자바는 객체지향 언어이기때문에 위 람다식 (() -> System.out.println("Hello, Java")) 은

  • 변수에 할당도 가능하고
    • RunSomething runSomething = () -> System.out.println("Hello, Java");
  • 메서드의 파라미터
  • 람다식 자체를 리턴도 가능하다.
  • 자바에서 함수형 프로그래밍
    • 함수를 First class object 로 사용할 수 있다.
    • 순수 함수 (Pure function)
      • 사이드 이펙트 만들 수 없다. (함수 밖에 있는 값을 변경하지 못한다.)

      • 상태가 없다 (함수 밖에 정의되어 있는)

          @FunctionalInterface
          public interface RunSomething {
              int doIt(int number);
          }
                    
          public static void main(String[] args) {
              RunSomething runSomething = (number -> {
                  return number + 10;
              });
                
              System.out.println(runSomething.doIt(1));
              System.out.println(runSomething.doIt(1));
              System.out.println(runSomething.doIt(1));
          }
        

        위 코드는 동일한 값을 넣었을 때 결과가 항상 동일한 코드이다. (상태가 없다)

        하지만,

          public static void main(String[] args) {
              int baseNumber = 10;
                        
              RunSomething runSomething = (new RunSomething() {
                  @Override
                  public int doIt(int number) {
                      return number + baseNumber;
                  }
              });
          }
        

        위 코드는 pure 한 함수가 아니다. 이러한 경우를 상태값을 가지고 있다. 상태값에 의존한다 라고 할 수 있다.

        입력 받은 값이 동일한 경우 결과가 항상 같아야 한다. 이것을 보장해주지 못한다면 함수형 프로그래밍이라고 보기 어렵다.

    • 고차 함수 (High-Order Function)
      • 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있다.
    • 불변성

자바에서 기본 제공하는 함수형 인터페이스 (java.util.function)

  • Function<T,R> (T: 입력값 타입, R : 리턴값 타입)

    T타입을 받아서 R타입을 리턴하는 함수 인터페이스로

    • R apply(T t) 를 구현하면 된다.
      public class Plus50 implements Function<Integer, Integer> {
          @Override
          public Integer apply(Integer integer) {
              return integer + 50;
          }
      }
    
      public class App {
        
      	public static void main(String[] args) {
              Plus50 plus50 = new Plus50();
              System.out.println("plus50.apply(10) = " + plus50.apply(10));
      	}
      }
    

    클래스를 지우고 람다식을 이용해서 바로 구현도 가능하다.

      public class App {
        
          public static void main(String[] args) {
              Function<Integer, Integer> plus100 = (i -> {
                  return i + 100;
              });
    
              System.out.println("plus100.apply(9) = " + plus100.apply(9));
          }
      }
    

    • 함수 조합용 메소드
      • compose
        • ()안의 function을 수행하고 나온 결과값을 두번째 function의 입력값으로 조합해준다.
          public class App {
                    
          	public static void main(String[] args) {
                  Function<Integer, Integer> plus100 = (i -> i + 100);
                  Function<Integer, Integer> multiply2 = i -> i * 2;
                    
                  Function<Integer, Integer> multiply2AndPlus100 = plus100.compose(multiply2);
                  System.out.println("multiply2AndPlus100.apply(2) = " + multiply2AndPlus100.apply(2));
                    
          	}
          }
        

      • andThen
        • 처음에 작성된 함수 먼저 실행되고 나온 결과값을 토대로 두번째로 작성된 function이 실행된다.
          public class App {
                    
              public static void main(String[] args) {
                  Function<Integer, Integer> plus100 = (i -> i + 100);
                  Function<Integer, Integer> multiply2 = i -> i * 2;
                    
                  Function<Integer, Integer> plusAndMulti = plus100.andThen(multiply2);
                  System.out.println("plusAndMulti.apply(2) = " + plusAndMulti.apply(2));
              }
          }
        

  • BiFunction<T, U, R> (T, U : 입력값 타입, R: 리턴값 타입)

    Function<T,R> 과 동일한데 입력값을 두 개 받는 함수 인터페이스

    • R apply(T t, U u)
      public class App {
        
          public static void main(String[] args) {
              BiFunction<Integer, Integer, Integer> sum = (n1, n2) -> {
                  return n1 + n2;
              };
              System.out.println("sum.apply(4, 1000) = " + sum.apply(4, 1000));   
          }
      }
    

  • Consumer<T> (T :입력값 타입 , 리턴값 없음)

    T 타입을 받고 아무값도 리턴하지 않는 함수 인터페이스

    • void Accept(T t)
      public class App {
        
          public static void main(String[] args) {
              Consumer<String> printStr = s -> System.out.println(s);
              printStr.accept("안녕");
          }
      }
    

    • 함수 조합용 메서드
      • andThen
        public class App {
              
            public static void main(String[] args) {
                Consumer<String> printStr = s -> System.out.println(s);
                Consumer<String> sleep = s -> {
                    System.out.println("두번째 " + s);
                };
                printStr.andThen(sleep).accept("메롱");
            }
        }
      

  • Supplier<T> (T : 리턴될 타입)

    T 타입의 값을 제공하는 함수 인터페이스

    • T get()
      public class App {
        
          public static void main(String[] args) {
              Supplier<Integer> get20 = () -> 20;
              System.out.println("get20.get() = " + get20.get());
          }
      }
    

  • Predicate<T> (T : 입력값 타입, boolean 리턴)

    T 타입을 받아서 boolean을 리턴하는 함수 인터페이스

    • boolean test(T t)
      public class App {
        
          public static void main(String[] args) {
              Predicate<String> startsWithZero = (s -> s.startsWith("0"));
              Predicate<Integer> isOdd = (i -> i % 2 !=0);
              System.out.println("startsWithZero.test() = " + startsWithZero.test("010-7777-7777"));
              System.out.println("isOdd.test(3) = " + isOdd.test(3));
          }
      }
    

    • 함수 조합용 메소드
      • and
        public class App {
              
            public static void main(String[] args) {
                Predicate<String> startsWithZero = (s -> s.startsWith("0"));
                Predicate<String> endWithZero = (s -> s.endsWith("0"));
              
                System.out.println(endWithZero.and(startsWithZero).test("010-3333-3331"));
            }
        }
      

      첫 번째 테스트는 통과했지만 두번째 테스트는 통과 못해서 false 리턴 (둘다 성공해야 true)

      • or
        public class App {
              
            public static void main(String[] args) {
                Predicate<String> startsWithZero = (s -> s.startsWith("0"));
                Predicate<String> endWithZero = (s -> s.endsWith("0"));
                System.out.println(endWithZero.or(startsWithZero).test("010-3333-3331"));
              
            }
      

      첫 번째 테스트만 통과해도 true (둘 중 하나 성공해야 true )

      • negate
        public class App {
              
            public static void main(String[] args) {
                Predicate<Integer> isOdd = (i -> i % 2 !=0);
              
                System.out.println(isOdd.negate().test(3));
            }
        }
      

      3은 홀수가 맞지만 negate 사용하면 ! 와 같은 효과를 나타낸다.

  • UnaryOperator<T> (T : 입력값 타입, 입력값과 동일한 타입 리턴)

    Function<T, R> 의 특수한 형태로 입력값 타입과 리턴값 타입이 동일할 때 사용하는 함수 인터페이스이다.

      public class App {
        
          public static void main(String[] args) {
              Function<Integer, Integer> plus100 = i -> i + 100;
              UnaryOperator<Integer> plus = i -> i + 100;
          }
      }
    
    • Function<T, R> 상속

    Function<T, R> 을 상속받기 때문에 Function이 제공하는 조합용 메서드도 사용 가능하다.

  • BinaryOperator<T>

    BiFunction<T, U, R> 이 입력값 두개, 리턴값 세개가 각자 다 다른 타입일 것으로 가정하고 만들어진 함수 인터페이스라면, BinaryOperator<T> 는 동일한 타입의 입력값 두개를 받아 리턴하는 인터페이스이다.

Variable Capture

변수 캡처란 예를들어,

public class VariableCapture {

    public static void main(String[] args) {
        VariableCapture test = new VariableCapture();
        test.run();
    }

    private void run() {
        String testStr = "변수 캡처";

        class LocalClass {
            void printStr() {
                System.out.println(testStr);
            }
        }
    }
}

run 메소드에서 갖고있는 변수 testStr 은 run 메소드 실행이 완료되면 사라진다. 그런데 run 메소드 안의 로컬클래스인 LocalClass 에서 testStr 를 사용해야 하니까 자바에서는 복사해오는데 이것이 변수 캡처이다.

이것은 내부클래스에 한한것은 아니고 저 상황이 일어날 수 있는 상황은 여러가지가 있다. (익명클래스, 내부클래스 …)

  • 익명클래스 변수 캡쳐

      public class VariableCapture2 {
        
          public static void main(String[] args) {
              VariableCapture2 test = new VariableCapture2();
              test.run();
          }
    
          private void run() {
              String testStr = "변수 캡처";
    
              Consumer<Integer> integerConsumer = new Consumer<Integer>() {
                  @Override
                  public void accept(Integer integer) {
                      System.out.println(testStr);
                  }
              };
          }
      }
    
  • 람다 변수 캡쳐

      public class VariableCapture2 {
        
          public static void main(String[] args) {
              VariableCapture2 test = new VariableCapture2();
              test.run();
          }
    
          private void run() {
              String testStr = "변수 캡처";
    
              IntConsumer intConsumer = (i) -> System.out.println(testStr + i);
              intConsumer.accept(3);
          }
      }
    

자바 8 이전에는 로컬 변수를 참조할 때 final 키워드를 사용해야 변수 캡처를 할 수 있었다.

private void run() {
    final String testStr = "변수 캡처"; // 자바 8 이전 에는 final 키워드 필수

    class LocalClass {
        void printStr() {
            System.out.println(testStr);
        }
    }
}

final 키워드를 사용하지 않은 경우 concurrency 문제가 생길 수 있어서 컴파일러가 방지한다.

  • concurrency 문제

    동일한 객체에 대해 공유하고 추후 그 객체가 변경되는 경우 예기치 않은 결과를 초래할 수 있고, 객체를 동시에 변경하면 손상 또는 일관성이 없는 상태가 될 수 있음.

그러나, 자바 8부터는 effective final 을 지원해 final 키워드를 사용하지 않은 변수를 로컬클래스, 익명 클래스 구현체 또는 람다에서 참조할 수 있다.

private void run() {
    String testStr = "변수 캡처"; // 자바 8 이후 effective final인 경우 키워드 생략 가능

    class LocalClass {
        void printStr() {
            System.out.println(testStr);
        }
    }
}

effective final 사실상 final이란 변수가 추후 변경되지 않는 상황의 변수를 지칭한다.

그러면 나중에 변수가 변경된다면 ? → effective final이 아니라면?

public class VariableCapture2 {

    public static void main(String[] args) {
        VariableCapture2 test = new VariableCapture2();
        test.run();
    }

    private void run() {
        int baseNumber = 10;

        class LocalClass {
            void printNumber() {
                System.out.println(baseNumber);    // 컴파일 에러 발생
            }
        }

        Consumer<Integer> consumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(baseNumber);     // 컴파일 에러 발생
            }
        };

        IntConsumer printInt = (i) -> {
            System.out.println(i + baseNumber);   // 컴파일 에러 발생
        };
        
        baseNumber++;
        
        LocalClass localClass = new LocalClass();
        localClass.printNumber();
        consumer.accept(1);
    }
}

Variable ‘baseNumber’ is accessed from within inner class, needs to be final or effectively final 같은 컴파일 에러가 발생된다.

람다 vs 익명클래스 vs 로컬클래스

  • 공통점
    • Variable Capture 지원
    • effective final 지원
  • 차이점
    • 쉐도잉
      • 익명클래스, 로컬클래스 쉐도잉 된다.
        • 왜? 각각 다른 스콥을 가지고 있기 때문에
      • 람다는 쉐도잉 안된다.
        • 왜? 람다는 해당 람다식을 구현한 곳(ex. 메소드)과 같은 스콥을 갖고 있기 때문에

쉐도잉

public class VariableCapture2 {

    public static void main(String[] args) {
        VariableCapture2 test = new VariableCapture2();
        test.run();
    }

    private void run() {
        int baseNumber = 10;

        class LocalClass {
            void printNumber() {
                int baseNumber = 11;
                System.out.println(baseNumber);
            }
        }

        Consumer<Integer> consumer = new Consumer<Integer>() {
            int baseNumber = 13;
            @Override
            public void accept(Integer integer) {
                System.out.println(baseNumber);
            }
        };

        LocalClass localClass = new LocalClass();
        localClass.printNumber();
        consumer.accept(1);
    }
}

run 메소드의 지역변수인 baseNumber 의 값은 10이었는데 로컬클래스, 익명클래스와 run 메소드는 서로 다른 scope 에 있기 때문에 가려져서 11과 13이 출력되는 것이다. 이것을 쉐도잉 이라한다.

하지만 람다는 run() 메소드와 같은 scope에 있어서 쉐도잉이 불가능하다.

메소드, 생성자 레퍼런스

method reference는 람다 표현식이 단 하나의 메소드만을 호출하는 경우에 해당 람다 표현식에서 불필요한 매개변수를 제거하고 사용할 수 있도록 해준다.

메소드 참조를 사용하면 불필요한 매개변수를 제거하고 ‘::’ 기호를 사용하여 표현할 수 있다.

✏️ - 스태틱 메소드 참조 클래스타입::스태틱메소드이름

  • 특정 객체의 인스턴스 메소드 참조 참조변수타입::인스턴스메소드이름
  • 임의 객체의 인스턴스 메소드 참조 타입::인스턴스메소드이름
  • 생성자 참조 타입::new

다음과 같은 클래스가 있을 때,

public class Greeting {

    private String name;

    public Greeting() {
    }

    public Greeting(String name) {
        this.name = name;
    }

    public String hello(String name) {
        return "hello " + name;
    }

    public static String hi(String name) {
        return "hi " + name;
    }
}
  • 스태틱 메소드 참조

      UnaryOperator<String> hi = Greething::hi;
      hi.apply("java");
    
  • 특정 객체의 인스턴스 메소드 참조

      Greeting greeting = new Greeting();
      UnaryOperator<String> hello = greeting::hello;
      hello.apply("java");
    
  • 임의 객체의 인스턴스 메소드 참조

      String[] names = {"younghee", "dongsu", "gildong"};
      Arrays.sort(names, (o1, o2) -> 0);
    

    위와 같은 코드가 가능한 것은 sort 의 두번째 인자가 Comparator로 함수형 인터페이스이기때문에 그 자리에 람다를 넣을 수 있는것이다.

    람다를 넣을 수 있다는 말은 메소들 레퍼런스를 사용할 수 있다는 말로, 다음과 같은 코드가 가능하다.

      public class App {
          public static void main(String[] args) {
              String[] names = {"younghee", "dongsu", "gildong"};
              Arrays.sort(names, String::compareToIgnoreCase);
              System.out.println(Arrays.toString(names));
          }
      }
    

    1. younghee 뒤에 오는 dongsucompareToIgnoreCase 의 파라미터로 넘겨서 int값을 리턴
    2. dongsugildong과 비교하여 int값 리턴

    → 임의의 인스턴스들 (younghee,dongsu, gildong) 를 거쳐가며 인스턴스 메소드 사용(compareToIgnoreCase)

  • 생성자 레퍼런스

    • default 생성자 (입력값 x)
      Function<String, Greeting> newGreeting = Greeting::new;
      Greeting greetingInstance = newGreeting.get();
    
    • 매개변수 받는 생성자 (입력값 o)
      Funtion<String, Greeting> dongsuGreeting = Greeting::new;
      Greeting dongsu = dongsuGreeting.apply("dongsu");
      System.out.println(dongsu.getName());
    

Reference

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

https://www.inflearn.com/course/the-java-java8/