이 글은 "자바 8 람다의 힘" 책을 정리한 글입니다.
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을 반환하기 때문이다.
119
48 48 116
|
그렇다면 숫자가 아닌 문자를 출력해주려면 어떻게 해야할까?
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 형식의 데이터를 다루는데, 조금 어렵게 느껴질지도 모르겠다. 하지만 걱정하지 말자.
익숙해지면 이런 방식의 프로그래밍이 얼마나 편하고 강력한지 깨닫게 될 것이다.
'Programing > Java' 카테고리의 다른 글
java.io.InvalidClassException (0) | 2015.07.15 |
---|---|
Serializable 과 transient (0) | 2015.06.29 |
Java의 System.out을 파일 출력으로 변경하는 방법 (0) | 2015.03.30 |
[자바8 람다의 힘] 2장 컬렉션의 사용 (0) | 2015.03.26 |
Java에서 정규표현식은 이렇게 사용하자 (0) | 2015.03.12 |