Java 8 函数式编程例子
Java8 提供了函数式编程的能力,这里介绍过 java8 的高阶函数,这篇文章通过一个实际的例子,展示函数式编程的思维。
需求是这样的,有一个 log 文件,文件格式类似下面
2017/01/14 10:39:05 user "Peter" login
2017/01/14 11:49:05 user "John" login
2017/01/14 12:39:05 user "Peter" login
2017/01/14 12:39:12 user "Peter" login
2017/01/14 12:39:16 user "Emma" login
2017/01/14 12:39:17 user "Tom" login
2017/01/14 12:41:18 user "Emma" login
2017/01/14 12:42:25 user "Tom" login
2017/01/14 12:44:45 user "Peter" login
2017/01/14 12:45:55 user "Peter" login
输入是这样一个文件,希望输出一个统计结果,按照登录次数排序,如果登录次数相同,按照登录时间倒序排序。
类似
ExLogAnalyzerTest.LogEntry(timestamp=2017-01-14T04:45:55Z, user=Peter, count=5)
ExLogAnalyzerTest.LogEntry(timestamp=2017-01-14T04:42:25Z, user=Tom, count=2)
ExLogAnalyzerTest.LogEntry(timestamp=2017-01-14T04:41:18Z, user=Emma, count=2)
ExLogAnalyzerTest.LogEntry(timestamp=2017-01-14T03:49:05Z, user=John, count=1)
首先,我们需要从输入文件得到一个 stream ,每一个元素是输入文件中的一行文字。
Stream<String> stream = Files.lines(Paths.get(fileName))
然后,每一行转换成为一个 Pojo 。这里使用了 lombok ,类似
@Value
@Builder
static class LogEntry {
Instant timestamp;
String user;
Integer count;
}
解析一行日志,找到我们感兴趣的登录日志。
private static final Pattern p = Pattern.compile(
"^([0-9]{4}/[0-9]{2}/[0-9]{2} *[0-9]{2}:[0-9]{2}:[0-9]{2}) *.*\"([a-zA-Z]*)\".*login");
private static final SimpleDateFormat simpleDateFormat= new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
public static LogEntry parse(String line) {
final Matcher matcher = p.matcher(line);
if (matcher.find()) {
try {
final String dateString = matcher.group(1);
return LogEntry.builder()
.timestamp(simpleDateFormat.parse(dateString).toInstant())
.user(matcher.group(2))
.count(1) // 登录次数默认为一次
.build();
} catch (ParseException e) {
e.printStackTrace();
return null;
}
} else {
return null;
}
}
}
这样我们就可以得到一组 log entry 了。
stream.map(LogEntry::parse)
log 文件中,可能有其他的 log,我们无法解析为 LogEntry ,那么就忽略这些记录
.filter(e -> e != null)
然后,我们要对分组,根据统计登录次数。
.collect(Collectors.groupingBy(LogEntry::getUser))
分组之后,返回的是一个 hashMap ,Key 是 String, 用户名, Value 是 List ,一组日志。list.size 就是登录次数。那么我们转换为登录次数和最后一次登录时间。
final Function<Map.Entry<String, List<LogEntry>>, List<LogEntry>> getValue
= Map.Entry::getValue;
final Function<Map.Entry<String, List<LogEntry>>, LogEntry> mergeLoginEntry
= getValue.andThen(list -> LogEntry.builder()
.count(loginCount.apply(list))
.timestamp(getLastLoginTime.apply(list))
.user(list.get(0).getUser())
.build());
.entrySet().stream().map(mergeLoginEntry)
对应的登录次数和最后一次登录时间的函数如下
final Function<List, Instant> getLastLoginTime =
list ->
Collections.max((List<LogEntry>) list, Comparator.comparing(LogEntry::getTimestamp))
.getTimestamp();
final Function<List, Integer> loginCount = List::size;
然后按照登录次数排序,倒序,如果登录次数一样,就应该按照最后一次登录时间排序。
.sorted(Comparator.comparing(LogEntry::getCount)
.thenComparing(LogEntry::getTimestamp)
最后,我们输出结果
.collect(Collectors.toList())
.forEach(System.out::println);
结论,我们可以看到高阶函数很好的表达能力,高阶函数,就是指一个函数的参数或者返回值,还是一个函数。这个例子大量使用了高阶函数。
- Comparator.comparing
- Comparator::thenComparing
- Collections::max
- Stream::map
- Stream::filter
- Function::andThen
- Stream::sorted
- Collectors::groupingBy
- List::forEach
延伸思考, Collector::groupingBy 需要创建一个全新的 list ,但是我们只需要统计 list 的长度和 list 中最大的登录时间,这里如果进一步优化,可以让 groupingBy 直接返回一个新的登录记录。用来减少空间复杂度,降低内存使用量。同时也可以减少后面排序的操作,降低了时间复杂度。
这里我们可以考虑使用 Collector::reduce ,函数式编程里面的万能妖刀。这个函数是很底层的函数,如果有其他函数能完成类似的功能,尽量不要使用这个函数。
在这个例子中,使用 Collector::reduce 可以让核心代码可以更加简洁了。
final BinaryOperator<LogEntry> reducer = (acc, a) -> LogEntry.builder()
.count(acc.getCount() + 1)
.timestamp(Collections.max(Arrays.asList(a.getTimestamp(), acc.getTimestamp())))
.user(a.getUser())
.build();
stream.map(LogEntry::parse)
.filter(e -> e != null)
.collect(Collectors.groupingBy(LogEntry::getUser, Collectors.reducing(reducer)))
.entrySet().stream()
.map(Map.Entry::getValue)
.filter(Optional::isPresent)
.map(Optional::get)
.sorted(Comparator.comparing(LogEntry::getCount)
.reversed()
.thenComparing(LogEntry::getTimestamp)
.reversed())
.collect(Collectors.toList())
.forEach(System.out::println);
附录:完整的代码
package org.wcy123.fp.imp;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Test;
import lombok.Builder;
import lombok.Value;
public class ExLogAnalyzerTest {
@Test
public void main() throws Exception {
String fileName = "log.txt";
try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
final Function<List, Instant> getLastLoginTime = list ->
Collections.max((List<LogEntry>) list, Comparator.comparing(LogEntry::getTimestamp)).getTimestamp();
final Function<List, Integer> loginCount = List::size;
final Function<Map.Entry<String, List<LogEntry>>, List<LogEntry>> getValue = Map.Entry::getValue;
final Function<Map.Entry<String, List<LogEntry>>, LogEntry> mergeLoginEntry = getValue.andThen(list -> LogEntry.builder()
.count(loginCount.apply(list))
.timestamp(getLastLoginTime.apply(list))
.user(list.get(0).getUser())
.build());
stream.map(LogEntry::parse)
.filter(e -> e != null)
.collect(Collectors.groupingBy(LogEntry::getUser))
.entrySet().stream()
.map(mergeLoginEntry)
.sorted(Comparator.comparing(LogEntry::getCount)
.reversed()
.thenComparing(LogEntry::getTimestamp)
.reversed())
.collect(Collectors.toList())
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
@Value
@Builder
static class LogEntry {
private static final Pattern p = Pattern.compile(
"^([0-9]{4}/[0-9]{2}/[0-9]{2} *[0-9]{2}:[0-9]{2}:[0-9]{2}) *.*\"([a-zA-Z]*)\".*login");
private static final SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Instant timestamp;
String user;
Integer count;
public static LogEntry parse(String line) {
final Matcher matcher = p.matcher(line);
if (matcher.find()) {
try {
final String dateString = matcher.group(1);
return LogEntry.builder()
.timestamp(simpleDateFormat.parse(dateString).toInstant())
.user(matcher.group(2))
.count(1) // 登陆次数默认为一次
.build();
} catch (ParseException e) {
e.printStackTrace();
return null;
}
} else {
return null;
}
}
}
}