1.2 什么是函数式编程

每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函 数,函数对一个值进行处理,映射成另一个值。

第二章 Lambda表达式

2.2 辨别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 表达式的类型依赖于上下文环境,是由编译器 推断出来的。

2.3 引用值,而不是变量

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

Java 8虽然放松了这一限制,可以引用非final变量,但是该变量在既成事实上必须是 final

//这里编译不会通过
	 String name = getUserName();
     name = formatUserName(name);
     button.addActionListener(event -> System.out.println("hi " + name));

这种行为也解释了为什么 Lambda 表达式也被称为闭包。未赋值的变量与周边环境隔离起 来,进而被绑定到一个特定的值
总而言之, lambda 引用的是值 而非变量

2.4 函数接口

使用只有一个方法的接口来表示某特定方法并反复使用,是很早就有的习惯。使用 Swing 编写过用户界面的人对这种方式都不陌生,这里无需再标新立 异,Lambda 表达式也使用同样的技巧,并将这种接口称为函数接口

2.5 类型推断

javac 根据 Lambda 表达式上下文信息 就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再显 式声明类型罢了。这就是所谓的类型推断。

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

2.6 要点回顾

  • Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。
  • Lambda 表达式的常见结构:BinaryOperator add = (x, y) → x + y。
  • 函数接口指仅具有单个抽象方法的接口,用来表示Lambda表达式的类型。

3.1 从外部迭代到内部迭代
java8 函数式编程读书笔记

java8 函数式编程读书笔记

3.2 实现机制

1、惰性求值法
如下,不会出现打印结果

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

2、及早求值法
如下,会打印结果

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

使用这些操作的理 想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是 它的合理之处。计数的示例也是这样运行的

3.3 常用的流操作

3.3.1 collect(toList())

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

这个例子也展示了本节中所有示例代码的通用格式。首先由列表生成一个 Stream ,然后 进行一些 Stream 上的操作,继而是 collect 操作,由 Stream 生成列表,最后使用断言 判断结果是否和预期一致。

3.3.2 map
java8 函数式编程读书笔记

使用普通的方式将数组中的数据改成大写

List<String> collected = new ArrayList<>();
for (String string : asList("a", "b", "hello")) {
          String uppercaseString = string.toUpperCase();
          collected.add(uppercaseString);
      }
assertEquals(asList("A", "B", "HELLO"), collected);

使用stream.map

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

看源码map的传参是function,正好适用将一个值变为另外一个值的场景
java8 函数式编程读书笔记

3.3.3 filter
java8 函数式编程读书笔记
filter的传参是Predicate接口,传入一个值,返回一个boolean判断,适用于筛选的场景
java8 函数式编程读书笔记

3.3.4 flatmap

flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream

List list =  Stream.of(Arrays.asList("a,b,c"),Arrays.asList("d,e,f")).flatMap(strings -> {
    return strings.stream().map(string-> string.toUpperCase());
}).collect(Collectors.toList());
System.out.println(list);

flatMap 方法的相关函数接口和 map 方法的一样,都是 Function 接口,只是方法的返回值 限定为 Stream 类型罢了。

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

3.3.5 max和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();
assertEquals(tracks.get(1), shortestTrack);

这里的stream调用 max 和 min 会获得一个 Optional对象,调用get才会获得具体的值
comparing的源码,最终返回的是一个Comparator函数

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

3.3.7 reduce

使用 reduce 求和

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

3.4 重构遗留代码

/**
 * 唱片筛选(参考函数式编程 3。4 重构遗留代码)
 */
public class AlbumScreen {

    /**
     * 专辑
     */
    static class Album{

        public Album(String albumName, List<Track> trackList) {
            this.albumName = albumName;
            this.trackList = trackList;
        }

        //专辑名称
        private String albumName;

        //曲目列表
        private List<Track> trackList;

        public String getAlbumName() {
            return albumName;
        }

        public void setAlbumName(String albumName) {
            this.albumName = albumName;
        }

        public List<Track> getTrackList() {
            return trackList;
        }

        public void setTrackList(List<Track> trackList) {
            this.trackList = trackList;
        }
    }

    /**
     * 曲目
     */
    static class Track{

        public Track(String trackName,Long seconds) {
            this.trackName = trackName;
            this.seconds = seconds;
        }

        //曲目名称
        private String trackName;

        //时长 秒
        private Long seconds;

        public Long getSeconds() {
            return seconds;
        }

        public void setSeconds(Long seconds) {
            this.seconds = seconds;
        }

        public String getTrackName() {
            return trackName;
        }

        public void setTrackName(String trackName) {
            this.trackName = trackName;
        }

    }

    /**
     * 筛选时长大于60秒以上的曲目
     * @param albumList
     * @return
     */
    public static Set<String> screen(List<Album> albumList){
        Set<String> trackNames = new HashSet<>();
        for(Album album : albumList) {
            for (Track track : album.getTrackList()) {
                if (track.seconds > 60) {
                    String name = track.getTrackName();
                    trackNames.add(name);
                }
            }
        }
        return trackNames;
    }

    /**
     * 筛选时长大于60秒以上的曲目(lambda版)
     * @param albumList
     * @return
     */
    public static Set<String> screenLambda(List<Album> albumList){
        return albumList.stream().flatMap(tracks -> tracks.trackList.stream()) //flatmap 将多个流合并成一个
                .filter(track -> track.seconds>60)  //filter 进行筛选
                .map(track -> track.trackName)    //map 通过一个值获取另外一个值,这里根据曲目对象获取名称
                .collect(Collectors.toSet());  //创建 set
    }

