2017년 6월 20일 화요일

정리: A Few Hidden Treasures in Java 8

유투브의 다음 영상에서 얘기한 내용들을 정리해본다.
https://www.youtube.com/watch?v=GphO9fWhlAg&list=WL&index=10

영상에 등장하는 인도인 강사가 나오는 강의를 몇 개 상당히 재밌게 본 기억이 있다. 유용한 내용도 많아 도움이 많이 된다.

강의 내용을 간단히 정리하면 다음과 같다.

1. string joining


List에 값을 넣고 이를 한 문장으로 해서 보여줄 때 ','를 사용하여 단어를 구분할 수 있다.
예제는 다음과 같다.

>>Tom, Jerry, Jane, Jack

위와 같이 표현하려고 할 때, 기존의 코드에서는 for문을 사용해 마지막에는 ','가 들어가지 않게 if문을 사용하는 방법을 사용한다.

Java 8 에서는 joining을 사용해서 이를 간단하게 처리할 수 있다.

import java.util.*;
import static java.util.stream.Collectors.*; 
public class Sample {
   public static void main(String[] args) {
      List names = Arrays.asList("Tom", "Jerry", "Jane", "Jack");
      System.out.println(
         names.stream()
            .map(String::toUpperCase())
            .collect(joining(", ")));
   }
}

2. static interface methods


interface에서 static method를 선언하여 사용할 수 있는 기능이다. 예제 코드는 다음과 같다.

import java.util.*;
import static java.util.stream.Collectors.*;

public class Sample {
   interface Util {
      public static int numberOfCores() {
         return Runtime.getRuntime().availableProcessors();
      }
   }

   public static void main(String[] args) {
      System.out.println(Util.numberOfCores());
   }
}

3. default methods


interface는 보통 메소드의 선언만 있지 구현체는 없다. 그러나 default method를 사용하면 해당 메소드에 구현체를 넣을 수 있다. 다음 예제를 보면 interface의 메소드에 "default"를 주어 메소드를 구현한 것을 볼 수 있다. 컴파일 시, 에러가 발생하지 않고 정상적으로 컴파일이 된다.

import java.util.*;
import static java.util.stream.Collectors.*;

interface Fly {
   default void takeOff() { System.out.println("Fly::takeOff"); }
   default void turn() { System.out.println("Fly::turn"); }
   default void cruise() { System.out.println("Fly::cruise"); }
   default void land() { System.out.println("Fly::land"); }
}

public class Sample {   
   public static void main(String[] args) {
      System.out.println("OK");
   }
}

또한 default 메소드에서 다른 메소드를 호출하게 하여 해당 인터페이스를 상속하여 구현한 메소드를 호출할 수 있게 할 수 있다.

...
default void land() { System.out.println("Fly::land");  getState();}
int getState();
...

default method에는 다음과 같은 4가지 Rule이 있다.
  1. you get what is in the base interface
  2. you may override a default method
  3. if a method is there in the class hierarchy then it takes precedence
  4. if there is no method on any of the classes in the hierarchy, but two of your interfaces that implements has the default method to solve this use rule 3.
위 Rule 1은 다음과 같다.

import java.util.*;
import static java.util.stream.Collectors.*;

interface Fly {
   default void takeOff() { System.out.println("Fly::takeOff"); }
   default void turn() { System.out.println("Fly::turn"); }
   default void cruise() { System.out.println("Fly::cruise"); }
   default void land() { System.out.println("Fly::land"); }
}

interface FastFly extends Fly {
}

class SeaPlane implements FastFly {
 
}

public class Sample {
   public void use() {
      SeaPlane seaPlane = new SeaPlane();
      seaPlane.takeOff();
      seaPlane.turn();
      seaPlane.cruise();
      seaPlane.land();
   }
   public static void main(String[] args) {
      new Sample().use();
   }
}

위 코드의 결과 "Fly::takeOff"등 Fly 인터페이스에서 구현된 내용들이 출력된다. 이것이 규칙 1번에 해당된다.

Rule 2번은 defaut method를 override하는 속성을 얘기한다.

...
interface FastFly extends Fly {
   default void takeOff() { System.out.println("FastFly::takeOff") };
}
...

위와 같이 코드를 적용할 경우, override한 메소드에서 "FastFly::takeOff"를 출력하게 된다.

