java8 functional style

정리

함수형 스타일 functional style 로 프로그래밍 할 수 있다. 이것을 위해 java8에 '함수형 인터페이스 + 람다식 + stream' 가 도입되었다

고차함수 high-ordered function 를 지원하여 함수도 이제 저장, 주고, 받는게 가능해 졌다. 즉, 저장( 함수형 인터페이스 타입의 변수에 람다식을 저장), 주고(함수-람다식을 리턴), 받는게(메소드의 매개변수로 함수형 인터페이스 타입) 가능해 졌다

고차함수를 사용하는 API의 등장으로 코딩 스타일에 진화가 이루어 졌다. 진화의 의미는 간결하고 서술적이다.

다음 예제 코드를 통해 심도 있게 이해를 해보자.

  • 컬렉션 collection
  • 비교 comparator
  • 설계 design
  • 자원 resource
  • 지연 lazy
  • 재귀 recursion
  • 조합 composing
  • 함수형 스타일 functional style

여기에서는 컬렉션을 사용하는 예제를 통해 함수형 스타일 코드를 이해하도록 한다.

컬렉션 collection

  • 이터레이션, 새로운 컬렉션으로 변형, 엘리먼트 추출, 엘리먼트 연결
이터레이션 iteration
  • 기존 방법과 진화된 방법으로 알아보자
        public static final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

        public static void main(final String[] args) {

        // 외부 Iterator를 사용

        // 명령형 프로그래밍 스타일

        // 1. OLD 방식
        for (int i = 0; i < friends.size(); i++) {
            System.out.println(friends.get(i));
        }

        // 2. 다른 방식 그러나 별로~
        // Iterator 인터페이스를 사용, hasNext(), next() 메소드 사용
        for (String name : friends) {
            System.out.println(name);
        }

        // for loop는 순차적인 방식 > 병렬화 어려움
        // for 구문에 컬렉션을 넘기는 방식 > non-polymorphic
        // Tell, don't ask 설계 원칙 위배 > 이터레이션을 직접 다룸

        // 내부 Iterator 사용
        // 함수형 프로그래밍 스타일
        // forEach() 메소드 제공, Consumer type을 파라미터로 사용
        // ( accept() 메소드 구현에서 자원을 '소비'한다는 의미 )

        // 1. 익명 이너 클래스 형태로 사용(Anonymous Inner Class)
        friends.forEach(new Consumer<String>() {
            public void accept(final String name) {
                System.out.println(name);
            }
        });
        // for 구문 대신에 내부 Iterator 인 forEach()로 변경
        // Iteration을 어떻게 해야 하는지? 대신에 해야할 작업으로 집중하여 코딩

        // ###. 람다 표현식을 사용
        // 코드 양이 줄었음
        // forEach
        friends.forEach((final String name) -> System.out.println(name));
        // 파라미터 축약#1
        friends.forEach((name) -> System.out.println(name));
        // 파라미터 축약#2 - 1개의 파라미터
        friends.forEach(name -> System.out.println(name));

        // ### 메소드 레퍼런스를 사용
        friends.forEach(System.out::println);

        //익명 이너 클래스 > 람다 표현식 > 메소드 레퍼런스
리스트 변형 map()
         public static final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

        // 불변 리스트의 엘리먼트를 모두 대문자로 변형하여 새로운 컬렉션으로 생성

        // 1. 기존방식
        // 빈 리스트를 만들어서 이터레이션하여 하나하나 add 하는 방식
        {
            final List<String> uppercaseNames = new ArrayList<String>();
            for (String name : friends) {
                uppercaseNames.add(name.toUpperCase());
            }
            System.out.println(uppercaseNames);
        }

        // 2. forEach(내부 이터레이션) 및 람다식을 사용한 방식
        {
            final List<String> uppercaseNames = new ArrayList<String>();
            friends.forEach(name -> uppercaseNames.add(name.toUpperCase()));
            System.out.println(uppercaseNames);
        }

        // 3. stream API를 활용한 방식

        // stream()은 모든 컬렉션에서 사용 가능
        // stream은 이터레이션과 유사하며 다양한 기능의 함수를 기본적으로 제공한다.
        // 예를 들어 map은 입력 순서에 따라 출력으로 맵핑, forEach와 전혀 다름, 람다식을 파라미터로 받음, 람다 표현식의
        // 실행 결과를 취합하여 결과 컬렉션으로 리턴,

        // stream IF의 map을 사용한 간결한 방식, forEach는 출력을 담당
        friends.stream().map(name -> name.toUpperCase()).forEach(name -> System.out.print(name + " "));

        // 입력과 결과 : 엘리먼트 수는 통일, 타입은 달라질수 있음,
        friends.stream().map(name -> name.length()).forEach(count -> System.out.print(count + " "));

        // 메소드 레퍼런스를 사용한 간결한 표현, 람다식이 간단하면 대체가 가능
        friends.stream().map(String::toUpperCase).forEach(name -> System.out.println(name));

        // 메소드 레퍼런스는 언제 사용하면 좋을까?
        // 람다식을 사용할 때, 파라미터를 전달하지 않는 경우에만 사용하는 것이 좋다

    }