    public static void main(String[] args){
        List<Track> trackList1 =
                Stream.of(new Track("燃烧我的卡路里",180L)
                        ,new Track("我已经爱上你",59L)
                        ,new Track("好汉歌",100L))
                        .collect(Collectors.toList());
        List<Track> trackList2 =
                Stream.of(new Track("一百万个可能",90L)
                        ,new Track("答案",30L)
                        ,new Track("一个人去巴黎",120L))
                        .collect(Collectors.toList());

        List<Album> albumList =
                Arrays.asList(new Album("火箭队",trackList1),new Album("银河队",trackList2));

        System.out.println(screen(albumList));
        System.out.println(screenLambda(albumList));
    }

}

3.8 要点回顾

内部迭代将更多控制权交给了集合类。
和Iterator类似,Stream是一种内部迭代方式。
将Lambda表达式和Stream上的方法结合起来,可以完成很多常见的集合操作。

4 类库

4.1 在代码中使用lambda表达式

使用 isDebugEnabled 方法降低日志性能开销

Logger logger = new Logger(); if (logger.isDebugEnabled()) {
         logger.debug("Look at this: " + expensiveOperation());
 }

//使用lambda表达式简化日志
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());

public void debug(Supplier<String> message) { if (isDebugEnabled()) {
             debug(message.get());
         }
}

4.2 基本类型

由于装箱类型是对象,因此在内存中存在额外开销。比如,整型在内存中占用 4 字节,整型对象却要占用 16 字节。这一情况在数组上更加严重,整型数组中的每个元素 只占用基本类型的内存,而整型对象数组中,每个元素都是内存中的一个指针,指向 Java 堆中的某个对象。在最坏的情况下,同样大小的数组,Integer[] 要比 int[] 多占用 6 倍 内存。

为了减小这些性能开销,Stream 类的某些方法对基本类型和装箱类型做了区分。图 4-1 所 示的高阶函数mapToLong和其他类似函数即为该方面的一个尝试

    //Stream 中的源码
    LongStream mapToLong(ToLongFunction<? super T> mapper);

java8 函数式编程读书笔记

//LongStream 中的源码
    <U> Stream<U> mapToObj(LongFunction<? extends U> mapper);

java8 函数式编程读书笔记

4.3 重载解析

Lambda 表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循 如下规则:

? 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出;
? 如果有多个可能的目标类型,由最具体的类型推导得出;
? 如果有多个可能的目标类型且最具体的类型不明确,则需人为指定类型。

4.4 @FunctionalInterface

该注释会强制 javac 检查一个接口是否符合函数接口的标准。如果该注释添加给一个枚举 类型、类或另一个注释,或者接口包含不止一个抽象方法,javac 就会报错。重构代码时, 使用它能很容易发现问题。

4.6 默认方法

因为接口的改造,接口方法的增加,会导致用旧的jdk编译的类有不兼容的问题,所以采用了default关键字,接口提供一个默认的实现方法

默认方法示例:forEach 实现方式

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

和类不同,接口没有成员变量,因此默认方法只能通过调用子类的方法来修改子类本身, 避免了对子类的实现做出各种假设。

4.7 多重继承

public interface Jukebox {
        public default String rock() { return "... all over the world!";
        } 
    }
    
    public interface Carriage {
        public default String rock() { return "... from side to side";
        } 
    }
    
    
    public class MusicalCarriage
    implements Carriage, Jukebox {
    
        @Override
            public String rock() {
                   return Carriage.super.rock();
             }
    }

javac 并不明确应该继承哪个接口中的方法,因此编译器会报错:class Musical Carriage inherits unrelated defaults for rock() from types Carriage and Jukebox。当然,在类 中实现 rock 方法就能解决这个问题

三定律
如果对默认方法的工作原理,特别是在多重继承下的行为还没有把握,如下三条简单的定 律可以帮助大家。

  1. 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义 的方法。
  2. 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法, 那么子类中定义的方法胜出。
  3. 没有规则三。如果上面两条规则不适用,子类要么需要实现该方法,要么将该方法声明 为抽象方法。
    其中第一条规则是为了让代码向后兼容。

4.9 接口的静态方法

Stream 是个接口, Stream.of是接口的静态方法。这也是Java 8中添加的一个新的语言特性,旨在帮助编写 类库的开发人员,但对于日常应用程序的开发人员也同样适用。

4.10 Optional

Optional 是为核心类库新设计的一个数据类型,用来替换 null 值。
使用 Optional 对象有两个目的:首先,Optional 对象鼓励程序员适时检查 变量是否为空,以避免代码缺陷;其次,它将一个类的 API 中可能为空的值文档化,这比 阅读实现代码要简单很多。

 //创建某个值的 Optional 对象
    Optional<String> a = Optional.of("a");
         assertEquals("a", a.get());
    
    //创建一个空的 Optional 对象,并检查其是否有值
    Optional emptyOptional = Optional.empty();
         Optional alsoEmpty = Optional.ofNullable(null);
         assertFalse(emptyOptional.isPresent());
    
    //使用 orElse 和 orElseGet 方法
    assertEquals("b", emptyOptional.orElse("b"));
    assertEquals("c", emptyOptional.orElseGet(() -> "c"));

4.11 要点回顾
? 使用为基本类型定制的Lambda表达式和Stream,如IntStream可以显著提升系统性能。
? 默认方法是指接口中定义的包含方法体的方法,方法名有default关键字做前缀。
? 在一个值可能为空的建模情况下,使用Optional对象能替代使用null值。

相关文章: