Java8函数式编程

Java8函数式编程

为什么要用Java8

Java8在并行处理大型集合上有很大优势。可以更好的利用多核处理器的优势。Java8可以用Lambda表达式很简便的写出复杂的处理集合的逻辑。

函数式编程

函数式编程是一种编程范式,我们常见的编程范式有命令式编程(Imperative programming),函数式编程,逻辑式编程,常见的面向对象编程是也是一种命令式编程。

命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的指令序列。而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。

函数式编程中的函数这个术语不是指计算机中的函数(实际上是Subroutine),而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。

Java是一门面向对象的语言,对函数式编程的支持并不全面,如果想深入理解学习函数式编程可以学习Scala、Clojure,这两种语言都是可以在jvm上运行的。

lambda表达式

Java8最大的改变就是加入了lambda表达式。什么是lambda表达式呢?

简单来说,编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。

比如有些函数你只会使用一次,但是你却给他定义一次,这个函数就成了污染函数,匿名函数就是用来干这个的。

举个栗子:

faculties.sort(new Comparator<Faculty>() {
            @Override
            public int compare(Faculty o1, Faculty o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });

为了给这个专业列表(faculties)排序,你专门写了一个匿名内部类,来实现比较大小的方法。这样看起来又臃肿又复杂,如果用lambda表达式就很简单了:

faculties.sort((f1, f2) -> f1.getName().compareTo(f2.getName()));

还可以用方法引用可以更简单:

faculties.sort(Comparator.comparing(Faculty::getName));

下面有各种不同的方法来写lambda表达式:

Runnable noArguments = () -> System.out.println("Hello World");	
ActionListener oneArgument = event -> System.out.println("button clicked");
Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println(" World");
};
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

lambda表达式可以接受一个或者多个参数,lambda表达式可以转换成一个接口不能转化成类,转化成接口要保证接口中只有一个可以复写的方法。不然会报错。

Streams

Stream可以让我们在更高级别的抽象上写集合的处理代码,stream有一系列的方法来让我们使用。

现在有一需求,需要筛选出来自伦敦的艺术家,通常我们会这么写:

int count = 0;
for (Artist artist : allArtists) {
	if (artist.isFrom("London")) { 
		count++;
	} 
}

用stream可以这么写:

long count = allArtists.stream()
						.filter(artist -> artist.isFrom("London"))
						.count();

这样看起来就清晰很多,而且效率也是差不多的,用stream不会循环两遍的,stream的方法分为eager和lazy两种,比如下面这段代码:

allArtists.stream()
      .filter(artist -> {
          System.out.println(artist.getName());
			 return artist.isFrom("London"); 
		});

它不会打印任何东西,filter就是一个lazy的方法,那怎么区分lazy和eager方法呢?如果返回的还是一个stream那么就是一个lazy的方法,如果返回的不是stream就是eager方法。例如上面的count()方法返回的是int,那么就会被立刻执行。

collect(toList())

List<String> collected = Stream.of("a", "b", "c")
                                   .collect(Collectors.toList());
    assertEquals(Arrays.asList("a", "b", "c"), collected);

collect(toList())把一个Stream转换成List。

map

List<String> collected = Stream.of("a", "b", "hello")
                               .map(string -> string.toUpperCase())
                               .collect(toList());

map可以把一种类型的stream数据转换成另一种。

filter

List<String> beginningWithNumbers
  = Stream.of("a", "1abc", "abc1")
		  .filter(value -> isDigit(value.charAt(0)))
		  .collect(toList());

filter可以把filter内返回为true的的数据过滤出来。

flatMap

List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
                               .flatMap(numbers -> numbers.stream())
                               .collect(toList());

看上面的图更好理解一点,flatmap可以把stream的元素转换成stream,然后再把这些stream合成一个新的stream,而map只能把stream中的元素从一种类型转换成另一种类型。

max and min

List<Track> tracks = asList(new Track("Bakai", 524),
							new Track("Violets for Your Furs", 378),
							new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
                            .min(Comparator.comparing(track -> track.getLength()))
                            .get();

在stream中找到最大值和最小值。

reduce

int count = Stream.of(1, 2, 3)
				  .reduce(0, (acc, element) -> acc + element);

reduce会把第一个参数作为初始的acc,遍历stream的元素作为element,然后每次计算的结果会作为下次的acc.上面的代码可以拓展为:

BinaryOperator<Integer> accumulator = (acc, element) -> acc + element; int count = accumulator.apply(
                    accumulator.apply(
                        accumulator.apply(0, 1),
2), 3);
综合

把上面的各种方法结合起来用一下:

Set<String> origins = album.getMusicians()
                               .filter(artist -> artist.getName().startsWith("The"))
                               .map(artist -> artist.getNationality())
                               .collect(toSet());
  1. 过滤出名字以The为开头的艺术家
  2. 取出艺术家的国籍
  3. 得到一个艺术家的国籍的Set(去重)

再看一个例子:

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());
}
  1. 把所有专辑的曲目取出来合并成一个stream
  2. 过滤出大于60秒的曲目
  3. 把曲目的名字取出来的
  4. 转换成一个Set

方法引用

artist -> artist.getName()
Artist::getName

(name, nationality) -> new Artist(name, nationality)
Artist::new

方法引用是一种更简洁的写法,而且比lambda表达式更容易理解。

对数据进行分类

一种分类方法是:partiioningBy

public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) { 
	return artists.collect(partitioningBy(artist -> artist.isSolo()));
}

有时候我们想根据某个值对数据进行分类就可以用partitionBy,方法的返回的boolean值作为key,满足条件的值合并成list作为value。如果不想得到list还以在后面再加一个collector。

public Map<Boolean, Integer> bandsAndSolo(Stream<Artist> artists) { 
	return artists.collect(partitioningBy(artist -> artist.isSolo(), list -> list.size()));
}

这样改一下就是把满足条件的值的数量作为value。

另一种是:groupingBy

    public Map<Boolean, List<String>> bandsAndSolo(Stream<String> artists) {
        return artists.collect(partitioningBy(artist -> artist.isSolo(), artist.size()));
    }

不同点在于partitioningBy只能把boolean作为key,而且永远只有两个entry,效率要高一点。而groupingBy可以把任意对象作为key。

总结

Java8加入的lambda表达式和stream库,可以让我们很方便的操作集合,聚合数据等等,熟练使用的话能很好提高编程效率,同时也提高了代码的可读性。当然它也有缺点,比如不方便debug,有一定的学习成本。

原文地址:https://www.cnblogs.com/ekoeko/p/9610718.html