엘리먼트 찾기
  • 컬렉션에서 문자 N으로 시작하는 엘리먼트 찾기 > filter()

         // 1. 기존방식
         {
             final List<String> startsWithN = new ArrayList<String>();
             for (String name : friends) {
                 if (name.startsWith("N")) {
                     startsWithN.add(name);
                 }
             }
             System.out.println(String.format("Found %d names", startsWithN.size()));
         }
    
         // 2. filter() 메소드의 사용
         // boolean 결과를 리턴하는 람다식이 필요, true면 결과 컬렉션에 추가, false면 다음으로 넘어감
         // filter가 리턴하는 결과 스트림을 리스트로 변경 collect
         // filter는 조건-람다식 에 맞는 subset을 리턴
    
         final List<String> startsWithN = friends.stream().filter(name -> name.startsWith("N"))
                 .collect(Collectors.toList());
    
         System.out.println(String.format("Found %d names", startsWithN.size()));
    
람다식의 재사용
  • 람다식의 중복을 제거하기 위한 방법 > 람다식을 변수에 저장하여 재사용
    //각 컬렉션에 있는 N으로 시작하는 이름 수를 찾기
    public static final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
    public static final List<String> editors = Arrays.asList("Brian", "Jackie", "John", "Mike");
    public static final List<String> comrades = Arrays.asList("Kate", "Ken", "Nick", "Paula", "Zach");

        // 람다식의 중복, 수정의 어려움
        {
            final long countFriendsStartN = friends.stream().filter(name -> name.startsWith("N")).count();
            final long countEditorsStartN = editors.stream().filter(name -> name.startsWith("N")).count();
            final long countComradesStartN = comrades.stream().filter(name -> name.startsWith("N")).count();
        }

        // 람다식을 변수에 저장하여 재사용, 객체 저장과 유사한 개녕
        {

            // filter()는 함수형 인터페이스 중 java.util.function.Predicate을 받음
            // 람다식은 Predicate IF의 public abstract인 test()의 구현으로 합성됨 by 컴파일러
            // filter에 들어가는 람다식은 Predicate 타입의 명시적인 레퍼런스에 저장하여 재사용이 가능하여 중복을 제거. DRY
            // 그러나 찜찜함, 중복 smell이 솔솔

            final Predicate<String> startsWithN = name -> name.startsWith("N");

            final long countFriendsStartN = friends.stream().filter(startsWithN).count();
            final long countEditorsStartN = editors.stream().filter(startsWithN).count();
            final long countComradesStartN = comrades.stream().filter(startsWithN).count();
        }
    }
  • 중복성 제거의 또 다른 방법 > 렉시컬 스코프 lexical scoping 와 클로저 closures
        // 'N'이나 'B'로 이름을 선택

        // 문자가 다른 이유로 2개의 Predicae 사용 > 중복
        {
            final Predicate<String> startsWithN = name -> name.startsWith("N");
            final Predicate<String> startsWithB = name -> name.startsWith("B");

            final long countFriendsStartN = friends.stream().filter(startsWithN).count();
            final long countFriendsStartB = friends.stream().filter(startsWithB).count();
        }

        // checkIfStartsWith 메소드를 사용한 방법

        {
            // 검색하려는 문자를 파라미터로 넘기면 즉!시! 람다식을 리턴한다
            final long countFriendsStartN = friends.stream().filter(checkIfStartsWith("N")).count();
            final long countFriendsStartB = friends.stream().filter(checkIfStartsWith("B")).count();
        }

    // 람다식을 리턴하는 메소드를 추가 > 고차함수
    // 리턴 타입은 함수형 인터페이스이며 이경우는 Predicate를 사용해야 함
    // 컬렉션의 각 엘리먼트를 파라미터로 받아 boolean 결과를 리턴하는 함수형 인터페이스인 Predicate가 적합

    // letter 변수는 이 리턴되는 람다식(익명함수)의 범위scope에 없음 > 람다식의 범위에서 letter를 찾음 > 렉시컬 스코프
    public static Predicate<String> checkIfStartsWith(final String letter) {
        return name -> name.startsWith(letter);
    }

    // 람다식을 익명 이너 클래스로 표현
    public static Predicate<String> checkIfStartsWithEasy(final String letter) {
        return new Predicate<String>() {
            public boolean test(String name) {
                return name.startsWith(letter); // letter 변수는 이 익명이너클래스에 없음 그러나
                              // java8에서 컴파일 에러는 발생하지 않느다
            }
        };

    }
  • 함수형 인터페이스인 Function을 활용
        // Function<T,R>은 타입T를 파라미터로 받고 타입R을 결과로 리턴하는 함수
        // 타입T를 파라미터로 받고 boolean을 결과로 리턴하는 Predicate보다 일반적
        // 입력값을 다른값으로 변경하는 경우 사용. map()

        // 결과 타입으로 Predicate인 람다식을 리턴하는 고차함수
        // 람다식을 리턴하는 람다식.. 흐 꼬인다 꼬여
        {
            final Function<String, Predicate<String>> startsWithLetter = (String letter) -> {
                Predicate<String> checkStarts = (String name) -> name.startsWith(letter);
                return checkStarts;
            };

            final long countFriendsStartN = friends.stream().filter(startsWithLetter.apply("N")).count();
            final long countFriendsStartB = friends.stream().filter(startsWithLetter.apply("B")).count();

            System.out.println(countFriendsStartN);
            System.out.println(countFriendsStartB);
        }

        // 좀 더 간결하게 표현
        final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);

        // 더더더 간결하게 표현
        final Function<String, Predicate<String>> startsWithLetter = letter -> name -> name.startsWith(letter);
