Item 47. 반환 타입으로는 stream보다는 collection이 낫다.

stream은 반복을 지원하지 않으므로, API를 stream만 반환하도록 짜놓으면 반환된 stream을 for-each로 반복하길 원하는 사용자는 사용이 힘들다

stream은 Iterable 인터페이스가 정의한 추상 method를 모두 포함할뿐 아니라, Iterable 인터페이스가 명시한 스펙대로 동작하지만 Iterable을 확장하고 있지는 않기 때문에 for-each문으로 반복이 불가능하다.

따라서 stream의 iterator method에 method 참조를 건네고, 이를 매개변수화된 Iterable로 적절히 형변환하면 동작은 한다.

1
2
3
for(ProcessHandle ph: (Iterable<ProcessHandler>) ProcessHandle.allProcesses()::iterator){
// process 처리
}

adapter method

하지만 이렇게 stream을 사용하기는 가독성이 떨어지므로, adapter method를 사용하는 방안이 있다.

1
2
3
4
// Stream<E> 를 Iterable<E>로 중개해주는 adapter 
public static <E> Iterable<E> iterableOf(Stream<E> stream){
return stream::iterator;
}

adapter method를 사용하면 어떤 stream도 for-each문으로 반복할 수 있다.

1
2
3
for(ProcessHandle p : iterableOf(ProcessHandle.allProcesses())){

}

반대의 상황으로서 API가 iterable만 반환하면 이를 stream 으로 사용하고자 하는 사용자는 사용이 힘들다. 따라서 adapter method를 제공하는 방안이 있다.

1
2
3
4
// Iterable<E>를 Stream<E>로 중개해주는 adapter 
public static <E> Stream<E> streamOf(Iterable<E> iterable){
return StreamSupport.stream(iterable.spliterator(),false);
}
Read more

Item51. method 시그니처를 신중히 설계하라

이번 아이템에서는 API 설계 권고 사항들을 정리하였다.

method 이름을 신중히 짓자.

  • 항상 표준 명명 규칙 (Item68)을 따라야 한다.
  • 같은 패키지에 속한 다른 이름들과 일관되게 짓는게 최우선이다.
  • 개발자 커뮤니티에서 널리 받아 들여지는 이름을 사용하자
  • 긴 이름은 피하자

편의 method를 너무 많이 만들지 말자

  • method가 너무 많은 class는 익히고 , 사용하고, 문서화하고 , 테스트하고 유지보수하기 어렵다. 따라서 아주 자주 쓰일때만 편의 method로 만들자

매개변수 목록은 짧게 유지하자

  • 매개변수가 4개를 넘어가면 가독성이 떨어진다.
  • 같은 type의 매개변수가 여러개 연달아 나오는 경우에는 특히 순서가 변경되어도 그대로 실행됨으로 좋지 않다.
Read more

Item50. 적시에 방어적 복사본을 만들라

클래스 불변식을 클라이언트가 꺠트릴 수 없도록 방어적으로 프로그래밍해야 한다.

다음과 같이 클래스 불변식을 유지하고자 하는 class가 있다고 가정하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class Period {

private final Date start;
private final Date end;

/***
* @param start 시작 시간
* @param end 종료 시각 , 시작 시간보다 뒤여야 한다.
* @throws IllegalArgumentException : 시작 시간이 종료시각보다 늦을떄 발생한다.
* @throws NullPointerException : start나 end가 null이면 발생한다.
*/
public Period(Date start, Date end) {
if(start.compareTo(end) > 0){
throw new IllegalArgumentException(start + " after " + end);
}
this.start = start;
this.end = end;
}

public Date start(){
return start;
}

public Date end(){
return end;
}
}

Period class는 한번 값이 설정되면 변경되지 않는 것 처럼 보이나 Date class 자체가 가변이라는 사실을 이용하면 클래스 불변식을 꺠트릴 수 있다.

1
2
3
4
5
6
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(68); // p 객체의 값이 변경된다.
}

따라서 Period class를 불변으로 유지하려면 불변 필드인 Instant를 사용해야 한다.

방어적 복사 (defensive copy)

