ㅤㅤㅤ

람다 표현식 보충자료 본문

プログラミング/JAVA

람다 표현식 보충자료

ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ 2017. 6. 7. 13:23

1. 헬로 람다 표현식

사고의 전환

함수형 스타일에 대해 간단히 알아보기

명령형(Imperative) 스타일에서 서술적(Declarative) 스타일로의 전환

명령형 스타일

for (String city : cities) {
    if (city.equals("Chicago")) {
        found = true;
        break;
    }
}
System.out.println("Found chicago? : " + found)

개선된 서술적 스타일의 예

System.out.println("Found chicago? : " + cities.contains("Chicago"))

위와 같이 코드를 수정하면 이전 코드에비해 더 나은 코드가 될 수 있다.

  • 난잡한 가변변수(Mutable Variable)의 사용을 방지
  • 이터레이션에 대한 코드가 외부로 드러나지 않기 때문에 개발자는 이터레이션 자체 코드에 대해서 신경 쓸 필요가 없음
  • 어수선한 코드의 사용을 막아줌
  • 코드에 대한 설명이 명확해짐
  • 비즈니스 의도는 유지하면서 코드는 명료해짐
  • 오류의 발생확률을 줄여줌
  • 이해하고 쉽고 유지보수가 쉬움

조금 더 복잡한 케이스

prices의 합계를 구하고, 20보다 크면 10%를 할인하는 로직

명령형 스타일

BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;

for(BigDecimal price : prices) {
  if(price.compareTo(BigDecimal.valueOf(20)) > 0) 
    totalOfDiscountedPrices = 
      totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);

서술적 스타일

final BigDecimal totalOfDiscountedPrices = 
    prices.stream()
        .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
        .map(price -> price.multiply(BigDecimal.valueOf(0.9)))
        .reduce(BigDecimal.ZERO, BigDecimal::add);

System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);

위의 코드는 기존의 습관적 방법으로 작성한 코드에 비해 상당히 향상되었다.

  • 코드가 어수선하지 않고 더 짜임새 있게 구성됐다.
  • 로우-레벨 오퍼레이션을 사용하지 않는다.
  • 로직을 강화하거나 변경하기에 더 쉽다.
  • 메서드를 사용할 때 이터레이션은 라이브러리에 의해 제어된다.
  • 더 효율적이다. 루프에 대한 레이지 이벨류에이션(lazy evaluation)이 가능하다.
  • 원하는 부분을 병렬화 하기가 더 쉽다.

명령형 프로그래밍의 번거로움을 해결하는 함수형 프로그래밍 방법의 중요한 기능 중 하나가 람다(Lambda)이다.

함수형 스타일 코드의 큰 이점

  • 변수의 명시적인 변경이나 재할당의 문제를 피할 수 있다. (immutability)
  • 함수형 버전은 쉽게 병렬화가 가능하다.
  • 서술적인 코드작성이 가능하다.
  • 함수형 버전은 더 간결하고, 직관적이다.

함수형 스타일로 코딩해야 하는 이유

문제의 핵심은 이터레이션

기존에는 컬렉션에 있는 각 엘리먼트를 반복하고 출력할 때, for loop를 사용하고, 몇개의 추가 가변변수 오퍼레이션을 지원하기 위해 loop안에 코드를 추가해야했다.

현재 자바에서는 다양한 오퍼레이션을 위해 특별한 내부 이터레이터를 제공한다. loop를 간단하게 하고, 데이터값을 매핑, 선택한 값을 필터링, 감소(reduce)시키며 최소, 최대 평균값을 구하는 편리한 기능을 제공하기도 한다. (2장에서 다시 설명)

개발할 때 고려해야 하는 정책의 강화

오퍼레이션이 보안인증을 받아야 한다는 것을 보장하는 경우에 대한 기존코드

Transaction transaction = getFromTransactionFactory();

// ...트랜잭션 안에서 실행하는 오퍼레이션...

checkProgressAndCommitOrRollbackTransaction();
UpdateAnditTrail();

람다함수를 이용한 코드

runWithinTransaction((Transaction transanction) -> {
// ...트랜잭션 안에서 실행하는 오퍼레이션...
});
  • 상태를 체크하는 정책과 감시기록을 업데이트하는 작업은 runWithinTransaction() 메서드 안에서 추상화되고 캡슐화된다.
  • try-catch문을 추가하더라도, runWithinTransaction() 메서드 안에서 처리가 가능하다.

정책 확장하기

확장성의 기본 구조는 하나 이상의 인터페이스로 되어있다는 점이다. 이러한 형태는 클래스의 계층구조를 주의 깊게 설계해야 하고, 복잡해지며 향후 유지보수와 확장이 어려워진다.

그에비해 함수형 인터페이스와 람다표현식을 사용하면 확장할 수 있는 정책을 설계하도록 도와준다. 추가적인 인터페이스를 작성하지 않고, 코드의 비헤이비어(Behavior)에 집중해야한다.

손쉬운 병렬화.

람다를 이용하면 순차방식과 병렬방식의 코드사이에 차이점이 없게 되므로, 손쉽게 병렬화가 가능하다.

스토리 텔링

비즈니스 로직과 코드를 유사하게 가져갈 수 있어, 유지보수비용이 감소한다.

//모든 주식시세에 대한 가격을 구하고, $500 이하의 주식가격을 찾아서 그 주식의 가격을 합산하라.
tickers.map(StockUtil::getPrice).filter(StockUtil::priceIsLessThan500).sum()

문제의 분리

조건에 따라 로직을 분리하려할 때, 기존 OOP에서는 전략패턴(Strategy Pattern)을 사용하여 해결하지만, 람다를 사용하여 더 적은 코드로 구현이 가능하다.

지연연산

몇 개의 오퍼레이션을 피하거나 적어도 잠시 실행을 연기시키는 것은 성능을 향상시킬 수 있는 가장 쉬운 방법이며, 해당기능을 손쉽게 제공한다.

혁명이 아닌 진화

자바 언어팀은 언어와 JDK에 함수형 기능을 넣기 위해 많은 시간과 노력을 쏟아왔다. 몇개의 가이드라인을 따르면 우리의 코드를 향상시킬 수 있다.

서술적(Be Declarative)

첫 예제에서 봤듯이, 불변 컬렉션(Immutable Collection)에서 contains()메서드를 서술적으로 사용하는 것이 명령적 스타일을 사용하는 경우보다 코드를 작성하는 면에서 얼마나 쉬운지 알아봤고, java8에서부터 이러한 기능들을 사용할 수 있게 됐다.

불변성의 증진(Promote Immutability)

다중 변수의 변경이 있는 코드는 이해하기 어렵고 병렬화하기가 상당히 까다롭다. 불변성은 근본적으로 이런 문제를 제거한다. 자바에서는 불변성을 지원하지만 강제하지는 않는다. 가능한 불변객체를 사용하도록 습관을 바꾸자.

사이드 이펙트의 회피

사이드 아펙트가 없는 함수는 불변(immutability)이며 함수를 사용하는 중에 함수에 대한 입력의 변경이 없다. 이 함수들은 이해하기가 쉽고 오류의 발생 가능성이 낮으며 더 쉽게 최적화 할 수 있다. (레이스컨디션, 동기화업데이트 문제를 해결해준다)

문법보다는 표현에 주력

for문을 사용하는 것보단, map(), sum()등의 서술문을 이용하는 것이 코드를 간결하게 만들고, 더욱 이해하기 쉽다.

고차 함수를 사용한 설계

고차함수(high order function) : 함수를 파라미터로 받거나, 함수를 리턴하는 함수.

//고차함수의 예 filter, map, reduce
prices.stream()
    .filter(price -> price.compareTo(BigDecimal.valueOf(20) > 0))
    .map(price -> price.multiplay(BigDecimal.valueOf(0.9)))
    .reduce(BigDecimal.ZERO, BigDecimal::add)






2. 컬렉션의 사용

이 장에서는 람다 표현식을 사용하여 컬렉션을 다루는 방법에 대해 알아본다.

리스트를 사용한 이터레이션

아래와 같은 코드를 이용하여, 불변(Immutable) 컬렉션을 쉽게 생성할 수 있다.

final List<String> friedns = 
    Arrays.asList("Brian", "Jake", "Lion", "Rabbit");

기존의 각 엘리먼트를 이터레이션하여 출력하는 방식

for (int i = 0; i < friedns.length; i++) {
    System.out.println(friedns.get(i));
}

for loop는 본질적으로 순차적인 방식이라 병렬화 하기가 어렵다.

람다표현식을 이용하여 아래와 같이 나타낸다.

//기본적인 람다 표현식 (타입추론은 컴파일러가 처리한다)
friedns.forEach((name) -> System.out.println(name))
//메서드 레퍼런스
friedns.forEach(System.out::println)

리스트의 변형

List 내부의 값을 변형할 때에도 람다식을 이용하여 변환이 가능하다.

기존의 코드

