java8 functional style
정리
함수형 스타일 functional style 로 프로그래밍 할 수 있다. 이것을 위해 java8에 '함수형 인터페이스 + 람다식 + stream' 가 도입되었다
고차함수 high-ordered function 를 지원하여 함수도 이제 저장, 주고, 받는게 가능해 졌다. 즉, 저장( 함수형 인터페이스 타입의 변수에 람다식을 저장), 주고(함수-람다식을 리턴), 받는게(메소드의 매개변수로 함수형 인터페이스 타입) 가능해 졌다
고차함수를 사용하는 API의 등장으로 코딩 스타일에 진화가 이루어 졌다. 진화의 의미는 간결하고 서술적이다.
다음 예제 코드를 통해 심도 있게 이해를 해보자.
- 컬렉션 collection
- 비교 comparator
- 설계 design
- 자원 resource
- 지연 lazy
- 재귀 recursion
- 조합 composing
- 함수형 스타일 functional style
여기에서는 컬렉션을 사용하는 예제를 통해 함수형 스타일 코드를 이해하도록 한다.
컬렉션 collection
- 이터레이션, 새로운 컬렉션으로 변형, 엘리먼트 추출, 엘리먼트 연결
이터레이션 iteration
- 기존 방법과 진화된 방법으로 알아보자
public static final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static void main(final String[] args) {
// 외부 Iterator를 사용
// 명령형 프로그래밍 스타일
// 1. OLD 방식
for (int i = 0; i < friends.size(); i++) {
System.out.println(friends.get(i));
}
// 2. 다른 방식 그러나 별로~
// Iterator 인터페이스를 사용, hasNext(), next() 메소드 사용
for (String name : friends) {
System.out.println(name);
}
// for loop는 순차적인 방식 > 병렬화 어려움
// for 구문에 컬렉션을 넘기는 방식 > non-polymorphic
// Tell, don't ask 설계 원칙 위배 > 이터레이션을 직접 다룸
// 내부 Iterator 사용
// 함수형 프로그래밍 스타일
// forEach() 메소드 제공, Consumer type을 파라미터로 사용
// ( accept() 메소드 구현에서 자원을 '소비'한다는 의미 )
// 1. 익명 이너 클래스 형태로 사용(Anonymous Inner Class)
friends.forEach(new Consumer<String>() {
public void accept(final String name) {
System.out.println(name);
}
});
// for 구문 대신에 내부 Iterator 인 forEach()로 변경
// Iteration을 어떻게 해야 하는지? 대신에 해야할 작업으로 집중하여 코딩
// ###. 람다 표현식을 사용
// 코드 양이 줄었음
// forEach
friends.forEach((final String name) -> System.out.println(name));
// 파라미터 축약#1
friends.forEach((name) -> System.out.println(name));
// 파라미터 축약#2 - 1개의 파라미터
friends.forEach(name -> System.out.println(name));
// ### 메소드 레퍼런스를 사용
friends.forEach(System.out::println);
//익명 이너 클래스 > 람다 표현식 > 메소드 레퍼런스
리스트 변형 map()
public static final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
// 불변 리스트의 엘리먼트를 모두 대문자로 변형하여 새로운 컬렉션으로 생성
// 1. 기존방식
// 빈 리스트를 만들어서 이터레이션하여 하나하나 add 하는 방식
{
final List<String> uppercaseNames = new ArrayList<String>();
for (String name : friends) {
uppercaseNames.add(name.toUpperCase());
}
System.out.println(uppercaseNames);
}
// 2. forEach(내부 이터레이션) 및 람다식을 사용한 방식
{
final List<String> uppercaseNames = new ArrayList<String>();
friends.forEach(name -> uppercaseNames.add(name.toUpperCase()));
System.out.println(uppercaseNames);
}
// 3. stream API를 활용한 방식
// stream()은 모든 컬렉션에서 사용 가능
// stream은 이터레이션과 유사하며 다양한 기능의 함수를 기본적으로 제공한다.
// 예를 들어 map은 입력 순서에 따라 출력으로 맵핑, forEach와 전혀 다름, 람다식을 파라미터로 받음, 람다 표현식의
// 실행 결과를 취합하여 결과 컬렉션으로 리턴,
// stream IF의 map을 사용한 간결한 방식, forEach는 출력을 담당
friends.stream().map(name -> name.toUpperCase()).forEach(name -> System.out.print(name + " "));
// 입력과 결과 : 엘리먼트 수는 통일, 타입은 달라질수 있음,
friends.stream().map(name -> name.length()).forEach(count -> System.out.print(count + " "));
// 메소드 레퍼런스를 사용한 간결한 표현, 람다식이 간단하면 대체가 가능
friends.stream().map(String::toUpperCase).forEach(name -> System.out.println(name));
// 메소드 레퍼런스는 언제 사용하면 좋을까?
// 람다식을 사용할 때, 파라미터를 전달하지 않는 경우에만 사용하는 것이 좋다
}
엘리먼트 찾기
컬렉션에서 문자 N으로 시작하는 엘리먼트 찾기 > filter()
// 1. 기존방식 { final List<String> startsWithN = new ArrayList<String>(); for (String name : friends) { if (name.startsWith("N")) { startsWithN.add(name); } } System.out.println(String.format("Found %d names", startsWithN.size())); } // 2. filter() 메소드의 사용 // boolean 결과를 리턴하는 람다식이 필요, true면 결과 컬렉션에 추가, false면 다음으로 넘어감 // filter가 리턴하는 결과 스트림을 리스트로 변경 collect // filter는 조건-람다식 에 맞는 subset을 리턴 final List<String> startsWithN = friends.stream().filter(name -> name.startsWith("N")) .collect(Collectors.toList()); System.out.println(String.format("Found %d names", startsWithN.size()));
람다식의 재사용
- 람다식의 중복을 제거하기 위한 방법 > 람다식을 변수에 저장하여 재사용
//각 컬렉션에 있는 N으로 시작하는 이름 수를 찾기
public static final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static final List<String> editors = Arrays.asList("Brian", "Jackie", "John", "Mike");
public static final List<String> comrades = Arrays.asList("Kate", "Ken", "Nick", "Paula", "Zach");
// 람다식의 중복, 수정의 어려움
{
final long countFriendsStartN = friends.stream().filter(name -> name.startsWith("N")).count();
final long countEditorsStartN = editors.stream().filter(name -> name.startsWith("N")).count();
final long countComradesStartN = comrades.stream().filter(name -> name.startsWith("N")).count();
}
// 람다식을 변수에 저장하여 재사용, 객체 저장과 유사한 개녕
{
// filter()는 함수형 인터페이스 중 java.util.function.Predicate을 받음
// 람다식은 Predicate IF의 public abstract인 test()의 구현으로 합성됨 by 컴파일러
// filter에 들어가는 람다식은 Predicate 타입의 명시적인 레퍼런스에 저장하여 재사용이 가능하여 중복을 제거. DRY
// 그러나 찜찜함, 중복 smell이 솔솔
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();
}
}
- 중복성 제거의 또 다른 방법 > 렉시컬 스코프 lexical scoping 와 클로저 closures
// 'N'이나 'B'로 이름을 선택
// 문자가 다른 이유로 2개의 Predicae 사용 > 중복
{
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();
}
// checkIfStartsWith 메소드를 사용한 방법
{
// 검색하려는 문자를 파라미터로 넘기면 즉!시! 람다식을 리턴한다
final long countFriendsStartN = friends.stream().filter(checkIfStartsWith("N")).count();
final long countFriendsStartB = friends.stream().filter(checkIfStartsWith("B")).count();
}
// 람다식을 리턴하는 메소드를 추가 > 고차함수
// 리턴 타입은 함수형 인터페이스이며 이경우는 Predicate를 사용해야 함
// 컬렉션의 각 엘리먼트를 파라미터로 받아 boolean 결과를 리턴하는 함수형 인터페이스인 Predicate가 적합
// letter 변수는 이 리턴되는 람다식(익명함수)의 범위scope에 없음 > 람다식의 범위에서 letter를 찾음 > 렉시컬 스코프
public static Predicate<String> checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}
// 람다식을 익명 이너 클래스로 표현
public static Predicate<String> checkIfStartsWithEasy(final String letter) {
return new Predicate<String>() {
public boolean test(String name) {
return name.startsWith(letter); // letter 변수는 이 익명이너클래스에 없음 그러나
// java8에서 컴파일 에러는 발생하지 않느다
}
};
}
- 함수형 인터페이스인 Function을 활용
// Function<T,R>은 타입T를 파라미터로 받고 타입R을 결과로 리턴하는 함수
// 타입T를 파라미터로 받고 boolean을 결과로 리턴하는 Predicate보다 일반적
// 입력값을 다른값으로 변경하는 경우 사용. map()
// 결과 타입으로 Predicate인 람다식을 리턴하는 고차함수
// 람다식을 리턴하는 람다식.. 흐 꼬인다 꼬여
{
final Function<String, Predicate<String>> startsWithLetter = (String letter) -> {
Predicate<String> checkStarts = (String name) -> name.startsWith(letter);
return checkStarts;
};
final long countFriendsStartN = friends.stream().filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB = friends.stream().filter(startsWithLetter.apply("B")).count();
System.out.println(countFriendsStartN);
System.out.println(countFriendsStartB);
}
// 좀 더 간결하게 표현
final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);
// 더더더 간결하게 표현
final Function<String, Predicate<String>> startsWithLetter = letter -> name -> name.startsWith(letter);
엘리먼트 선택
- 엘리먼트 중 하나 선택, 컬렉션에서 하나의 엘리먼트를 선택하기 Optional, orElse, ifPresent
- 단, 필요한 만큼의 기능만 수행 > 다양한 조작이 필요한 경우는 명령형
// 기존 방식, null 체크 필요
public static void pickName(final List<String> names, final String startingLetter) {
String foundName = null; // 문제1. 변수 생성 및 null 초기화 > 가변성, null pointer exception 우려
for (String name : names) { //문제2. 외부 이터레이터의 사용
if (name.startsWith(startingLetter)) {
foundName = name;
break;
}
}
if (foundName != null) { //문제3. null 점검 코드 작성
System.out.println(foundName);
} else {
System.out.println("No name found");
}
}
// 람다식과 API를 활용한 방식
// findFirst() 스트림에서 첫번째 값을 추출, Optioal 객체 리턴
// Optional은 결과가 없는 경우에 대한 처리 null 처리 > null pointer exception 방어
final Optional<String> foundName = names.stream().filter(name -> name.startsWith(startingLetter)).findFirst();
//orElse()는 대신하는 값을 지정
System.out.println(String.format("A name starting with %s: %s", startingLetter, foundName.orElse("No name found")));
// orElse()처럼 대신하는 값을 넣기 보다, 값이 존재할때만 처리
// ifPresent()는 객체 존재를 확인하여 있으면 get()으로 얻어옴
final Optional<String> foundName = friends.stream().filter(name -> name.startsWith("N")).findFirst();
foundName.ifPresent(name -> System.out.println("Hello " + name));
}
엘리먼트 합치기(리듀스 reduce)
- 컬렉션을 연산하여 하나의 값으로 추출 > 맵리듀스(MapReduce) 패턴
// 1. 엘리먼트 문자의 수를 전부 합산하는 예제
//map 오퍼레이션 종류 중 mapToInt 를 사용하고 sum()을 통해 리듀스(reduce) > MapReduce pattern
//sum이외에 max(), main(), sorted(), average() 존재
{
System.out.println("Total number of characters in all names: "
+ friends.stream().mapToInt(name -> name.length()).sum());
}
// 2. 컬렉션에서 가장 긴 이름 중 첫번째 찾기
//최대길이 파악하고 첫번째 찾기 > 2개의 list 필요, 비효율 > reduce로 한방에 처리
//reduce는 컬렉션을 이터레이션하고 람다식으로 연산 결과를 얻어냄
//reduce와 람다식은 분리됨 > 전략패턴의 경량화
//람다식 파라미터가 2개, BinaryOperator 함수형 인터페이스의 apply() 를 따름
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)));
//기본값 Steve 설정
final String steveOrLonger = friends.stream().reduce( "Steve", (name1, name2) -> name1.length() >= name2.length() ? name1 : name2);
엘리먼트 조인 join
- 엘리먼트를 연결하는 마지막 단계 Reduce operation 종단 오퍼레이션
//컬렉션을 ', '를 사용하여 하나의 스트링으로 합치기
public static void main(final String[] args) {
// 1. 기존방식, 마지막 엘리먼트에도 ,가 붙는다
for (String name : friends) {
System.out.print(name + ", ");
}
System.out.println();
// 2. 마지막 엘리먼트에 뒤에 ','가 없도록 함, 이터레이션을 조작 및 마지막 엘레먼트 별도 처리 > 복잡
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));
// 3. String의 join() method 사용
System.out.println(String.join(", ", friends));
// 4. MapReduce 패턴을 사용한 대문자로 변환 후 합치기
// 엘리먼트를 연결하는 마지막 단계는 Reduce operation
// collect는 값을 타깃(java.util.stream.Collector)으로 값을 모으는 reduce의 일종
// * collection(List, Map, Set)과 혼동하지 말 것.
System.out.println(friends.stream().map(String::toUpperCase).collect(joining(", ")));
엘리먼트 모으기 collect 와 Collectors
- 엘리먼트의 스트림을 결과 컨테이너(arraylist등)로 모음(collect의미)
final List<Person> people = Arrays.asList(new Person("John", 20), new Person("Sara", 21),new Person("Jane", 21), new Person("Greg", 35));
{
// 새로운 ArrayList를 수동 생성 후 저장
// low-level code, 명령적(imperative), 병렬문제(스레드 세이프티, 가변성)
List<Person> olderThan20 = new ArrayList<>();
people.stream().filter(person -> person.getAge() > 20).forEach(person -> olderThan20.add(person));
System.out.println("People older than 20: " + olderThan20);
}
{
// 새로운 ArrayList를 collect 메소드에서 자동 생성
// collect : 엘리먼트의 스트림을 결과 컨테이너(arraylist등)로 모음(collect의미)
// collect() 컬렉션을 가변 컬렉션으로 변경하는 리듀스 오퍼레이션 + Collectors의 유틸과 조합하여 시너지
// 3개의 param, 결과컨테이너 생성, 컨테이너에 추가하는 방법, 컨테이너를 하나로 합치기(병렬인경우)
// 서술적declarative, 병렬화가 쉬움,
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);
}
{
// 더 간단한 Collectors.toList() 사용
// param은 supplier, accumulator, combiner 오퍼레이션의 인터페이스 역할 > .toList() 는 인테페이스 구현
// toSet, toMap, joining, mapping, collectingAndThen, minBy, maxBy, groupingBy등 여러 오퍼레이션 지원
System.out.println("//" + "START:COLLECT_TO_LIST_OUTPUT");
List<Person> olderThan20 = people.stream().filter(person -> person.getAge() > 20)
.collect(Collectors.toList());
System.out.println("People older than 20: " + olderThan20);
System.out.println("//" + "END:COLLECT_TO_LIST_OUTPUT");
}
디렉토리 리스팅
- 현재경로 파일 리스팅
//list()로 파라미터 패스의 스트림을 얻어온다.
Files.list(Paths.get(".")).forEach(System.out::println);
- 상대경로 디렉토리 리스팅
//filter 적용, isDirectory는 정적static 메소드 레퍼런스 표현 Files.list(Paths.get(".\\src\\fpij")).filter(Files::isDirectory).forEach(System.out::println);
- hidden 파일 리스팅
//함수형 인터페이스 FileFilter 사용 final File[] files = new File(".").listFiles(file -> file.isHidden()); Arrays.stream(files).forEach(System.out::println);
특정 파일(.java) 선택 리스팅
//list는 함수형 인터페이스를 파라미터로 받음 //여기서는 익명 이너 클래스로 구현 final String[] files = new File(".\\src\\fpij\\compare").list(new java.io.FilenameFilter() { public boolean accept(final File dir, final String name) { return name.endsWith(".java"); } }); //결과 출력 //명령식 코드 for (int i = 0; i <files.length; i++){ System.out.format("[%2d] = '%s'%n", i, files[i]); } //서술식 코드 Arrays.stream(files).forEach(System.out::println); //2nd 파라미터는 filter Files.newDirectoryStream(Paths.get("."), path -> path.toString().endsWith(".java")) .forEach(System.out::println);
- 와치서비스 : 스레드 생성, 이벤트 polling 스트림 처리
public class WatchFileChange {
public static void main(String[] args) throws Exception {
// 람다식 스레드 객체 생성 및 시작
new Thread(() -> watchFileChange()).start();
// //익명 이너 클래스로 작성해 봄
// new Thread(new Runnable() {
// public void run() {
// watchFileChange();
// }
// }).start();
// 임의 파일 생성
final File file = new File("sample.txt");
file.createNewFile();
// 기다렸다가
Thread.sleep(5000);
// 파일 정보 수정
file.setLastModified(System.currentTimeMillis());
}
public static void watchFileChange() {
try {
// 현재디렉토리 Path에 와치서비스 등록, 수정이벤트 대상
final Path path = Paths.get(".");
final WatchService watchService = path.getFileSystem().newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
System.out.println("Report any file changed within next 1 minute...");
System.out.println("here1!");
final WatchKey watchKey = watchService.poll(1, TimeUnit.MINUTES);
System.out.println("here2!");
if (watchKey != null) {
System.out.println("watchkey!");
// 와치서비스는 이벤트가 생기면 이 이벤트를 키에 넣음
//키를 얻어와서poll 키에 담긴 이벤트를 스트림으로 추출하여 표시
watchKey.pollEvents().stream().forEach(event -> System.out.println(event.context()));
}
} catch (InterruptedException | IOException ex) {
System.out.println(ex);
}
}
}
자원 resource
자원 종료 close 처리의 캡슐화
- 리소스 클린업 close 처리
- 외부 리소스의 GC는 프로그래머의 책임
- db, file, socket, native resource
- EAV 패턴 execute around method
기존 클린업 보장
- try-catch-finally
- ARM automatic resource management
- 이 방식은 프로그래머가 이를 사용법을 미리 알아야 함 AutoCloseable, try-with-resource
//ARM- Automatic resource management in Java 7 부터 제공
public class FileWriterARM implements AutoCloseable {
private final FileWriter writer;
public FileWriterARM(final String fileName) throws IOException {
writer = new FileWriter(fileName);
}
public void writeStuff(final String message) throws IOException {
writer.write(message);
}
public void close() throws IOException {
System.out.println("close called automatically...");
writer.close();
}
// ...
public static void main(final String[] args) throws IOException {
try (final FileWriterARM writerARM = new FileWriterARM("peekaboo.txt")) {
writerARM.writeStuff("peek-a-boo");
System.out.println("done with the resource...");
}
}
//블럭을 빠져 나오면 close가 자동 호출됨
- EAM execute around method
- 컴파일러의 도움, private 사용 > 팩토리 역할 필요
public class FileWriterEAM {
private final FileWriter writer;
//private, 외부에서 생성할 수 없다.
private FileWriterEAM(final String fileName) throws IOException {
writer = new FileWriter(fileName);
}
//private, 외부에서 호출할 수 없다.
private void close() throws IOException {
System.out.println("close called automatically...");
writer.close();
}
public void writeStuff(final String message) throws IOException {
writer.write(message);
}
// ...
// 외부에서 사용자가 사용하는 인터페이스
// UseInstance은 함수형 인터페이스(FunctionalInterface)이다. 예외 처리를 반영하기 위해 생성함.
// 파라미터에 함수형인터페이스가 정의되면 람다식을 입력할 수 있다.
public static void use(final String fileName, final UseInstance<FileWriterEAM, IOException> block)
throws IOException {
final FileWriterEAM writerEAM = new FileWriterEAM(fileName); // 리소스 자원 생성
try {
block.accept(writerEAM); // void accept(T instance) throws X;
} finally {
writerEAM.close(); // 자원해제
}
}
public static void main(final String[] args) throws IOException {
FileWriterEAM.use("eam.txt", writerEAM -> writerEAM.writeStuff("sweet"));
//코드가 길어지면 별도의 메소드로 옮겨서 이에 대한 메소드 레퍼런스로 사용하는 방식으로 간결하게 하자
FileWriterEAM.use("eam2.txt", writerEAM -> {
writerEAM.writeStuff("how");
writerEAM.writeStuff("sweet");
});
}
지연 lazy
파라미터 이밸류에이션, 중간/종단 오퍼레이션
- 지연 초기화
- 무거운 객체를 필요할 시점에 생성하여 사용
- 싱글톤 패턴의 스레드 세이프를 고려한 코드
public class HolderThreadSafe {
//무거운 객체
private Heavy heavy;
//생성자 - 객체 생성 시점에 로깅
public HolderThreadSafe() {
System.out.println("Holder created");
}
// 동시에 여러개 스레드가 호출했을 때, 쫑나지 않게 한다. 동시 실행 등...
// synchronized는 비용이 발생한다. 메모리 장벽 등 > getHeavy()를 호출하 때마다...
public synchronized Heavy getHeavy() {
if (heavy == null) {
heavy = new Heavy();
}
return heavy;
}
public static void main(final String[] args) {
final HolderThreadSafe holder = new HolderThreadSafe();
System.out.println("deferring heavy creation...");
System.out.println(holder.getHeavy());
System.out.println(holder.getHeavy());
}
}
- synchronized 반복 호출을 제거한 코드
- 가상 프록시 패턴 virtual proxy pattern
public class Holder {
// Supplier는 추상 메소드 get을 가지고 있다. get()을 실행해야 함수가 실행된다.
// 아래 코드는 함수를 실행하는 것이 아니고 함수를 정의하는 코드일 뿐.
private Supplier<Heavy> heavy = () -> createAndCacheHeavy();
// 참조 : anonymous function으로 작성
private Supplier<Heavy> heavy_anony = new Supplier<Heavy>() {
@Override
public Heavy get() {
return createAndCacheHeavy();
}
};
public Holder() {
System.out.println("Holder created");
}
public Heavy getHeavy() {
// 함수를 실행한다.
return heavy.get();
}
//
private synchronized Heavy createAndCacheHeavy() {
class HeavyFactory implements Supplier<Heavy> {
private final Heavy heavyInstance = new Heavy();
public Heavy get() {
return heavyInstance;
}
}
// 최초 호출 시 실행되어 heavy 레퍼런스를 HeavyFactory로 바꾼다.
if (!HeavyFactory.class.isInstance(heavy)) {
heavy = new HeavyFactory();
}
return heavy.get();
}
public static void main(final String[] args) {
final Holder holder = new Holder();
System.out.println("deferring heavy creation...");
System.out.println(holder.getHeavy());
System.out.println(holder.getHeavy());
}
- 스트림의 레이지
- 결과를 얻기 위해 필요한 순간이 될 때까지 실제 작업을 하지 않음
public class LazyStreams {
private static int length(final String name) {
System.out.println("getting length for " + name);
return name.length();
}
private static String toUpper(final String name) {
System.out.println("converting to uppercase: " + name);
return name.toUpperCase();
}
public static void main(final String[] args) {
List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", "Mike", "Susan", "George", "Robert",
"Julia", "Parker", "Benson");
//중간연산자인 filter() + map()의 람다식은 실행되지 않고 캐쉬됨
//종단연산자인 findFirst()가 호출되어서야 종단연산자 특성에 맞게 filter와 map이 수행됨 > lazy 방식
{
final String firstNameWith3Letters =
names.stream()
.filter(name -> length(name) == 3)
.map(name -> toUpper(name))
.findFirst()
.get();
System.out.println(firstNameWith3Letters);
}
{
//종단 연산자를 분리한 코드. 아래는 아무것도 실행되지 않는다.
Stream<String> namesWith3Letters =
names.stream().
filter(name -> length(name) == 3)
.map(name -> toUpper(name));
System.out.println("Stream created, filtered, mapped...");
System.out.println("ready to call findFirst...");
//종단 연산자가 실행이 되어서야 비로수 map과 filter가 수행된다.
final String firstNameWith3Letters = namesWith3Letters.findFirst().get();
System.out.println(firstNameWith3Letters);
}
}
}
- 무한 스트림 infinite stream
- 무한으로 증가하는 데이터 집합 생성
무한 컬렉션에서 유한 엘리먼트를 생성
소수 구하는 예제, 소수 : 2887.. java.lang.StackOverflowError 발생
public class PrimeFinder {
//소수인지 검증하는 메소드, IntStream 및 rangeClosed을 이용
public static boolean isPrime(final int number) {
return number > 1 && IntStream.rangeClosed(2, (int) Math.sqrt(number)).noneMatch(divisor -> number % divisor == 0);
}
}
public class NaivePrimes {
// 숫자 배열에 숫자 합치기
public static List<Integer> concat(final int number, final List<Integer> numbers) {
final List<Integer> values = new ArrayList<>();
values.add(number);
values.addAll(numbers);
return values;
}
// don't try this at the office
public static List<Integer> primes(final int number) {
if (isPrime(number)) {
System.out.println("소수 : " + number);
return concat(number, primes(number + 1));
} else {
return primes(number + 1);
}
}
public static void main(final String[] args) {
try {
primes(1);
} catch (StackOverflowError ex) {
System.out.println(ex);
}
}
}
- 무한 스트림 생성 Stream.iterate(p1, p2) + limit()
- 시드seed와 데이터 생성 함수UnaryOperator
- 지연 : iterate() 메소드는 마지막 메서드를 사용하기 전까지 엘리먼트 생성을 지연
public class Primes {
private static int primeAfter(final int number) {
if (isPrime(number + 1))
return number + 1;
else
return primeAfter(number + 1);
}
public static List<Integer> primes(final int fromNumber, final int count) {
//무한 순차 스트림을 생성. 1st 파라미터 : 초기값(시드) 2nd 파라미터 : UnaryOperator 함수형 인터페이스
return Stream.iterate(primeAfter(fromNumber - 1), Primes::primeAfter)
.limit(count)
.collect(Collectors.<Integer>toList());
}
public static void main(final String[] args) {
System.out.println("10 primes from 1: " + primes(1, 10));
System.out.println("5 primes from 100: " + primes(100, 5));
}
}
재귀 recursion
테일콜, 메모이제이션
조합 composing
함수조합, 함수체인
- 함수조합, 함수체인, 오퍼레이션 체인
- 맵필터리듀스 패턴 MapFilterReduce
쉬운 병렬화 multi core, 시간 소모가 크고 매우 큰 컬렉션 대상
예제 : 주식이름으로 가격을 가져와서 500이하 가격 주식 중 가장 비싼 주식 찾기
//함수형 스타일로 변환한 코드
//서술적, 불변성, 간결
//쉬운 병렬화
public static void findHighPriced(final Stream<String> symbols) {
//symbol collection > stockInfo stream > filter stream > reduce stockInfo
final StockInfo highPriced =
symbols
.map(StockUtil::getPrice)
.filter(StockUtil.isPriceLessThan(500))
.reduce(StockUtil::pickHigh).get();
System.out.println("High priced under $500 is " + highPriced);
}
public static void main(final String[] args) {
//1. 순차 코드, 순차 스트림을 리턴한다
findHighPriced(Tickers.symbols.stream());
//2. 병렬화 적용코드, 병렬 스트림을 리턴한다. 리턴 타입은 순차방식과 같다.
//병렬화 조건 혹은 고려사항
//불변성, 사이드이펙트, 레이스컨디션, 스레드세이프티
findHighPriced(Tickers.symbols.parallelStream());
}
기존 방식, 설명한 내용을 3단계로 코드화
// 명령형 스타일로 작성된 코드 public static void main(final String[] args) { final List<StockInfo> stocks = new ArrayList<>(); for (String symbol : Tickers.symbols) { // symbol 갯수만큼 StockInfo를 생성하여 add 한다. stocks.add(StockUtil.getPrice(symbol)); } final List<StockInfo> stocksPricedUnder500 = new ArrayList<>(); final Predicate<StockInfo> isPriceLessThan500 = StockUtil.isPriceLessThan(500); for (StockInfo stock : stocks) { //500보다 작은 가격 선택 if (isPriceLessThan500.test(stock)) stocksPricedUnder500.add(stock); } //가장 높은 가격을 찾음 StockInfo highPriced = new StockInfo("", BigDecimal.ZERO); for (StockInfo stock : stocksPricedUnder500) { highPriced = StockUtil.pickHigh(highPriced, stock); } System.out.println("High priced under $500 is " + highPriced); }
기존 방식 축약
StockInfo highPriced = new StockInfo("", BigDecimal.ZERO); final Predicate<StockInfo> isPriceLessThan500 = StockUtil.isPriceLessThan(500); for (String symbol : Tickers.symbols) { StockInfo stockInfo = StockUtil.getPrice(symbol); if (isPriceLessThan500.test(stockInfo)) highPriced = StockUtil.pickHigh(highPriced, stockInfo); } System.out.println("High priced under $500 is " + highPriced); }
종합
문법의 변화가 아니다. 함수형 스타일 프로그래밍은 개발 패러다임을 바꾸는 일 > 노오력!
- 명령형 + 객체지향 + 함수형 > 발란스! 새로운 형태의 설계, 코드 패턴 등장
- 스터디 + 아이디어 + 피드백 + 리뷰 + 세션 + 잡담
보다 서술적
- 한 줄 한 줄의 코드보다 목표를 코드로 설명
- 높은 추상화 레벨 abstraction level
- 가변 변수 사용을 줄여줌
List<Integer> prices = Arrays.asList(10, 20, 30, 40, 50, 60);
//명령적
{
int max = 0;
for (int price : prices) {
if (max < price)
max = price;
}
}
//서술적
final int max = prices.stream().reduce(0, Math::max);
System.out.println(max);
불변성
- 가변변수 > 문제의 씨앗
사이드이펙트 감소
- 항상 입출력이 동일, 일관성
- 참조투명성 referential transparency
- 컴파일러 최적화
문장보다는 표현
- 표현식expression 액션 수행 후 결과 리턴, 문장 statement 액션 수행 후 리턴이 없음
- 표현식은 두 개 이상의 표현식을 조합, 함수체인
고차함수로 설계
- 기존에는 객체를 주고 받았으나 이제 함수도 주고 받을 수 있다.
- 함수형 인터페이스를 파라미터화 < 익명 이너 클래스, 람다식, 메소드 레퍼런드도 인수로 넘길 수 있음
//익명 이너 클래스로 작성
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
JOptionPane.showMessageDialog(frame, "you clicked!");
}
});
//람다식으로 작성, 간결,
button.addActionListener(event ->
JOptionPane.showMessageDialog(frame, "you clicked!"));
성능
- 컴파일러 최적화
- invokedynamic 바이트코드 명령어 등장
- parallalStream()