외부 공격으로부터 class 불변식을 유지하려면 생성자에서 받은 가변 매개변수는 모두 방어적으로 복사해야 한다.

예시로든 Period class에 방어적 복사를 적용하면 다음과 같이 생성자에서 복사본을 필드로 가지게 만들고, 접근자에서도 복사본을 반환함으로서 클래스 불변식을 유지할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(this.start.compareTo(this.end) > 0){
throw new IllegalArgumentException(start + " after " + end);
}
}

public Date start(){
return new Date(start.getTime());
}

public Date end(){
return new Date(end.getTime());
}

Read more

Item49. 매개변수가 유효한지 검사하라

매개변수 검사 시점

method와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기를 바란다. 예컨대 인덱스 값은 음수이면 안되며, 객체 참조는 null이 아니어야 한다는 식이다.

이런 제약은 반드시 문서화해야 하며, method 몸체가 시작되기 전에 검사해야 한다.

method 몸체가 실행되기 전에 매개변수를 확인한다면, 잘못된 값이 넘어왔을때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.
그렇지 않은 경우에는 다음과 같은 단점을 가질 수 있다.

  1. method가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
  2. 잘못된 결과가 나온다.
  3. 객체를 이상한 상태로 변경해서 , 해당 method에서는 정상적으로 수행되는 것 처럼 보이지만, 미래의 시점에서 해당 method와 관련없는 오류를 낸다.

매개변수 검사 관련 문서화

public 과 protected method는 매개변수 값이 잘못됬을 떄 던지는 예외를 문서화해야한다. (@throws javadoc 이용)
예를 들면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/***
* 현재값의 나머지 값을 반환한다.
* 이 method는 항상 음이 아닌
* @param m 계수 (양수여야 한다.)
* @return 현재값의 나머지 값
* @throws ArithmeticException m이 보다 작으면 발생한다.
*
*/
public BigInteger mod(BigInteger m){
if(m.signum() <= 0 ){
throw new ArithmeticException("계수(m)는 양수여야 합니다. " + m);
}
//...
}
Read more

Item48. Stream 병렬화는 주의해서 적용하라

  • jdk 8 부터는 parallel method만 한번 호출하면 pipeline을 병렬 실행할수 있는 stream을 지원하기 시작했다.
  • stream 병렬화는 함부로 사용하면 오히려 성능 저하를 야기할 수 있다.