엘리먼트 선택
  • 엘리먼트 중 하나 선택, 컬렉션에서 하나의 엘리먼트를 선택하기 Optional, orElse, ifPresent
  • 단, 필요한 만큼의 기능만 수행 > 다양한 조작이 필요한 경우는 명령형
    // 기존 방식, null 체크 필요
      public static void pickName(final List<String> names, final String startingLetter) {

        String foundName = null; // 문제1. 변수 생성 및 null 초기화 > 가변성, null pointer exception 우려

        for (String name : names) { //문제2. 외부 이터레이터의 사용
            if (name.startsWith(startingLetter)) {
                foundName = name;
                break;
            }
        }

        if (foundName != null) { //문제3. null 점검 코드 작성
            System.out.println(foundName);
        } else {
            System.out.println("No name found");
        }
    }

    // 람다식과 API를 활용한 방식

        // findFirst() 스트림에서 첫번째 값을 추출, Optioal 객체 리턴
        // Optional은 결과가 없는 경우에 대한 처리 null 처리 > null pointer exception 방어
        final Optional<String> foundName = names.stream().filter(name -> name.startsWith(startingLetter)).findFirst();

        //orElse()는 대신하는 값을 지정
        System.out.println(String.format("A name starting with %s: %s", startingLetter, foundName.orElse("No name found")));

        // orElse()처럼 대신하는 값을 넣기 보다, 값이 존재할때만 처리
        // ifPresent()는 객체 존재를 확인하여 있으면 get()으로 얻어옴
        final Optional<String> foundName = friends.stream().filter(name -> name.startsWith("N")).findFirst();
        foundName.ifPresent(name -> System.out.println("Hello " + name));
    }
