在上一篇文章中,我们大致介绍了函数式编程的情况,包括函数式编程的缘起,意义,还有高阶函数,柯里化等特点,今天我们将从lambda风格,流操作,高阶函数,并行化等几个方面,一起简单聊聊函数式编程在Java8中的实现。
Lambda风格
在上篇介绍函数式编程的文章中,我们大致了解了lambda表达式的来源以及定义,那么lambda表达式在java8中是怎么使用的呢?
lambda表达式允许函数作为一个方法的参数,在代码的实现上显得更加简洁紧凑。
lambda表达式的格式语法如下
(parameters) -> expression
(parameters) -> {statement;}
在lambda表达式出现之前,如果想将代码作为数据传递,一般会采用匿名类的方式。比如在swing编程中,如果想在button上添加点击操作对应的事件监听器
botton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event){
// Print something
}
}
);
如果使用lambda表达式,可以一行代码解决
botton.addActionListener(event -> System.out.println("something"));
使用匿名类需要显式声明参数类型ActionEvent,但是会使用lambda表达式无需指定类型,这是由于javac根据程序的上下文在后台推断出event的类型
流操作
流操作中包括惰性求值和及早求值两种,一般来说,对于只描述stream,不产生新集合的方法我们称为惰性求值;而对于最终会从stream产生值的方法,我们称为及早求值。常见的惰性求值有map,reduce,filter等,及早求值包括count,sum,collect等。
下面,我们采用渐进式重构的方式来举例介绍常用的几个流操作。
- 命令式编程
public Set<String> findLongTracks(List<Albulm>){
Set<String> trackNames = new HashSet<>();
for(Album album : albums) {
for (Track track : album.getTrackList()) { if (track.getLength() > 60) {
String name = track.getName();
trackNames.add(name);
}
}
}
return trackNames;
}
- stream化,并使用foreach进行重构
public Set<String> findLongTracks(List<Albulm>){
Set<String> trackNames = new HashSet<>();
albums.stream().
forEach(album -> {
album.getTracks().forEach(track -> {
if (track.getLength() > 60) {
String name = track.getName();
trackNames.add(name);
}
);
}
);
return trackNames;
}
- 使用map,filter进行重构
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.forEach(album -> {
album.getTracks()
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.forEach(name -> trackNames.add(name));
});
return trackNames;
}
- 使用map,collect继续进行重构
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.map(album -> {
album.getTracks()
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.forEach(name -> trackNames.add(name));
}).collect(Collecotor.toSet());
return trackNames;
}
- 使用flatMap进行重构
public Set<String> findLongTracks(List<Album> albums) {
return albums.stream()
.flatMap(album -> album.getTracks())
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.collect(toSet());
}
高阶函数
高阶函数是使用其他函数作为参数,并返回其他函数作为结果。它可以帮助我们将创建和处理区分开来,通过这种方式,新的业务逻辑处理对象就可以轻易的添加进来,而没有必要同对象创建逻辑相耦合。
public interface Block<T> {
void apply(T t);
}
public void doWithContact(String fileName, Block<Contact> block) {
try {
String contacStr = FileUtils.readFileToString(new File(fileName));
Contact.apply(contact);
block.apply(contact);
}
catch (IOException e) {
System.out.println("cloudn't load contact file: " + e.getMessage());
}
catch (ParseException e) {
System.out.println("cloudn't parse contact file: " + e.getMessage());
}
}
//usage
doWithContact("custerX.vcf", c -> ContactDao.save(c))
并行化
并发(Concurrent)与并行(Parallel)
这是两个比较容易混淆的概念。二者都可以表示两个或者多个任务一起执行,但是侧重点不同;并发侧重于多个任务交替执行,而多个任务之间有可能是串行的;并发是逻辑上的同时发生,而并行是物理上的同时发生。严格意义上,并行的多个任务是真实同时执行,而对于并发,则是交替的,一会执行任务A,一会执行任务B,系统会不断地在两者之间进行切换,
并行化操作流
并行化操作流只需要改变一个方法的调用,如果已经存在y一个stream对象,调用它的parallel方法就能让其立即拥有并行操作的能力。
public Set<String> findLongTracks(List<Album> albums) {
return albums.parallelStream()
.flatMap(album -> album.getTracks())
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.collect(toSet());
}
parallelStream的底层与Fork/Join类似,默认的并发程度是可用CPU数-1
在使用并行化编程时,需要注意:
- 线程安全
- 合理参数配置:比如线程池的大小,等待队列大小,并行度大小以及等待超时时间等等,都需要根据自己的业务不断的调优防止出现队列不够用或者超时时间不合理等