오늘 한 일

  • java 공부
    • 람다식 (lambda expression)
      자바에서 함수형 프로그래밍(functional programming)을 구현하는 방식으로 Java 8부터 지원한다. 클래스를 생성하지 않고 함수의 호출만으로 기능을 수행한다.

      • 함수형 프로그래밍:
        순수 함수(pure function)을 구현하고 호출함으로써 외부 자료에 부수적인 영향을 주지 않고 매개변수만을 사용하도록 만든 함수이다. 함수를 기반으로 구현하며, 입력 받은 자료를 기반으로 수행되고 외부에 영향을 미치지 않으므로 병렬처리 등이 가능하다. 안정적인 확장성이 있는 프로그래밍 방식이다.

      • 람다식 구현하기
        매개 변수와 매개 변수를 활용한 실행문으로 구현된 익명 함수를 만든다. 함수 이름과 반환형을 생략하고 -> 를 사용한다.
        (int x, int y) -> { return x + y; }
        

        javascript의 arrow function과 유사 (=>)

      • 람다식 문법
        • 매개 변수가 하나인 경우 자료형과 괄호 생략가능
          str -> { System.out.println(str); }
          
        • 매개 변수가 두 개인 경우 괄호를 생략할 수 없음
          (x, y) -> { System.out.println(x + y); }
          
        • 중괄호 안의 구현부가 한 문장인 경우 중괄호 생략가능
          str -> System.out.println(str);
          
        • 중괄호 안의 구현부가 한 문장이여도 return 문은 중괄호 생략할 수 없음
          str -> { return str.length(); }
          
        • 중괄호 안의 구현부가 반환문 하나라면 return과 중괄호 모두 생략 가능
          (x, y) -> x + y;
          str -> str.length();
          


      • 함수형 인터페이스
        함수형 인터페이스란 람다식을 선언하기 위한 인터페이스이다. 익명 함수와 매개 변수만으로 구현되므로 단 하나의 메서드만을 가져야한다. 만약 두 개이 이상의 메서드가 선언되는 경우 어떤 메서드의 호출인지 모호해지는 문제가 발생하기 때문이다.

        함수형 인터페이스라는 의미에서 @FunctionalInterface annotation을 붙여 작성하며, 이 경우 여러개의 메서드를 선언하면 에러가 발생한다.

        @FunctionalInterface
        interface MyNumber {
          int getMaxNumber(int num1, int num2);
        }
        
        public class TestMyNumber {
          public static void main(String[] args) {
            MyNumber maxNum = (x, y) -> (x >= y) ? x : y;
            int max = maxNum.getMaxNumber(10,  20);  // 20
          }
        }
        


      • 익명 객체를 생성하는 람다식 자바는 객체 지향 언어로, 객체를 생성해야 메서드가 호출된다. 따라서 람다식으로 메서드를 구현하고 호출하면 내부에서 익명 클래스가 생성된다.
        // 람다식 구현
        MyNumber maxNum = (x, y) -> (x >= y) ? x : y;
        
        // 실제 구현
        MyNumber maxNum = new MyNumber() {
          @Override
          public int getMaxNumber(int x, int y) {
            return (x >= y) ? x : y;
          }
        };
        

        람다식에서 외부 메서드의 지역변수는 상수로 처리된다. 이는 지역 내부 클래스와 동일한 원리이다.

        public static void main(String[] args) {
          int num = 100;
          // MyNumber maxNum = (x, y) -> num += 100;  // Error
        }
        


      • 함수를 변수처럼 사용하는 람다식
        1. 인터페이스형 변수에 람다식 대입
        2. 매개 변수로 전달하는 람다식
        3. 반환 값으로 쓰이는 람다식
        interface PrintString {
          void showString(String str);
        }
        
        public class TestLambda {
          public static void main(String[] args) {
            PrintString lambdaStr = s -> System.out.println(s);  // 1. 인터페이스형 변수에 람다식 대입
            lambdaStr.showString("hello lambda_1");
            ShowMyString(lambdaStr);  // 2. 메서드의 매개 변수로 람다식을 대입한 변수 전달
        
            PrintString reStr = returnString();  // 변수로 반환받기
            reStr.showString("hello ");
          }
        
          public static void ShowMyString (PrintString p) {  // 매개 변수를 인터페이스 형으로 받음
            p.showString("hello lambda_2");
          }
        
          // 3. 람다식을 반환하는 메서드
          public static PrintString returnString() {
            return s -> System.out.println(s + "world");
          }
        }
        


    • 스트림 (stream)

      1. 자료의 대상과 관계없이 동일한 연산을 수행한다.
        배열, 컬렉션 등을 대상으로도 동일한 연산을 수행하기 때문에 일관성 있는 연산으로 자료의 처리를 쉽고 간단하게 한다.

      2. 한 번 생성하고 사용한 스트림은 재사용할 수 없다.
        연산을 수행한 스트림은 소모되기 때문에 다른 연산을 위해서는 새로운 스트림을 생성해야 한다.

      3. 스트림 연산은 기존 자료를 변경하지 않는다.
        스트림을 생성하며 별도의 메모리 공간을 사용하므로 기존의 자료를 변경하지 않는다.

      4. 스트림 연산은 중간 연산과 최종 연산으로 구분된다.
        스트림에 대해 중간 연산은 여러 개 적용될 수 있지만 최종 연산은 마지막에 한 번만 적용된다. 최종 연산이 호출되어야 중간 연산의 결과가 모두 적용되며, 이를 ‘지연 연산’이라 한다.

      • 중간 연산
        중간 연산은 자료를 거르거나 변경하여 또 다른 자료를 내부적으로 생성한다.
        filter(), map(), sorted() 등 다양한 중간 연산이 존재한다.

        filter(): 조건에 맞는 요소를 추출

        /*
         * 문자열의 길이가 5 초과인 요소만 출력
         * 
         * sList.stream(): 스트림 생성
         * filter(s -> s.length() >= 5): 중간 연산
         * forEach(s -> System.out.println(s)): 최종 연산
         */
        sList.stream().filter(s -> s.length() > 5).forEach(s -> System.out.println(s));
        

        map(): 요소를 변환

        // 고객 클래스에서 고객 이름만 가져오기
        customerList.stream().map(c -> c.getName()).forEach(s -> System.out.println(s));
        

        sorted(): 정렬

        /*
         * 문자열 정렬
         * 
         * sorted() 메서드를 사용하려면 정렬 방식에 대한 정의가 필요하다.
         * 따라서 사용하는 자료 클래스가 Comparable 인터페이스를 구현해야 한다.
         * sorted() 메서드의 매개 변수로 Comparator 인터페이스를 구현한 객체를 넘길 수도 있다.
         */
        sList.stream().sorted().forEach(s -> System.out.println(s));
        
      • 최종 연산
        최종 연산은 스트림의 자료를 소모하며 연산을 수행하기 때문에 최종 연산이 수행되고 나면 해당 스트림은 더 이상 사용할 수 없다.
        최종 연산으로는 forEach(), count(), sum(), reduce() 등이 있다.

        reduce(): 지정한 연산을 거치며 누적된 연산의 결과를 반환

        /*
         * 가장 긴 문자열 찾기
         * 
         * T reduce(T identify, BinaryOperator<T> accumulator)
         * identify: 초깃값
         * accumulator: 수행해야할 기능
         * 
         * BinaryOperator 인터페이스를 구현한 람다식을 직접 써도 되고,
         * 인터페이스를 구현한 클래스의 인스턴스를 생성하여 대입해도 된다.
         * 후자의 경우에는 초깃값을 넘기지 않는다.
         */
        class CompareString implements BinaryOperator<String> {
          @Override
          public String apply(String s1, String s2) {
            return s1.getBytes().length >= s2.getBytes().length ? s1 : s2;
          }
        }
        
        public class ReduceTest {
          public static void main(String[] args) {
            String[] greetings = { "안녕하세요~~~~~~", "hello", "Good morning" };
        
            // 람다식
            System.out.println(Arrays.stream(greetings).reduce("", (s1, s2) -> {
              return s1.getBytes().length >= s2.getBytes().length ? s1 : s2;
            }));
                  
            // BinaryOperator 인터페이스 구현
            String str = Arrays.stream(greetings).reduce(new CompareString()).get();
            System.out.println(str);
          }
        }
        

참조