태그

레이블이 JAVA인 게시물을 표시합니다. 모든 게시물 표시
레이블이 JAVA인 게시물을 표시합니다. 모든 게시물 표시

2014년 7월 20일 일요일

[번역] JDK 8 3/3 - The Stream API

KSUG에서 번역 요청한 세 번재 포스트입니다.

본문 링크 : 
http://blog.hartveld.com/2013/03/jdk-8-33-stream-api.html

게시자 : 
게시일 : 2013/03/22
번역자 : 조인석(isi.cho@gmail.com)
번역일 : 2014/07/19

번역을 하다가 한글로 변경하기 애매 한 경우는 의역하거나 내버려 두었습니다. 참고하세요..
조언이나 교정은 언제든지 감사한 마음으로 받겠습니다. :)
Api 관련 링크가 깨져서 오라클 공식 링크로 다 변경하였습니다.


=====================================================================



Posted  by 

이 포스트는 JDK 8를 위한 짧은 시리즈 중에 마지막 포스트이다 - 나의 첫 번째 포스트인 default methods for interfaces (번역), 그리고 이전 포스트인 lambda expressions(번역)도 참고하기 바란다.

Introduction to the Stream API (Stream API 소개)

JDK 8 이전의 자바의 단점 중 하나는 컬랙션(collection)들을 가공하기 위해서는 (수작업으로 for 루프 혹은 while 루프의) 이터레이터(iterators)를 통해서만 할 수 있다는 것이다. 이러한 루핑의 명확한 폼은 전통적인 명령적 프로그래밍 언어에서 비롯되었으며, 알고리즘 단계를 실행하기 위해서 개발자가 명확하게 프로그램을 작성해야 한다는 아이디어에서 비롯되었다. 예제를 살펴보자 :

List<Block> blocks = /* ... */;

int sumOfWeights = 0;
for (Block block : blocks) {
  if (block.getColor() == Color.RED) {
    sumOfWeights += block.getWeight();
  }
}


위 예제는 "blocks 컬렉션에 있는 빨간 블록들의 무게의 합"을 구하는 상당히 거추장스러운 예제이다. 이는 알고리즘이 어떻게 실행되어야 하는지 명확하게 그리고 있다. 이러한 방식의 코딩은 간혹 발생하는 성능 과부하 상황에서 요구되어질 수도 있겠다. 하지만 이는 소스를 꽤 복잡하게 만들게 한다: 개발자는 컴퓨터가 알고리즘을 어떻게 실행해야 하는 지 표현하고 있다. 대부분의 상황에서 이러한 방식의 코딩은 그 다지 중요하지 않다. 개발자는 대개 컴퓨터가 무엇을 계산해 낼지에 대해서만 관심을 가진다. (본문을 시작하는) 하나의 영어 문장은 같은 알고리즘을 (코드의 장황성 측면에서) 더 효율적으로 작성할 수 있게 만드는 것에 초점을 맞추고 있다.

이것 때문에, 다른 언어들은 이미 컬렉션을 위한 파이프와 필터 기반의 API(pipes-and-filters-based API)를 제공하고 있다. 예를 들자면 닷넷을 위한 LINQ to objects 혹은 스칼라의 컬렉션 API(collections API)를 들 수 있다. 이러한 API는 역제어(inversion of control)의 모양새를 사용하고 있다: 프레임워크 함수들(가령, 필터와 맵)의 인자를 미리 정의해 놓은 이터레이션 함수의 커스마이징의 일환으로 사용되어지는 (람다) 표현식으로 구성한다. 이 것을 내부이터레이션(internal iteration)이라고 부른다(vs 외부 이터레이션(external iteration), 위 예제와 같이 클라이언트가 이터레이터의 제어권 안에 들어가는 경우). 내부 이터레이션의 이점은 프레임워크를 제어 할 수 있다는 것이다. 개발자는 이터레이션을 어떻게 생성해야 하는지 더 이상 고민할 필요가 없다. 이는 잠재적으로 사용이 가능한 전략들(strategies)과 반복 할 수 있게 한다: 연속적으로, 2개 혹은 그 이상의 다중 프로세스 유닛 기반의 병렬 수행 혹은 아마존 AWS(Amazon AWS)와 같이 외부에서 관리되는 서비스(역자주: 클라우드 기반 인프라)에 의해 실행될 수 있다.

Java 역시, 위 API들과 유사한 컬렉션을 위한 API를 통해 확장되어졌다. 위 예제의 for 루프는 가독성 향상을 위하여 JDK 8의 Stream API를 통해 다음과 같이 작성될 수 있으며, 람다 표현식을 포괄적으로 사용하고 있다 : 


List<Block> blocks = /* ... */;
int sumOfWeights = blocks.stream()
                      .filter(b -> b.getColor() == Color.RED)
                      .map(b -> b.getWeight())
                      .sum();


blocks 컬렉션(List<T>)은 Collection 인터페이스의 stream 함수에 의해 Stream 속으로 변환된다. 반환값은 Stream<T> 이다. 신규 컬렉션 가공 연산자들은 이 인터페이스를 위하여 정의되어졌다.