엘리먼트 합치기(리듀스 reduce)
  • 컬렉션을 연산하여 하나의 값으로 추출 > 맵리듀스(MapReduce) 패턴
        // 1. 엘리먼트 문자의 수를 전부 합산하는 예제
        //map 오퍼레이션 종류 중 mapToInt 를 사용하고 sum()을 통해 리듀스(reduce) > MapReduce pattern
        //sum이외에 max(), main(), sorted(), average() 존재
        {
            System.out.println("Total number of characters in all names: " 
                    + friends.stream().mapToInt(name -> name.length()).sum());
        }

        // 2. 컬렉션에서 가장 긴 이름 중 첫번째 찾기

        //최대길이 파악하고 첫번째 찾기 > 2개의 list 필요, 비효율 > reduce로 한방에 처리
        //reduce는 컬렉션을 이터레이션하고 람다식으로 연산 결과를 얻어냄
        //reduce와 람다식은 분리됨 > 전략패턴의 경량화
        //람다식 파라미터가 2개, BinaryOperator 함수형 인터페이스의 apply() 를 따름
        final Optional<String> aLongName = friends.stream().reduce(    (name1, name2) -> name1.length() >= name2.length() ? name1 : name2 );
        aLongName.ifPresent( name -> System.out.println(String.format("A longest name: %s", name)));

        //기본값 Steve 설정
        final String steveOrLonger = friends.stream().reduce( "Steve", (name1, name2) -> name1.length() >= name2.length() ? name1 : name2);
