1. 헬로 람다 표현식
사고의 전환
for (String city : cities) {
if (city.equals("Chicago")) {
found = true;
break;
}
}
System.out.println("Found chicago? : " + found)
System.out.println("Found chicago? : " + cities.contains("Chicago"))
- 난잡한 가변변수(Mutable Variable)의 사용을 방지
- 이터레이션에 대한 코드가 외부로 드러나지 않기 때문에 개발자는 이터레이션 자체 코드에 대해서 신경 쓸 필요가 없음
- 어수선한 코드의 사용을 막아줌
- 코드에 대한 설명이 명확해짐
- 비즈니스 의도는 유지하면서 코드는 명료해짐
- 오류의 발생확률을 줄여줌
- 이해하고 쉽고 유지보수가 쉬움
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
for(BigDecimal price : prices) {
if(price.compareTo(BigDecimal.valueOf(20)) > 0)
totalOfDiscountedPrices =
totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
final BigDecimal totalOfDiscountedPrices =
prices.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
.map(price -> price.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
- 코드가 어수선하지 않고 더 짜임새 있게 구성됐다.
- 로우-레벨 오퍼레이션을 사용하지 않는다.
- 로직을 강화하거나 변경하기에 더 쉽다.
- 메서드를 사용할 때 이터레이션은 라이브러리에 의해 제어된다.
- 더 효율적이다. 루프에 대한 레이지 이벨류에이션(lazy evaluation)이 가능하다.
- 원하는 부분을 병렬화 하기가 더 쉽다.
함수형 스타일 코드의 큰 이점
- 변수의 명시적인 변경이나 재할당의 문제를 피할 수 있다. (immutability)
- 함수형 버전은 쉽게 병렬화가 가능하다.
- 서술적인 코드작성이 가능하다.
- 함수형 버전은 더 간결하고, 직관적이다.
함수형 스타일로 코딩해야 하는 이유
Transaction transaction = getFromTransactionFactory();
checkProgressAndCommitOrRollbackTransaction();
UpdateAnditTrail();
runWithinTransaction((Transaction transanction) -> {
});
- 상태를 체크하는 정책과 감시기록을 업데이트하는 작업은 runWithinTransaction() 메서드 안에서 추상화되고 캡슐화된다.
- try-catch문을 추가하더라도, runWithinTransaction() 메서드 안에서 처리가 가능하다.
tickers.map(StockUtil::getPrice).filter(StockUtil::priceIsLessThan500).sum()
지연연산
혁명이 아닌 진화
//고차함수의 예 filter, map, reduce
prices.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20) > 0))
.map(price -> price.multiplay(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add)
2. 컬렉션의 사용
리스트를 사용한 이터레이션
final List<String> friedns =
Arrays.asList("Brian", "Jake", "Lion", "Rabbit");
for (int i = 0; i < friedns.length; i++) {
System.out.println(friedns.get(i));
}
friedns.forEach((name) -> System.out.println(name))
friedns.forEach(System.out::println)
리스트의 변형
final List<String> uppercaseNames = new ArrayList<String>();
friends.forEach(name -> uppercaseNames.add(name.toUpperCase()));
System.out.println(uppercaseNames);
friends.stream()
.map(name -> name.toUpperCase())
.forEach(name -> System.out.print(name + " "));
friends.stream()
.map(name::toUpperCase)
.forEach(name -> System.out.print(name + " "));
- stream() 메서드는 jdk8의 모든 컬렉션에서 사용할 수 있으며 스트림 인스턴스에 대한 컬렉션을 래핑한다.
- map 메서드는 입력 엘리먼트 수와 결과 엘리먼트의 수가 동일하다는 것을 보장한다.
엘리먼트 찾기
final List<String> startsWithN = new ArrayList<String>();
for(String name : friends) {
if(name.startsWith("N")) {
startsWithN.add(name);
}
}
final List<String> startsWithN =
friends.stream()
.filter(name -> name.startsWith("N"))
.collect(Collectors.toList());
람다 표현식의 재사용성
final Predicate<String> startsWithN = name -> name.startsWith("N");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN)
.count();
final long countEditorsStartN =
editors.stream()
.filter(startsWithN)
.count();
final long countComradesStartN =
comrades.stream()
.filter(startsWithN)
.count();
렉시컬 스코프와 클로저 사용하기
final Predicate<String> startsWithN = name -> name.startsWith("N");
final Predicate<String> startsWithB = name -> name.startsWith("B");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN).count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithB).count();
public static Predicate<String> checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}
final long countFriendsStartN =
friends.stream()
.filter(checkIfStartsWith("N")).count();
final long countFriendsStartB =
friends.stream()
.filter(checkIfStartsWith("B")).count();
- 변수 letter의 범위는 startsWith 함수의 범위에 있지 않기 때문에 람다표현식의 정의에 대해 범위를 정하고 그 범위 안에서 변수 letter를 찾는다. 이것을 렉시컬 스코프(lexical scope)라고 한다. (함수가 선언된 위치에 따라 정의되는 스코프)
엘리먼트 선택
String foundName = null;
for(String name : names) {
if(name.startsWith(startingLetter)) {
foundName = name;
break;
}
}
if(foundName != null) {
System.out.println(foundName);
} else {
System.out.println("No name found");
}
- 데이터를 찾지 못할경우에 대비하여 null체크를 해야한다.
final Optional<String> foundName =
names.stream()
.filter(name ->name.startsWith(startingLetter))
.findFirst();
System.out.println(foundName.orElse("No name found"));
foundName.ifPresent(name -> System.out.println(name))
- Optional클래스는 결과가 없을수도 있는 경우에 유용하다. (NullPointException이 발생하는 것을 막아준다)
컬렉션을 하나의 값으로 리듀스
final Optional<String> aLongName =
friends.stream()
.reduce((name1, name2) ->
name1.length() >= name2.length() ? name1 : name2);
aLongName.ifPresent(name ->
System.out.println(String.format("A longest name: %s", name)));
- reduce가 parameter로 받는 람다표현식은 BinaryOperator라는 함수형 인터페이스다.
엘리먼트 조인
friends.stream()
.map(String::toUpperCase)
.collect(joining(", ")));
// A, B, C, D와 같은 형태로 결과가 노출됨. (collect 는 3장에서 자세히 다룸)
3. String, Comparator, 그리고 filter
Method Reference
List<String> list = Arrays.asList("a", "b", "c");
list.chars()
.map(x -> x.toUpperCase())
.forEach(x -> System.out.println(x));
list.chars()
.map(String::toUpperCase)
.forEach(System.out::println);
- 인스턴스 메서드일경우
- 메서드는 파라미터를 호출하는 타깃이 된다. => parameter.toUpperCase()
- 파라메터는 메서드의 인수로 라우팅 => System.out.println(parameter)
Comparator 인터페이스의 구현
people.stream()
.sorted((person1, person2) -> person1.ageDifference(person2))
.collect(toList());
people.stream()
.sorted(Person::ageDifference)
.collect(toList());
Comparator의 재사용
Comparator<Person> compareAscending =
(person1, person2) -> person1.ageDifference(person2);
Comparator<Person> compareDescending = compareAscending.reversed();
여러가지 비교연산
final Function<Person, String> byName = person -> person.getName();
people.stream()
.sorted(comparing(byName));
final Function<Person, Integer> byAge = person -> person.getAge();
final Function<Person, String> byTheirName = person -> person.getName();
people.stream()
.sorted(comparing(byAge).thenComparing(byTheirName))
.collect(toList()));
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
collect 메서드와 Collectors 클래스 사용하기
List<Person> olderThan20 = new ArrayList<>();
people.stream()
.filter(person -> person.getAge() > 20)
.forEach(person -> olderThan20.add(person));
List<Person> olderThan20 =
people.stream()
.filter(person -> person.getAge() > 20)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
- supplier(결과컨테이너를 만드는 방법) => ArrayList::new
- acumulator(하나의 엘리먼트를 결과 컨테이너에 추가하는 방법) => ArrayList:add
- combiner(하나의 결과 컨테이너를 다른 결과와 합치는 방법) => ArrayList::addAll
List<Person> olderThan20 =
people.stream()
.filter(person -> person.getAge() > 20)
.collect(Collectors.toList());
Map<Integer, List<Person>> peopleByAge =
people.stream()
.collect(Collectors.groupingBy(Person::getAge));
Map<Integer, List<String>> nameOfPeopleByAge =
people.stream()
.collect(
groupingBy(Person::getAge, mapping(Person::getName, toList())));
디렉터리에서 모든 파일 리스트하기
Files.list(Paths.get("."))
.filter(Files::isDirectory)
.forEach(System.out::println);
flatMap을 사용하여 서브 디렉터리 리스트하기
List<File> files = new ArrayList<>();
File[] filesInCurrentDir = new File(".").listFiles();
for(File file : filesInCurrentDir) {
File[] filesInSubDir = file.listFiles();
if(filesInSubDir != null) {
files.addAll(Arrays.asList(filesInSubDir));
} else {
files.add(file);
}
}
List<File> files =
Stream.of(new File(".").listFiles())
.flatMap(file -> file.listFiles() == null ?
Stream.of(file) : Stream.of(file.listFiles()))
.collect(toList());
}
Stream.flatMap은 (A -> Stream[A]) 함수를 받아, Stream[Stream[A]]와 같은 형태의 데이터를 Stream[A]의 형태로 만들어주는 고차함수이다. flatMap()은 많은 노력을 제거해주며, 단항 조합(monadic composition)이라고 하는 두 개의 오퍼레이션의 순서를 하나의 과정으로 조합해준다.
4. 람다 표현식을 이용한 설계
람다를 사용한 문제의 분리
설계 문제 살펴보기
public class Asset {
public enum AssetType { BOND, STOCK };
private final AssetType type;
private final int value;
public Asset(final AssetType assetType, final int assetValue) {
type = assetType;
value = assetValue;
}
public AssetType getType() { return type; }
public int getValue() { return value; }
}
public static int totalAssetValues(final List<Asset> assets) {
return assets.stream()
.mapToInt(Asset::getValue)
.sum();
}
복잡한 문제 다루기
public static int totalAssetValues(final List<Asset> assets,
final Predicate<Asset> assetSelector) {
return assets.stream()
.filter(assetSelector)
.mapToInt(Asset::getValue)
.sum();
}
System.out.println("Total of all assets: " +
totalAssetValues(assets, asset -> true));
System.out.println("Total of bonds: " +
totalAssetValues(assets, asset -> asset.getType() == AssetType.BOND));
System.out.println("Total of stocks: " +
totalAssetValues(assets, asset -> asset.getType() == AssetType.STOCK));
람다 표현식을 사용하여 딜리게이트하기
딜리게이트 생성
private Function<String, BigDecimal> priceFinder;
public BigDecimal computeStockWorth(final String ticker, final int shares) {
return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));
}
public CalculateNAV(final Function<String, BigDecimal> aPriceFinder) {
priceFinder = aPriceFinder;
}
람다 표현식을 사용한 데코레이팅
필터 설계
public class Camera {
private Function<Color, Color> filter;
public Color capture(final Color inputColor) {
final Color processedColor = filter.apply(inputColor);
return processedColor;
}
}
public void setFilters(final Function<Color, Color>... filters) {
filter = Stream.of(filters)
.reduce((filter, next) -> filter.andThen(next))
.orElse(color -> color);
}
funcA.andThen(funcB) => funcB(funcA(input))
=> input -> funcA -> funcB -> result
funcA.compose(funcB) => funcA(funcB(input))
=> input -> funcB -> funcA -> result
camera.setFilters(Color::darker);
camera.setFilters(Color::brighter);
camera.setFilters(Color::darker, Color::brighter);
디폴트 메서드 들여다보기
- 서브 타입은 슈퍼 타입으로부터 자동으로 디폴트 메서드를 넘겨받는다.
- 디폴트 메서드를 사용하는 인터페이스에서 서브타입에서 구현된 디폴트 메서드는 슈퍼타입에 있는 것보다 우선한다.
- 추상 선언을 포함하여 클래스에 있는 구현은 모든 인터페이스의 디폴트보다 우선한다.
- 두 개 이상의 디폴트 메서드의 구현이 충돌하는 경우 혹은 두 개의 인터페이스에서 디폴트 추상 충돌(default-abstract conflict)이 발생하는 경우, 상속하는 클래스가 명확하게 해야한다.
Java8에서 컬렉션을 어떻게 사용하는지 알아보기 전에 Java에서 Collection은 무엇인지부터 알아보자.
다음은 Java Collection Framework의 기본 상속 구조이다.
Java 프로그래밍을 하다보면 크게 3가지 객체타입을 마주하게 된다.
- List
Element 들의 순서가 있으며, 중복을 허용한다.
- Set
Element 들의 순서는 없고, 중복이 허용되지 않는다.
- Map
List와 Set이 집합적 개념이라면, Map은 검색적 개념이 가미된 Interface이다.
key와 value로 구성되며, key를 통해 value에 접근할 수 있다.
주로 사용되는 HashMap의 경우 Java 버전에 따라 동장 원리가 조금씩 차이가 있다.
(상세하게 알고 싶다면 아래 참고 사이트를 확인해라)
Java8에서는 Collection을 iteration하고, transform(변형)하고, Element를 추출하거나, 그 Element들을 연결하는 여러가지 방법을 제공한다. 하나씩 살펴보자.
예를 들어, 아래와 같이 friends라는 친구들의 이름을 Element로 가지고 있는 List가 있다고 하자
private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott"); |
친구들의 이름을 출력해야 한다면, 아마도 다들 아래와 같이 코딩을 하게 될 것이다.
public class Iteration { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { for (String name : friends) { System.out.println(name); } } } |
java 8에서 제공하는 forEach와 Consummer<T> 인터페이스를 이용하면 아래와 같이 된다.
public class Iteration { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { friends.forEach(new Consumer<String>() { public void accept(final String name) { System.out.println(name); } }); } } |
※ Consummer<T> 인터페이스는 T 타입의 객체를 소비하는 인터페이스이다. accept() 라는 하나의 method를 가지고 있다.
여기에 Lambda 표현식을 추가해 보자.
public class Iteration { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { friends.forEach(name -> System.out.println(name)); friends.forEach(System.out::println);
} } |
위 코드들은 모두 동일한 결과가 나온다.
조금 흥미로은 점은 마지막 코드 예제인데.
System.out::println 으로 method reference라고 한다. 추후 Steam API를 공부하게 되면 자연스럽게 접할 기회가 많으니, 여기서는 이런 방법도 있다 정도로 하고 넘어가면 좋을 것 같다.
2. Transform(변형)
친구들의 이름을 모두 대문자로 변형하는 코드를 작성해보자.
public class Transform { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { final List<String> upperCaseNames = new ArrayList<String>(); for (String name : friends) {
upperCaseNames.add(name.toUpperCase()); } System.out.println(upperCaseNames);
} } |
뭔가 장황한 느낌이다. 이 코드를 개선해보자.
- foreach() 적용 및 Lambda 적용
- Stream API 적용
- Method Reference 적용
public class Transform { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { final List<String> upperCaseNames = new ArrayList<String>(); friends.forEach(name -> upperCaseNames.add(name.toUpperCase()));
System.out.println(upperCaseNames);
} } |
public class Transform { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { friends.stream() .map(name -> name.toUpperCase()) .forEach(name -> System.out.print(name + " ")); } } |
public class Transform { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { friends.stream() .map(String::toUpperCase) .forEach(System.out::println); } } |
Method Reference 는 언제 사용하는 것일까?
람다 표현식을 사용할 때 파라미터를 전달하지 않는 경우라면 사용할 수 있다.
친구들 이름 중에 N으로 시작하는 친구를 찾아보자.
public class PickElements { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { final List<String> startsWithN = new ArrayList<String>(); for (String name : friends) { if (name.startsWith("N")) { startsWithN.add(name); } } System.out.println(startsWithN);
} } |
아마도 대부분의 개발자들이 위처럼 코드를 작성할지도 모른다. 간단한 기능임에도 다소 장황하게 보인다.
filter() 함수를 통해 코드를 좀 더 직관적이고 간단하게 작성할 수 있다.
public class PickElements { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { final List<String> startsWithN = friends.stream() .filter(name -> name.startsWith("N")) .collect(Collectors.toList()); System.out.println(startsWithN); } } |
좀 더 직관적이고 간단하게 코딩할 수 있다. 하지만 약간의 센스있는 개발자라면 개선할 점이 있다는 부분을 발견할 수 있을 것이다.
만약 친구리스트가 아니라 에디터리스트 중에서도 N으로 시작하는 에디터를 출력한다고 해보자.
public class PickElements { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott"); private final static List<String> editors = Arrays.asList("Brian", "Jackie", "John", "Mike");
public static void main(String[] args) { final List<String> friendsStartsWithN = friends.stream() .filter(name -> name.startsWith("N")) .collect(Collectors.toList()); System.out.println(friendsStartsWithN);
final List<String> editorsStartsWithN = editors.stream() .filter(name -> name.startsWith("N")) .collect(Collectors.toList()); System.out.println(editorsStartsWithN); } } |
중복코드가 발생했다. 이런 경우 아래 코드가 중복 코드이다.
name -> name.startsWith("N")
filter() 함수는 java.util.function.Predicate 함수형 인터페이스를 받는다. 이를 이용하여 일부 중복을 제거할 수 있다.
public class PickElements { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott"); private final static List<String> editors = Arrays.asList("Brian", "Jackie", "John", "Mike");
private final static Predicate<String> startsWithN = name -> name.startsWith("N");
public static void main(String[] args) { final List<String> friendsStartsWithN = friends.stream() .filter(startsWithN) .collect(Collectors.toList()); System.out.println(friendsStartsWithN);
final List<String> editorsStartsWithN = editors.stream() .filter(startsWithN) .collect(Collectors.toList()); System.out.println(editorsStartsWithN); } } |
아직 더 개선할 점이 많지만 여기서는 Predicate<T> 함수형 인터페이스를 어떻게 활용할 수 있는지에 대해서만 언급하고 넘어가기로 한다.
다른 경우를 생각해보자. N으로 시작하는 친구목록과 B로 시작하는 친구 목록을 각각 출력해보자.
public class PickElements { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
private final static Predicate<String> startsWithN = name -> name.startsWith("N"); private final static Predicate<String> startsWithB = name -> name.startsWith("B");
public static void main(String[] args) { final List<String> friendsStartsWithN = friends.stream() .filter(startsWithN) .collect(Collectors.toList()); System.out.println(friendsStartsWithN);
final List<String> friendsStartsWithB = friends.stream() .filter(startsWithB) .collect(Collectors.toList()); System.out.println(friendsStartsWithB); } } |
여기서 보면 Predicate<T> 형 변수가 N과 B 문자열 차이만 있을 뿐 중복이다.
중복을 제거할 방법이 있을까?
public class PickElements { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { final List<String> friendsStartsWithN = friends.stream() .filter(checkIfStartWith("N")) .collect(Collectors.toList()); System.out.println(friendsStartsWithN);
final List<String> friendsStartsWithB = friends.stream() .filter(checkIfStartWith("B")) .collect(Collectors.toList()); System.out.println(friendsStartsWithB); }
public static Predicate<String> checkIfStartWith(final String letter) { return name -> name.startsWith(letter); } } |
public static Predicate<String> checkIfStartWith(final String letter) { return name -> name.startsWith(letter); } |
public class PickElements { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
private final static Function<String, Predicate<String>> startsWithLetter = letter -> { return name -> name.startsWith(letter); }; public static void main(String[] args) { final List<String> friendsStartsWithN = friends.stream()
.filter(startsWithLetter.apply("N")) .collect(Collectors.toList()); System.out.println(friendsStartsWithN);
final List<String> friendsStartsWithB = friends.stream() .filter(startsWithLetter.apply("B")) .collect(Collectors.toList()); System.out.println(friendsStartsWithB); } } |
Predicate<String> 을 생성하면 된다.
가만히 보면 checkIfStartWith 함수의 파라미터인 letter가 람다표현식 내에서 사용되었다.
letter 변수는 참다 표현식 범위에 있지 않음에도 이 코드는 정상적으로 수행된다.
이를 Lexical Scope 라고 한다.
Lexical Scope란? 사용한 하나의 컨텍스트에서 제공한 값을 캐시해 두었다가 나중에 다른 컨텍스트에서 사용할 수 있는 기술이다.
이 때 그 값은 변경이 불가능하며, final 로 선언되거나 effectively final 과 같은 의미를 가지는 변수이다. 만약 값을 변경하려고 한다면 컴파일 오류가 발생한다.
Function<T, R> 함수형 인터페이스도 활용할 수 있다.
Generic 표현에서도 알 수 있듯이, T 타입의 파라미터를 받아서 R 타입을 반환한다. 위 예제에서는 letter를 받아서 Predicate<String> 을 반환하고 있다.
위 예제에서 코드를 더 간단하게 작성이 가능한데,
private final static Function<String, Predicate<String>> startsWithLetter = letter -> { return name -> name.startsWith(letter); }; |
private final static Function<String, Predicate<String>> startsWithLetter = letter -> name -> name.startsWith(letter); |
Predicate<T> 와 Function<T, R> 두 개의 함수형 인터페이스에 대해서도 함께 알아보았다. 나중에 상당히 많이 활용되는 인터페이스이니 꼭 알아두길 권장한다.
보통 Collection 에서 여러 Element 를 찾는 것 보다 하나의 Element 를 찾는 것이 간단하다는 것은 당연하다.
그러나 습관적으로 코딩하다 보면 복잡도(Complexity) 가 비효율적으로 높은 경우가 있는데. 이에 대해서 알아보고 Lambda를 통해 해결해보자.
친구들 이름에서 특정 알파벳으로 시작하는 친구의 이름을 찾아보자.
public class PickAnElement { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { pickName(friends, "B"); pickName(friends, "A"); }
public static void pickName(final List<String> names, final String startingLetter) { String foundName = null; for (String name : names) {
if (name.startsWith(startingLetter)) { foundName = name; break; } } System.out.println(String.format("A name starting with %s", startingLetter));
if (foundName != null) {
System.out.println(foundName); } else { System.out.println("No name found"); } } } |
foundName 변수를 최초 초기화시 null 로 선언했다. 이는 잠재적인 문제가 될 수 있다.
또한 외부 데이터를 통해 루프를 수행하고 엘리먼트를 찾으면 루프를 빠져나온다. 이 또한 잠재적인 문제가 될 수 있다.
Lambda 와 Optional<T> 인터페이스를 통해 코드를 좀 더 간결하고 안정적으로 개선해보자.
public class PickAnElement { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { pickName(friends, "B"); pickName(friends, "A"); }
public static void pickName(final List<String> names, final String startingLetter) { final Optional<String> foundName = names.stream() .filter(name -> name.startsWith(startingLetter)) .findFirst(); System.out.println(String.format("A name starting with %s : %s",
startingLetter, foundName.orElse("No name found"))); } } |
Optional<T> 클래스에 대해서 알아보자.
이는 null 값으로 인해 NullPointException 이 발생하는 것을 막아준다.
몇가지 지원하는 함수가 있는데.
- isPresent() - 객체가 존재하는지?
- get() - 현재 값을 가져온다.
- orElse() - 값이 없는 경우 별도 처리를 할 수 있다.(위 코드 참조)
4. Collection Reduce
먼저 간단하게 전체 글자수를 세어보자.
public class PickALongest { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { System.out.println("Total number of characters in all names : " + friends.stream() .mapToInt(name -> name.length()) .sum()); } } |
map 오퍼레이션(mapToInt, mapToDouble) 을 통해서 값을 변형하고, reduce 오퍼레이션(sum, max, min, sorted, average) 로 결과의 길이를 reduce 시킨다.
그럼 친구들 중에 가장 긴 이름을 가진 친구 이름을 찾아 출력해보자.
public class PickALongest { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { final Optional<String> aLongName = friends.stream() .reduce((name1, name2) -> name1.length() >= name2.length() ? name1 : name2); aLongName.ifPresent(name ->
System.out.println(String.format("A longest name : %s", name)) ); } } |
reduce() 메서드에 전달되는 Lambda 표현식은 두 개의 파라메터를 갖는다. 길이를 비교하여 하나만 return 한다.
이는 reduce() 메서드가 전달되는 Lambda 표현식의 의도를 전혀 알지 못하며, 전달되는 Lambda 표현식과는 분리되어 있다.
이를 전략 패턴(Strategy Pattern) 이라고 한다.
여기서 Lambda 표현식은 BinaryOperator 라는 함수형 인터페이스로 apply() 메서드를 따른다.
만약 특정 이름보다 긴 이름을 찾고자 한다면 어떻게 해야할까?
public class PickALongest { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { final String steveOverLonger = friends.stream() .reduce("Steve", (name1, name2) -> name1.length() >= name2.length() ? name1 : name2); System.out.println(String.format("A name over Steve's length : %s", steveOverLonger));
} } |
이 경우 Optional 을 return 하지 않는다. 그 이유는 만약 reduce() 한 결과가 비어 있다고 하더라도 기본값인 “Steve” 가 return 될 것이기 때문이다.
5. Element Join
친구들의 이름을 ,(쉼표) 를 구분자로 하여 출력을 해보자.
public class PrintList { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { for (String name : friends) { System.out.print(name + ", "); } System.out.println(); } } |
public class PrintList { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { for (int i=0; i < friends.size() - 1; i++) { System.out.print(friends.get(i) + ", "); } if (friends.size() > 0) {
System.out.println(friends.get(friends.size() - 1)); } } } |
마지막 ,(쉼표)를 제거하기 위해 몇 줄의 코드가 더 필요했다.
String 클래스에 join() 메서드가 추가되면서 더 이상 이런 지저분한 코드는 필요 없게 되었다.
public class PrintList { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { System.out.println(String.join(",", friends)); } } |
이젠 Stream API 를 통해서 Element 들을 join 해보자.
public class PrintList { private final static List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(String[] args) { System.out.println( friends.stream() .map(String::toUpperCase) .collect(Collectors.joining(", ")) ); } } |
여기까지 Java Collection 에 대해서 알아보았다.
Lambda 표현식과 Stream API 를 활용하면 코드를 좀 더 직관적이고 간단하게 구현할 수 있다.
Chapter 3. String, Comparator 그리고 filter
1. String Iteration
문자열(String) 의 각 문자들을 출력해보자.
public class IteraeString { public static void main(String[] args) { final String str = "w00t"; str.chars().forEach(System.out::println);
} } |
2장 컬렉션 사용에서 공부한 것 처럼 method reference와 함께 사용할 수 있다.
여기서 chars() 는 CharSequence 인터페이스로부터 파생한 String 클래스의 새로운 메소드이다.
위 코드를 실행해보면 아래와 같이 숫자로 출력되는데, 이는 chars() 가 기본적으로 IntStream을 반환하기 때문이다.
그렇다면 숫자가 아닌 문자를 출력해주려면 어떻게 해야할까?
IntStream 내의 Element 들을 변형해 주어야 한다. Stream API 의 mapToObj() 를 사용해보자.
public class IteraeString { public static void main(String[] args) { final String str = "w00t"; str.chars()
.mapToObj(ch -> Character.valueOf((char)ch)) .forEach(System.out::println); } } |
w 0 0 t |
코드를 수행해보면 문자로 출력됨을 확인할 수 있다.
샘플코드에서 보면 문자열 데이터가 “w00t” 이다. 여기서 가운데 “00” 는 숫자이다.
만약 이렇게 숫자만 출력하고 싶으면 어떻게 할까? filter() 를 활용할 수 있다.
public class IteraeString { public static void main(String[] args) { final String str = "w00t"; str.chars()
.filter(Character::isDigit) .mapToObj(ch -> Character.valueOf((char)ch)) .forEach(System.out::println); } } |
Character 클래스 내에 숫자 여부를 판단하는 isDigit() 이라는 static method를 사용하면 된다.
아주 간단하게 해결 되었다.
2. Comparator 인터페이스
Comparator 인터페이스는 Java에서 자주 사용되는 인터페이스 중 하나이다.
검색부터 정렬, 역정렬 등 많이 사용되는데, Java8에서는 함수형 인터페이스로 변경되었다.
Comparator를 이용해서 정렬을 한 번 해보자.
예를 들어 사람들 정보가 있을 때, 나이순으로 정렬하는 문제를 해결해 보자.
먼저 Person 클래스를 하나 생성한다.
public class Person { private final String name; private final int age;
public Person(final String name, final int age) { this.name = name; this.age = age; }
public String getName() { return name; }
public int getAge() { return age; }
public int ageDifference(final Person other) { return age - other.age; }
public String toString() { return String.format("%s - %s", name, age); } } |
나이 오름차순으로 정렬 해보자.
public class Compare { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { List<Person> ascendingAge = people.stream() .sorted((person1, person2) -> person1.ageDifference(person2)) .collect(Collectors.toList()); printPeople("Sorted in ascending order by age : ", ascendingAge); }
private static void printPeople(String message, List<Person> people) { System.out.println(message); people.forEach(System.out::println); } } |
Sorted in ascending order by age : John - 20 Sara - 21 Jane - 21 Greg - 35 |
코드를 수행해보면 나이 오름차순으로 잘 정렬됨을 확인할 수 있다.
.sorted((person1, person2) -> person1.ageDifference(person2)) |
.sorted(Person::ageDifference) |
위처럼 sorted() 함수 파라메터인 람다 표현식을 개선할 수 도 있다.
이제 내림차순으로도 정렬해보자.
.sorted((person1, person2) -> person2.ageDifference(person1)) |
sorted() 람다표현식을 위 처럼 변경해주면 된다.
Comparator<T> 인터페이스를 적용하여 재사용 가능한 Comparator 로 만들어 둘 수 있다.
public class Compare { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
private static final Comparator<Person> ascending = (person1, person2) -> person1.ageDifference(person2); private static final Comparator<Person> descending = ascending.reversed();
public static void main(String[] args) { printPeople("Sorted in ascending order by age : ", people.stream() .sorted(ascending) .collect(Collectors.toList())); printPeople("Sorted in descending order by age : ", people.stream() .sorted(descending) .collect(Collectors.toList())); }
private static void printPeople(String message, List<Person> people) { System.out.println(message); people.forEach(System.out::println); } } |
이제 정렬문제는 해결되었다.
그럼 가장 젊은 사람 또는 가장 나이가 많은 사람을 선택해보자.
public class Compare { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { people.stream() .min(Person::ageDifference) .ifPresent(youngest -> System.out.println(youngest)); } } |
public class Compare { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { people.stream() .max(Person::ageDifference) .ifPresent(eldest -> System.out.println(eldest)); } } |
min(), max() 함수는 Optional 타입을 반환하는데, 이는 List가 비어 있는 경우 가장 어린 사람 또는 가장 나이가 많은 사람이 존재하지 않을 수 있기 때문이다.
3. 여러가지 비교 연산
지금까지 나이 순으로 정렬, 가장 어린 사람 또는 나이가 많은 사람을 선택해 보았다.
이제는 이름 순으로 정렬을 해보자.
단순히 ageDifference라는 함수처럼 name 변수에 대해서도 추가하여 method reference 를 적용해 볼 수 있지만, Comparator 인터페이스에 새로 추가된 method를 이용하여 간단하게 구현해보자.
public class Compare { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { people.stream() .sorted((person1, person2) -> person1.getName().compareTo(person2.getName())) .forEach(System.out::println);; } } |
public class Compare { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { final Function<Person, String> byName = person -> person.getName(); final Function<Person, Integer> byAge = person -> person.getAge(); printPeople("이름순으로 정렬", people.stream()
.sorted(Comparator.comparing(byName)) .collect(Collectors.toList())); printPeople("나이순으로 정렬", people.stream() .sorted(Comparator.comparing(byAge)) .collect(Collectors.toList())); }
private static void printPeople(String message, List<Person> people) { System.out.println(message); people.forEach(System.out::println); } } |
Comparator.comparing() 를 이용하여 좀 더 간편하게 구현할 수 있다.
4. collect() 와 Collectors 클래스
collect() 함수에 대해서 좀 더 알아보도록 하자.
먼저 people 변수 내에서 20살 이상의 사람들만 선택하는 코드를 생성해보면 아래와 같이 작성할 수 있을 것이다.
public class OlderThan20 { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { List<Person> olderThan20 = new ArrayList<Person>(); people.stream() .filter(person -> person.getAge() > 20) .forEach(person -> olderThan20.add(person)); System.out.println("People older than 20 : " + olderThan20);
} } |
코드를 수행해보면 결과도 잘 나온다. 원하는 결과를 얻었지만 여기에는 몇 가지 문제점이 있다.
1. 타켓 컬렉션(olderThan20) 에 Element를 추가하는 Operation이 너무 low level이다.
자바8에서는 서술적 프로그래밍을 권장하는데, 이는 너무 명령적이다.
2. Thread unsafe
병렬처리시 문제가 될 수 있다.
즉, 2번 항목인 병렬처리시 Thread-unsafe 하다는 것인데. collect() 를 이용하면 손쉽게 해결이 가능하다.
collect() 는 Element 에 대한 Stream 을 가지며, 결과 컨테이너에 각 Stream 들을 모은다.
* 결과 컨테이너를 만드는 방법(ArrayList::new)
* 하나의 Element 를 결과 컨테이너에 추가하는 방법(ArrayList::add)
* 하나의 결과 컨테이너를 다른 것과 합치는 방법(ArrayList::addAll)
public class OlderThan20 { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { List<Person> olderThan20 = people.stream() .filter(person -> person.getAge() > 20) .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); System.out.println("People older than 20 : " + olderThan20);
} } |
이로써 서술적인 프로그래밍이 되었고, Thread-safe를 보장하게 되었다.
참고로 ArrayList 가 Thread-unsafe 하더라도 문제가 되지 않는다.
Collectors 클래스를 활용하여 좀 더 간단하게 프로그래밍을 할 수도 있다.
public class OlderThan20 { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { List<Person> olderThan20 = people.stream() .filter(person -> person.getAge() > 20) .collect(Collectors.toList()); System.out.println("People older than 20 : " + olderThan20);
} } |
toList() 외에도 다양한 편리한 메소드를 제공한다.
* toSet()
* toMap()
* joining()
* mapping()
* collectingAndThen()
* minBy()
* maxBy()
* groupingBy()
Collectors 에서 제공하는 groupingBy룰 한 번 사용 해보자.
사람들 정보를 나이별로 grouping 해보자.
public class OlderThan20 { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { Map<Integer, List<String>> nameOfPeopleByAge = people.stream() .collect(Collectors.groupingBy( Person::getAge, Collectors.mapping(Person::getName, Collectors.toList())) ); System.out.println("People grouped by age : " + nameOfPeopleByAge);
} } |
여기서 groupingBy() 는 두 개의 파라미터를 가진다.
1. 그룹을 만드는 기준
여기서는 나이이다.
2. mapping() 의 결과인 Collector
나이로 그룹핑한 결과에 대한 Stream값
좀 더 복잡한 예제를 한 번 수행해보자.
이름의 첫 글자를 기준으로 그룹핑하고 각 그룹별로 나이가 가장 많은 사람의 정보를 가지도록 해보자.
public class OlderThan20 { private static final List<Person> people = Arrays.asList( new Person("John", 20), new Person("Sara", 21), new Person("Jane", 21), new Person("Greg", 35) );
public static void main(String[] args) { Comparator<Person> byAge = Comparator.comparing(Person::getAge); Map<Character, Optional<Person>> oldestPersonOfEachLetter =
people.stream() .collect(Collectors.groupingBy( person -> person.getName().charAt(0), Collectors.reducing(BinaryOperator.maxBy(byAge))) ); System.out.println("People grouped by age : " + oldestPersonOfEachLetter);
} } |
1. 기준 : 각 사람 이름의 첫 글자
person -> person.getName().charAt(0)
2. 그룹핑
2-1. reducing 적용
그룹핑된 각 Stream에 reducing() 연산 적용
2-2 BinaryOperator 적용
reducing() 연산 수행시 비교 기준 정의
이렇게 Collectors 클래스를 잘 활용하면 복잡한 Operation 도 서술식으로 프로그래밍 할 수 있을 것으로 생각이된다.
5. 디렉토리 모든 파일 리스트
File 클래스의 list() 나 listFiles() 를 이용하면 특정 디렉토리 하위의 파일 리스트를 가져올 수 있다.
이를 함수형 인터페이스를 도입해서 파일 리스트를 iteration 해보자.
이를 위해서 CloseableStream 을 이용하여 구현해야 한다.
public class ListFiles { public static void main(String[] args) throws Exception { Files.list(Paths.get(".")).forEach(System.out::println); } } |
./.classpath ./.git ./.gitignore ./.project ./.settings ./build ./README.md ./src |
코드를 수행해보면 현재 디렉토리(\.) 하위에 있는 모든 파일 리스트들이 출력된다.
Files 클래스의 list() 메소드를 사용하여 CloseableStream 을 얻어오게 된다.
Files.list() 를 한 번 살펴보자.
public static Stream<Path> list(Path dir) throws IOException { DirectoryStream<Path> ds = Files.newDirectoryStream(dir); try { final Iterator<Path> delegate = ds.iterator();
// Re-wrap DirectoryIteratorException to UncheckedIOException Iterator<Path> it = new Iterator<Path>() { @Override public boolean hasNext() { try { return delegate.hasNext(); } catch (DirectoryIteratorException e) { throw new UncheckedIOException(e.getCause()); } } @Override public Path next() { try { return delegate.next(); } catch (DirectoryIteratorException e) { throw new UncheckedIOException(e.getCause()); } } };
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.DISTINCT), false) .onClose(asUncheckedRunnable(ds)); } catch (Error|RuntimeException e) { try { ds.close(); } catch (IOException ex) { try { e.addSuppressed(ex); } catch (Throwable ignore) {} } throw e; } } |
내부 상세 구현 내용은 몰라도 무방하다.
Path 타입의 찾고자 하는 디렉토리를 파라메터로 받아서 Path Stream 을 반환한다.
Path Stream 을 통해서 iteration을 하게되는 것이다.
만약 디렉토리만 출력하고 싶다면,
filter() 를 이용하여 Stream Element 중 일부를 선택할 수 있다.
예를 들어서 특정 디렉토리 하위 파일 중 .java 파일만 가져오도록 해보자.
public class ListSelectFiles { public static void main(String[] args) { final String[] files = new File("./src/main/com/silverboyf/java8/sample/ch3") .list( new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".java"); } } ); Arrays.asList(files)
.stream() .forEach(System.out::println); } } |
Compare.java IteraeString.java ListFiles.java ListSelectFiles.java OlderThan20.java Person.java |
FilenameFilter 인터페이스 구현을 통해서 특정 파일을 선택할 수 있다.
하지만 뭔가 코드가 복잡해보이고, 서술적이지 않다는 느낌을 받는다.
자바8의 DirectoryStream 을 이용하면 좀 더 간단하고 서술적으로 구현이 가능하다.
public class ListSelectFiles { public static void main(String[] args) throws Exception { Files.newDirectoryStream( Paths.get("./src/main/com/silverboyf/java8/sample/ch3"), path -> path.toString().endsWith(".java") ) .forEach(System.out::println); } } |
./src/main/com/silverboyf/java8/sample/ch3/Compare.java ./src/main/com/silverboyf/java8/sample/ch3/IteraeString.java ./src/main/com/silverboyf/java8/sample/ch3/ListFiles.java ./src/main/com/silverboyf/java8/sample/ch3/ListSelectFiles.java ./src/main/com/silverboyf/java8/sample/ch3/OlderThan20.java ./src/main/com/silverboyf/java8/sample/ch3/Person.java |
6. flatMap 과 서브 디렉토리
주어진 디렉토리 하위에 존재하는 파일 또는 서브 디렉토리 리스트를 출력하는 방법에 대해서 알아 보았다.
만약 서브 디렉토리 하위의 파일에 대해서도 함께 출력하고 싶다면 어떻게 해야할까?
public class ListSubDirs { public static void main(String[] args) { listTheHardWay(); System.out.println(); listTheBetterWay(); }
public static void listTheHardWay() { List<File> files = new ArrayList<File>(); File[] filesInCurrentDir = new File(".").listFiles();
for (File file : filesInCurrentDir) { File[] filesInSubDir = file.listFiles(); if (filesInSubDir != null) { files.addAll(Arrays.asList(filesInSubDir)); } else { files.add(file); } } System.out.println(files);
System.out.println("Count : " + files.size()); }
public static void listTheBetterWay() { List<File> files = Stream.of(new File(".").listFiles()) .flatMap(file -> file.listFiles() == null ? Stream.of(file) : Stream.of(file.listFiles())) .collect(Collectors.toList()); System.out.println(files);
System.out.println("Count : " + files.size()); } } |
flatMap() 을 사용하면 다중 Stream 을 하나의 flat Stream 에 할당할 수 있다.
7. 파일 변경 살펴보기
특정 디렉토리에 파일이 생성되거나 수정 또는 삭제됐을 때 알고 싶은 경우를 구현해보자.
아래 샘플 코드는 대부분 자바7부터 제공하던 기능이며, 자바8에서 달라진 부분은 내부 iterator의 편의성이다.
final Path path = Paths.get("."); final WatchService watchService = path.getFileSystem().newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
System.out.println("Report and file changed within new 1 minute..."); |
final WatchKey watchKey = watchService.poll(1, TimeUnit.MINUTES);
if (watchKey != null) { watchKey.pollEvents() .stream() .forEach(event -> System.out.println(event.context())); } |
Report and file changed within new 1 minute... |
먼저 원하는 디렉토리에 위 코드처럼 WatchService 를 하나 등록하자.
만약 해당 Path에 수정이 발생하면 WatchKey 를 통해 알려준다.
WatchKey를 통해 event 를 받아서 어떤 파일이 변경이 되었는지 확인할 수 있다.
혹시 Stream 형식의 데이터를 다루는데, 조금 어렵게 느껴질지도 모르겠다. 하지만 걱정하지 말자.
익숙해지면 이런 방식의 프로그래밍이 얼마나 편하고 강력한지 깨닫게 될 것이다.
출처: http://tomining.tistory.com/52 [마이너의 일상]
Chapter 4. 람다 표현식을 이용한 설계
앞에서 다른 내용들만 봐도 람다를 이용하여 코드를 좀 더 간결하고 읽기 쉽게 작성할 수 있다는 것을 알았다.
이 장에서는 람다 표현식을 사용하여 여러가지 패턴이나 설계들을 구현하는 방법에 대해서 알아보자.
1. 전략패턴(Strategy Pattern)
클래스를 생성하는 이유 중 하나는 코드를 재사용하기 위함이다. 좋은 의도이긴 하나 항상 그런 것은 아니다.
자산의 총합을 구하는 예제를 통해 클래스 내의 코드 재사용성을 향상시켜 보자.
먼저 Asset 클래스를 생성한다.
public class Asset { public enum AssetType { BOND, STOCK }; private final AssetType type; private final int value;
public Asset(final AssetType type, final int value) { this.type = type; this.value = value; }
public AssetType getAssetType() { return type; }
public int getValue() { return value; } } |
간단하게 자산종류와 자산가치 라는 두 변수만 가지는 Asset 클래스를 생성했다.
이젠 자산의 총합을 개산해보자.
public class AssetUtil { public static void main(String[] args) { final List<Asset> assets = Arrays.asList( new Asset(AssetType.BOND, 1000), new Asset(AssetType.BOND, 2000), new Asset(AssetType.STOCK, 3000), new Asset(AssetType.STOCK, 4000) );
System.out.println("Total of all assets : " + totalAssetValues(assets)); }
public static int totalAssetValues(final List<Asset> assets) { return assets.stream() .mapToInt(Asset::getValue) .sum(); } } |
Total of all assets : 10000 |
간단하게 Stream을 생성하여 합계를 계산하였다. 결과도 잘 나오는 것을 확인할 수 있다.
결과에서도 보듯이 동작에는 아무런 문제가 없다. 다만 여기에는 구조상 3가지 문제가 얽혀있다.
* iteration 을 어떻게하고
* 어떤 값들에 대한 합계를 계산하며
* 그 합계를 어떻게 구하는가
이렇게 뒤엉켜 있는 로직은 재사용성이 떨어진다.
만약 Bond(채권) 자산에 대해서만 합계를 구한다고 하면...
public class AssetUtil { public static void main(String[] args) { final List<Asset> assets = Arrays.asList( new Asset(AssetType.BOND, 1000), new Asset(AssetType.BOND, 2000), new Asset(AssetType.STOCK, 3000), new Asset(AssetType.STOCK, 4000) );
System.out.println("Total of bonds : " + totalBondsValues(assets)); }
public static int totalBondsValues(final List<Asset> assets) { return assets.stream() .filter(asset -> asset.getAssetType() == AssetType.BOND) .mapToInt(Asset::getValue) .sum(); } } |
Total of bonds : 3000 |
Stock(주식) 자산의 총합을 구한다면 또 다시 totalBondsValues() 를 복사하여 filter에 있는 부분을 변경해줘야 한다.
public class AssetUtil { public static void main(String[] args) { final List<Asset> assets = Arrays.asList( new Asset(AssetType.BOND, 1000), new Asset(AssetType.BOND, 2000), new Asset(AssetType.STOCK, 3000), new Asset(AssetType.STOCK, 4000) );
System.out.println("Total of stocks : " + totalBondsValues(assets)); }
public static int totalBondsValues(final List<Asset> assets) { return assets.stream() .filter(asset -> asset.getAssetType() == AssetType.STOCK) .mapToInt(Asset::getValue) .sum(); } } |
Total of stocks : 7000 |
총 자산, 채권, 주식 각각의 자산의 총합을 계산하는데, 유사한 코드가 너무 많이 생산되었다.
이를 해결하기 위해 전략 패턴이라는 것을 적용해보자.
(디자인 패턴의 종류로 자세한 내용은 디자인 패턴 책을 찾아보길 바란다.)
public class AssetUtil { public static void main(String[] args) { final List<Asset> assets = Arrays.asList( new Asset(AssetType.BOND, 1000), new Asset(AssetType.BOND, 2000), new Asset(AssetType.STOCK, 3000), new Asset(AssetType.STOCK, 4000) ); System.out.println("Total of bonds : " + totalAssetValues(assets, asset -> asset.getAssetType() == AssetType.BOND)); System.out.println("Total of stocks : " + totalAssetValues(assets, asset -> asset.getAssetType() == AssetType.STOCK)); System.out.println("Total of assets : " + totalAssetValues(assets, asset -> true)); }
public static int totalAssetValues(final List<Asset> assets, final Predicate<Asset> assetSelector) { return assets.stream() .filter(assetSelector) .mapToInt(Asset::getValue) .sum(); } } |
totalAssetValues() 구조를 살펴보면,
filter() 를 통해서 합계 계산 대상을 정하고
mapToInt() 를 통해서 합계 대상 value를 추출하고
sum() 을 통해서 합산을 하게 됩니다.
즉, 합계 대상 객체 추출 -> 합계 value 추출 -> 합계 수행 순서로 진행됩니다.
2. Delegate
람다 표현식과 전략패턴을 활용하여 문제를 분리했다.
문제 분리를 클래스 레벨에서도 분리가 가능한데, 이 때 재사용 측면에서 보면 Delegation(위임)을 이용하면 더 좋다.
Delegation(위임) 을 람다표현식을 통해 구현해보자.
CalculateNAV 클래스를 사용하여 웹 서비스에서 받은 데이터를 사용하여 재무 연산을 해보자.
public class CalculateNAV { private Function<String, BigDecimal> priceFinder;
public CalculateNAV(final Function<String, BigDecimal> priceFinder) { this.priceFinder = priceFinder; }
public BigDecimal computeStockWorth(final String ticker, final int shares) { return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares)); } } |
computeStockWorth() 메서드에서 priceFinder에 대한 주식시세표(ticker)를 요청하고 주식의 시세에 따라 기치를 결정한다.
이제 priceFinder가 필요하다.
예제에서는 Yahoo 주식시세표를 가져와 계산해 볼 예정이나, 먼저 Stub을 통해 TC를 만들어보자.
public class CalculateNAVTest { @Test public void computeStockWorth() { final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01")); BigDecimal expected = new BigDecimal("6010.00");
Assert.assertEquals(0, calculateNAV.computeStockWorth("GOOG", 1000).compareTo(expected), 0.001 ); } } |
Stub 을 작성해서 주식시세표에 따른 가치를 계산해 보았다.
실제로 Yahoo주식시세표를 가져와서 계산해보자.
public class YahooFinance { public static BigDecimal getPrice(final String ticker) { try { final URL url = new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);
final BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream())); final String data = reader.lines().skip(1).findFirst().get(); final String[] dataItems = data.split(","); return new BigDecimal(dataItems[dataItems.length - 1]); } catch (Exception e) { throw new RuntimeException(e); } } } |
@Test public void computeYahooStockWorth() { final CalculateNAV calculateNAV = new CalculateNAV(YahooFinance::getPrice); System.out.println(String.format("100 shares of Google worth : $%.2f", calculateNAV.computeStockWorth("GOOG", 100))); } |
100 shares of Google worth : $54161.00 |
현재 구글의 한주 당 주식가치는 541.61$ 인 듯하다. 100주의 가치가 54161$로 계산되었다.
3. Decorate
앞에서 설명한 Delegation 은 뛰어난 기능이다. 여기에 Delegation 을 chain 으로 묶어서 Behavior 를 추가할 수 있다면 더 유용하게 사용할 수 있을 것이다.
다음 예제에서 람다표현식을 사용하여 Delegate 를 조합해보자.
(Decorator 패턴을 어떻게 사용하는지 알아보기 위한 예제이다.)
Camera 라는 클래스를 만들고 capture() 를 통해 Color 를 처리하는 필터를 만들어보자.
public class Camera { private Function<Color, Color> filter;
public Color capture(final Color inputColor) { final Color processedColor = filter.apply(inputColor);
return processedColor; } } |
위와 같이 Camera 는 Function 타입의 filter 변수를 갖고 있고, 이는 Color 를 받아서 처리하고 결과로 Color 를 반환한다. 이는 하나의 필터만 사용하지만 다양한 필터를 적용할 수 있도록 수정해보자.
유연성을 얻기 위해 Java8에서 등장하는 Default Method 를 사용할 것이다.
쉽게 이야기하면 Interface 에 구현부가 있는 것인데, 추상 Method 에 덧붙여서 인터페이스는 구현 부분이 있는 Method 를 갖게되며, Default 로 마크된다. 이 Method 는 해당 인터페이스를 구현한 클래스에 자동으로 추가된다.
public class Camera { private Function<Color, Color> filter;
public Color capture(final Color inputColor) { final Color processedColor = filter.apply(inputColor);
return processedColor; }
public void setFilters(final Function<Color, Color> ... filters) { filter = Stream.of(filters) .reduce((filter, next) -> filter.compose(next)) .orElse(color -> color); }
public Camera() { setFilters(); } } |
setFilters() 를 이용하여 각 필터를 iteration 하고 compose 를 통해서 chain 으로 연결한다.
만약 filter 가 없다면 Optional Empty 를 반환한다.
4. Default Method
5. 예외 처리
출처: http://tomining.tistory.com/111 [마이너의 일상]