Java8 - 函数式编程

2019-07-04 00:00:00 Java8

本文笔记于书记《Java8 函数式编程》

Java中提供java.util.concurrent包还有第三方库来处理并发。但对于大型数据集合,Java还需要高效的并行操作。Java8增加Lambda表达式来处理批量数据。

面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。在编写回调函数和事件处理程序时不必纠结于匿名内部类的冗繁和可读性。

函数式编程,函数对输入进行处理获取输出。

button.addActionListener(new ActionListener(){
  public void actionPerformed(ActionEvent event){
    System.out.println("button clicked");
  }
})

上面使用匿名类,创建了实现ActionListener接口的新对象,并在新对象中实现了该接口的唯一方法actionPerformed。实际上,我们传递了一种代表某种行为的对象作为参数。

匿名内部类将代码作为数据传递,但是我们并不想传入对象,只是想传入行为。Java8中提供Lambda表达式来解决这个问题,传入没有名称的函数代码块。同时,这里的Lambda表达式并没有指定event的数据类型,javac根据上下文推断。(也可以指定数据类型,有时编译器并不一定能根据上下文推断出参数类型)

button.addActionListener(event -> System.out.println("button clicked"));

若匿名类使用所在方法中的变量,该变量必须是final。Java8虽然可以引用非final变量,但该变量在既成事实上必须是final,即必须是终态变量,否则,编译器会报错。Lambda表达式引用的是值,而不是变量。这也解释了为什么Lambda表达式被称为闭包。

匿名类引用方法中的变量:

final String name = getUserName();
button.addActionListener(new ActionListener(){
  public void actionPerformed(ActionEvent event){
    System.out.println("hi " + name);
  }
});

Lambda表达式本身的类型为函数接口。

函数接口是只有一个抽象方法的接口,用作Lambda表达式的类型。接口中唯一方法的命名并不重要,只要方法签名和Lambda表达式的类型匹配即可,参数命名可以更有意义。

Lambda表达式和匿名内部类在JVM上的区别:

匿名内部类是一个不需要显示指定类名的类,编译器会为该类取名,匿名类生成.class文件。

Lambda表达式不会产生新的类,被封装成主类的一个私有方法,并通过invokedynamic指令进行调用。

为了引入Lambda表达式,java8新增了java.util.function包来包含常用的函数接口。

《Java8 - 函数式编程》
《Java8 - 函数式编程》

类型推断:

Javac只是根据Lambda表达式上下文来推断参数的正确类型,程序依然要经过类型检查来保证运行的安全性,只是不显式声明类型。

Predicate是用来判断真假的函数接口。

public interface Predicate<T>{
  boolean test(T t);
}

下面是使用Predicate的例子。

Predicate<Integer> atLeast5 = x -> x>5;

x>5是表达式的主体,返回值就是表达式主体的值。

BinaryOperator接口接收两个参数,返回一个值,返回值和参数的数据类型相同。

BinaryOperator<Long> addLongs = (x,y) -> x + y;

Lambda表达式是一个匿名方法,将行为像数据一样传递。

同时,java集合框架也新增部分接口来于Lambda表达式对接。下面是新增的接口,继承类中会实现该接口。这些新加入的方法大部分要用到java.util.function包下的接口。

《Java8 - 函数式编程》
《Java8 - 函数式编程》

spliterator()方法跟iterator()方法有点像,它既可以像iterator那样逐个迭代,也可以批量迭代。批量迭代可以降低迭代的开销。可通过调用Spliterator<T> trySplit()方法来分成两个,一个是this,另一个是新返回的那个,这两个迭代器代表的元素没有重叠。可以多次调用该方法获得不重叠的多个迭代器,以便多线程处理。

Stream是java中函数式编程的主角,它是数据源的一种视图,可以通过集合类获得,如:Collection.stream();Collection.parallelStream();Arrays.stream(T[] array)

《Java8 - 函数式编程》
《Java8 - 函数式编程》

Stream并无数据存储,不会修改背后的数据源。所有惰性操作以pipeline的方式执行,减少迭代次数,计算完成后stream便失效。下面是Stream接口的常用函数。

《Java8 - 函数式编程》
《Java8 - 函数式编程》