//friends를 모두 대문자료 변형하여 출력
final List<String> uppercaseNames = new ArrayList<String>();
friends.forEach(name -> uppercaseNames.add(name.toUpperCase()));
System.out.println(uppercaseNames);

람다에서는 map 함수를 이용하여 값을 변경할 수 있다.

//람다 표현식
friends.stream()
    .map(name -> name.toUpperCase())
    .forEach(name -> System.out.print(name + " "));     
//메서드 레퍼런스
friends.stream()
    .map(name::toUpperCase)
    .forEach(name -> System.out.print(name + " "));
  • stream() 메서드는 jdk8의 모든 컬렉션에서 사용할 수 있으며 스트림 인스턴스에 대한 컬렉션을 래핑한다.
  • map 메서드는 입력 엘리먼트 수와 결과 엘리먼트의 수가 동일하다는 것을 보장한다.

엘리먼트 찾기

N으로 시작하는 엘리먼트를 선택하는 예제

기존의 코드

final List<String> startsWithN = new ArrayList<String>();
for(String name : friends) {
  if(name.startsWith("N")) {
      startsWithN.add(name);
  }
}

람다식 이용

//filter 메서드는 boolean 결과를 리턴하는 람다 표현식이 필요함.
final List<String> startsWithN =
friends.stream()
      .filter(name -> name.startsWith("N"))
      .collect(Collectors.toList()); //Collector는 3장에서 자세히...

람다 표현식의 재사용성

N으로 시작하는 엘리컨트를 선택하는 람다표현식을 재사용

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();

렉시컬 스코프와 클로저 사용하기

람다 표현식에서의 중복

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();

렉시컬 스코프로 중복 제거하기

public static Predicate<String> checkIfStartsWith(final String letter) {
  return name -> name.startsWith(letter);
}
final long countFriendsStartN =
  friends.stream()
         .filter(checkIfStartsWith("N")).count();
final long countFriendsStartB =
  friends.stream()
         .filter(checkIfStartsWith("B")).count();
  • 변수 letter의 범위는 startsWith 함수의 범위에 있지 않기 때문에 람다표현식의 정의에 대해 범위를 정하고 그 범위 안에서 변수 letter를 찾는다. 이것을 렉시컬 스코프(lexical scope)라고 한다. (함수가 선언된 위치에 따라 정의되는 스코프)

엘리먼트 선택

컬렉션에서 하나의 엘리먼트를 찾아 출력하는 메서드

기존 코드에서의 문제점

String foundName = null;
for(String name : names) {
  if(name.startsWith(startingLetter)) {
    foundName = name;
    break;
  }
}
if(foundName != null) {
  System.out.println(foundName);
} else {
  System.out.println("No name found");
}
  • 데이터를 찾지 못할경우에 대비하여 null체크를 해야한다.

람다표현식에서 Optional을 이용한 처리

final Optional<String> foundName = 
  names.stream()
       .filter(name ->name.startsWith(startingLetter))
       .findFirst();
//데이터가 존재할경우, 존재하지 않을경우에 대한 처리
System.out.println(foundName.orElse("No name found"));

//ifPresent 이용하여 값이 존재하는 경우만 출력
foundName.ifPresent(name -> System.out.println(name))
  • Optional클래스는 결과가 없을수도 있는 경우에 유용하다. (NullPointException이 발생하는 것을 막아준다)

컬렉션을 하나의 값으로 리듀스

리스트에서 엘리먼트의 길이가 가장 긴 항목을 찾고, 그중 첫번째 엘리먼트를 출력하는 예제

//friends가 empty일수도 있어서 Optional이다.
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)));
  • reduce가 parameter로 받는 람다표현식은 BinaryOperator라는 함수형 인터페이스다.

엘리먼트 조인

friends.stream()
     .map(String::toUpperCase)
     .collect(joining(", "))); 

// A, B, C, D와 같은 형태로 결과가 노출됨. (collect 는 3장에서 자세히 다룸)






3. String, Comparator, 그리고 filter

Method Reference

Method Reference란 람다표현식에서 메서드호출 1회로 끝나는 코드를 손쉽게 작성할 수 있게 해주는 문법이다.

람다식을 이용한 스트링 이터레이션

List<String> list = Arrays.asList("a", "b", "c");

list.chars()
  .map(x -> x.toUpperCase())
  .forEach(x -> System.out.println(x));

메서드레퍼런스를 이용하면 아래와 같은 방식으로 사용하면 훨씬 더 깔끔하게 코딩이 가능하다.

list.chars()
  .map(String::toUpperCase)
  .forEach(System.out::println);

위처럼 메서드레퍼런스 방식을 사용하게되면, 컴파일러가 파라메터 라우팅(Parameter Routing)을 어떻게 해야할지 결정해야 한다. 컴파일러는 우선 해당 메서드가 인스턴스 메서드인지, 정적메서드인지 체크한다.

  • 인스턴스 메서드일경우
    • 메서드는 파라미터를 호출하는 타깃이 된다. => parameter.toUpperCase()
  • 정적 메서드일경우

    • 파라메터는 메서드의 인수로 라우팅 => System.out.println(parameter)
  • 정적메서드, 인스턴스메서드 모두 존재하는 경우에는 컴파일러 오류가 발생한다!

Comparator 인터페이스의 구현

해당 인터페이스에서는 jdk내에서도 다양하게 사용된다, 해당 인터페이스는 java8부터 함수형 인터페이스로 바뀌었다. comparator를 구현하기위한 다양한 기법들을 사용하면 많은 이점을 얻을 수 있다.

기본적인 comparator 구현 (사람을 나이기준으로 정렬한다)

people.stream()
      .sorted((person1, person2) -> person1.ageDifference(person2))
      .collect(toList());

collect()메서드는 이터레이션의 타깃 멤버를 원하는 타입의 포맷으로 변환하는 리듀서(reducer)이다. toList()는 Collectors 컨비니언스 클래스의 정적메서드이다.

위 코드는 메서드레퍼런스를 이용하여 아래처럼도 작성이 가능하다.

people.stream()
      .sorted(Person::ageDifference)
      .collect(toList());

Comparator의 재사용

내림차순정렬을 구현할 때, 미리 만들어둔 오름차순으로 생성이 가능하다.

Comparator<Person> compareAscending = 
  (person1, person2) -> person1.ageDifference(person2);
Comparator<Person> compareDescending = compareAscending.reversed();
//이번에 default method로 추가된 컨비니언스 메서드이다

여러가지 비교연산

Comparator 인터페이스에 추가된 새로운 컨비니언스 메서드에 대해 알아보자.

comparing 메서드, (person1.getName().compareTo(person2.getName())) 와 같은 형식으로 작성해야할 코드를 손쉽게 해준다.

final Function<Person, String> byName = person -> person.getName();
people.stream()
      .sorted(comparing(byName));

comparing을 이용한 2차정렬 구현

final Function<Person, Integer> byAge = person -> person.getAge();
final Function<Person, String> byTheirName = person -> person.getName();

people.stream()
      .sorted(comparing(byAge).thenComparing(byTheirName))
      .collect(toList()));

thenComparing은 두개의 Comparator를 합성(composite)해서 새로운 Comparator를 생성한다. (아래 코드 참고 : Comparator의 default 메서드로 구현되어있음)

default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);
    return (Comparator<T> & Serializable) (c1, c2) -> {
        int res = compare(c1, c2);
        return (res != 0) ? res : other.compare(c1, c2);
    };
}

collect 메서드와 Collectors 클래스 사용하기

기존에 이미 collect()메서드를 사용했다, 이 메서드는 컬렉션을 다른 형태, 즉 가변 컬렉션(mutable collection)으로 변경하는 데 유용한 리듀스(reduce) 오퍼레이션이다.

필터링한 결과를 List로 합치고 싶을 경우

List<Person> olderThan20 = new ArrayList<>();
people.stream()
      .filter(person -> person.getAge() > 20)
      .forEach(person -> olderThan20.add(person));

forEach를 사용하면 기존 for문을 사용했을때랑 별반 다를 것이 없다. collect 메서드를 이용하면 아래와 같이 서술적으로 구현이 가능하다.

List<Person> olderThan20 = 
  people.stream()
        .filter(person -> person.getAge() > 20)
        .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

collect 메서드는 3가지의 파라메터를 받는다. 아래와 같은 메서드로 라이브러리에서 병렬로 합치는 것도 가능하게 해준다!

  • supplier(결과컨테이너를 만드는 방법) => ArrayList::new
  • acumulator(하나의 엘리먼트를 결과 컨테이너에 추가하는 방법) => ArrayList:add
  • combiner(하나의 결과 컨테이너를 다른 결과와 합치는 방법) => ArrayList::addAll

Collectors 유틸리티를 이용하면, collect에 필요한 대부분의 기능들을 미리 만들어놨다. 위에서 본 코드는 아래처럼 변경이 가능하다!

List<Person> olderThan20 = 
  people.stream()
        .filter(person -> person.getAge() > 20)
        .collect(Collectors.toList());

데이터를 그룹으로 묶는 groupingBy()