우선, 스트림 내용은 서술식을 인자값으로 취하는 filter 함수에 의해 필터링 된다. 서술식은 람다 표현식에 의해 표현되며, 이는 함수형 인터페이스라 할 수 있다. filter 함수의 반환값은 Stream<T>의 새 인스턴스이다. 기존 스트림의 각 요소들은 서술식에 의하여 테스트되어 진다. 만약, 테스트 결과가 참이면, 그 요소는 신규 스트림속으로 배치된다.

그 다음 단계는 우리가 처음 부터 관심있었던 빨간 블록들의 합을 구하기 위하여, 각 블록은 해당 블록의 무게로 치환된다. 
map 함수는 스트림의 각 요소들을 신규 요소로 변환되는 데 사용된다. 다시 말하자면, 람다 표현식이 (이번 경우에는 Function 타입) 사용된다. 왜냐하면 Function은 두 타입의 매개 변수(R과 T, and a method R apply(T t))를 가지고 있고, 이는 각 "앞으로 올" 요소를 다른 타입으로의 변환 방식으로 정의케 해준다 - 이번 예제는 Block 을 int 로 변환한다. map의 반환값은 Stream<R>이다. 이 Stream<R>은 기존 스트림의 전체 요소중에 람다 표현식에 의해 걸러진 결과값의 집합이다. 이번 예제에서는 타켓 타입이 int 이기 때문에 ToIntFunction 매개변수가 사용되었으며, IntStream 이 반환되었다(성능상 이유로 Stream을 위한 int, long, double과 같은 primitive 타입을 제공한다).

마지막으로, IntStream은 sum 함수로 스트림내의 모든 요소값들의 합계를 계산하며, 한개의  int 형으로 값을 반환한다. 한번 사용되어진 스트림은 더 이상 사용 할 수 없음을 기억하라. 만약, 다시 한번 이터레이션을 수행하려면 기존 컬렉션으로부터 신규 스트림을 생성해야만 한다.

API overview (API 개요)

JDK 8 Stream API 패키지인 java.util.stream 의 가장 중요한 타입은 Stream 인터페이스이다. 이 인터페이스에는 여러 카테고리로 나눌수 있는 연산자들의 리스트를 정의하고 있다. 각 연산자들은 인자값이 없거나, 대부분 함수형 인터페이스 형태의 인자값을 가지고 있으며, 람다 표현식으로 표현이 가능하다. 결론적으로, Stream API를 지원하는 신규 타입이 몇 개 존재한다. 이번 섹션에서는 이런 타입과 함수들을 살펴 보도록 하자.

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

대부분의 Stream 연산자들은 함수형 인터페이스 타입(functional interface types)을 인자값으로 취한다. 함수형 인터페이스 타입은 클라이언트 코드내에서 람다 표현식에 의해 초기화될수 있다. (번역)
이러한 함수형 인터페이스의 대부분은 java.util.function 패키지안에 정의되어져 있다. 통상적으로 많이 쓰여지는 것들은 아래와 같다.
  • Function와 BiFunction은  한 개 혹은 두 개의 (아마도 서로 다른 타입의) 인자값을 제 3의 다른 타입으로 변환하는 경우에 사용한다. 하위 타입인 UnaryOperator와 BinaryOperator은 단 하나의 타입만을 사용하게 제한된다. 해서, 인풋 타입과 결과값의 타입이 항상 같다.
  • Predicate와 BiPredicate은 (가령 필터링 목적으로) 테스트 되어질 요소들에 대하여 서술하는 데 사용한다. 위 클래스에는 테스트 결과를 참/거짓으로 반환하는 함수를 포함한다. 추가로,   BiPredicate은 추가 인자값을 취한다.
  • ConsumerBiConsumer와 Supplier는 새 값을 소비하고 생성하기 위해 사용된다. forEach 연산자는 각 요소들에 대한 작업을 위하여, 혹은 통상적으로 Stream API로부터 개발자 코드로 데이터를 추출할 목적으로 Consumer를 사용한다. Suppliers는 사용자 코드로 부터 Stream API로 데이터를 삽입하는 목적으로 사용된다.
유틸리티 클래스인 Functions 와 Predicates 안에는 위 인터페이스들을 구현한 정적 함수가 여러개 존재한다.

또 하나의 주요한 함수형 인터페이스는 java.util.stream 에 정의되어 있는 FlatMapper다.  이 타입은 flatMap을 인자값으로 취하는 함수를 대표한다. (Stream을 위한 단항 결합, 아래 함수 명세로 상세한 사항을 알아보자).

Optional and Spliterator (Optional 과 Spliterator)

Stream API에 사용되는 한 가지 흥미로운 신규 클래스는 바로 java.util 안의 Optional 이다. 이는 기본적으로 null을 명확하게 표한하는 대안이다 - 이는 반환값이 있는지 없는지 명확하지 않을때 특정 스트림 연산자에 의해 반환된다(예: 감소하기). 값의 존재 유무를 파악하기 위해서는 isPresent 가 호출된다. 만약, Option이 값을 가지고 있다면, get 이 값을 반환한다.