外部迭代:首先调用iterator方法生成新的Iterator对象,进而控制整个迭代过程。外部迭代本质上是一种串行化操作。

内部迭代:调用stream()方法返回内部迭代的接口Stream。Stream是函数式编程方式在集合类上进行复杂操作的工具。

Stream的一些方法返回的Stream对象不是新集合,而是创建新集合的配方,如filter方法。这种方法被称为惰性求值方法,而count方法最终从Stream中生成值的方法称为及早求值方法。判断方法类型只需要看其返回值,若返回Stream,为惰性求值,否则为及早求值。

常用的流操作:

collect(toList())

该方法由Stream里的值生成列表,为及早求值操作。Stream的of方法使用一组初始值生成新的Stream。

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

map的Lambda表达式必须是Function接口的一个实例,接收一个参数。

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

filter方法遍历数据并检查其中的元素。

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

flatMap方法可用Stream替换值,将多个Stream连接成一个Stream。stream方法将每个列表转换成Stream对象,其余部分由flatMap方法处理将多个stream合并成一个stream。

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

min和max方法用来获取最小值和最大值。

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();
     

assertEquals(tracks.get(1), shortestTrack);

我们需要传入Comparator对象并实现其静态方法comparing来实现比较器。

思考:我们的函数真的需要暴漏List或Set对象给用户吗?或许暴漏Stream比较好,这样可以很好的封装内部实现的数据结构。

下面是一个使用Stream来重构代码的例子。

原代码:

public Set<String> findLongTracks(List<Album> albums) {
 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重构后的代码:

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());

注意:在重构代码时的每一步都要进行单元测试,保证代码能够正常工作。

高阶函数是指接受另外一个函数作为参数,或返回一个函数的函数。

Java8中引入默认方法和接口的静态方法,即接口中的方法也可以包含代码体。

装箱类型和基本类型相比,不但空间占有大,而且装箱,拆箱的时间开销都比较大。为了减少这些性能开销,stream类的某些方法对基本类型和装箱类型做了区分。参数是基本类型的,命名使用To+基本类型+函数名,如ToLongFunction,返回值是基本类型,基本类型+函数名,如:LongFunction。高阶函数使用基本类型,在操作后加后缀To再加基本类型,如mapToLong。如果有可能应尽可能多的使用对基本类型做过特殊处理的方法,今儿改善性能。

下面是一个使用基本数据类型的例子。

public static void printTrackLengthStatistics(Album album) { 
  IntSummaryStatistics trackLengthStats
 = album.getTracks()
   .mapToInt(track -> track.getLength())
                        
   .summaryStatistics();
         
  
  System.out.printf("Max: %d, Min: %d, Ave: %f, Sum: %d",
     
     trackLengthStats.getMax(),
     trackLengthStats.getMin(),
     
     trackLengthStats.getAverage(),
       
     trackLengthStats.getSum());
}

上面例子中的mapToInt方法返回IntStream对象,包含summaryStatistics方法,会计算一些统计信息。这些统计值在所有特殊处理的Stream,如DoubleStream,LongStream中都可以得出。如不需要全部统计值,可分别调用min,max,average,sum来获得单个统计值。

当使用Lambda表达式作为参数时,其类型由他的目标类型推导得出:1.只有一个目标类型时,由函数接口的参数类型推导得出;2. 如果有多个目标类型,由最具体的类型推导得出; 3.有多个目标类型且最具体的类型不明确,需要人为指定类型。

java中有一些接口,虽然只含有一个方法,但并不是为了使用Lambda表达式来实现的,如Closeable。每个用作函数接口的接口都应该添加注释@FunctionalInterface.Lambda表达式的意义在于将代码块作为数据打包起来。该注释会强制javac检查接口是否符合函数接口的标准。

Java保证后向兼容,如在父类中暴露新方法必须在子类中实现该新方法。Java8中添加默认方法来解决这个问题。即在父类中提供默认方法,若子类中没有实现该方法,就使用父类中的默认方法。在任何接口中,无论是函数接口还是非函数接口,都可以使用该方法。

Collection接口实现默认方法stream, Iterable接口增加默认方法forEach,跟for循环类似,但允许使用Lambda表达式作为循环体。