아래와 같아 처음 20개의 메르센 소수를 생성하는 프로그램이 있다고 가정하자

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
primes().map(p->TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
static Stream<BigInteger> primes(){
return Stream.iterate(TWO ,BigInteger::nextProbablePrime);
}

이를 병렬로 처리하고자 stream pipeline의 .parallel() method를 호출하면 오히려 프로그램 수행 시간이 극단적으로 느려지게 된다.

그 이유는 stream library 가 pipeline을 병렬화하는 방법을 찾지 못했기 때문인데, 데이터 소스가 stream.iterate거나,중간 연산으로 limit를 쓰면 pipeline병렬화로는 성능개선을 기대할 수 없다.

limit을 다룰때 CPU 코어개수가 남는다면 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정하는데, 위 메르센소수를 찾는 프로그램은 원소하나의 계산비용이 그 원소 이전에 계산한 모든 원소들의 비용을 합친 것 만큼 든다.

즉 메르센 소수를 찾는 프로그램의 경우 몇 개 더 처리한 후 제한된 개수 이후의 결과를 얻는 불필요한 비용 자체도 많이 든다.

Stream 병렬화 효과가 좋은 데이터

대체로 Stream의 Source가 ArrayList , HashMap , HashSet , ConcurrentHashMap 의 인스턴스거나 , 배열일때 병렬화의 효과가 가장 좋다.

위 병렬화에 적합한 자료구조들은 다음과 같은 특징을 가지고 있다.

  • 원하는 크기로 정확하게 손쉽게 나눌 수 있다. 즉 다수의 thread에게 일을 분배하기 좋다는 특징이 있다.

  • 참조 지역성(locality of reference)이 뛰어나다. 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있다 (공간 지역성)
    만약 참조 지역성이 낮으면 Thread는 데이터가 메모리로 전송될떄까지 대기하게 된다.
    따라서 참조지역성은 다량의 데이터를 처리하는 벌크 연산을 병렬화할때 아주 중요한 요소로 작동하는데, 참조 지역성이 가장 뛰어난 자료구조는 기본 타입 배열이다.
    (참조 지역성이란 기억 장치(SSD,HDD)로부터 데이터가 참조될때, 시간적/공간적/순차적으로 분포가 집중되는 성질 )

Read more

Item46. Stream에서는 부작용 없는 함수를 사용하라

stream 은 api긴 하지만 함수형 프로그래밍에 기초한 패러다임이다.

stream 패러다임의 핵심은 계산 연산을 일련의 변환 단계들로 재구성하는 것이다. 각 변환 단계는 가능한 이전 단계의 결과를 받아서 처리하는 순수함수여야 한다.

  • 순수 함수 : 오직 입력만이 결과에 영향을 주고, 다른 가변 상태를 참조하지도 않는 함수

Item45. Stream은 주의해서 사용하라

Stream API 개념

Stream API는 Jdk 8부터 다량의 데이터 처리 작업 (순차적,병렬적) 을 도와주기 위해 추가되었다.

Stream API의 핵심 개념은 다음과 같다.

  • Stream : 유한 또는 무한개의 데이터 원소 시퀀스이다.
  • Stream pipeline : 원소들로부터 수행하는 각 연산단계를 표현하는 개념

Stream API 특징

  1. 데이터를 담고 있는 저장소가 아니다.
  2. Stream안의 데이터 원소들은 객체 참조나 기본 타입 값 (int,long,double 3가지 ) 이다
  3. Functional in nature. 원본 데이터를 변경하지 않는다.
  4. Laziness-seeking.
    stream API는 2가지 연산으로 나뉜다. map,filer 와 같은 intermediate operation (중개 연산) , collect와 같은 terminal operation (종료 연산)이 있다.
    중개 연산은 stream을 반환하고 종료 연산은 stream 타입을 반환하지 않는다. 중개 연산은 lazy evalutation(지연 평가) 된다고 공식 문서에 적혀 있는데,
    이는 종료형 연산( 예를 들면 .collect(toList())) 이 오기전까지는 중개 연산이 실행자체가 되지 않기 떄문이다.
1
2
3
4
5
List.of("a", "b", "c").stream().map((s) -> {
System.out.println("s = " + s);
return s.toUpperCase();
});
// terminal operation이 수행되기 전까지 실행되지 않음 (lazy)
  1. Stream으로 처리되는 데이터는 오직 한번만 처리된다 (각각의 stream 내 operator를 한번씩만 지나간다. )
  2. 스트림내 데이터 원소가 무제한일수도 있다. (short circuit method를 사용해서 제한할 수 있다. )
  3. 병렬처리가 가능하다.
1
2
3
4
5
6
7
8
// parallelStream : Jvm 이 병렬처리해줌 
// 데이터가 적은 경우에 불필요하게 병렬처리시에는 thread context swithing overhead 가 있어 오히려 느려지는 경우도 있음
foods.parallelStream().forEach((food)-> System.out.println("current thread : " + Thread.currentThread().getName()));
/*
current thread : ForkJoinPool.commonPool-worker-5
current thread : ForkJoinPool.commonPool-worker-19
current thread : main
*/

보다 자세한 stream에 대한 설명은 아래 reference 에서 확인할 수 있다.

(https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html)

Read more

Item44. 표준 함수형 인터페이스를 사용하라

표준 함수형 인터페이스

Java가 람다를 지원하면서 API를 작성하는 모범사례도 크게 바뀌었는데, 대표적으로 template pattern이 함수 객체를 받는 정적 팩토리 method나 생성자를 제공하는 형태로 변경되었다. 즉 함수 객체를 매개변수로 받는 생성자와 method를 더 많이 만들어야 하는데 , 이때 함수형 매개변수 타입을 올바르게 선택해야 한다.

예시로 LinkedHashMap의 removeEldestEntry method는 put method에 의해 호출되어 true를 반환하면 map에서 가장 오래된 원소를 제거한다.

1
2
3
protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
return size() > 100;
}