파이프라인(pipeline)의 병렬 수행을 지원하기 위해, 기존 컬렉션의 데이터 요소들은 다중 쓰레드 위에 분배되어야 한다. java.util 안에 있는 Spliterator 인터페이스는 이러한 기능을 제공한다. trySplit 함수는 기존 Spliterator의 요소들의 서브셋을 관리하는 신규 Spliterator를 반환한다. 이후에 기존 Spliterator는  서브셋에 위임된 요소들은 넘어간다(skip). 이상적인 Spliterator는 신규 Spliterator로 요소들의 절반의 데한 제어권을 위임할 것이다(확실한 한계점에 의거하여),  이로 인해 개발자들은 (예를 들자면 병렬수행을 위해) 데이터의 셋을 쉽게 조각 낼 수 있다. 일반적인 구현체들의 팩토리 함수들은 Spliterators  클래스 안에서 찾아 볼 수 있다.

Creating streams (스트림 생성하기)

스트림은 Collection 인터페이스의 확장 혹은 구현체의 타입으로 생성이 가능하다. stream 함수는 순차적인 스트림 구현체를 반화하며, parallelStream 함수는 병렬 구조에서 수행 가능한 스트림을 반환한다. Streams 클래스는 여러개의 일반적인 팩토리 함수들을 포함하고 있으며, 이미 기 존재하는 Arrays 클래스는 여러개의 스트림 팩토리 메소드를 오버로딩하여 확장되어져 있다.

Stream operators (스트림 연산자들)

Stream 상의 연산자들(함수들)은 스트림을 통하여 흘러가는 데이터를 가공한다. map과 같이 스트림 상에 중간 연산자로 불리는 녀석들은 단지 파이프라인 형태로 세팅되지만, 그 즉시 변환 되어지지는 않는다 - 이 것을 게이른(lazy) 연산자라고 부른다. forEach와 같이 터미널 연산자로 불리는 것들은 전체 스트림 파이프라인을 실행시킨다: 그때 데이터는 가공된다.

Intermediate operators (중간 연산자들)

연산자들은 무상태(stateless) 연산자와 상태유지(stateful) 연산자들로 구성된다. map 혹은 filter와 같은 무상태 연산자들은 다음 연산자에게 연산 결과를 그저 넘기기만 한다. sorted와 같은 상태유지 연산자들은 연산 수행시의 데이터 일부를 가지고 있다가 상위스트림의 연산자로부터 모든 데이터가 도착할때까지 대기 할 수도 있다.

 Stream 인터페이스의 가장 중요한 무상태 중간 연산자는 다음과 같다:
  • <R> Stream<R> map(Function<? super T,? extends R> mapper)
    T 타입의 요소를 R 타입의 요소로 매핑한다. 각 요소는 mapper 함수에 의해 매핑된 스트림을 통하여 전달된다. mapper 함수의 반환값은 R 타입의 스트림이다. 
  • Stream<T> filter(Predicate<? super T> predicate)
    주어진 서술식에 의하여 성공적으로 테스트 되지 않은 모든 요소들을 걸러낸다. 필터링된 요소값들을 신규 Stream은 필터링된 요소값들을 포함혀여 반환된다. 
  • <R> Stream<R> flatMap(Function<T,Stream<? extends R>> mapper)
    flatMap 연산자는 단항 결합(monadic bind)과 같다. 첫 버전의 flatMap에서는 Stream<T>를 취하고 mapper 함수는 T 요소를 Stream<R>로 변환한다(그래서 단일 요소 T 가 다중 요소를 지닌 R 로 반환될 수 있다). 그리고 나서 결과값 스트림은 반환된 단일 Stream<R> 안으로 들어간다.
     
  • <R> Stream<R> flatMap(FlatMapper<? super T,R> mapper)
    두 번째 버전의 flatMap은 훨씬 개발자에게 친숙하며, 다음 단계를 위해 역제어(IoC)의 원칙을 취한다 : 컨테이너(스트림 프레임워크)는  R 타입의 모든 결과값들을 담을 수 있는 Consumer<R> 타입의 그릇을 제공한다.
상태유지 연산자들의 예제는 다음과 같다:
  • Stream<T> sorted()
    Comparable.compareTo 구현체에 의해 정렬된 스트림이 반환된다.
    Stream<T> sorted(Comparator<? super T> comparator)
    주어진 Comparator 구현체에 의해 정렬된 스트림이 반환된다.

Terminal operators (터미널 연산자들)

