객체 지향 프로그래밍은 동작부를 캡슐화해 코드를 이해하기 쉽게 만든다. 함수형 프로그래밍은 동작부를 최소화해 코드를 이해하기 쉽게 만든다. - 마이클 페더스
8-1. 익명 클래스 대신 람다 사용하기
Map<Double, Double> values = new HashMap<>();
Double square(Double x) {
Function<Double, Double> squareFunction =
new Function<Double, Double>() {
@Override
public Double apply(Double value) {
return value * value;
}
};
return values.computeIfAbsent(x, squareFunction);
}
위와 같이 익명클래스(squareFuction)을 만드는 것보다 람다 표현식이 낫다
Map<Double, Double> values = new HashMap<>();
Double square(Double value) {
Function<Double, Double> squareFunction = factor -> factor * factor;
return values.computeIfAbsent(value, squareFunction);
}
아래와 같이 멀티라인으로도 가능하다
Function<Double, Double> squareFunction = factor -> {
return factor * factor;
};
8-2 .명령행 방식 대신 함수형
List<Supply> supplies = new ArrayList<>();
long countDifferentKinds() {
List<String> names = new ArrayList<>();
for (Supply supply : supplies) {
if (supply.isUncontaminated()) {
String name = supply.getName();
if (!names.contains(name)) {
names.add(name);
}
}
}
return names.size();
}
위 코드는 한줄씩 내려가면서 내용을 파악해야 하는데, 람다를 사용하면 훨씬 읽기 쉽다.
List<Supply> supplies = new ArrayList<>();
long countDifferentKinds() {
return supplies.stream()
.filter(supply -> supply.isUncontaminated())
.map(supply -> supply.getName())
.distinct()
.count();
}
https://docs.oracle.com/javase/9/docs/api/java/util/stream/package-summary.html#StreamOps
8-3. 람다 대신 메서드 참조
List<Supply> supplies = new ArrayList<>();
long countDifferentKinds() {
return supplies.stream()
.filter(supply -> !supply.isContaminated())
.map(supply -> supply.getName())
.distinct()
.count();
}
람다 표현식이 간단하면 관계없지만 로직이 복잡하면 오류 가능성이 있다. 스트림 내에서 메서드 참조를 사용할 수 있다.
List<Supply> supplies = new ArrayList<>();
long countDifferentKinds() {
return supplies.stream()
.filter(Supply::isUncontaminated)
.map(Supply::getName)
.distinct()
.count();
}
8-4. Side effect 피하기
List<Supply> supplies = new ArrayList<>();
long countDifferentKinds() {
List<String> names = new ArrayList<>();
Consumer<String> addToNames = name -> names.add(name);
supplies.stream()
.filter(Supply::isUncontaminated)
.map(Supply::getName)
.distinct()
.forEach(addToNames);
return names.size();
스트림 내에서 forEach 사용은 병렬처리 등에서 문제가 발생할 수 있다. collect() 나 reduce()를 쓰도록 노력하라.
List<Supply> supplies = new ArrayList<>();
long countDifferentKinds() {
List<String> names = supplies.stream()
.filter(Supply::isUncontaminated)
.map(Supply::getName)
.distinct()
.collect(Collectors.toList());
return names.size();
}
8-5. 복잡한 스트림 종료시 컬렉트 사용하기
List<Supply> supplies = new ArrayList<>();
Map<String, Long> countDifferentKinds() {
Map<String, Long> nameToCount = new HashMap<>();
Consumer<String> addToNames = name -> {
if (!nameToCount.containsKey(name)) {
nameToCount.put(name, 0L);
}
nameToCount.put(name, nameToCount.get(name) + 1);
};
supplies.stream()
.filter(Supply::isUncontaminated)
.map(Supply::getName)
.forEach(addToNames);
return nameToCount;
}
조건을 만족하는 리스트 내용에 대해 이름과 이름별 개수를 추출하고자 하는 코드이다. 아래와 같이 하면 훨씬 가독성이 높아진다. groupingBy 외에도 partitioningBy(), mapBy(), joining(), mapping(), summingInt(), averagingLong(), reducing(), filtering(), flatMapping() 등 다양한 기능을 제공하고 있다.
List<Supply> supplies = new ArrayList<>();
Map<String, Long> countDifferentKinds() {
return supplies.stream()
.filter(Supply::isUncontaminated)
.collect(Collectors.groupingBy(Supply::getName,
Collectors.counting())
);
}
8-6. 스트림 내 예외 피하기
static List<LogBook> getAll() throws IOException {
return Files.walk(Paths.get("/var/log"))
.filter(Files::isRegularFile)
.filter(LogBook::isLogbook)
.map(path -> {
try {
return new LogBook(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
})
.collect(Collectors.toList());
}
스트림내에서는 IOException과 같이 checked exception을 쓸 수 없어서, UncheckedIOException으로 바꿔서 전달하고 있는데, 안전해 보이지 않는다. map()대신 flatMap()을 이용해서 다른 타입의 Stream으로 매핑하여 Stream.of 가 수행되도록 하였다.
static List<LogBook> getAll() throws IOException {
try (Stream<Path> stream = Files.walk(Paths.get("/var/log"))) {
return stream.filter(Files::isRegularFile)
.filter(LogBook::isLogbook)
.flatMap(path -> {
try {
return Stream.of(new LogBook(path));
} catch (IOException e) {
return Stream.empty();
}
})
.collect(Collectors.toList());
}
}
8-7. 널 대신 Optional
class Communicator {
Connection connectionToEarth;
void establishConnection() {
// used to set connectionToEarth, but may be unreliable
}
Connection getConnectionToEarth() {
return connectionToEarth;
}
}
class Usage {
static void main() {
Communicator communicator = new Communicator();
communicator.getConnectionToEarth()
.send("Houston, we got a problem!");
}
}
getConnectionToEarch()가 null을 반환하면 NullPointException이 발생한다.
class Communicator {
Connection connectionToEarth;
void establishConnection() {
// used to set connectionToEarth, but may be unreliable
}
Optional<Connection> getConnectionToEarth() {
return Optional.ofNullable(connectionToEarth);
}
}
위와 같이 Optional로 반환하면 호출할 때 connection이 없을 수도 있다는 내용을 인지하게 해준다. 아래와 같이 orElse를 호출하면 여전히 null이 반환되므로 ifPresend()를 통해 conneciton이 있을 경우에만 출력을 할 수 있다.
static void main() {
Communicator communicator = new Communicator();
Connection connection = communicator.getConnectionToEarth()
.orElse(null);
connection.send("Houston, we got a problem!");
}
static void main2() {
Communicator communicationSystem = new Communicator();
communicationSystem.getConnectionToEarth()
.ifPresent(connection ->
connection.send("Houston, we got a problem!")
);
}
8-8. 선택 필드나 매개변수 피하기
class Communicator {
Optional<Connection> connectionToEarth;
void setConnectionToEarth(Optional<Connection> connectionToEarth) {
this.connectionToEarth = connectionToEarth;
}
Optional<Connection> getConnectionToEarth() {
return connectionToEarth;
}
}
위 코드는 Communicator 클래스내에 변수에도 Optional을 사용해서 존재/부재, 그리고 Optional이 null인 경우까지 3가지 경우가 발생한다. 지역 변수나 매개변수는 Optional을 쓰지않는게 낫다.
class Communicator {
Connection connectionToEarth;
void setConnectionToEarth(Connection connectionToEarth) {
this.connectionToEarth = Objects.requireNonNull(connectionToEarth);
}
Optional<Connection> getConnectionToEarth() {
return Optional.ofNullable(connectionToEarth);
}
void reset() {
connectionToEarth = null;
}
}
8-9. 옵셔널을 스트림으로 사용하기
void backupToEarth() {
Optional<Connection> connectionOptional =
communicator.getConnectionToEarth();
if (!connectionOptional.isPresent()) {
throw new IllegalStateException();
}
Connection connection = connectionOptional.get();
if (!connection.isFree()) {
throw new IllegalStateException();
}
connection.send(storage.getBackup());
}
위 코드는 아래와 같이 향상가능하다. filter()를 사용해서 연결이 이어져있고, 사용할 수 있는지 확인하고 그렇지 않으면 IllegalStateException을 발생시킨다.
void backupToEarth() {
Connection connection = communicator.getConnectionToEarth()
.filter(Connection::isFree)
.orElseThrow(IllegalStateException::new);
connection.send(storage.getBackup());
}