Rule 3번은 class hierarchy에 해당 메소드가 있으면 그걸 사용한다는 의미이다.
예제 코드를 보면

...
class Vehicle {
   public void land() { System.out.println("Vehicle::land"); }
}

class SeaPlane extends Vehicle implements FastFly {
 
}
...

위와 같은 코드일 경우 SeaPlane 인스턴스에서 land를 호출 시, "Vehicle:land"가 출력된다.

Rule 4번은 class hierarchy에 메소드가 없고 두 개의 interface에 해당 메소드가 모두 구현되어 있을 경우의 해결책으로 Rule 3을 사용하라는 의미이다.

예제코드는 다음과 같다.

...
class Sail {
   public void cruise() { System.out.println("Sail::cruise"); }
}

class SeaPlane extends Vehicle implements FastFly, Sail {
 
}
...

위 코드에서 FastFly와 Sail을 동시에 implements를 하면 cruise메소드로 인해 충돌이 발생하여 컴파일 에러가 발생한다. 이와 같을 경우 Rule 3을 활용하여 다음과 같이 할 수 있다.

...
class SeaPlane extends Vehicle implements FastFly, Sail {
   public void cruise() {
      System.out.println("SeaPlane::cruise");
   }
}
...

그러면 에러가 발생하지 않고 정상적으로 기능하게 된다. 하지만, FastFly의 cruise 메소드를 호출하려면 다음과 같이 한다.

...
class SeaPlane extends Vehicle implements FastFly, Sail {
   public void cruise() {
      System.out.println("SeaPlane::cruise");
      FastFly.super.cruise();
   }
}
...

위 코드에서 super를 사용한 이유는 super를 사용하지 않으면 static 메소드를 호출하려고 하기 때문이다. super를 사용해야 default method를 호출할 수 있다.

4. sorting


Java 8에서는 좀 더 추상화된 기능을 통해 손쉽게 소팅하게 해준다.
다음 예제 코드를 보자.

import java.util.*;
import static java.util.Comparator.comparing;

public class Sample {
   public static List createPeople() {
      return Arrays.asList(
         new Person("Sara", Gender.FEMALE, 20),
         new Person("Sara", Gender.FEMALE, 22),
         new Person("Bob", Gender.MALE, 20),
         new Person("Paula", Gender.FEMALE, 32),
         new Person("Paul", Gender.MALE, 32),
         new Person("Jack", Gender.MALE, 2),
         new Person("Jack", Gender.MALE, 72),
         new Person("Jill", Gender.FEMALE, 12)
      );
   }

   public static void printSorted(List people, Comparator comparator) {
      people.stream()
                 .sorted(comparator)
                 .forEach(System.out::println);
   }

   public static void main(String[] args) {
      List people = createPeople();
   
      printSorted(people, comparing(Person::getName));
   }
}

위와 같은 예제를 실행하면 이름의 알파벳 순서대로 출력이 된다. (Bob, Jack, Jack, Jill.. 순으로)

그런데 소팅을 나이에 따라 하고 싶으면 다음과 같이 하면 된다.

...
printSorted(people, comparing(Person::getAge));
...

그러면 나이가 가장 젊은 것부터 순서대로 정렬된다.

여기에서 더 나아가서 나이로 정렬한 다음에 나이가 동일한 데이터가 다수 있으면 그 중에서도 이름으로 정렬을 해야할 경우가 있다.

이때엔 다음과 같이 comparing을 사용한다.

...
printSorted(people, comparing(Person::getAge).thenComparing(Person:getName));
...

정렬순서를 반대로 바꾸고 싶으면 reversed 를 호출해주면 된다.

...
printSorted(people, comparing(Person::getAge).thenComparing(Person:getName).reversed());
...

이와 같이 소팅을 참 쉽게 처리할 수 있다.

5. grouping


4의 예제 코드를 가지고 아래의 코드를 추가하여 나이 데이터로 그룹화 할 수 있다.