下面是默认方法的示例。

default void forEach(Consumer<? super T> action){
    for(T t : this){
        action.accept(t);
    }
}

当默认方法和类中的方法冲突时,采用类中的方法。增加默认方法主要是为了在接口上向后兼容。

接口允许多重继承,若2个父进程中包含签名相同的默认方法,编译器由于不知道使用哪个默认方法而报错。可以通过在类中实现该方法来解决这个问题。

public class MusicalCarriage
             implements Carriage, Jukebox {
     @Override
public String rock() {
         return Carriage.super.rock(); 
     }

}

使用super表示使用接口Carriage中定义的默认方法。

接口允许多重继承,却没有成员变量,抽象类可以继承成员变量,却不能多重继承。

Stream为接口,而Stream.of为接口的静态方法。

Optional用来替换null值,相当于值的容器,该值可以通过get方法提取。empty()方法用来提供空的Optional,ofNullable方法可将空值转换成Optional对象。isPresent方法用来检查Optional对象里是否有值。

Optional<String> a = Optional.of("a");
assertEquals("a",a.get());
Optional emptyOptional = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);
assertFalse(emptyOptional.isPresent());

Optional对象可以在调用get()方法前,先使用isPresent检查Optional对象是否有值。orElse方法可以在Optional对象为空时提供备选值,也可以使用orElseGet方法来接受一个Supplier对象作为参数。

assertEquals("b",emptyOptional.orElse("b"));
assertEquals("c",emptyOptional.orElseGet(()->"c"));

Lambda表达式经常调用参数,java8提供方法引用作为简写语法。标准语法:Classname::methodName.凡是使用Lambda表达式的地方,就可以使用方法引用。

artist -> artist.getName() Artist::getName //等价于上面方法引用方式

方法引用可以细分为四类:

Integer::sum  //引用静态方法
list::add     //引用某个对象的方法
String::length//引用某个类的方法
HashMap::new  //引用构造函数

下面对构造函数使用方法引用。

(name, nationality) -> new Artist(name, nationality)
等价于 Artist::new
String[]::new 创建字符串型的数组

方法引用自动支持多个参数,但必须选对正确的函数接口。

Function.identity()为Function接口中的静态方法,用来返回和输入一样的输出。

在有序集合上创建流,流中的元素按出现顺序排列,如List。如果集合是无序的,由此生成的流也是无序的。一些操作在有序的流上开销更大,调用unordered方法消除这种顺序就能解决该问题。大多数操作都是在有序流上效率更高。使用并行流时,forEach方法不能保证元素是按顺序处理的,如果需要保证按顺序处理,应使用forEachOrdered.

收集器是一种通用的,从流生成复杂值的结构。只要将它传给collect 方法,所有的流就都可以使用它。标准库在java.util.stream.Collectors中提供了一些静态导入的收集器。

下面是collect函数的定义:

<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R,R> combiner)
<R,A> R collect(Collector<? super T,A,R> collector)

如果没有收集器,我们要提供3个函数,下面已ArrayList为例。

《Java8 - 函数式编程》
《Java8 - 函数式编程》

但是使用Collector时,只需要传入Collectors.toList()即可。虽然Collector.tolist()和Collector.toSet()函数只是返回list和set的接口类型,并不知道具体是什么容器。若需要指定容器的具体类型,可以使用Collectors.toCollection()。

ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));

有时,我们需要将stream输出为集合。toList生成list,toSet, toCollection用来生成Set和Collection。调用这些方法时,不需要指定具体的类型,Stream类库自动挑选合适的类型。当你需要指定类型时,如使用TreeSet而不是提供的Set,可以使用toCollection,它接受一个函数作为参数来创建集合。

stream.collect(toCollection(TreeSet::new));

有时需要将流生成一个。minBy和maxBy允许用户按某种特定的顺序生成一个值。下面的例子通过比较乐队成员的数量来获取成员最多的乐队。

public Optional<Artist> biggestGroup(Stream<Artist> artists){
    Function<Artist, Long> getCount = artist -> artist.getMembers().count();
    return artists.collect(maxBy(comparing(getCount)));
}