Map<Integer, List<Person>> peopleByAge = 
  people.stream()
        .collect(Collectors.groupingBy(Person::getAge));
//{35=[a - 35], 20=[b - 20], 30=[c - 30, d - 30, e - 30]}

//mapping 파라메터를 추가하면 결과를 다른형태로

mapping기능을 이용해 결과를 다른 형태로도 수집이 가능하다.

Map<Integer, List<String>> nameOfPeopleByAge = 
  people.stream()
        .collect(
          groupingBy(Person::getAge, mapping(Person::getName, toList())));
//{35=[a], 20=[b], 30=[c, d, e]}

Collectors에는 위와 같은 기능 외에도 여러가지 메서드가 존재한다. (toSet(), toMap(), joining(), minBy(), maxBy(), groupingBy() ...) 자세한 내용은 API문서를 확인해보자(https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html)

디렉터리에서 모든 파일 리스트하기

java8에서는 CloseableStream 인터페이스를 이용해서, 파일을 읽는 기능을 손쉽게 만들어준다.

Files.list(Paths.get("."))
   .filter(Files::isDirectory)
   .forEach(System.out::println);

flatMap을 사용하여 서브 디렉터리 리스트하기

주어진 디렉터리에서 서브 디렉터리를 탐색하는 로직을 flatMap기능을 이용하여 구현해보자.

List<File> files = new ArrayList<>();

File[] filesInCurrentDir = new File(".").listFiles();
for(File file : filesInCurrentDir) {
  File[] filesInSubDir = file.listFiles();
  if(filesInSubDir != null) {
    files.addAll(Arrays.asList(filesInSubDir));
  } else {
    files.add(file);
  }
}

위와 같은 로직은 아래처럼 표현이 가능하다.

List<File> files = 
  Stream.of(new File(".").listFiles())
        .flatMap(file -> file.listFiles() == null ? 
            Stream.of(file) : Stream.of(file.listFiles()))
        .collect(toList());
  } 

Stream.flatMap은 (A -> Stream[A]) 함수를 받아, Stream[Stream[A]]와 같은 형태의 데이터를 Stream[A]의 형태로 만들어주는 고차함수이다. flatMap()은 많은 노력을 제거해주며, 단항 조합(monadic composition)이라고 하는 두 개의 오퍼레이션의 순서를 하나의 과정으로 조합해준다.







4. 람다 표현식을 이용한 설계

자바에서 OOP와 함수형 스타일은 서로 보완관계이며 함께 사용했을 때 상당한 효과를 볼 수 있다. 이 두가지 방법을 사용하면 수정과 확장이 쉽고 유연하며 효율적인 설계를 할 수 있다.

람다를 사용한 문제의 분리

클래스를 만드는 이유는 코드를 재사용하기 위해서이다. 이것은 좋은 의도이기는 하나 항상 올바른 것은 아니다. 고차 함수를 사용하면 클래스의 복잡한 구조 없이도 같은 목적을 달성할 수 있다.

설계 문제 살펴보기

문제를 분리하는 설계 아이디어를 설명하는 방법으로 asset 값들을 더하는 예제부터 시작해보자.

Asset Bean

public class Asset {
  public enum AssetType { BOND, STOCK }; 
  private final AssetType type;
  private final int value;
  public Asset(final AssetType assetType, final int assetValue) {
    type = assetType;
    value = assetValue;
  }
  public AssetType getType() { return type; }
  public int getValue() { return value; }
}

주어진 asset의 모든 값을 더하는 메서드를 작성해보자.

public static int totalAssetValues(final List<Asset> assets) {
  return assets.stream()
               .mapToInt(Asset::getValue) //primitive type을 지원하기위한 맵
               .sum();
}

mapToInt는 primitive type을 지원하기위한 함수이다. IntStream으로 반환되며, IntStream은 sum()이라는 추가적인 기능을 제공한다.

복잡한 문제 다루기

전체의 합계가 아닌 채권(Bond), 주식(Stock) 각각에 대해서 합계를 구하는 함수를 만들어 보자. (모든값을 더하는 메서드를 작성하였지만 생략함)

전체의 합, 채권의 합, 주식의 합을 구하는 메서드는 Strategy Pattern을 적용하기에 좋은 경우이다. 자바에서는 이 패턴을 구현하기 위해 종종 인터페이스, 클래스 등을 생성하지만, 람다 표현식으로 적용해보면 아래처럼 구현이 가능하다.

public static int totalAssetValues(final List<Asset> assets,
      final Predicate<Asset> assetSelector) {
  return assets.stream()
               .filter(assetSelector)
               .mapToInt(Asset::getValue)
               .sum();
}

//사용 (assets는 생략)
System.out.println("Total of all assets: " + 
  totalAssetValues(assets, asset -> true));

System.out.println("Total of bonds: " + 
  totalAssetValues(assets, asset -> asset.getType() == AssetType.BOND));

System.out.println("Total of stocks: " + 
  totalAssetValues(assets, asset -> asset.getType() == AssetType.STOCK));

Predicate 인터페이스는 참,거짓을 리턴하는 함수형이다.

람다 표현식을 사용하여 딜리게이트하기

클래스 레벨에서 문제를 분리하기. 재사용 측면에서 보면 딜리게이트(delegate)는 상속보다 더 좋은 설계 도구이다. 딜리게이트를 사용하면 다양한 구현을 쉽게 만들 수 있으며 좀 더 동적으로 여러 가지 비헤이비어(Behavior)를 추가할 수도 있다.

딜리게이트 생성

책임져야 할 부분을 다른 클래스에 딜리게이트하는 것보다, 메서드 레퍼런스로 딜리게이트하는 것이 더 좋다. 이러한 방법은 클래스의 개수가 늘어나는 것을 막아준다.

private Function<String, BigDecimal> priceFinder; //주식시세값을 반환한다.
//주식시세표와, 주식수로 주식의 가치를 결정하는 함수
public BigDecimal computeStockWorth(final String ticker, final int shares) {
  return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));
}

위 예제에서는 주식시세값을 반환하는 부분을 딜리게이트하였다. 이부분을 클래스에서 직접 구현하기보다, 외부에서 주입이 가능하도록 변경해보자 (Dependency inversion principle 원칙) 이렇게 하는 이유는 코드를 좀 더 확장할 수 있도록 한다.

//생성자
public CalculateNAV(final Function<String, BigDecimal> aPriceFinder) {
  priceFinder = aPriceFinder;
}

람다 표현식을 사용한 데코레이팅

데코레이터 패턴(decorator pattern)은 강력하지만 프로그래머는 종종 이 기능을 사용하는 것에 대해 주저한다. 이유는 클래스와 인터페이스의 계층에 대해 부담을 느끼기 때문이다. 다음 예제를 통하여 람다표현식을 사용하여 데코레이터 패턴을 어떻게 사용하는지 알아보자.

필터 설계

카메라에 필터를 추가하는 작업을 체인 형태로 연결해보자.

public class Camera {  
  private Function<Color, Color> filter;

  public Color capture(final Color inputColor) {
      final Color processedColor = filter.apply(inputColor);
      //... more processing of color...
      return processedColor;
  }
}

위 코드에서 filter는 Color를 받아서 처리하고 처리한 Color를 리턴하는 함수이다. 클래스가 설계된 것을 보면 하나의 필터만 사용하지만, 다양한 필터를 사용할 수 있도록 수정해보자.

public void setFilters(final Function<Color, Color>... filters) {
  filter = Stream.of(filters)
          .reduce((filter, next) -> filter.andThen(next)) //책에는 compose로 작성되었는데, 잘못나온것 같음...
          .orElse(color -> color);
}

위 코드에서는 Function 인터페이스가 제공해주는 default method인 andThen() 함수를 이용한다. andThen함수는 함수를 합성해주는 함수이다. 함수를 합성해주는 함수는 andThen이외에도 compose함수가 존재한다.

funcA.andThen(funcB) => funcB(funcA(input))
    => input -> funcA -> funcB -> result
funcA.compose(funcB) => funcA(funcB(input))
    => input -> funcB -> funcA -> result

다시 필터로 돌아가서 코드를 살펴보면 reduce를 통해 filter함수를 합성하는 방식으로, 합성된 하나의 filter함수를 얻는다. setFilter함수의 기능으로 우리는 filter들의 다양한 조합을 만들 수 있는 데코레이터 패턴을 손쉽게 구현이 가능하다.

//어두운 필터
camera.setFilters(Color::darker);

//밝은 필터
camera.setFilters(Color::brighter);

//어둡고, 밝은 필터를 동시에..
camera.setFilters(Color::darker, Color::brighter);

디폴트 메서드 들여다보기

