2014년 2월 22일 토요일

자바의 람다식. Java Lambda JAVA8

본인의 해석이 여의치 않아, 내 멋대로 번역하여 정리한다.
출처 : http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

1. 가정
   관리자가 특정 조건(기준)을 만족하는 직원들을 찾아 출력하고자 하는 상황. 가령, 실적이 우수하다거나 특정 대상을 기준을 만족하는 직원을 포상하거나 처벌하려는 경우.

> 현 직원 클래스 정의

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}
이다.


 접근 1 : 한 특정 조건을 만족하는 직원을 찾는 메서드 만들기.

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    } 
      }  

  문제 : 나이가 적은 사람을 검색하기 위해선 또 메서드를 추가해야 하는가.

 접근 2 : 좀더 범용적인 탐색 메서드 만들기.

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low >= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}
  문제 : 만일 클래스 구조가 변경되면? 또는 조건(기준)이 좀더 복잡해진다면 코드가 늘어난다.

 접근 3 : 기준을 만족하는지를 판단하는 기능을 분담하는 지역 클래스를 생성

       interface CheckPerson {
    boolean test(Person p);
}
  이런 식으로 기능을 분리한 후,
public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}
 이와 같이 변경한다. 그리고 선발 기준을 만족하는지를 판단하는 알고리즘을 구현한 클래스를 만들어야 한다.

 class CheckPersonEligibleForSelectiveService implements CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 18 &&
            p.getAge() <= 25;
    }
}
 길군...

호출은 따라서 다음과 같이 할 수 있다.
printPersons(
    roster, new CheckPersonEligibleForSelectiveService());


  문제 : 물론 이것이 전보다 더 불필요한 코드를 양산하는 것은 아니다만, 여전히 추가적인 코드가 많아진다. 인터페이스 정의부터 구현 클래스 내용까지.

 접근 4 : 익명 클래스를 이용한다.
printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);
  현재 여기까지가 JAVA7 까지의 방법론.


 접근 5 : 람다표현식을 이용한 방법
 
 위의 경우처럼 추상 메서드를 단 하나만 소유한 인터페이스를 기능성 인터페이스(functional interface)라 부른다(물론 디폴트 메서드나 정적 메서드는 여럿 포함할 수 있다).

 이 때, 람다식을 이용한다. 그 예는 다음과 같다.
printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);
 '->'을 기준으로 좌측이 매개변수, 우측이 표현식.

 참고점 : interface를 하나 선언하였다.

 접근 6 : 표준 기능성 인터페이스를 람다식과 함께 사용.
interface CheckPerson {
    boolean test(Person p);
}
 이 친구는 반환형 존재하며, 인자를 하나 받는다. 반환형은 boolean으로 고정이다.

 자바에 표준 기능성 인터페이스가 이미 있다. 이는 다음과 같은 제네릭을 이용한다.
interface Predicate<T> {
    boolean test(T t);
}
  반환형 타입, 매개변수 숫자가 같다. 그럼 CheckPerson 대신 이용하자.
public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}
 좋군. 호출은 다음과 같다. ( 똑같다. ) CheckPerson 인터페이스도 필요 없다.
printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);
 
 참고점 : p.printPerson() 말고 다른 일을 할 수는 없는가. 그럼 결국 메서드는 기능별로 추가될 수 밖에 없는가.

 접근 7 : 네 응용프로그램 전체에 람다식을 사용. (뭔 소린가.. 번역이 좋지 않군)

 여러 표준형 인터페이스가 존재한다. 그 중 다음을 살펴보자. Consumer<T> 인터페이스 이 친구는 메서드 void accept(T t)를 포함한다. 이를 p.printPerson()과 비교했을 때 반환형 없고 인자 하나 있다는 게 보인다.

 람다식은 accept와 같은 기능성 인터페이스를 이용해 그 메서드 내용도 내멋대로 구현하여 인자로 넘긴다.
public static void processPersons(
    List<Person> roster,
    Predicate<Person> tester,
    Consumer<Person> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
}
 이와 같이 선언을 바꾼 후,
processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()     //acept() 메서드의 내용이 이것이 된다.
);
이렇게 호출한다.

 만일 더 기능을 추가하여 구현하고 싶다면, 여러 기능성 인터페이스 사용해라. 여기서는 Function<T, R> 인터페이스의 R apply(T t)를 이용한다.

public static void processPersonsWithFunction(
    List<Person> roster,
    Predicate<Person> tester,
    Function<Person, String> mapper,
    Consumer<String> block) {
    for (Person p : roster) {
        if (tester.test(p)) {
            String data = mapper.apply(p);
            block.accept(data);
        }
    }
}

이와 같은 선언으로 변경 한 뒤,
processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),         //이것이 apply() 메서드의 몸체의 내용이다.
    email -> System.out.println(email)
);
이렇게 호출한다.

 접근 8 : 범용적으로 바꿔서 이용.

 public static <X, Y> void processElements(
    Iterable<X> source,
    Predicate<X> tester,
    Function <X, Y> mapper,
    Consumer<Y> block) {
    for (X p : source) {
        if (tester.test(p)) {
            Y data = mapper.apply(p);
            block.accept(data);
        }
    }
}

 이렇게 바꿨다. 고로 호출도 다음처럼 바뀐다.

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

이 호출은 문자 하나 변경된 것이 없다.


 이 예제는 지극히 다음을 위한 흐름같아 보였다.

 접근 9 : 아예 집합 연산을 사용한다.
roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));
 나참.

stream()에서 객체의 원본을 얻어내고
filter에서 조건에 맞는 객체를 걸러내고
map에서 얻어낸 객체를 특정 다른 값으로 사상하고
foreach에서 관련 기능을 수행한다.

다 똑같다. filter가 Predicate 인터페이스를 인자로 받는 것, map이 Function<T, R>을 인자로 받는 것, forEach가 Consumer<T>를 인자로 받는 것 모두가 위의 processElements 메서드의 각 인자들과 그 타입이 완벽하게 일치한다.,

------------------------------------------------------------------------
람다 구문(?)

  괄호 내에 동봉된 ','로 구분된 파라미터 리스트 (파라미터가 1개면 괄호 생략 가능, 타입 생략 가능)

 '->'

 닫일 표현식이나 여러 문장 블럭으로 구성된 몸체. (문장이 여러개면 중괄호로 묶는다. 만일 단일 표현식이면 그것 자체가 리턴 값이며, return으로 명시한 구문은 표현이 아니다. 즉, ';'을 어미에 삽입하고 중괄호로 묶어라)

 끝.
------------------------------------------------------------------------
지역 변수 접근.

 람다표현식 내의 지역변수는 메서드 내에 선언된 익명 클래스내 메서드에서 지역 변수들을 참조할 때와 동일한 규칙을 따른다. 단, final로 선언을 반드시 할 필요는 없으나 메서드 내에서 변경을 가해서는 안된다.

 이름이 겹치어도 변수를 가리지 않으며 this나 클래스명.this.변수명을 통해 여러 접근 범위내의 변수들의 값에 접근이 가능하다고 한다.


타겟 타이핑.

 람다식의 타입을 말하며 이는 가장 유사한 기능성 인터페이스의 타입을 따르게 된다.

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,                 //이 친구 타입은 Predicate
    p -> p.getEmailAddress(),                //이 친구 타입은 Function
    email -> System.out.println(email)       //이 친구 타입은 Consumer
);

 메서드 파라미터들의 타겟 타이핑.

 컬렉션이 포함하고 있는 객체가 무엇인지, <> 안에 명시한 클래스가 무엇인지에 따라 결정한다. 이를 통해 오버로딩된 메서드간 호출이 가능하다.

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}
오버로드된 메서드 두개.

String s = invoke(() -> "done");

 이 호출은 
<T> T invoke(Callable<T> c) {
    return c.call();
}
 이 친구 호출하는 거다.

댓글 없음:

댓글 쓰기