중간 연산자들은 기존 스트림과는 다른 스트림을 반환하는 데에 비해, 터미널 연산자는 종료된 스트림 파이프라인으로 부터 추출된 실제 결과값을 반환한다. 터미널 연산자들의 예를 살펴보자 : 
  • void forEach(Consumer<? super T> consumer)
    이 함수는 스트림내의 각 요소에 대하여 반드시 수행되어야 하는 코드를 작성하는 데 사용된다. - 기본적으로 프로그래밍 언어의 for-each 구문과 같은 개념이다.
  • <R> R collect(Collector<? super T,R> collector)
    수정이 가능한 결과값으로 모든 요소를 모은다. 예시로는 List에 담은 스트림의 모든 요소들의 컬렉션 혹은 모든 스트림 요소로부터의 긴 문자열의 생성을 들 수 있다. Collector<T, R>의 세 함수와 동일한 세 인자값을 가지는 함수형 인터페이스로 오버로딩 되는 것도 존재한다. (역자주: 해석이 매끄럽지 못하네요. An example is the collection of all elements of a stream into a List, or the creation of a long string from all stream elements. There is also an overload which takes as three arguments the functional interface equivalents of the three methods of Collector<T, R>). Collectoer의 일반적인 구현체는Collectors 에서 찾아 볼 수 있다.
  • Optional<T> reduce(BinaryOperator<T> reducer)
    스트림의 값들을 감소하여 단일 Optional 값으로 변환한다. 감소를 위한 초기 값 세팅을 위하여 추가되는 인자값을 취하는 두 개의 오버로딩된 함수가 있다.
  • Iterator<T> iterator()
    외부 이터레이션에서 사용하기 위해 스트림의 모든 요소들을 제어할수 있는 Iterator<T>를 반환한다.

Concurrency (동시성)

Stream 인스턴스는 stream 함수로 생성되어진다. 이 함수는 순차적으로 수행되는 혹은 단일 쓰레드 기반의 스트림을 생성한다. 반면에, 내부 이터레이션의 이점 중 하나는 추가적인 실행 전략들을 적용할 가능성이 있다는 것이다. 가장 명확한 예시는 멀티 프로세스 위에 수행되는 병렬 수행이다. Stream API는 이 시나리오를 위한 기능을 제공한다. 어떤 순차적 스트림은 parallel 연산자를 사용함으로써 병렬 수행이 가능해진다. parallel 연산자는 기존 스트림의 전체 요소들을 품고 있는 스트림을 반환하며, 이후의 연산자들도 병렬 적으로 호출된다. Spliterator를 사용한다면, 인풋 컬렉션은 여러개의 서브 컬렉션으로 나눠진다. 각 서브컬렉션들은 서로 다른 (병렬) 쓰레드에 의해 스트림 파이프라인 형태로 사용 되어진다.

Streams for primitive types (프리미티브 타입을 위한 스트림)