averagingInt方法接受Lambda表达式作为参数,将流中元素转换成整数,再计算平均值。double和long类型有对应的重载方法。

public double averageNumberOfTracks(List<Album> albums) { 
    return albums.stream() 
                      .collect(averagingInt(album -> album.getTrackList().size()));
} 

stream的数据源可以是数组、容器,但不能为Map。但可以从stream生成Map。Collectors提供了下面3个函数迎来生成Map。

Collectors.toMap()
Collectors.partitionBy()
Collectors.groupingBy()

数据分块:使用收集器partitioningBy,接收一个流,并将其分为两部分。它使用Predicate对象判断一个元素该属于哪个部分,根据boolean值返回一个Map到列表。

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

数据分组:使用收集器groupingBy根据提供参数将流分组。

public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums){
    return albums.collect(groupingBy(Album::getMainMusician));
}

partitioningBy接收Predicate,而groupingBy接收Function。

下面使用Collectors.joining方法将流中演奏者的名字连接成字符串,提供分隔符,前缀和后缀。

String result = artists.stream().map(Artist::getName)
    .collect(Collectors.joining(", ","[","]"));

Collectors.joining()用3种形式:

Collectors.joining()//直接将元素拼接 Collectors.joining(,)//元素之间使用","作为分隔符 Collectors.joining(",","{","}")//指定元素分隔符和前后字符

组合使用收集器:

下面示例将groupingBy和counting结合使用,获取每个艺术家的专辑数。

public Map<Artist, Long> numberOfAlbums(){
    return albums.collect(groupingBy(Album::getMainMisician, counting()));
}

先使用groupingBy将元素按照艺术家分块,下游收集器counting收集每块中的元素。

mapping允许在收集器的容器上执行类似map的操作,需要指定使用什么集合类存储结果。

public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums){
    return albums.collect(groupingBy(Album::getMainMusician,mapping(Album::getName,toList())));
}

上面例子中的counting(),mapping()为下游收集器。收集器用来生成过程最终结果,而下游收集器用来生成部分结果,主收集器中会用到下游收集器。

重构和定制收集器:还是没懂怎么定制收集器???

Java8中提供新方法computeIfAbsent,该方法接收Lambda表达式,值不存在时,使用该Lambda表达式计算新值。

public Artist getArtist(String name){
    return artistCache.computeIfAbsent(name, this::readArtistFromDB);
}

stream 对象调用它的parallel方法就能让其拥有并行操作的能力。集合类调用paralleStream能获得拥有并行能力的流。下面方法并行计算专辑曲目长度。

public int parallelArraySum(){
    return albums.parallelStream()
        .flatMap(Album::getTracks)
        .mapToInt(Track::getLength)
        .sum();
}

让代码并行化运行,代码写的必须符合一些约定。reduce方法的初始值必须为组合函数的恒等值,即恒等值和其他值做reduce操作时,其他值保持不变,如求和中的0。reduce另一个限制时符合结合律,即组合操作的顺序不重要。避免持有锁,流框架会在需要时自己处理同步操作。

sequential方法使得流操作串行化,如果同时调用parallel和sequential,最后调用的起作用。

关于测试的书籍:Test-Driven Development, Growing Object-Oriented Software.

下面的代码可重用方面可以优化。

public long countMusicians(){
    return albums.stream()
        .mapToLong(album -> album.getMusicians.count())
        .sum();
}

public long countTracks(){
    return albums.stream()
        .mapToLong(album -> album.getTracks().count())
        .sum();
}

可以提供Function接口来重构。

public long countFeature(ToLongFunction<Album> function){
    return albums.stream()
        .mapToLong(function)
        .sum();
}

public long countTracks(){
    return countFeature(album -> album.getTracks().count());
}

public long countMusicians(){
    return countFeature(album -> album.getMusicians().count());
}

Lambda表达式因为没有名字,给单元测试带来了一些麻烦。解决方法有2种L:第一种将Lambda表达式放入方法测试,测试方法而不是Lambda表达式本身。方法2是不用Lambda表达式,使用方法引用。任何Lambda表达式都能被改写为普通方法,然后使用方法引用直接引用。