엘리먼트 조인 join
  • 엘리먼트를 연결하는 마지막 단계 Reduce operation 종단 오퍼레이션
    //컬렉션을 ', '를 사용하여 하나의 스트링으로 합치기
    public static void main(final String[] args) {

        // 1. 기존방식, 마지막 엘리먼트에도 ,가 붙는다
        for (String name : friends) {
            System.out.print(name + ", ");
        }

        System.out.println();

        // 2. 마지막 엘리먼트에 뒤에 ','가 없도록 함, 이터레이션을 조작 및 마지막 엘레먼트 별도 처리 > 복잡
        for (int i = 0; i < friends.size() - 1; i++) {
            System.out.print(friends.get(i) + ", ");
        }
        if (friends.size() > 0)
            System.out.println(friends.get(friends.size() - 1));

        // 3. String의 join() method 사용
        System.out.println(String.join(", ", friends));

        // 4. MapReduce 패턴을 사용한 대문자로 변환 후 합치기
        // 엘리먼트를 연결하는 마지막 단계는 Reduce operation
        // collect는 값을 타깃(java.util.stream.Collector)으로 값을 모으는 reduce의 일종 
        // * collection(List, Map, Set)과 혼동하지 말 것. 
        System.out.println(friends.stream().map(String::toUpperCase).collect(joining(", ")));
엘리먼트 모으기 collect 와 Collectors
  • 엘리먼트의 스트림을 결과 컨테이너(arraylist등)로 모음(collect의미)
final List<Person> people = Arrays.asList(new Person("John", 20), new Person("Sara", 21),new Person("Jane", 21), new Person("Greg", 35));

        {
            // 새로운 ArrayList를 수동 생성 후 저장
            // low-level code, 명령적(imperative), 병렬문제(스레드 세이프티, 가변성)
            List<Person> olderThan20 = new ArrayList<>();
            people.stream().filter(person -> person.getAge() > 20).forEach(person -> olderThan20.add(person));
            System.out.println("People older than 20: " + olderThan20);
        }

        {
            // 새로운 ArrayList를 collect 메소드에서 자동 생성
            // collect : 엘리먼트의 스트림을 결과 컨테이너(arraylist등)로 모음(collect의미)
            // collect() 컬렉션을 가변 컬렉션으로 변경하는 리듀스 오퍼레이션 + Collectors의 유틸과 조합하여 시너지
            // 3개의 param, 결과컨테이너 생성, 컨테이너에 추가하는 방법, 컨테이너를 하나로 합치기(병렬인경우)
            // 서술적declarative, 병렬화가 쉬움, 
            List<Person> olderThan20 = people.stream().filter(person -> person.getAge() > 20)
                    .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
            System.out.println("People older than 20: " + olderThan20);
        }

        {
            // 더 간단한 Collectors.toList() 사용
            // param은 supplier, accumulator, combiner 오퍼레이션의 인터페이스 역할 > .toList() 는 인테페이스 구현
            // toSet, toMap, joining, mapping, collectingAndThen, minBy, maxBy, groupingBy등 여러 오퍼레이션 지원
            System.out.println("//" + "START:COLLECT_TO_LIST_OUTPUT");
            List<Person> olderThan20 = people.stream().filter(person -> person.getAge() > 20)
                    .collect(Collectors.toList());
            System.out.println("People older than 20: " + olderThan20);
            System.out.println("//" + "END:COLLECT_TO_LIST_OUTPUT");
        }
디렉토리 리스팅
  • 현재경로 파일 리스팅
    //list()로 파라미터 패스의 스트림을 얻어온다.
    Files.list(Paths.get(".")).forEach(System.out::println);
  • 상대경로 디렉토리 리스팅
      //filter 적용, isDirectory는 정적static 메소드 레퍼런스 표현
      Files.list(Paths.get(".\\src\\fpij")).filter(Files::isDirectory).forEach(System.out::println);
    
  • hidden 파일 리스팅
      //함수형 인터페이스 FileFilter 사용
      final File[] files = new File(".").listFiles(file -> file.isHidden());
      Arrays.stream(files).forEach(System.out::println);
    
  • 특정 파일(.java) 선택 리스팅

      //list는 함수형 인터페이스를 파라미터로 받음
      //여기서는 익명 이너 클래스로 구현
      final String[] files = new File(".\\src\\fpij\\compare").list(new java.io.FilenameFilter() {
          public boolean accept(final File dir, final String name) {
              return name.endsWith(".java");
          }
      });
    
      //결과 출력
      //명령식 코드
      for (int i = 0; i <files.length; i++){
            System.out.format("[%2d] = '%s'%n", i, files[i]);
      }
      //서술식 코드
      Arrays.stream(files).forEach(System.out::println);
    
      //2nd 파라미터는 filter
      Files.newDirectoryStream(Paths.get("."), path -> path.toString().endsWith(".java"))
          .forEach(System.out::println);
    
  • 와치서비스 : 스레드 생성, 이벤트 polling 스트림 처리

public class WatchFileChange {
    public static void main(String[] args) throws Exception {

        // 람다식 스레드 객체 생성 및 시작
        new Thread(() -> watchFileChange()).start();

        // //익명 이너 클래스로 작성해 봄
        // new Thread(new Runnable() {
        // public void run() {
        // watchFileChange();
        // }
        // }).start();

        // 임의 파일 생성
        final File file = new File("sample.txt");
        file.createNewFile();

        // 기다렸다가
        Thread.sleep(5000);

        // 파일 정보 수정
        file.setLastModified(System.currentTimeMillis());
    }

    public static void watchFileChange() {
        try {
            // 현재디렉토리 Path에 와치서비스 등록, 수정이벤트 대상
            final Path path = Paths.get(".");
            final WatchService watchService = path.getFileSystem().newWatchService();
            path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

            System.out.println("Report any file changed within next 1 minute...");

            System.out.println("here1!");
            final WatchKey watchKey = watchService.poll(1, TimeUnit.MINUTES);
            System.out.println("here2!");

            if (watchKey != null) {
                System.out.println("watchkey!");
                // 와치서비스는 이벤트가 생기면 이 이벤트를 키에 넣음
                //키를 얻어와서poll 키에 담긴 이벤트를 스트림으로 추출하여 표시
                watchKey.pollEvents().stream().forEach(event -> System.out.println(event.context()));
            }
        } catch (InterruptedException | IOException ex) {
            System.out.println(ex);
        }
    }
}

자원 resource

자원 종료 close 처리의 캡슐화

  • 리소스 클린업 close 처리
  • 외부 리소스의 GC는 프로그래머의 책임
  • db, file, socket, native resource
  • EAV 패턴 execute around method

기존 클린업 보장

  • try-catch-finally
 - ARM automatic resource management
 - 이 방식은 프로그래머가 이를 사용법을 미리 알아야 함 AutoCloseable, try-with-resource

    //ARM- Automatic resource management in Java 7 부터 제공
    public class FileWriterARM implements AutoCloseable {

    private final FileWriter writer;

    public FileWriterARM(final String fileName) throws IOException {
        writer = new FileWriter(fileName);
    }

    public void writeStuff(final String message) throws IOException {
        writer.write(message);
    }

    public void close() throws IOException {
        System.out.println("close called automatically...");
        writer.close();
    }

    // ...

    public static void main(final String[] args) throws IOException {

        try (final FileWriterARM writerARM = new FileWriterARM("peekaboo.txt")) {
            writerARM.writeStuff("peek-a-boo");

            System.out.println("done with the resource...");
        }

    }
    //블럭을 빠져 나오면 close가 자동 호출됨
  • EAM execute around method
  • 컴파일러의 도움, private 사용 > 팩토리 역할 필요
    public class FileWriterEAM {

    private final FileWriter writer;

    //private, 외부에서 생성할 수 없다.
    private FileWriterEAM(final String fileName) throws IOException {
        writer = new FileWriter(fileName);
    }

    //private, 외부에서 호출할 수 없다. 
    private void close() throws IOException {
        System.out.println("close called automatically...");
        writer.close();
    }

    public void writeStuff(final String message) throws IOException {
        writer.write(message);
    }
    // ...

    // 외부에서 사용자가 사용하는 인터페이스 
    // UseInstance은 함수형 인터페이스(FunctionalInterface)이다. 예외 처리를 반영하기 위해 생성함.
    // 파라미터에 함수형인터페이스가 정의되면 람다식을 입력할 수 있다.
    public static void use(final String fileName, final UseInstance<FileWriterEAM, IOException> block)
            throws IOException {

        final FileWriterEAM writerEAM = new FileWriterEAM(fileName); // 리소스 자원 생성

        try {
            block.accept(writerEAM); // void accept(T instance) throws X;
        } finally {
            writerEAM.close(); // 자원해제
        }
    }

    public static void main(final String[] args) throws IOException {

        FileWriterEAM.use("eam.txt", writerEAM -> writerEAM.writeStuff("sweet"));

        //코드가 길어지면 별도의 메소드로 옮겨서 이에 대한 메소드 레퍼런스로 사용하는 방식으로 간결하게 하자
        FileWriterEAM.use("eam2.txt", writerEAM -> {
            writerEAM.writeStuff("how");
            writerEAM.writeStuff("sweet");
        });

    }

지연 lazy

파라미터 이밸류에이션, 중간/종단 오퍼레이션

  • 지연 초기화
  • 무거운 객체를 필요할 시점에 생성하여 사용
  • 싱글톤 패턴의 스레드 세이프를 고려한 코드
 public class HolderThreadSafe {

    //무거운 객체
    private Heavy heavy;

    //생성자 - 객체 생성 시점에 로깅
    public HolderThreadSafe() {
        System.out.println("Holder created");
    }

    // 동시에 여러개 스레드가 호출했을 때, 쫑나지 않게 한다. 동시 실행 등...
    // synchronized는 비용이 발생한다. 메모리 장벽 등 > getHeavy()를 호출하 때마다...
    public synchronized Heavy getHeavy() {
        if (heavy == null) {
            heavy = new Heavy();
        }

        return heavy;
    }

    public static void main(final String[] args) {
        final HolderThreadSafe holder = new HolderThreadSafe();
        System.out.println("deferring heavy creation...");
        System.out.println(holder.getHeavy());
        System.out.println(holder.getHeavy());
    }
}
  • synchronized 반복 호출을 제거한 코드
  • 가상 프록시 패턴 virtual proxy pattern
public class Holder {

    // Supplier는 추상 메소드 get을 가지고 있다. get()을 실행해야 함수가 실행된다.
    // 아래 코드는 함수를 실행하는 것이 아니고 함수를 정의하는 코드일 뿐.
    private Supplier<Heavy> heavy = () -> createAndCacheHeavy();

    // 참조 : anonymous function으로 작성
    private Supplier<Heavy> heavy_anony = new Supplier<Heavy>() {
        @Override
        public Heavy get() {
            return createAndCacheHeavy();
        }
    };

    public Holder() {
        System.out.println("Holder created");
    }

    public Heavy getHeavy() {
        // 함수를 실행한다.
        return heavy.get();
    }

    //
    private synchronized Heavy createAndCacheHeavy() {

        class HeavyFactory implements Supplier<Heavy> {
            private final Heavy heavyInstance = new Heavy();

            public Heavy get() {
                return heavyInstance;
            }
        }

        // 최초 호출 시 실행되어 heavy 레퍼런스를 HeavyFactory로 바꾼다.
        if (!HeavyFactory.class.isInstance(heavy)) {
            heavy = new HeavyFactory();
        }

        return heavy.get();
    }

    public static void main(final String[] args) {
        final Holder holder = new Holder();
        System.out.println("deferring heavy creation...");
        System.out.println(holder.getHeavy());
        System.out.println(holder.getHeavy());
    }
  • 스트림의 레이지
  • 결과를 얻기 위해 필요한 순간이 될 때까지 실제 작업을 하지 않음
public class LazyStreams {

    private static int length(final String name) {
        System.out.println("getting length for " + name);
        return name.length();
    }

    private static String toUpper(final String name) {
        System.out.println("converting to uppercase: " + name);
        return name.toUpperCase();
    }

    public static void main(final String[] args) {

        List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", "Mike", "Susan", "George", "Robert",
                "Julia", "Parker", "Benson");


        //중간연산자인 filter() + map()의 람다식은 실행되지 않고 캐쉬됨
        //종단연산자인 findFirst()가 호출되어서야 종단연산자 특성에 맞게 filter와 map이 수행됨 > lazy 방식

        {
            final String firstNameWith3Letters = 
                    names.stream()
                    .filter(name -> length(name) == 3)
                    .map(name -> toUpper(name))
                    .findFirst()
                    .get();

            System.out.println(firstNameWith3Letters);
        }

        {
            //종단 연산자를 분리한 코드. 아래는 아무것도 실행되지 않는다. 
            Stream<String> namesWith3Letters = 
                    names.stream().
                    filter(name -> length(name) == 3)
                    .map(name -> toUpper(name));

            System.out.println("Stream created, filtered, mapped...");
            System.out.println("ready to call findFirst...");

            //종단 연산자가 실행이 되어서야 비로수 map과 filter가 수행된다.
            final String firstNameWith3Letters = namesWith3Letters.findFirst().get();

            System.out.println(firstNameWith3Letters);
        }

    }

}
  • 무한 스트림 infinite stream
  • 무한으로 증가하는 데이터 집합 생성
  • 무한 컬렉션에서 유한 엘리먼트를 생성

  • 소수 구하는 예제, 소수 : 2887.. java.lang.StackOverflowError 발생

public class PrimeFinder {

    //소수인지 검증하는 메소드, IntStream 및 rangeClosed을 이용
    public static boolean isPrime(final int number) {

        return number > 1 && IntStream.rangeClosed(2, (int) Math.sqrt(number)).noneMatch(divisor -> number % divisor == 0);
    }
}

public class NaivePrimes {

    // 숫자 배열에 숫자 합치기
    public static List<Integer> concat(final int number, final List<Integer> numbers) {
        final List<Integer> values = new ArrayList<>();
        values.add(number);
        values.addAll(numbers);
        return values;
    }

    // don't try this at the office
    public static List<Integer> primes(final int number) {
        if (isPrime(number)) {
            System.out.println("소수 : " + number);
            return concat(number, primes(number + 1));
        } else {
            return primes(number + 1);
        }
    }

    public static void main(final String[] args) {
        try {
            primes(1);
        } catch (StackOverflowError ex) {
            System.out.println(ex);
        }
    }
}
  • 무한 스트림 생성 Stream.iterate(p1, p2) + limit()
  • 시드seed와 데이터 생성 함수UnaryOperator
  • 지연 : iterate() 메소드는 마지막 메서드를 사용하기 전까지 엘리먼트 생성을 지연
public class Primes {

    private static int primeAfter(final int number) {
        if (isPrime(number + 1))
            return number + 1;
        else
            return primeAfter(number + 1);
    }

    public static List<Integer> primes(final int fromNumber, final int count) {

        //무한 순차 스트림을 생성. 1st 파라미터 : 초기값(시드) 2nd 파라미터 : UnaryOperator 함수형 인터페이스
        return Stream.iterate(primeAfter(fromNumber - 1), Primes::primeAfter)
                .limit(count)
                .collect(Collectors.<Integer>toList());
    }

    public static void main(final String[] args) {
        System.out.println("10 primes from 1: " + primes(1, 10));
        System.out.println("5 primes from 100: " + primes(100, 5));
    }

}

재귀 recursion

테일콜, 메모이제이션

조합 composing

함수조합, 함수체인

  • 함수조합, 함수체인, 오퍼레이션 체인
  • 맵필터리듀스 패턴 MapFilterReduce
  • 쉬운 병렬화 multi core, 시간 소모가 크고 매우 큰 컬렉션 대상

  • 예제 : 주식이름으로 가격을 가져와서 500이하 가격 주식 중 가장 비싼 주식 찾기

    //함수형 스타일로 변환한 코드
    //서술적, 불변성, 간결
    //쉬운 병렬화

    public static void findHighPriced(final Stream<String> symbols) {

        //symbol collection > stockInfo stream > filter stream > reduce stockInfo

        final StockInfo highPriced = 
                symbols
                .map(StockUtil::getPrice)
                .filter(StockUtil.isPriceLessThan(500))
                .reduce(StockUtil::pickHigh).get();

        System.out.println("High priced under $500 is " + highPriced);
    }

    public static void main(final String[] args) {

        //1. 순차 코드, 순차 스트림을 리턴한다
        findHighPriced(Tickers.symbols.stream());

        //2. 병렬화 적용코드, 병렬 스트림을 리턴한다. 리턴 타입은 순차방식과 같다.
        //병렬화 조건 혹은 고려사항
        //불변성, 사이드이펙트, 레이스컨디션, 스레드세이프티
        findHighPriced(Tickers.symbols.parallelStream());
    }
  • 기존 방식, 설명한 내용을 3단계로 코드화

      // 명령형 스타일로 작성된 코드
      public static void main(final String[] args) {
    
          final List<StockInfo> stocks = new ArrayList<>();
    
          for (String symbol : Tickers.symbols) {
              // symbol 갯수만큼 StockInfo를 생성하여 add 한다.
              stocks.add(StockUtil.getPrice(symbol));
          }
    
          final List<StockInfo> stocksPricedUnder500 = new ArrayList<>();
          final Predicate<StockInfo> isPriceLessThan500 = StockUtil.isPriceLessThan(500);
    
          for (StockInfo stock : stocks) {
              //500보다 작은 가격 선택
              if (isPriceLessThan500.test(stock))
                  stocksPricedUnder500.add(stock);
          }
    
          //가장 높은 가격을 찾음
          StockInfo highPriced = new StockInfo("", BigDecimal.ZERO);
          for (StockInfo stock : stocksPricedUnder500) {
              highPriced = StockUtil.pickHigh(highPriced, stock);
          }
    
          System.out.println("High priced under $500 is " + highPriced);
      }
    
  • 기존 방식 축약

          StockInfo highPriced = new StockInfo("", BigDecimal.ZERO);
    
          final Predicate<StockInfo> isPriceLessThan500 = StockUtil.isPriceLessThan(500);
    
          for (String symbol : Tickers.symbols) {
              StockInfo stockInfo = StockUtil.getPrice(symbol);
    
              if (isPriceLessThan500.test(stockInfo))
                  highPriced = StockUtil.pickHigh(highPriced, stockInfo);
          }
    
          System.out.println("High priced under $500 is " + highPriced);
    
      }
    

    종합

  • 문법의 변화가 아니다. 함수형 스타일 프로그래밍은 개발 패러다임을 바꾸는 일 > 노오력!

  • 명령형 + 객체지향 + 함수형 > 발란스! 새로운 형태의 설계, 코드 패턴 등장
  • 스터디 + 아이디어 + 피드백 + 리뷰 + 세션 + 잡담
보다 서술적
  • 한 줄 한 줄의 코드보다 목표를 코드로 설명
  • 높은 추상화 레벨 abstraction level
  • 가변 변수 사용을 줄여줌

    List<Integer> prices = Arrays.asList(10, 20, 30, 40, 50, 60);

    //명령적
    {
        int max = 0;

        for (int price : prices) {
            if (max < price)
            max = price;
        }
    }

    //서술적
    final int max = prices.stream().reduce(0, Math::max);

    System.out.println(max);
불변성
  • 가변변수 > 문제의 씨앗
사이드이펙트 감소
  • 항상 입출력이 동일, 일관성
  • 참조투명성 referential transparency
  • 컴파일러 최적화
문장보다는 표현
  • 표현식expression 액션 수행 후 결과 리턴, 문장 statement 액션 수행 후 리턴이 없음
  • 표현식은 두 개 이상의 표현식을 조합, 함수체인
고차함수로 설계
  • 기존에는 객체를 주고 받았으나 이제 함수도 주고 받을 수 있다.
  • 함수형 인터페이스를 파라미터화 < 익명 이너 클래스, 람다식, 메소드 레퍼런드도 인수로 넘길 수 있음
    //익명 이너 클래스로 작성
    button.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent event) {
            JOptionPane.showMessageDialog(frame, "you clicked!");
        }
    });

    //람다식으로 작성, 간결, 
    button.addActionListener(event -> 
                  JOptionPane.showMessageDialog(frame, "you clicked!"));
성능
  • 컴파일러 최적화
  • invokedynamic 바이트코드 명령어 등장
  • parallalStream()

results matching ""

    No results matching ""