반응형
아이템 42 - 익명 클래스보다는 람다를 사용하라
함수 객체와 람다식
-
함수 객체 : 추상 메서드를 하나만 담은 인터페이스로 람다식이 등장하기 전에는 익명 클래스를 사용해서 주로 만들었음
Collections.sort(words, new Comparator<String>() { public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); } })
-
자바8에와서 추상 메서드 하나짜리 인터페이스는 특별한 의미를 인정받아 특별한 대우를 받게됨
람다식을 이용해서 훨씬 간결한 코드를 만들 수 있게됨
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
- 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자
람다의 단점
- 람다는 이름이 없고 문서화도 못해서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다
- 람다는 한 줄일 때 가장 좋고 길어야 세 줄 안에 끝내는 게 좋다
- 람다가 길거나 가독성이 떨어진다면 더 간단히 줄여보거나 람다를 쓰지 않는 쪽으로 리팩토링 필요함
람다로 대체할 수 없는 것
- 추상 클래스나 추상 메서드가 여러 개인 인터페이스의 인스턴스를 만들 때는 람다를 쓸 수 없어서 익명 클래스를 써야 함
- 함수 객체가 자신을 참조해야 하는 경우 익명 클래스를 써야 함
- 람다는 자신을 참조할 수 없음, 람다에서의 this 키워드는 바깥 인스턴스를 가리킴
- 익명클래스의 this 키워드는 인스턴스 자신을 가리킴
람다 사용시 유의할 점
- 직렬화 형태가 구현별로 다를 수 있어서 람다를 직렬화하는 일은 극히 삼가야 한다(익명 클래스의 인스턴스도 마찬가지)
아이템 43 - 람다보다는 메서드 참조를 사용하라
- 함수 객체를 람다보다도 더 간결하는 만드는 방법이 바로 메서드 참조임
<람다식 사용 방법>
map.merge(key, 1, (count, incr) -> count + incr);
<메서드 참조 사용 방법>
map.merge(key, 1, Integer::sum);
메서드 참조를 사용하는 편이 보통은 더 짧고 간결하므로, 람다로 구현했을 때 너무 길거나 복잡하다면 메서드 참조가 좋은 대안이 되어줌
때로는 람다가 메서드 참조보다 간결할 때도 있음, 주로 메서드와 람다가 같은 클래스에 있을 때임
service.execute(GoshThisClassNameIsHumongous::action);
service.execute(() -> action());
메서드 참조 유형
메서드 참조 유형 | 예 | 같은 기능을 하는 람다 |
---|---|---|
정적 | Integer::parseInt | str -> Integer.parseInt(str) |
한정적(인스턴스) | Instant.now()::isAfter | Instant then = Instant.now(); t -> then.isAfter(t) |
비한정적(인스턴스) | String::toLowerCase | str -> str.toLowerCase() |
클래스 생성자 | TreeMap<K,V>::new | () -> new TreeMap<K,V>() |
배열 생성자 | int[]::new | len -> new Int[len] |
메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고 그렇지 않을 때만 람다를 사용하라
아이템 44 - 표준 함수형 인터페이스를 사용하라
- 필요한 용도에 맞는게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라
- java.util.function 패키지에는 총 43개의 인터페이스가 있지만 기본 6개의 인터페이스만 기억하면 나머지를 충분히 유추해낼 수 있다
인터페이스 | 함수 시그니처 | 예 |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaryOperator | T apply(T t1, T t2) | BigInteger::add |
Predicate | boolean test(T t) | Collection::isEmpty |
Function<T,R> | R apply(T t) | Arrays::asList |
Supplier | T get() | Instant::now |
Consumer | void accept(T t) | System.out::println |
주의점
- 표준 함수형 인터페이스 대부분은 기본 타입만 지원함, 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말 것
- 동작은 하지만 계산량이 많을 때는 성능이 처참히 느려질 수 있다
전용 함수형 인터페이스 구현이 필요한 경우
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다
- 반드시 따라야 하는 규약이 있다
- 유용한 디폴트 메서드를 제공할 수 있다
@FunctionalInterface의 목적
- 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다
- 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다
아이템 45 - 스트림은 주의해서 사용하라
- 스트림 파이프라인은 지연 평가(lazy evaluation)된다. 평가를 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다
- 스트림 API는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있다. 하지만 할 수 있다는 뜻이지, 해야 한다는 뜻은 아니다
- 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다
<Stream을 사용하지 않은 코드>
public class Anagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
Map<String, Set<String> groups = new HashMap<>();
try(Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word),
(unused) -> new TreeSet<>()).add(word);
}
}
for (Set<String group : groups.values())
if (group.size() >= minGroupSize)
System.out.println(group.size() + ": " + group);
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
<Stream을 과도하게 사용한 코드>
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(
groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
}
}
<Stream을 적절하게 사용한 코드>
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
}
}
// alphabetize 메서드는 위의 예제 코드와 동일함
}
- 스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다
- 람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인 가독성이 유지된다
- 도우미 메서드를 적절히 활용하는 일의 중요성은 일반 반복 코드에서보다는 스트림 파이프라인에서 훨씬 크다
- 기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 보일 때만 반영하자
함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 일
- 지역 변수를 읽고 수정하는 것, 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있다
- return문을 사용해 메서드를 빠져나가거나 break, countinue문을 사용하는 것
스트림 사용이 안성맞춤인 일
- 원소들의 시퀀스를 일관되게 반환한다
- 원소들의 시퀀스를 필터링한다
- 원소들의 시퀀스를 하나의 연산을 사용해 결합한다
- 원소들의 시퀀스를 컬렉션에 모은다
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다
아이템 46 - 스트림에서는 부작용 없는 함수를 사용하라
- 스트림은 그저 또 하나의 API가 아닌, 함수형 프로그래밍에 기초한 패러다임이다
- 스트림이 제공하는 표현력, 속도, 병렬성을 얻으려면 이 패러다임도 함께 받아들여야 한다
- 스트림 패러다임의 핵심은 계산을 입력의 변환으로 재구성 하는 부분이다
- 이때 각 변환 단계는 가능한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다
- 순수 함수 : 오직 입력만이 결과에 영향을 주는 함수
- 다른 가변 상태를 참조하지 않고 함수 스스로도 다른 상태를 변경하지 않도록 하려면 스트림 연산에 건네는 함수 객체는 모두 부작용 (side effect)가 없어야 한다
- forEach 연산은 종단 연산 중 기능이 가장 적고 가장 '덜' 스트림답다, forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고 계산하는 데는 쓰지 말자
- 스트림을 사용하려면 수집기를 잘 알아둬야 한다
가장 중요한 수집기 팩터리
- toList
- toSet
- toMap
- groupingBy
- joining
아이템 47 - 반환 타입으로는 스트림보다 컬렉션이 낫다
- 스트림은 반복을 지원하지 않는다
- 반복하기 위한 방법은 있으나 추가적인 변환 코드 작성이 필요하다
- Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다
- 따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는게 일반적으로 최선이다
- 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안됨
- 원소의 갯수가 많다면 전용 컬렉션을 구현할지 고민하라
아이템 48 - 스트림 병렬화는 주의해서 적용하라
- 자바8에서부터 parallel 메서드 한 번 호출로 파이프라인을 병렬 실행할 수 있는 스트림을 지원
- 자바로 동시성 프로그램을 작성하기가 점점 쉬워지고는 있지만, 이를 올바르고 빠르고 작성하는 일은 여전히 어려운 작업임
- 동시성 프로그래밍을 할 때에는 안정성(safety), 응답 가능(liveness) 상태를 유지하기 위해 애써야하는데, 병렬 스트림 파이프라인 프로그래밍에서도 다를 바 없음
- 환경이 아무리 좋더라도 데이터 소스가
Stream.iterate
거나 중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없음 - 대체로 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스이거나 배열, int 범위, long 범위일 때 벙렬화의 효과가 가장 좋음
- 이 자료구조들은 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있기 때문
- 스트림 파이프라인의 종단 연산의 동작 방식 역시 병렬 수행 효율에 영향을 줌
- 중단 연산 중 병렬화에 가장 적합한 것은 축소(reduction)
- Stream의 reduce 메소드 중의 하나, 혹은 min, max, count, sum
- anyMatch, allMatch, noneMatch처럼 조건에 맞으면 바로 반환되는 메서드도 적합함
- 가변 축소 연산을 수행하는 Stream의 collect 메서드는 병렬화에 적합하지 않음 (컬렉션들을 합치는 부담이 큼)
- 중단 연산 중 병렬화에 가장 적합한 것은 축소(reduction)
- 스트림을 잘못 병렬화하면 성능이 나빠질 뿐만아니라 결과 자체가 잘못되거나 예상 못한 동작 발생 가능성도 있음
- 스트림 병렬화는 오직 성능 최적화 수단임을 기억해야함
- 다른 최적화와 마찬가지로 변경 전후로 반드시 성능을 테스트하여 병렬화 사용 가치가 있는지 확인 필요
- 조건이 잘 갖춰지면 parallel 메서드 호출하나로 거의 프로세서 코어 수에 비례하는 성능 향샹 만끽 가능함
반응형
'개발 > 자바' 카테고리의 다른 글
자바 성능을 결정짓는 코딩 습관과 튜닝 이야기 (0) | 2020.06.09 |
---|---|
Effective Java 3/E - 8장 메서드 요약정리 (0) | 2019.05.26 |
JSON 스키마 (0) | 2017.06.30 |
JSON STREAMING API (0) | 2017.06.30 |
FileLock (0) | 2017.01.11 |