import static java.util.stream.Collectors.*;
...
   public static void main(String[] args) {
      List people = createPeople();
      System.out.println(
         people.stream()
                    .collect(groupingBy(Person::getAge));
   }
...

위와 같이 하면 나이가 같은 데이터를 하나의 그룹으로 묶어서 처리하게 된다.
==> 32=[Paula -- FEMALE -- 32, Paul -- MALE -- 32], 2=[Jack -- MALE -- 2], ...

여기에서 그룹화된 데이터 중, 이름만을 사용하겠다고 하면 다음과 같이 할 수 있다.

      System.out.println(
         people.stream()
                    .collect(groupingBy(Person::getAge,
                       mapping(Person::getName, toList())));

그러면 결과는 다음과 같다.
==> 32=[Paula, Paul], 2=[Jack], ...

6. Combining Predicates and Functions


Predicate를 Function을 사용해서 사용하는 법과 다수의 Predicate를 결합하여 사용하는 방법에 대하여 알아보자.

예제 코드는 다음과 같다.

import java.util.*;
import java.util.function.Predicate;

public class Sample {
   public static void print(int number, Predicate predicate, String msg) {
       System.out.println(number + " " + msg + ":" + predicate.test(number));
   }
 
   public static void main(String[] args) {
      Predicate isEven = e -> e % 2 == 0;
   
      print(5, isEven, "is even?");
   }

}

위와 같은 예제 코드를 사용하여 Predicate를 Function과 함께 사용할 수 있다.
출력된 결과는 다음과 같다.
==> 5 is even?:false

여기서 Predicate 여러개를 and, or 와 같은 연산을 적용하여 함께 사용할 수 있다.

...
   public static void main(String[] args) {
      Predicate isEven = e -> e % 2 == 0;
      Predicate isGT4 = e -> e > 4;
   
      print(5, isGT4.and(isEven), "is > 4 && is even?");
      print(5, isGT4.or(isEven), "is > 4 && is even?");
   }
...


7. Map's convenience functions


Map이 제공하는 사용하기 편한 function들을 사용하라는 내용이다.

Map sqrt = HashMap();

위와 같은 Map 인스턴스가 있을 때, sqrt에 4의 제곱근 값 2가 없을 경우 이를 넣어주는 코드를 보통 생각하면

if( !sqrt.containsKey(4))
   sqrt.put(4, Math.sqrt(4));

와 같이 코드를 만들 수 있다. 그런데 이런 경우엔 Map이 가지고 있는 함수를 사용하면 쉽게 할 수 있다.

sqrt.computeIfAbsent(4, Math::sqrt);

8. Parallelizing streams


stream으로 연산 처리 시, parallel 하게 처리하는 부분에 대해 설명한다.

import java.util.*;

public class Sample {
   public static int doubleIt(int number) {
      System.out.println(number + " : " + Thread.currentThread());
      return number * 2;
   }

   public static void main(String[] args) {
      List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
   
      System.out.println(
         numbers.stream()
                       .map(Sample::doubleIt)
                       .reduce(0, Integer::sum));
   }
}

위와 같은 코드를 실행했을 때, 하나의 Thread로 순차적으로 값을 2배로 만들고 값을 합산한다.
이를 parallel 하게 사용하려면 다음과 같이 코드를 작성한다.

...
      System.out.println(
         numbers.parallelStream()
                       .map(Sample::doubleIt)
                       .reduce(0, Integer::sum));
...

위와 같이 호출할 경우, Thread를 ForkJoinPool을 사용하여 parallel하게 처리한다. 즉, 시스템의 코어 수만큼의 Thread를 사용하여 연산을 처리하게 된다.

여기서 thread의 개수를 더 많이 늘려서 처리를 하고 싶을 경우엔 옵션을 사용해 JVM 레벨에서 처리할 수 있다.
java.util.concurrent.ForkJoinPool.common.parallelism=100

그러면 Thread 100개를 가지고 연산을 처리할 수 있다.

위 옵션이 없을 경우엔 시스템의 코어 개수(예로 8개라면)만큼의 Thread로 연산을 처리하게 되므로 위 코드의 numbers에 들어간 값이 100개라면 한 번에 8개씩 처리가 된다. 그러나 옵션을 사용해 100개의 Thread를 사용하면 한 번에 모두 처리할 수 있다. 



정리

이상으로 정리를 해보았는데 왠만한 내용은 모두 들어가 있는 듯하다. 그래도 동영상을 보고 싶으신 분은 아래 url로 가면 된다.
https://www.youtube.com/watch?v=GphO9fWhlAg&list=WL&index=9