자바 컴파일러는 디폴트 메서드를 사용하기 위해 다음과 같은 간단한 규칙을 따른다.

  • 서브 타입은 슈퍼 타입으로부터 자동으로 디폴트 메서드를 넘겨받는다.
  • 디폴트 메서드를 사용하는 인터페이스에서 서브타입에서 구현된 디폴트 메서드는 슈퍼타입에 있는 것보다 우선한다.
  • 추상 선언을 포함하여 클래스에 있는 구현은 모든 인터페이스의 디폴트보다 우선한다.
  • 두 개 이상의 디폴트 메서드의 구현이 충돌하는 경우 혹은 두 개의 인터페이스에서 디폴트 추상 충돌(default-abstract conflict)이 발생하는 경우, 상속하는 클래스가 명확하게 해야한다.





이 글은 "자바 8 람다의 힘" 책을 정리한 글입니다.

Chapter 2. 컬렉션의 사용


Java8에서 컬렉션을 어떻게 사용하는지 알아보기 전에 Java에서 Collection은 무엇인지부터 알아보자.

다음은 Java Collection Framework의 기본 상속 구조이다.





Java 프로그래밍을 하다보면 크게 3가지 객체타입을 마주하게 된다.

     - List
        Element 들의 순서가 있으며, 중복을 허용한다.

     - Set

        Element 들의 순서는 없고, 중복이 허용되지 않는다.

     - Map

        List와 Set이 집합적 개념이라면, Map은 검색적 개념이 가미된 Interface이다.
        key와 value로 구성되며, key를 통해 value에 접근할 수 있다.
        주로 사용되는 HashMap의 경우 Java 버전에 따라 동장 원리가 조금씩 차이가 있다.
        (상세하게 알고 싶다면 아래 참고 사이트를 확인해라)



      Java8에서는 Collection을 iteration하고, transform(변형)하고, Element를 추출하거나, 그 Element들을 연결하는 여러가지 방법을 제공한다. 하나씩 살펴보자.

      1. Iteration

      예를 들어, 아래와 같이 friends라는 친구들의 이름을 Element로 가지고 있는 List가 있다고 하자

      private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

      친구들의 이름을 출력해야 한다면, 아마도 다들 아래와 같이 코딩을 하게 될 것이다.

      public class Iteration {
          private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

          public static void main(String[] args) {
              for (String name : friends) {
                  System.out.println(name);
              }
          }
      }

      java 8에서 제공하는 forEach와 Consummer<T> 인터페이스를 이용하면 아래와 같이 된다.

      public class Iteration {
          private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

          public static void main(String[] args) {
              friends.forEach(new Consumer<String>() {
                  public void accept(final String name) {
                      System.out.println(name);
                  }
              });
          }
      }


      ※ Consummer<T> 인터페이스는 T 타입의 객체를 소비하는 인터페이스이다. accept() 라는 하나의 method를 가지고 있다.


      여기에 Lambda 표현식을 추가해 보자.

      public class Iteration {
          private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

          public static void main(String[] args) {
              friends.forEach(name -> System.out.println(name));

              friends.forEach(System.out::println);
          }
      }

      위 코드들은 모두 동일한 결과가 나온다.
      조금 흥미로은 점은 마지막 코드 예제인데.

      System.out::println 으로 method reference라고 한다. 추후 Steam API를 공부하게 되면 자연스럽게 접할 기회가 많으니, 여기서는 이런 방법도 있다 정도로 하고 넘어가면 좋을 것 같다.


      2. Transform(변형)

      친구들의 이름을 모두 대문자로 변형하는 코드를 작성해보자.

      public class Transform {
          private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

          public static void main(String[] args) {
              final List<String> upperCaseNames = new ArrayList<String>();

              for (String name : friends) {
                  upperCaseNames.add(name.toUpperCase());
              }

          
          System.out.println(upperCaseNames);
          }
      }

      뭔가 장황한 느낌이다. 이 코드를 개선해보자.

         - foreach() 적용 및 Lambda 적용

         - Stream API 적용

         - Method Reference 적용


          public class Transform {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  final List<String> upperCaseNames = new ArrayList<String>();

                  friends.forEach(name -> upperCaseNames.add(name.toUpperCase()));

              
              System.out.println(upperCaseNames);
              }
          }
          public class Transform {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  friends.stream()
                      .map(name -> name.toUpperCase())
                      .forEach(name -> System.out.print(name + " "));
              }
          }
          public class Transform {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  friends.stream()
                      .map(String::toUpperCase)
                      .forEach(System.out::println);
              }
          }

          Method Reference 는 언제 사용하는 것일까?

          람다 표현식을 사용할 때 파라미터를 전달하지 않는 경우라면 사용할 수 있다.


          3. Element 찾기

          친구들 이름 중에 N으로 시작하는 친구를 찾아보자.

          public class PickElements {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  final List<String> startsWithN = new ArrayList<String>();
                  for (String name : friends) {
                      if (name.startsWith("N")) {
                          startsWithN.add(name);
                      }
                  }

              
              System.out.println(startsWithN);
              }
          }

          아마도 대부분의 개발자들이 위처럼 코드를 작성할지도 모른다. 간단한 기능임에도 다소 장황하게 보인다.

          filter() 함수를 통해 코드를 좀 더 직관적이고 간단하게 작성할 수 있다.

          public class PickElements {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  final List<String> startsWithN = friends.stream()
                                                                      .filter(name -> name.startsWith("N"))
                                                                      .collect(Collectors.toList());
                  System.out.println(startsWithN);
              }
          }

          좀 더 직관적이고 간단하게 코딩할 수 있다. 하지만 약간의 센스있는 개발자라면 개선할 점이 있다는 부분을 발견할 수 있을 것이다.

          만약 친구리스트가 아니라 에디터리스트 중에서도 N으로 시작하는 에디터를 출력한다고 해보자.

          public class PickElements {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
              private final static List<String> editors = Arrays.asList("Brian", "Jackie", "John", "Mike");

              public static void main(String[] args) {
                  final List<String> friendsStartsWithN = friends.stream()
                                                                              .filter(name -> name.startsWith("N"))
                                                                              .collect(Collectors.toList());
                  System.out.println(friendsStartsWithN);

                  final List<String> editorsStartsWithN = editors.stream()
                                                                              .filter(name -> name.startsWith("N"))
                                                                              .collect(Collectors.toList());
                  System.out.println(editorsStartsWithN);
              }
          }

          중복코드가 발생했다. 이런 경우 아래 코드가 중복 코드이다.
          name -> name.startsWith("N")

          filter() 함수는 java.util.function.Predicate 함수형 인터페이스를 받는다. 이를 이용하여 일부 중복을 제거할 수 있다.

          public class PickElements {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
              private final static List<String> editors = Arrays.asList("Brian", "Jackie", "John", "Mike");

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

              public static void main(String[] args) {
                  final List<String> friendsStartsWithN = friends.stream()
                                                                              .filter(startsWithN)
                                                                              .collect(Collectors.toList());
                  System.out.println(friendsStartsWithN);

                  final List<String> editorsStartsWithN = editors.stream()
                                                                              .filter(startsWithN)
                                                                              .collect(Collectors.toList());
                  System.out.println(editorsStartsWithN);
              }
          }

          아직 더 개선할 점이 많지만 여기서는 Predicate<T> 함수형 인터페이스를 어떻게 활용할 수 있는지에 대해서만 언급하고 넘어가기로 한다.

          다른 경우를 생각해보자. N으로 시작하는 친구목록과 B로 시작하는 친구 목록을 각각 출력해보자.

          public class PickElements {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

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

              public static void main(String[] args) {
                  final List<String> friendsStartsWithN = friends.stream()
                                                                              .filter(startsWithN)
                                                                              .collect(Collectors.toList());
                  System.out.println(friendsStartsWithN);

                  final List<String> friendsStartsWithB = friends.stream()
                                                                              .filter(startsWithB)
                                                                              .collect(Collectors.toList());
                  System.out.println(friendsStartsWithB);
              }
          }

          여기서 보면 Predicate<T> 형 변수가 N과 B 문자열 차이만 있을 뿐 중복이다.
          중복을 제거할 방법이 있을까?

          public class PickElements {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  final List<String> friendsStartsWithN = friends.stream()
                                                                              .filter(checkIfStartWith("N"))
                                                                              .collect(Collectors.toList());
                  System.out.println(friendsStartsWithN);

                  final List<String> friendsStartsWithB = friends.stream()
                                                                              .filter(checkIfStartWith("B"))
                                                                              .collect(Collectors.toList());
                  System.out.println(friendsStartsWithB);
              }

              public static Predicate<String> checkIfStartWith(final String letter) {
                  return name -> name.startsWith(letter);
              }
          }
          public static Predicate<String> checkIfStartWith(final String letter) {
                  return name -> name.startsWith(letter);
          }
          public class PickElements {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              private final static Function<String, Predicate<String>> startsWithLetter =
                  letter -> {
                      return name -> name.startsWith(letter);
                  };

              public static void main(String[] args) {
                  final List<String> friendsStartsWithN = friends.stream()
                                                                              .filter(startsWithLetter.apply("N"))
                                                                              .collect(Collectors.toList());
                  System.out.println(friendsStartsWithN);

                  final List<String> friendsStartsWithB = friends.stream()
                                                                              .filter(startsWithLetter.apply("B"))
                                                                              .collect(Collectors.toList());
                  System.out.println(friendsStartsWithB);
              }
          }

          Predicate<String> 을 생성하면 된다.
          가만히 보면 checkIfStartWith 함수의 파라미터인 letter가 람다표현식 내에서 사용되었다.
          letter 변수는 참다 표현식 범위에 있지 않음에도 이 코드는 정상적으로 수행된다.
          이를 Lexical Scope 라고 한다.
               Lexical Scope란? 사용한 하나의 컨텍스트에서 제공한 값을 캐시해 두었다가 나중에 다른 컨텍스트에서 사용할 수 있는 기술이다.
               이 때 그 값은 변경이 불가능하며, final 로 선언되거나 effectively final 과 같은 의미를 가지는 변수이다. 만약 값을 변경하려고 한다면 컴파일 오류가 발생한다.

          Function<T, R> 함수형 인터페이스도 활용할 수 있다.
          Generic 표현에서도 알 수 있듯이, T 타입의 파라미터를 받아서 R 타입을 반환한다. 위 예제에서는 letter를 받아서 Predicate<String> 을 반환하고 있다.

          위 예제에서 코드를 더 간단하게 작성이 가능한데, 

          private final static Function<String, Predicate<String>> startsWithLetter =
              letter -> {
                  return name -> name.startsWith(letter);
              };
          private final static Function<String, Predicate<String>> startsWithLetter =
              letter -> name -> name.startsWith(letter);

          Predicate<T> 와 Function<T, R> 두 개의 함수형 인터페이스에 대해서도 함께 알아보았다. 나중에 상당히 많이 활용되는 인터페이스이니 꼭 알아두길 권장한다.

          3. Element 찾기

          보통 Collection 에서  여러 Element 를 찾는 것 보다 하나의 Element 를 찾는 것이 간단하다는 것은 당연하다.
          그러나 습관적으로 코딩하다 보면 복잡도(Complexity) 가 비효율적으로 높은 경우가 있는데. 이에 대해서 알아보고 Lambda를 통해 해결해보자.

          친구들 이름에서 특정 알파벳으로 시작하는 친구의 이름을 찾아보자.

          public class PickAnElement {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
              
              pickName(friends, "B");
                  pickName(friends, "A");
              }

              public static void pickName(final List<String> names, final String startingLetter) {
              
              String foundName = null;

                  for (String name : names) {
                      if (name.startsWith(startingLetter)) {
                          foundName = name;
                          break;
                      }
                  }

              
              System.out.println(String.format("A name starting with %s", startingLetter));

                  if (foundName != null) {
                      System.out.println(foundName);
                  } else {
                      System.out.println("No name found");
                  }
              }
          }

          foundName 변수를 최초 초기화시 null 로 선언했다. 이는 잠재적인 문제가 될 수 있다.
          또한 외부 데이터를 통해 루프를 수행하고 엘리먼트를 찾으면 루프를 빠져나온다. 이 또한 잠재적인 문제가 될 수 있다.

          Lambda 와 Optional<T> 인터페이스를 통해 코드를 좀 더 간결하고 안정적으로 개선해보자.

          public class PickAnElement {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
              
              pickName(friends, "B");
                  pickName(friends, "A");
              }

              public static void pickName(final List<String> names, final String startingLetter) {
                  final Optional<String> foundName =
                      names.stream()
                              .filter(name -> name.startsWith(startingLetter))
                              .findFirst();

              
              System.out.println(String.format("A name starting with %s : %s",
                      startingLetter, foundName.orElse("No name found")));
              }
          }

          Optional<T> 클래스에 대해서 알아보자.
          이는 null 값으로 인해 NullPointException 이 발생하는 것을 막아준다.
          몇가지 지원하는 함수가 있는데.
          • isPresent() - 객체가 존재하는지?
          • get() - 현재 값을 가져온다.
          • orElse() - 값이 없는 경우 별도 처리를 할 수 있다.(위 코드 참조)


          4. Collection Reduce

          먼저 간단하게 전체 글자수를 세어보자.

          public class PickALongest {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
              
              System.out.println("Total number of characters in all names : " +
                      friends.stream()
                                  .mapToInt(name -> name.length())
                                  .sum());
              }
          }

          map 오퍼레이션(mapToInt, mapToDouble) 을 통해서 값을 변형하고, reduce 오퍼레이션(sum, max, min, sorted, average) 로 결과의 길이를 reduce 시킨다.

          그럼 친구들 중에 가장 긴 이름을 가진 친구 이름을 찾아 출력해보자.

          public class PickALongest {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  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))
                  );
              }
          }

          reduce() 메서드에 전달되는 Lambda 표현식은 두 개의 파라메터를 갖는다. 길이를 비교하여 하나만 return 한다.
          이는 reduce() 메서드가 전달되는 Lambda 표현식의 의도를 전혀 알지 못하며, 전달되는 Lambda 표현식과는 분리되어 있다.
          이를 전략 패턴(Strategy Pattern) 이라고 한다.

          여기서 Lambda 표현식은 BinaryOperator 라는 함수형 인터페이스로 apply() 메서드를 따른다.

          R apply(T t, U u);

          만약 특정 이름보다 긴 이름을 찾고자 한다면 어떻게 해야할까?

          public class PickALongest {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  final String steveOverLonger =
                      friends.stream()
                              .reduce("Steve", (name1, name2) -> name1.length() >= name2.length() ? name1 : name2);

              
              System.out.println(String.format("A name over Steve's length : %s", steveOverLonger));
              }
          }

          이 경우 Optional 을 return 하지 않는다. 그 이유는 만약 reduce() 한 결과가 비어 있다고 하더라도 기본값인 “Steve” 가 return 될 것이기 때문이다.


          5. Element Join

          친구들의 이름을 ,(쉼표) 를 구분자로 하여 출력을 해보자.

          public class PrintList {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  for (String name : friends) {
                      System.out.print(name + ", ");
                  }
                  System.out.println();
              }
          }
          public class PrintList {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
                  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));
                  }
              }
          }

          마지막 ,(쉼표)를 제거하기 위해 몇 줄의 코드가 더 필요했다.
          String 클래스에 join() 메서드가 추가되면서 더 이상 이런 지저분한 코드는 필요 없게 되었다.

          public class PrintList {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
              
              System.out.println(String.join(",", friends));
              }
          }

          이젠 Stream API 를 통해서 Element 들을 join 해보자.

          public class PrintList {
              private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

              public static void main(String[] args) {
              
              System.out.println(
                      friends.stream()
                              .map(String::toUpperCase)
                              .collect(Collectors.joining(", "))
                  );
              }
          }

          여기까지 Java Collection 에 대해서 알아보았다.
          Lambda 표현식과 Stream API 를 활용하면 코드를 좀 더 직관적이고 간단하게 구현할 수 있다.




          이 글은 "자바 8 람다의 힘" 책을 정리한 글입니다.

          Chapter 3. String, Comparator 그리고 filter

          1. String Iteration

          문자열(String) 의 각 문자들을 출력해보자.

          public class IteraeString {
              public static void main(String[] args) {
                  final String str = "w00t";

                  str.chars().forEach(System.out::println);
              }
          }

          2장 컬렉션 사용에서 공부한 것 처럼 method reference와 함께 사용할 수 있다.
          여기서 chars() 는 CharSequence 인터페이스로부터 파생한 String 클래스의 새로운 메소드이다.

          위 코드를 실행해보면 아래와 같이 숫자로 출력되는데, 이는 chars() 가 기본적으로 IntStream을 반환하기 때문이다.

          119
          48
          48
          116

          그렇다면 숫자가 아닌 문자를 출력해주려면 어떻게 해야할까?
          IntStream 내의 Element 들을 변형해 주어야 한다. Stream API 의 mapToObj() 를 사용해보자.

          public class IteraeString {
              public static void main(String[] args) {
                  final String str = "w00t";

                  str.chars()
                      .mapToObj(ch -> Character.valueOf((char)ch))
                      .forEach(System.out::println);
              }
          }
          w
          0
          0
          t

          코드를 수행해보면 문자로 출력됨을 확인할 수 있다.

          샘플코드에서 보면 문자열 데이터가 “w00t” 이다. 여기서 가운데 “00” 는 숫자이다.
          만약 이렇게 숫자만 출력하고 싶으면 어떻게 할까? filter() 를 활용할 수 있다.

          public class IteraeString {
              public static void main(String[] args) {
                  final String str = "w00t";

                  str.chars()
                      .filter(Character::isDigit)
                      .mapToObj(ch -> Character.valueOf((char)ch))
                      .forEach(System.out::println);
              }
          }

          Character 클래스 내에 숫자 여부를 판단하는 isDigit() 이라는 static method를 사용하면 된다.
          아주 간단하게 해결 되었다.

          2. Comparator 인터페이스

          Comparator 인터페이스는 Java에서 자주 사용되는 인터페이스 중 하나이다.
          검색부터 정렬, 역정렬 등 많이 사용되는데, Java8에서는 함수형 인터페이스로 변경되었다.

          Comparator를 이용해서 정렬을 한 번 해보자.
          예를 들어 사람들 정보가 있을 때, 나이순으로 정렬하는 문제를 해결해 보자.

          먼저 Person 클래스를 하나 생성한다.

          public class Person {
              private final String name;
              private final int age;

              public Person(final String name, final int age) {
                  this.name = name;
                  this.age = age;
              }

              public String getName() {
                  return name;
              }

              public int getAge() {
                  return age;
              }

              public int ageDifference(final Person other) {
                  return age - other.age;
              }

              public String toString() {
                  return String.format("%s - %s", name, age);
              }
          }

          나이 오름차순으로 정렬 해보자.

          public class Compare {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              public static void main(String[] args) {
              
              List<Person> ascendingAge = people.stream()
                                                                  .sorted((person1, person2) -> person1.ageDifference(person2))
                                                                  .collect(Collectors.toList());
                  printPeople("Sorted in ascending order by age : ", ascendingAge);
              }

              private static void printPeople(String message, List<Person> people) {
              
              System.out.println(message);
                  people.forEach(System.out::println);
              }
          }
          Sorted in ascending order by age :
          John - 20
          Sara - 21
          Jane - 21
          Greg - 35

          코드를 수행해보면 나이 오름차순으로 잘 정렬됨을 확인할 수 있다.

          .sorted((person1, person2) -> person1.ageDifference(person2))
          .sorted(Person::ageDifference)

          위처럼 sorted() 함수 파라메터인 람다 표현식을 개선할 수 도 있다.

          이제 내림차순으로도 정렬해보자.

          .sorted((person1, person2) -> person2.ageDifference(person1))

          sorted() 람다표현식을 위 처럼 변경해주면 된다.

          Comparator<T> 인터페이스를 적용하여 재사용 가능한 Comparator 로 만들어 둘 수 있다.

          public class Compare {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              private static final Comparator<Person> ascending = (person1, person2) -> person1.ageDifference(person2);
              private static final Comparator<Person> descending = ascending.reversed();

              public static void main(String[] args) {
              
              printPeople("Sorted in ascending order by age : ",
                      people.stream()
                          .sorted(ascending)
                          .collect(Collectors.toList()));
                  printPeople("Sorted in descending order by age : ",
                      people.stream()
                          .sorted(descending)
                          .collect(Collectors.toList()));
              }

              private static void printPeople(String message, List<Person> people) {
              
              System.out.println(message);
                  people.forEach(System.out::println);
              }
          }

          이제 정렬문제는 해결되었다.
          그럼 가장 젊은 사람 또는 가장 나이가 많은 사람을 선택해보자.

          public class Compare {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              public static void main(String[] args) {
                  people.stream()
                          .min(Person::ageDifference)
                          .ifPresent(youngest -> System.out.println(youngest));
              }
          }
          public class Compare {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );


              public static void main(String[] args) {
                  people.stream()
                          .max(Person::ageDifference)
                          .ifPresent(eldest -> System.out.println(eldest));
              }
          }

          min(), max() 함수는 Optional 타입을 반환하는데, 이는 List가 비어 있는 경우 가장 어린 사람 또는 가장 나이가 많은 사람이 존재하지 않을 수 있기 때문이다.

          3. 여러가지 비교 연산

          지금까지 나이 순으로 정렬, 가장 어린 사람 또는 나이가 많은 사람을 선택해 보았다.
          이제는 이름 순으로 정렬을 해보자.

          단순히 ageDifference라는 함수처럼 name 변수에 대해서도 추가하여 method reference 를 적용해 볼 수 있지만, Comparator 인터페이스에 새로 추가된 method를 이용하여 간단하게 구현해보자.


          public class Compare {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );


              public static void main(String[] args) {
                  people.stream()
                          .sorted((person1, person2) ->
                                      person1.getName().compareTo(person2.getName()))
                          .forEach(System.out::println);;
              }
          }
          public class Compare {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              public static void main(String[] args) {
                  final Function<Person, String> byName = person -> person.getName();
                  final Function<Person, Integer> byAge = person -> person.getAge();

              
              printPeople("이름순으로 정렬", people.stream()
                                                                  .sorted(Comparator.comparing(byName))
                                                                  .collect(Collectors.toList()));
                  printPeople("나이순으로 정렬", people.stream()
                                                                  .sorted(Comparator.comparing(byAge))
                                                                  .collect(Collectors.toList()));
              }

              private static void printPeople(String message, List<Person> people) {
              
              System.out.println(message);
                  people.forEach(System.out::println);
              }
          }

          Comparator.comparing() 를 이용하여 좀 더 간편하게 구현할 수 있다.

          4. collect() 와 Collectors 클래스

          collect() 함수에 대해서 좀 더 알아보도록 하자.

          먼저 people 변수 내에서 20살 이상의 사람들만 선택하는 코드를 생성해보면 아래와 같이 작성할 수 있을 것이다.

          public class OlderThan20 {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              public static void main(String[] args) {
              
              List<Person> olderThan20 = new ArrayList<Person>();
                  people.stream()
                          .filter(person -> person.getAge() > 20)
                          .forEach(person -> olderThan20.add(person));

              
              System.out.println("People older than 20 : " + olderThan20);
              }
          }

          코드를 수행해보면 결과도 잘 나온다. 원하는 결과를 얻었지만 여기에는 몇 가지 문제점이 있다.

          1. 타켓 컬렉션(olderThan20) 에 Element를 추가하는 Operation이 너무 low level이다.
               자바8에서는 서술적 프로그래밍을 권장하는데, 이는 너무 명령적이다.
          2. Thread unsafe
               병렬처리시 문제가 될 수 있다.

          즉, 2번 항목인 병렬처리시 Thread-unsafe 하다는 것인데. collect() 를 이용하면 손쉽게 해결이 가능하다.

          collect() 는 Element 에 대한 Stream 을 가지며, 결과 컨테이너에 각 Stream 들을 모은다.
          * 결과 컨테이너를 만드는 방법(ArrayList::new)
          * 하나의 Element 를 결과 컨테이너에 추가하는 방법(ArrayList::add)
          * 하나의 결과 컨테이너를 다른 것과 합치는 방법(ArrayList::addAll)

          public class OlderThan20 {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              public static void main(String[] args) {
              
              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);
              }
          }

          이로써 서술적인 프로그래밍이 되었고, Thread-safe를 보장하게 되었다.
          참고로 ArrayList 가 Thread-unsafe 하더라도 문제가 되지 않는다.

          Collectors 클래스를 활용하여 좀 더 간단하게 프로그래밍을 할 수도 있다.

          public class OlderThan20 {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              public static void main(String[] args) {
              
              List<Person> olderThan20 =
                      people.stream()
                              .filter(person -> person.getAge() > 20)
                              .collect(Collectors.toList());

              
              System.out.println("People older than 20 : " + olderThan20);
              }
          }

          toList() 외에도 다양한 편리한 메소드를 제공한다.
          * toSet()
          * toMap()
          * joining()
          * mapping()
          * collectingAndThen()
          * minBy()
          * maxBy()
          * groupingBy()

          Collectors 에서 제공하는 groupingBy룰 한 번 사용 해보자.
          사람들 정보를 나이별로 grouping 해보자.

          public class OlderThan20 {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              public static void main(String[] args) {
              
              Map<Integer, List<String>> nameOfPeopleByAge =
                      people.stream()
                              .collect(Collectors.groupingBy(
                                          Person::getAge,
                                          Collectors.mapping(Person::getName, Collectors.toList()))
                              );

              
              System.out.println("People grouped by age : " + nameOfPeopleByAge);
              }
          }

          여기서 groupingBy() 는 두 개의 파라미터를 가진다.
          1. 그룹을 만드는 기준
               여기서는 나이이다.
          2. mapping() 의 결과인 Collector
               나이로 그룹핑한 결과에 대한 Stream값

          좀 더 복잡한 예제를 한 번 수행해보자.
          이름의 첫 글자를 기준으로 그룹핑하고 각 그룹별로 나이가 가장 많은 사람의 정보를 가지도록 해보자.

          public class OlderThan20 {
              private static final List<Person> people = Arrays.asList(
                  new Person("John", 20),
                  new Person("Sara", 21),
                  new Person("Jane", 21),
                  new Person("Greg", 35)
              );

              public static void main(String[] args) {
              
              Comparator<Person> byAge = Comparator.comparing(Person::getAge);

              
              Map<Character, Optional<Person>> oldestPersonOfEachLetter =
                      people.stream()
                              .collect(Collectors.groupingBy(
                                      person -> person.getName().charAt(0),
                                      Collectors.reducing(BinaryOperator.maxBy(byAge)))
                              );

                  System.out.println("People grouped by age : " + oldestPersonOfEachLetter);
              }
          }

          1. 기준 : 각 사람 이름의 첫 글자
               person -> person.getName().charAt(0)
          2. 그룹핑
               2-1. reducing 적용
                    그룹핑된 각 Stream에 reducing() 연산 적용
               2-2 BinaryOperator 적용
                    reducing() 연산 수행시 비교 기준 정의

          이렇게 Collectors 클래스를 잘 활용하면 복잡한 Operation 도 서술식으로 프로그래밍 할 수 있을 것으로 생각이된다.

          5. 디렉토리 모든 파일 리스트

          File 클래스의 list() 나 listFiles() 를 이용하면 특정 디렉토리 하위의 파일 리스트를 가져올 수 있다.
          이를 함수형 인터페이스를 도입해서 파일 리스트를 iteration 해보자.

          이를 위해서 CloseableStream 을 이용하여 구현해야 한다.

          public class ListFiles {
              public static void main(String[] args) throws Exception {
              
              Files.list(Paths.get(".")).forEach(System.out::println);
              }
          }
          ./.classpath
          ./.git
          ./.gitignore
          ./.project
          ./.settings
          ./build
          ./README.md
          ./src

          코드를 수행해보면 현재 디렉토리(\.) 하위에 있는 모든 파일 리스트들이 출력된다.

          Files 클래스의 list() 메소드를 사용하여 CloseableStream 을 얻어오게 된다.
          Files.list() 를 한 번 살펴보자.

          public static Stream<Path> list(Path dir) throws IOException {
              DirectoryStream<Path>
           ds = Files.newDirectoryStream(dir);
             
           try {
                 
           final Iterator<Path> delegate = ds.iterator();

                 
           // Re-wrap DirectoryIteratorException to UncheckedIOException
                  Iterator<Path>
           it = new Iterator<Path>() {
                     
           @Override
                     
           public boolean hasNext() {
                         
           try {
                             
           return delegate.hasNext();
                          }
           catch (DirectoryIteratorException e) {
                             
           throw new UncheckedIOException(e.getCause());
                          }
                      }
                     
           @Override
                     
           public Path next() {
                         
           try {
                             
           return delegate.next();
                          }
           catch (DirectoryIteratorException e) {
                             
           throw new UncheckedIOException(e.getCause());
                          }
                      }
                  };

                 
           return StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.DISTINCT), false)
                                      .onClose(asUncheckedRunnable(
          ds));
              }
           catch (Error|RuntimeException e) {
                 
           try {
                     
           ds.close();
                  }
           catch (IOException ex) {
                     
           try {
                         
           e.addSuppressed(ex);
                      }
           catch (Throwable ignore) {}
                  }
                 
           throw e;
              }
          }

          내부 상세 구현 내용은 몰라도 무방하다.
          Path 타입의 찾고자 하는 디렉토리를 파라메터로 받아서 Path Stream 을 반환한다.
          Path Stream 을 통해서 iteration을 하게되는 것이다.

          만약 디렉토리만 출력하고 싶다면, 
          filter() 를 이용하여 Stream Element 중 일부를 선택할 수 있다.

          예를 들어서 특정 디렉토리 하위 파일 중 .java 파일만 가져오도록 해보자.

          public class ListSelectFiles {
              public static void main(String[] args) {
                  final String[] files =
                      new File("./src/main/com/silverboyf/java8/sample/ch3")
                      .list(
                              new FilenameFilter() {
                                  @Override
                                  public boolean accept(File dir, String name) {
                                      return name.endsWith(".java");
                                  }
                              }
                      );

              
              Arrays.asList(files)
                      .stream()
                      .forEach(System.out::println);
              }
          }
          Compare.java
          IteraeString.java
          ListFiles.java
          ListSelectFiles.java
          OlderThan20.java
          Person.java

          FilenameFilter 인터페이스 구현을 통해서 특정 파일을 선택할 수 있다.
          하지만 뭔가 코드가 복잡해보이고, 서술적이지 않다는 느낌을 받는다.

          자바8의 DirectoryStream 을 이용하면 좀 더 간단하고 서술적으로 구현이 가능하다.

          public class ListSelectFiles {
              public static void main(String[] args) throws Exception {
              
              Files.newDirectoryStream(
                      Paths.get("./src/main/com/silverboyf/java8/sample/ch3"),
                      path -> path.toString().endsWith(".java")
                  )
                  .forEach(System.out::println);
              }
          }
          ./src/main/com/silverboyf/java8/sample/ch3/Compare.java
          ./src/main/com/silverboyf/java8/sample/ch3/IteraeString.java
          ./src/main/com/silverboyf/java8/sample/ch3/ListFiles.java
          ./src/main/com/silverboyf/java8/sample/ch3/ListSelectFiles.java
          ./src/main/com/silverboyf/java8/sample/ch3/OlderThan20.java
          ./src/main/com/silverboyf/java8/sample/ch3/Person.java

          6. flatMap 과 서브 디렉토리

          주어진 디렉토리 하위에 존재하는 파일 또는 서브 디렉토리 리스트를 출력하는 방법에 대해서 알아 보았다.
          만약 서브 디렉토리 하위의 파일에 대해서도 함께 출력하고 싶다면 어떻게 해야할까?

          public class ListSubDirs {
              public static void main(String[] args) {
              
              listTheHardWay();
                  System.out.println();
                  listTheBetterWay();
              }

              public static void listTheHardWay() {
              
              List<File> files = new ArrayList<File>();

              
              File[] filesInCurrentDir = new File(".").listFiles();
                  for (File file : filesInCurrentDir) {
                      File[] filesInSubDir = file.listFiles();
                      if (filesInSubDir != null) {
                          files.addAll(Arrays.asList(filesInSubDir));
                      } else {
                          files.add(file);
                      }
                  }

              
              System.out.println(files);
                  System.out.println("Count : " + files.size());
              }

              public static void listTheBetterWay() {
              
              List<File> files =
                      Stream.of(new File(".").listFiles())
                      .flatMap(file -> file.listFiles() == null ?
                          Stream.of(file) : Stream.of(file.listFiles()))
                          .collect(Collectors.toList());

                  System.out.println(files);
                  System.out.println("Count : " + files.size());
              }
          }

          flatMap() 을 사용하면 다중 Stream 을 하나의 flat Stream 에 할당할 수 있다.

          7. 파일 변경 살펴보기

          특정 디렉토리에 파일이 생성되거나 수정 또는 삭제됐을 때 알고 싶은 경우를 구현해보자.

          아래 샘플 코드는 대부분 자바7부터 제공하던 기능이며, 자바8에서 달라진 부분은 내부 iterator의 편의성이다.

          final Path path = Paths.get(".");
          final WatchService watchService = path.getFileSystem().newWatchService();

          path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

          System.out.println("Report and file changed within new 1 minute...");
          final WatchKey watchKey = watchService.poll(1, TimeUnit.MINUTES);

          if (watchKey != null) {
              watchKey.pollEvents()
              
              .stream()
                  .forEach(event ->
                      System.out.println(event.context()));
          }
          Report and file changed within new 1 minute...

          먼저 원하는 디렉토리에 위 코드처럼 WatchService 를 하나 등록하자.
          만약 해당 Path에 수정이 발생하면 WatchKey 를 통해 알려준다.
          WatchKey를 통해 event 를 받아서 어떤 파일이 변경이 되었는지 확인할 수 있다.


          혹시 Stream 형식의 데이터를 다루는데, 조금 어렵게 느껴질지도 모르겠다. 하지만 걱정하지 말자.
          익숙해지면 이런 방식의 프로그래밍이 얼마나 편하고 강력한지 깨닫게 될 것이다.


          출처: http://tomining.tistory.com/52 [마이너의 일상]






          이 글은 "자바 8 람다의 힘" 책을 정리한 글입니다.

          Chapter 4. 람다 표현식을 이용한 설계

          앞에서 다른 내용들만 봐도 람다를 이용하여 코드를 좀 더 간결하고 읽기 쉽게 작성할 수 있다는 것을 알았다.
          이 장에서는 람다 표현식을 사용하여 여러가지 패턴이나 설계들을 구현하는 방법에 대해서 알아보자.

          1. 전략패턴(Strategy Pattern)

          클래스를 생성하는 이유 중 하나는 코드를 재사용하기 위함이다. 좋은 의도이긴 하나 항상 그런 것은 아니다.

          자산의 총합을 구하는 예제를 통해 클래스 내의 코드 재사용성을 향상시켜 보자.
          먼저 Asset 클래스를 생성한다.

          public class Asset {
          public enum AssetType { BOND, STOCK };
          private final AssetType type;
          private final int value;

          public Asset(final AssetType type, final int value) {
          this.type = type;
          this.value = value;
          }

          public AssetType getAssetType() {
          return type;
          }

          public int getValue() {
          return value;
          }
          }

          간단하게 자산종류와 자산가치 라는 두 변수만 가지는 Asset 클래스를 생성했다.
          이젠 자산의 총합을 개산해보자.

          public class AssetUtil {
          public static void main(String[] args) {
          final List<Asset> assets = Arrays.asList(
          new Asset(AssetType.BOND, 1000),
          new Asset(AssetType.BOND, 2000),
          new Asset(AssetType.STOCK, 3000),
          new Asset(AssetType.STOCK, 4000)
          );

          System.
          out.println("Total of all assets : " + totalAssetValues(assets));
          }

          public static int totalAssetValues(final List<Asset> assets) {
          return assets.stream()
          .mapToInt(Asset::getValue)
          .sum();
          }
          }
          Total of all assets : 10000

          간단하게 Stream을 생성하여 합계를 계산하였다. 결과도 잘 나오는 것을 확인할 수 있다.
          결과에서도 보듯이 동작에는 아무런 문제가 없다. 다만 여기에는 구조상 3가지 문제가 얽혀있다.

          * iteration 을 어떻게하고
          * 어떤 값들에 대한 합계를 계산하며
          * 그 합계를 어떻게 구하는가

          이렇게 뒤엉켜 있는 로직은 재사용성이 떨어진다.

          만약 Bond(채권) 자산에 대해서만 합계를 구한다고 하면...

          public class AssetUtil {
          public static void main(String[] args) {
          final List<Asset> assets = Arrays.asList(
          new Asset(AssetType.BOND, 1000),
          new Asset(AssetType.BOND, 2000),
          new Asset(AssetType.STOCK, 3000),
          new Asset(AssetType.STOCK, 4000)
          );

          System.
          out.println("Total of bonds : " + totalBondsValues(assets));
          }

          public static int totalBondsValues(final List<Asset> assets) {
          return assets.stream()
          .filter(
          asset -> asset.getAssetType() == AssetType.BOND)
          .mapToInt(Asset::getValue)
          .sum();
          }
          }
          Total of bonds : 3000

          Stock(주식) 자산의 총합을 구한다면 또 다시 totalBondsValues() 를 복사하여 filter에 있는 부분을 변경해줘야 한다.

          public class AssetUtil {
          public static void main(String[] args) {
          final List<Asset> assets = Arrays.asList(
          new Asset(AssetType.BOND, 1000),
          new Asset(AssetType.BOND, 2000),
          new Asset(AssetType.STOCK, 3000),
          new Asset(AssetType.STOCK, 4000)
          );

          System.
          out.println("Total of stocks : " + totalBondsValues(assets));
          }

          public static int totalBondsValues(final List<Asset> assets) {
          return assets.stream()
          .filter(
          asset -> asset.getAssetType() == AssetType.STOCK)
          .mapToInt(Asset::getValue)
          .sum();
          }
          }
          Total of stocks : 7000

          총 자산, 채권, 주식 각각의 자산의 총합을 계산하는데, 유사한 코드가 너무 많이 생산되었다.

          이를 해결하기 위해 전략 패턴이라는 것을 적용해보자.
          (디자인 패턴의 종류로 자세한 내용은 디자인 패턴 책을 찾아보길 바란다.)

          public class AssetUtil {
          public static void main(String[] args) {
          final List<Asset> assets = Arrays.asList(
          new Asset(AssetType.BOND, 1000),
          new Asset(AssetType.BOND, 2000),
          new Asset(AssetType.STOCK, 3000),
          new Asset(AssetType.STOCK, 4000)
          );
          System.
          out.println("Total of bonds : " + totalAssetValues(assets, asset -> asset.getAssetType() == AssetType.BOND));
          System.
          out.println("Total of stocks : " + totalAssetValues(assets, asset -> asset.getAssetType() == AssetType.STOCK));
          System.
          out.println("Total of assets : " + totalAssetValues(assets, asset -> true));
          }

          public static int totalAssetValues(final List<Asset> assets, final Predicate<Asset> assetSelector) {
          return assets.stream()
          .filter(
          assetSelector)
          .mapToInt(Asset::getValue)
          .sum();
          }
          }

          totalAssetValues() 구조를 살펴보면,
          filter() 를 통해서 합계 계산 대상을 정하고
          mapToInt() 를 통해서 합계 대상 value를 추출하고
          sum() 을 통해서 합산을 하게 됩니다.

          즉, 합계 대상 객체 추출 -> 합계 value 추출 -> 합계 수행 순서로 진행됩니다.




          2. Delegate

          람다 표현식과 전략패턴을 활용하여 문제를 분리했다.
          문제 분리를 클래스 레벨에서도 분리가 가능한데, 이 때 재사용 측면에서 보면 Delegation(위임)을 이용하면 더 좋다.

          Delegation(위임) 을 람다표현식을 통해 구현해보자.

          CalculateNAV 클래스를 사용하여 웹 서비스에서 받은 데이터를 사용하여 재무 연산을 해보자.

          public class CalculateNAV {
          private Function<String, BigDecimal> priceFinder;

          public CalculateNAV(final Function<String, BigDecimal> priceFinder) {
          this.priceFinder = priceFinder;
          }

          public BigDecimal computeStockWorth(final String ticker, final int shares) {
          return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));
          }
          }

          computeStockWorth() 메서드에서 priceFinder에  대한 주식시세표(ticker)를 요청하고 주식의 시세에 따라 기치를 결정한다.
          이제 priceFinder가 필요하다.

          예제에서는 Yahoo 주식시세표를 가져와 계산해 볼 예정이나, 먼저 Stub을 통해 TC를 만들어보자.

          public class CalculateNAVTest {
          @Test
          public void computeStockWorth() {
          final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01"));
          BigDecimal
           expected = new BigDecimal("6010.00");

          Assert.assertEquals(0,
          calculateNAV.computeStockWorth("GOOG", 1000).compareTo(expected), 0.001
          );
          }
          }

          Stub 을 작성해서 주식시세표에 따른 가치를 계산해 보았다.
          실제로 Yahoo주식시세표를 가져와서 계산해보자.

          public class YahooFinance {
          public static BigDecimal getPrice(final String ticker) {
          try {
          final URL url =
          new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);

          final BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
          final String data = reader.lines().skip(1).findFirst().get();
          final String[] dataItems = data.split(",");
          return new BigDecimal(dataItems[dataItems.length - 1]);
          }
           catch (Exception e) {
          throw new RuntimeException(e);
          }
          }
          }
          @Test
          public void computeYahooStockWorth() {
          final CalculateNAV calculateNAV = new CalculateNAV(YahooFinance::getPrice);
          System.
          out.println(String.format("100 shares of Google worth : $%.2f",
          calculateNAV.computeStockWorth("GOOG", 100)));
          }
          100 shares of Google worth : $54161.00

          현재 구글의 한주 당 주식가치는 541.61$ 인 듯하다. 100주의 가치가 54161$로 계산되었다.



          3. Decorate

          앞에서 설명한 Delegation 은 뛰어난 기능이다. 여기에 Delegation 을 chain 으로 묶어서 Behavior 를 추가할 수 있다면 더 유용하게 사용할 수 있을 것이다.

          다음 예제에서 람다표현식을 사용하여 Delegate 를 조합해보자.
          (Decorator 패턴을 어떻게 사용하는지 알아보기 위한 예제이다.)

          Camera 라는 클래스를 만들고 capture() 를 통해  Color 를 처리하는 필터를 만들어보자.

          public class Camera {
          private Function<Color, Color> filter;

          public Color capture(final Color inputColor) {
          final Color processedColor = filter.apply(inputColor);

          return processedColor;
          }
          }

          위와 같이 Camera 는 Function 타입의 filter 변수를 갖고 있고, 이는 Color 를 받아서 처리하고 결과로 Color 를 반환한다. 이는 하나의 필터만 사용하지만 다양한 필터를 적용할 수 있도록 수정해보자.
          유연성을 얻기 위해 Java8에서 등장하는 Default Method 를 사용할 것이다.

          쉽게 이야기하면 Interface 에 구현부가 있는 것인데, 추상 Method 에 덧붙여서 인터페이스는 구현 부분이 있는 Method 를 갖게되며, Default 로 마크된다. 이 Method 는 해당 인터페이스를 구현한 클래스에 자동으로 추가된다.

          public class Camera {
          private Function<Color, Color> filter;

          public Color capture(final Color inputColor) {
          final Color processedColor = filter.apply(inputColor);

          return processedColor;
          }

          public void setFilters(final Function<Color, Color> ... filters) {
          filter = Stream.of(filters)
          .reduce((
          filter, next) -> filter.compose(next))
          .orElse(
          color -> color);
          }

          public Camera() {
          setFilters();
          }
          }

          setFilters() 를 이용하여 각 필터를 iteration 하고 compose 를 통해서 chain 으로 연결한다.
          만약 filter 가 없다면 Optional Empty 를 반환한다.

          4. Default Method



          5. 예외 처리


          출처: http://tomining.tistory.com/111 [마이너의 일상]

          'プログラミング > JAVA' 카테고리의 다른 글

          JAVADOC 기본 참고 자료  (0) 2017.06.07
          자바7에서 추가된 자동 리소스 닫기 try-catch-resources  (0) 2017.06.07
          16. 네트워킹(Networking)  (0) 2017.06.07
          15. 입출력(I/O)  (0) 2017.06.07
          14. 람다와 스트림  (0) 2017.06.07
          Comments