当我们想记录流中的数据,需要将流转换成Collection,但这样就不再是流。我们可以使用peek方法来查看每个值,同时继续操作流。

Set<String> nationalities = album.getMusicians()
    .filter(artist->artist.getName().startWith("The"))
    .map(artist -> artist.getNationality())
    .peek(nation -> System.out.println("Found nationality: " + nation))
    .collect(Collectors.<String>toSet());

由于peek可以获得流中的元素,我们可以通过在peek中的函数上设置断点就可以实现在流中间设置断点,就可以逐个调试流中的元素。

策略模式:

《Java8 - 函数式编程》
《Java8 - 函数式编程》

我们会定义策略接口和该接口的具体实现。

public interface CompressionStrategy{
    public OutputStream compress(OutputStream data) throws IOException;
}

public class GzipCompressionStrategy implements CompressionStrategy {

    @Override
    public OutputStream compress(OutputStream data) throws IOException {
        return new GZIPOutputStream(data); 
    }

}

public class ZipCompressionStrategy implements CompressionStrategy {
    @Override
    
public OutputStream compress(OutputStream data) throws IOException {
        return new ZipOutputStream(data); 
    }

}

然后实现压缩器,可以设置不同的压缩策略。

public class Compressor {
    private  final CompressionStrategy strategy;
    public Compressor(CompressionStrategy strategy) { 
        this.strategy = strategy;
    }
    
public void compress(Path inFile, File outFile) throws IOException { 
        try (OutputStream outStream = new FileOutputStream(outFile)) {
               Files.copy(inFile, strategy.compress(outStream));
             
        }
    
} 
}

用户可以用不同的压缩策略来创建压缩器并文件进行压缩。

Compressor gzipCompressor = new Compressor(new GzipCompressionStrategy());
gzipCompressor.compress(inFile, outFile);

Compressor zipCompressor = new Compressor(new ZipCompressionStrategy()); 
zipCompressor.compress(inFile, outFile);

观察者模式:

实现观察者接口和不同的观察者,被观察对象中存放所有观察者列表,可以添加观察者并在事件发生时调用每个观察者的方法。

public interface LandingObserver {
    public void observeLanding(String name); 
}

public class Aliens implements LandingObserver {
    @Override
    
public void observeLanding(String name) {
        if (name.contains("Apollo")) {
            
System.out.println("They're distracted, lets invade earth!");
        } 
    }

}

public class Nasa implements LandingObserver { 
    @Override
    public void observeLanding(String name) { 
        if (name.contains("Apollo")) {
     
            System.out.println("We made it!");
        }
    } 
}

//被观察者
public class Moon {
    private  final List<LandingObserver> observers = new ArrayList<>();
    public void land(String name) {
        for (LandingObserver observer : observers) {
 
             observer.observeLanding(name);
              
        }
    
}

    
public void startSpying(LandingObserver observer) { 
        observers.add(observer);
    } 
}

//Object执行观察
Moon moon = new Moon(); 
moon.startSpying(new Nasa()); 
moon.startSpying(new Aliens());
     
moon.land("An asteroid");
     
moon.land("Apollo 11");

SOLID原则上设计面向对象程序时的一些基本原则,分别代表Single responsibility单一功能原则, Open/closed开闭原则,对扩展开放,对修改闭合, Liskov substitution, Interface segregation, Dependency inversion依赖反转是指实现依赖于抽象.这些原则能指导开发出易于维护和扩展的代码。

下面是使用函数式编程实现的统计素数个数的功能。

public long countPrimes(int upTo) {
  return IntStream.range(1, upTo)
    .parallel()
    .filter(this::isPrime)
    .count();
}

private boolean isPrime(int number) {
  return IntStream.range(2, number)
    .allMatch(x -> (number % x) != 0);
}

Java标准类库的NIO 提供了非阻塞式I/O的接口。

Stream上有2个通用的规约操作:reduce()和collect()。

下面是reduce()的3种重写形式:identity为初始值,combiner是怎么合并结果。

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T,U> accumulator, BinaryOperator<U> combiner)

    原文作者:天长水远
    原文地址: https://zhuanlan.zhihu.com/p/37091876
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章