다음과 같이 100개 미만의 크기를 가진 map의 경우에는 원소를 제거하지 않고, 100개 이상부터 오래된 원소를 제거하도록 구현할 수 있는데, 이를 함수형 인터페이스로 변경가능하다.

주의할점은 위 method가 instance method이기 떄문에, 람다를 사용할떄에는 Map instance도 매개변수로 받아야 한다.

1
2
3
4
@FunctionalInterface
public interface EldestEntryRemovalFunction<K,V> {
boolean remove(Map<K,V> map , Map.Entry<K, V> eldest);
}

위처럼 직접 선언하는 것도 가능은 하지만 java 내장 library (java.util.function) 에 다양한 용도의 표준 함수형 인터페이스가 담겨 있다. 따라서 적절한게 있다면 직접 구현하지말고 표준 함수형 인터페이스를 사용하는게 좋다.

위의 직접 구현한 함수형 인터페이스인 EldestEntryRemovalFunction도 표준 함수형 인터페이스인 BiPredicate<Map<K,V> , Map.Entry<K,V>> 로 대체할 수 있다.

표준 함수형 인터페이스 종류

  • java.util.function에 총 43개의 함수형 인터페이스가 존재한다.
  • 대표적인 표준 함수형 인터페이스들은 다음과 같다.
Read more

Item43. 람다보다는 메소드 참조를 사용하라

method reference

아래는 임의의 키와 Integer값을 매핑하는 map 인데, 이떄의 merge method는 키,값,함수를 매개변수로 받아서 주어진 키가 맵안에 없다면 키,값쌍을 그대로 저장하고, 키가 이미 있다면 세번쨰 인수로 들어온 함수에 현재 값과 주어진 값을 적용해 키,함수의 결과쌍을 저장한다.

1
map.merge(key,1,(count,incr)->count+incr);

위에서 세번쨰 매개변수로 함수형 인터페이스를 구현한 람다는 그저 두 인수의 합을 반환하는 역할을 하는데 Integer의 static method인 sum을 이용하면 코드의 가독성을 증가시킬 수 있다.

1
map.merge(key,1,Integer::sum);

또한 람다로 구현했을때, 코드가 너무 길거나 복잡한 경우에 람다의 코드를 method로 추출해서 해당 method reference를 사용하는 방식이다.

이렇게 하게 되면 method에 적합한 naming을 할 수 있게 해준다.

Read more

Item42. 익명 클래스보다 람다를 사용하라

  • 함수 객체(function object) : 추상 method를 하나만 담은 interface 의 instance (JDK1.1이전)
  • 익명 class : Jdk 1.1이 등장하면서 함수 객체를 만드는데 사용됨.
1
2
3
4
5
6
7
// 익명 class 예시 
Collections.sort(words, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});

익명 class의 단점으로 추상 method를 하나만 구현하는데, 코드가 길어진다는 단점이 존재한다.

Lambda expression

  • Functional Interface : 추상 method 를 하나만 가지고 있는 interface
1
2
3
4
5
6
@FunctionalInterface
public interface FI {
void doSomething();
// default method는 있어도 상관없음
default void print(String str){ System.out.println("str = " + str); }
}
  • Lambda expression, Lambda : Function interface의 객체를 간결하게 구현할 수 있도록 지원해줌
  • 람다식을 이용하면 기존에 길었던 익명 class 코드를 줄일 수 있다. (paramter 의 type 추론도 지원해줌.)
1
Collections.sort(words,(s1,s2)->Integer.compare(s1.length(),s2.length()));

주의사항 : compiler가 람다식의 매개변수 타입을 추론할때, generic raw type 을 사용하면 추론이 불가능해짐으로, 꼭 타입매개변수를 적어주어야 함.

method reference 의 종류는 다음과 같다, 다음 Item 43에서 자세한 종류를 확인할 수 있다.

  • Static 메소드 레퍼런스
  • Instance 메소드 레퍼런스
  • Constructor 메소드 레퍼런스
Read more