Stream 인터페이스는 제너릭 타입이다. 스트림이 프리미티브 타입과 함께 쓰여질 때, 무척 많은 (un)boxing(역자주: 예) int <-> Integer) 이 수행되어야 한다. 이는 큰 사이즈의 데이터를 가공할 때 비효율적이다. 반면에, int, long 그리고 double 을 위한 효율적인 구현 방식을 제공하기 위한 프리미티브-지향적인 인터페이스를 제공한다: 예를 들어 이 IntStreamLongStream DoubleStream 인터페이스들은  T를 int로 변환하여 반환하는 데 사용된다. 이 기능을 제공하기 위하여, 프리미티브 매개변수 와/혹은 반환값 타입을 위한 특화된 함수형 인터페이스가 있다: ToIntFunctionIntConsumerIntSupplier, 기타 등등.

    Limitations of the Stream API (Stream API의 제약사항)

    JDK 8의 Stream API는 Java 언어에게 수 많은 강력한 표현 방법을 제공한다. 하지만, 해당 API와 프레임워크를 사용하는 데 있어서 몇 개의 제약사항을 가진다. 또한, 다른 언어의 프레임워크와 비교하자면, 주요 기능 일부가 누락된 것을 알 수 있다.

    Stateless lambda expressions (무상태 람다 표현식)

    이 프로그래밍 모델이 내재하고 있는 제약사항으로는 람다 표현식이 반드시 무상태(stateless) 이여만 한다는 것이다. 상태유지 람다는 순차적으로 수행시에는 대개 문제가 되지 않지만, 병렬적으로 수행될 경우에는 깨지기 마련이다. 이번 예제는 map 함수를 사용한 상태유지 람다 표현식의 사용법이다. 이는 예상하는데로 동작되지 않는다:

    List<String> ss = ...;
    List<String> result = ...;
    
    Stream<String> stream = ss.stream();
    
    stream.map(s -> {
            synchronized (result) {
              if (result.size() < 10) {
                result.add(s);
              }
            }
        })
        .forEach(e -> { });
    
    // The contents of result will depend on the order of execution.
    // If this stream is run in parallel, result.add(s) is called
    // for some elements, depending on which thread/spliterator is
    // called first.
    // 결과값은 실행 순서에 의해 변경된다.
    // 스트림이 병력적으로 수행 된다면, 먼저 호출된 쓰레드/spliterator에 의해 
    // 요소들 중 일부가 result.add(s)에 의해 호출될 것이다.

    Streams are not reusable (스트림 재사용 불가)

    만들어진 스트림 트리는 다신 재사용 할 수 없다 - 스트림 트리는 사용 후 버려진다(혹은 익셉션이 발생한다). 조금 이상해보일수 있지만, 이는 중간 스트림 연산자들은 게이르기 때문(lazy, 역자주 : 미리 초기화 되는 것이 아니라 호출하는 시점에 초기화되는 것) 이다. 만약, 복잡한 단계를 수행한 후 스트림 재사용이 가능했더라면, 무척 흥미로운 주제였을 것이다.

    Plain streams, no query trees (순수 스트림, 쿼리 트리 없음)

    C#인 경우, LINQ를 지원하기 위해 쿼리 트리와 같은 패러다임이 구현되어져 있다. 이는 (부분) 쿼리의 재사용을 가능케 한다. JDK 8 은 이러한 기능은 제공하지 않는다: 전체 쿼리 트리 계층은 JDK 8 Stream API에는 (의도적으로) 생략되어졌다. 다른 한편으로는, public 인터페이스는 내부 사항들을 많이 노출하지 않고 있다. 이는  아마도 유사한 기능을 미래에 구현 할 수도 있다고 해석할 수 있겠다.

    No custom operators (커스텀 연산자 없음)

    개발자는 자신만의 연산자를 생성할 수 없다. 물론, map/flatMap 연산자들을 사용하면 대부분의 것들이 구현 가능하겠지만, 알다시피 많은 연산자들이 사용하기 편리하게 구현된 것 만은 아니다. C#의 LINQ에서 객체/상호적 확장을 위하여 제공하는 모든 연산자들을 살펴보면, 당신은 의문을 제기 하게 될 것이다 -C#의  extension methods 는 정말 끝내준다.

    Posted  by 

    2014년 7월 12일 토요일

    [번역] JDK 8 2/3 - Lambda expressions

    KSUG에서 번역 요청한 두 번재 포스트입니다.

    본문 링크 : http://blog.hartveld.com/2013/03/jdk-8-23-lambda-expressions.html

    게시자 : 
    게시일 : 2013/03/22
    번역자 : 조인석(isi.cho@gmail.com)
    번역일 : 2014/07/12

    번역을 하다가 한글로 변경하기 애매 한 경우는 의역하거나 내버려 두었습니다. 참고하세요..
    조언이나 교정은 언제든지 감사한 마음으로 받겠습니다. :)

    =====================================================================




    Posted  by 


    이 포스트는 JDK 8를 위한 짧은 시리즈 중에 두 번째이다 - 나의 이전 포스트인 default methods for interfaces (번역), 그리고 다음 포스트인 Stream API도 참고하기 바란다.
    JDK 8이 Java에 제공 할 신규 기능 중 하나는 익명 함수(anonymous function)를 정의 할 수 있는 가능성을 열어 놓은 것이다. 보통 자바 월드에서는  람다(lambda) 표현식이라고 부른다. 기본적으로, 이 기능은 익명 내부 클래스 정의를 적은 코드로 간편하게 해준다. - AIC(역자주 : Architecture-Independent checkpointing의 약자로 모든 변수 타입을 명시적으로 선언해야 한다는 차원에서 나온 용어로 보여지며,  플랫폼 종속적이지 않은 Java 언어의 특징 중 하나 임(참고))s 의 장황성과 비교되는 군더더기 없는 코드는 새로운 타입의 API를 만드는 것을 가능케 한다. 이번 포스트에서는 람다 표현식의 설계, 구현 방식 그리고 사용법에 대해 조금 더 자세히 들여다 보겠다.

    Lambda expression types: functional interfaces

    (람다 표현식 타입: 함수형 인터페이스)

    람다 표현식의 타입은 단 하나의 추상 메소드를 가지고 있는 함수형 인터페이스(functional interface)라 할 수 있다. (이러한 이유로 이전에는 SAM(Single Abstract Method) 인터페이스로 불려짐) 실제 JDK 인터페이스 기반의 함수형 인터페이스의 예시를 살펴 보자.
    
    
    // Existing interfaces that are now considered functional interfaces:
    
    
    interface java.lang.Runnable {
     void run();
    }
    
    interface java.util.concurrent.Executor {
     void execute(java.lang.Runnable command);
    }
    
    interface java.lang.Iterable<T> {
     java.util.Iterator<T> iterator();
    }
    
    interface java.awt.event.ActionListener {
      void actionPerformed(java.awt.event.ActionEvent e);
    }
    
    // New functional interfaces:
    
    interface java.util.function.Consumer<T> {
      void accept(T t);
    }
    
    interface java.util.function.Function<T, R> {
      R applly(T t);
    }
    
    interface java.util.function.Predicate<T> {
      boolean test(T t);
    }
    
    
    
    
    
    
    
    
    
    Runnable, Executor, Iterable, ActionListener 와 같은 일부 인터페이스들은 단 하나의 추상 메소드를 지니고 있는(함수형 인터페이스의 규칙을 따르고 있는) 기존의 인터페이스들이다. 나머지 Consumer, Function, Predicate와 같은 인터페이스들은 람다 표현식의 편리한 사용을 위하여 java.util.function 라는 이름의 신규 패키지 안에서 찾아 볼 수가 있다. 

    Lambda expressions (람다 표현식)

    Java 7에서는 함수형 인터페이스(혹은 SAM 인터페이스)의 구현체는 익명 내부 클래스 정의를 위하여 5줄 정도의 코드로 작성 해야 한다. (내부 클래스 밖의 함수 본체 라인은 세지 않았음) - 이 거추장스러운 구문을 보자:
    java.util.function.BiFunction<String, String, String> concat
              = new java.util.function.BiFunction<String, String, String>() {
                @Override
                public String apply(String t, String u) {
                  return t + u;
                }
              };
    
    
    
    
    
    
    람다 표현식(혹은 익명 함수)은 프로그래머가 같은 구문을 무척 간결하게 작성할 수 있게 해준다. 이는 타 언어들(other programming languages)과도 무척 닮았다. 위 구문과 같은 내용의 람다 표현식을 아래의 예제를 통해 확인 해 보자. - 두 개 String 변수 연결하기(concatenation)
    java.util.function.BiFunction<String, String, String> concat1
     = (String s, String t) -> {
       return s + t;
     };
    
    java.util.function.BiFunction<String, String, String> concat2
     = (s, t) -> s + t;
    
    
    첫 번째(concat1) 예제는 Java 8 람다 표현식의 장황한 버전이다. 대개 매개 변수 타입의 정의를 생략하여 더욱 간결하게 표현한 두 번째(concat2) 예제 역시 유효하다. 또한, 함수가 단 하나의 반환 구문을 가지는 경우, 중괄호({}) / return 키워드 / 끝 부분의 세미클론(;)의 제거가 가능하다.

    반면에 어떤 경우에는 명확하게 매개 변수 타입을 명시해야 하는 경우도 있다. 한 가지 예를 들자면, 함수형 인터페이스의 매개 변수 변경에 의한 오버로딩된 함수를 들 수 있겠다. 이러한 경우에는 API 설계시 람다 매개 변수의 타입을 명시한 폼(위 예제 중 concat1) 에 의해 명확성을 요구하는 API 를 사용하여 코드가 작성되었는 지를 기술 해야만 한다. Stram.flatMap 오버로딩의 모호함(Ambiguity of Stream.flatMap overloads)은 람다 개발자 메일링 리스트(lambda-dev)에서 논의 되어지고 있다. 이러한 함수 같은 경우는 오버로딩 하지 말고, 오버로딩된 함수의 이름을 변경하기를 권장한다.

    Functional interfaces and default methods (함수형 인터페이스와 기본 함수)

    함수형 인터페이스는 기본 함수(default methods)를 지닐수 있다. 해서 함수형 함수는 단 하나의 추상 메소드를 가질 수 밖에 없다는 제약사항에서 벗어 날 수 있다.(기본 함수는 추상 함수가 아니기 때문이다.) 좋은 예로 Iterable는 함수형 함수이며 람다 표현식과 함께 구현되어 질 수 있다.
    interface java.lang.Iterable<T> {
      abstract Iterator<T> iterator();
    
      default void forEach(Consumer<? super T> consumer) {
        for (T t : this) {
          consumer.accept(t);
        }
      }
    }
    
    java.lang.Iterable<Object> i = () -> java.util.Collection.emptyList().iterator();

    Method references (함수 참조)

    익명 함수로 함수형 인터페이스를 정의하는 것 대신에, 함수의 참조로써 정의 하는 것도 가능하다. 함수형 인터페이스 명세를 따르기만 한다면, 아래의 예제도 가능하다.
    public class MethodReferences {
      public void f() { }
    
      public void g() {
        Runnable r = this::f;
        r.run(); // f() is called.
      }
    }

    Higher-order functions in Java (Java내의 고순위 함수)

    함수형 함수의 명세를 정의한 다양한 타입과 (익명) 함수 정의를 허용함으로써, Java는 고순위 함수를 제공하기 위하여 확장되어졌다. 이는 더욱 간결한 코드 작성을 위해 여러 가지 타입의 신규 프로그램 생성자를 작성 가능 하게 하며, 무척 흥미로운 부분이다. JDK 8의 이러한 신규 기능들은 데이터스트림 중심의 프로그래밍 파라다임을 적용할 수 있게 하는 예시를 Stream API를 통하여 제공하며, 이는 .NET의 interactive extensions 혹은 스칼라의 collections API 와 닮았다 : 
    List<String> ss = new ArrayList<>();
    ss.add("x,y,z");
    ss.add("x.y.z");
    ss.add("a,b,c");
    ss.add("i,j,k");
    
    String result = ss.stream()
        .map(s -> s.replace(",", "."))
        .filter(s -> !s.contains("b"))
        .distinct()
        .sorted()
        .collect(Collectors.toStringJoiner(",")).toString();
    
    System.out.println("Result: " + result);
    
    == Output ==
    Result: i.j.k,x.y.z
    
    
    이번 시리즈 중 세 번째 포스트에서는 스트림 API에 대해서 더 상세하게 알아 볼 수 있다.

    Java 8 은 타 함수형 프로그래밍 언어에서 제공하는 부분적 사용 함수(partially apply functions)는 제공하지 않고 있다. Java 8은 여전히 완전한 객체 지향적이며, 명령형 프로그래밍 언어이다.

    Advanced example (고급 예제)

    Java 8 언어 기능들로 이루어진 조금 더 복잡해 보이는 아래 예제는 이번 포스트의 최종 예제이다. 이는 IObservable 인터페이스의 무척 단순한 구현체이며, Microsoft's Reactive Extensions for .NET에서 사용하는 표준 쿼리 연산자(standard query operators)의 일부를 포함하고 있다. - 기본 함수와 많은 람다 표현식을 가지고 있는 인터페이스 : 
    package com.hartveld.examples;
    
    import java.util.function.Consumer;
    import java.util.function.Function;
    import java.util.function.Predicate;
    
    public interface Observable<T> {
    
      void subscribe(Consumer<T> onNext);
    
      default <R> Observable<R> map(Function<T, R> mapper) {
        return onNext -> {
          subscribe(e -> {
            onNext.accept(mapper.apply(e));
          });
        };
      }
    
      default Observable<T> filter(Predicate<T> filter) {
        return onNext -> {
          subscribe(e -> {
            if (filter.test(e)) {
              onNext.accept(e);
            }
          });
        };
      }
    
      default <R> Observable<R> bind(Function<T, Observable<R>> binder) {
        return onNext -> {
          subscribe(e -> {
            binder.apply(e).subscribe(onNext);
          });
        };
      }
    
      // Alternative filter implementation based on bind.
      default Observable<T> filter2(Predicate<T> filter) {
        return bind(t -> {
          return onNext -> {
            if (filter.test(t)) {
              onNext.accept(t);
            }
          };
        });
      }
    }
    
    
    위 소스를 위한 테스트 케이스는 여기(here)를 참고하기 바란다. JUnit 과 Mockito 를 클래스패스에 추가한 뒤 실행 해야 한다.


    Conclusion (결론)

    람다 표현식의 추가로 인해 Java 프로그래머의 도구함에는 강력하고 표현력이 강한 신규 언어 기능이 추가 되었다. 비록 위 Observable 예제와 같이 남용하게 되면 복잡성이 증가하게 되겠지만(여전히 표현성은 높다), 이 언어 기능은 많은 기존의 프로그램들을 개선하는 데 사용될 수 있다. 많은 익명 내부 클래스 초기화 구문을 제거하고, Stream API 기반의 전통적인 for 루프를 전환하는 것 만으로도 해당 코드의 가독성을 증대시키는 데 기여 할 수 있다.

    Posted  by 

    =======================================================================

    2014년 7월 6일 일요일

    [번역] JDK 8 1/3 - Interface default method implementations

    KSUG 페북에 아래 글에 대한 번역이 요청이 빈발(?) 하여 손들고 한번 해 봅니다.

    본문 링크 : http://blog.hartveld.com/2013/03/jdk-8-13-interface-default-method.html

    게시자 : 
    게시일 : 2013/03/22
    번역자 : 조인석(isi.cho@gmail.com)
    번역일 : 2014/07/06

    번역을 하다가 한글로 변경하기 애매 한 경우는 의역하거나 내버려 두었습니다. 참고하세요..
    조언이나 교정은 언제든지 감사한 마음으로 받겠습니다. :)



    ==============================================================


    Posted 
     by 

    이 포스트는 JDK 8를 위한 짧은 시리즈 중에 첫 번째이다 - 나의 다음 포스트인 lambda expressions, 그리고 마지막 포스트인 Stream API도 참고하기 바란다.

    현재, OpenJDK 개발팀은 JDK 8 기능들을 완성하기 위해 바쁜 나날을 보내고 있다. OpenJDK의 최근 Project lambda 빌드에서는 Java 8 프로그램을 작성할 수 있는 꽤 완벽한 자바 개발 환경을 포함하고 있다. 필자는 아직까지 이번 JDK에 대해 어떤 문제도 발견할수 없었고, 근래의 lambda-dev 메일링 리스트에도 버그 레포트 관련 내용은 찾아보기 힘들다. Java 8의 신규 기능들 중 필자가 설명할 람다 관련 기능 셋은 무척 흥미로운 주제다. 이번 포스트는 그 중에 하나인 "default methods(기본 함수)"를 다루고 있다. (역자 주 : default methods는 함수 접근 제어자가 default인 것을 의미하여 아래 글에서는 '기본 함수'로 표현하겠음)


    Extending interfaces: default methods(확장 인터페이스: 기본 함수)

    추상 메소드로 선언된 일반적인 인터페이스 함수는 해당 인터페이스를 구현한 클래스에서 반드시 정의(must be defined) 되어져야 한다. 이러한 '짐(burdens)'은 매 선언 함수를 구현해야 할 책임을 개발자에게 지게 한다. 더 중요한 것은 '배포(publication)' 이후에는 인터페이스의 확장이 불가하다는 것이다. 다르게 말하자면, 모든 개발자는 개발된 소스와 망가진 이전 소스, 컴파일된 바이너리 클래스와의 호환성을 반드시 고려해야 한다.

    이러한 문제점들을 해결하기 위해서, JDK 8의 신규 기능 중 하나인 기본함수(default methods)는 이미 배포된 인터페이스의 확장을 가능케 한다. 기본 함수는 독립적으로 선언될 뿐만 아니라 인터페이스 내부에 정의도 가능하다.

    예제를 한번 살펴 보자.:


    public interface Person {
    
      public abstract String getFirstName();
      public abstract String getLastName();
    
      public default String getDisplayableFullName() {
        return getLastName() + ", " + getFirstName();
      }
    
      public abstract boolean isStudent();
    
      public default boolean isNotStudent() {
        return !isStudent();
      }
    
    }

    Person 인터페이스를 구현한 개발자는 추상함수인 getFirstName(), getLastName(), isStudent()를 반드시 구현해야 한다.  하지만 기본함수인 getDisplayableFullName() 혹은 isNotStudent() 는 구현하지 않아도 된다.  개발자는 마치 하위 클래스에 일반적인 non-final 클래스 함수를 오버라이딩 하는 것 처럼, 여전히 기본 함수를 오버라이딩 할 수 있다. 추상 클래스는 기본 함수를 추상 함수로 (재)정의 할 수 있으며, 하위 클래스로 하여금 재구현 할 수 있게 제한 할 수 있다. (간혹 '재추상화(re-abstraction)' 라고도 불림).


    Use cases for default methods (기본 함수를 위한 유스케이스)


    1) Extending existing interfaces (기존 인터페이스의 확장)
    JDK 라이브러리 중 이 신규 기능을 위한 가장 중요한 유스케이스는 인터페이스에 대한 기존 구현체를 수정하지 않고도 확장이 가능하다는 점이다 : 이전에는 인터페이스에 신규 추상 메소드를 추가하면 모든 기존 구현체에 추가된 추상 메소드를 구현해야만 했었다. 그리고 기존 인터페이스를 확장하는 것은 앞으로 Collections API(나의 JDK 8에 대한 세 번째 포스트(third blog post) 참고)로 확장 될 예정인 신규 Stream API 의 이전 버전의 구현체와 호환이 되는지 확인 할 필요가 있었다.

    신규 Collection 인터페이스에 대한 코드를 살펴보자:

    public interface Collection<E> extends Iterable<E> {
      
      // Traditional, abstract methods:
    
      int size();
      boolean contains(Object o);
      boolean add(E e);
      boolean remove(Object o);
      // Etc...
    
      // New methods stream() and parallelStream() with default implementations:
    
      default Stream<E> stream() {
        return Streams.stream(() -> Streams.spliterator(iterator(), size(), Spliterator.SIZED), Spliterator.SIZED);
      }
    
      default Stream<E> parallelStream() {
        return stream().parallel();
      }
    
    }

    Collection 인터페이스는 stream() 와 parallelStream() 함수에 의해 확장된다. 두 함수 모두 반드시 기본 함수로 정의 되어져야 한다. 그렇지 않으면 기존 Collection을 구현체들 (가령, ArrayList나 3rd 파티 모듈들)은 깨지거나 다시 구현해야만 한다.

    2) Additional design flexibility in the inheritance hierarchy (상속 계층에서의 추가 설계 유연성)

    간혹, 서로 다른 구현 클래스간에 공유하는 기능이 있을 수 있다. 전통적으로는 이 클래스들을 위한 상위 (추상) 클래스를 작성하게 된다. 다른 방식으로는 이벤트 리스너를 정의하여 여러 함수를 가진 인터페이스가 구현 되어져야만 하지만, 대개 개발자는 단 하나의 이벤트(싱글 함수)에만 관심을 가지고 있다. 일반적으로는 추상 (어댑터) 클래스(abstract (adapter) class)를 확장함으로써, 이러한 인터페이스 함수를 위한 기본 구현체를 제공한다.
    기본 함수 구현체는 위 두 사례에 대하여 더 나은 설계 유연성을 제공한다. 스칼라의 특성과 마찬가지로(Similar to traits in Scala), 기본 함수는 서로 다른 타입에 대하여 행위를 공유 혹은 재사용 할 수 있다. 그리고 클래스의 다중 인터페이스 구현이 가능해짐에 따라, 다중 상속의 안전성과 제약적인 형식(safe and limited form)이 제공된다. 상태를 공유하기 위해서는 (추상) 상위 클래스가 여전히 요구되어진다.


    3) Alternative: extension methods (대안: 확장 함수)

    C# 확장 함수(C# extension methods)의 다른 형태가 고려되어져 왔으며 이에 대한 대안이 된다. 그럼에도 불구하고, 확장 함수는  새로운 '정적-성(static-ness)'을 소개하고 있다 : 확장 함수는 첫 번째 인자값을 실행 대상 객체로 취하는 일종의 정적 함수이다.  또한 확장 함수는 기본적으로 코딩시 편리함을 제공한다 : IDE를 사용하는 프로그래머는 '.'를 통해 확장함수를 쉽게 호출 할 수 있다.
    이러한 시도의 한 가지 문제점은 개발자가 함수를 오버라이딩 하지 않고, 전체 클래스 계층이 모든 케이스에 사용이 가능하게 구현된 소스로 인해 정체될 수 있다는 부분이다. 대개 이런식으로 큰 규모내의 추가된 정적 함수를 소개 하는 것은 바람직 하지 않다(considered undesirable). 또한, 확장 함수는 reflection을 사용하는 경우 제대로 동작하지 않는다 : 확장 함수는 클래스의 정의된 함수가 아니기 때문에 찾을 수가 없다 (예를 들자면, 메일 클래스가 로딩되고 난 다음 reflection을 통해서만 확장 함수를 포함한 클래스가 로드 되었다고 치자 - JVM은 확장 함수의 존재를 알 방법이 없다).