侧边栏壁纸
  • 累计撰写 120 篇文章
  • 累计创建 281 个标签
  • 累计收到 11 条评论
标签搜索
隐藏侧边栏

《java8实战》笔记(1-5章)

骐骏
2022-05-15 / 0 评论 / 0 点赞 / 336 阅读 / 6,549 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-15,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Java 8实战
Raoul

关于本书

Java 8的流支持这种简明的数据库查询式编程——但用的是Java语法,而无需了解数据库!其次,流被设计成无需同时将所有的数据调入内存(甚至根本无需计算),这样就可以处理无法装入计算机内存的流数据了。

Java 8可以对流做一些集合所不能的优化操作,例如,它可以将对同一个流的若干操作组合起来,从而只遍历一次数据,而不是花很大代价去多次遍历它。更妙的是,Java可以自动将流操作并行化(集合可不行)。

第1章 为什么要关心Java 8

从有点修正主义的角度来看,在Java 8中加入Streams可以看作把另外两项扩充加入Java 8的直接原因:把代码传递给方法的简洁方式(方法引用、Lambda)和接口中的默认方法。

Java 8可以透明地把输入的不相关部分拿到几个CPU内核上去分别执行你的Stream操作流水线,但是写代码时不能访问共享的可变数据,即需要保证在多线程操作时,数据是线程安全的

在多个处理器内核之间使用synchronized,其代价往往比你预期的要大得多,因为同步迫使代码按照顺序执行,而这与并行处理的宗旨相悖

编程语言的整个目的就在于操作值,要是按照历史上编程语言的传统,这些值因此被称为一等值(或一等公民,这个术语是从20世纪60年代美国民权运动中借用来的)。

编程语言中的其他结构也许有助于我们表示值的结构,但在程序执行期间不能传递,因而是二等公民。前面所说的值是Java中的一等公民,但其他很多Java概念(如方法和类等)则是二等公民。用方法来定义类很不错,因此,Java 8的设计者决定允许方法作为值,让编程更轻松。

在Java 8里写下File::isHidden的时候,你就创建了一个方法引用,

我们说使用这些概念的程序为函数式编程风格,这句话的意思是“编写把函数作为一等值来传递的程序”。

我们把这种数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代。

Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。

Java中从函数式编程中引入的两个核心思想:将方法和Lambda作为一等值,以及在没有可变共享状态时,函数或方法可以有效、安全地并行执行。

第2章 通过行为参数化传递代码

**行为参数化**是可以帮助你处理频繁变更的需求的一种软件开发模式

行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。

DRY(Don't Repeat Yourself,不要重复自己)的软件工程原则。

第3章 Lambda表达式

可以把**Lambda表达式**理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

一言以蔽之,**函数式接口就是只定义一个抽象方法的接口,**哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。

Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法,当然这个Lambda表达式的签名要和函数式接口中抽象方法一样

注意你使用了Java 7中的带资源的try语句,它已经简化了代码,因为你不需要显式地关闭资源了)


public String processFile()throws IOException{

  try(BufferReader br = new BufferReader(new File("text")){
    return br.readLine()
  }
}

java7 try-with-resources的实现方式,是使用了语法糖,需要关闭的资源的对象要实现AutoClose接口,在写在try语句块中,进行编译时就会在finally语句中调用close方法

Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。

函数式接口的抽象方法的签名称为函数描述符

java8 中的函数式接口

Predicate

java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。

Consumer

java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个方法

Function

java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口

Supplier

java.util.function.Supplier接口定义了一个get方法,它没有入参,只有一个返回泛型T的对象
��

针对基础数据类型,为避免装箱带来的不必要的开销(如创建包装类消耗更多的内存),java提供了针对基础类型的函数式接口,如:IntPredicate,DoublePredicate,IntCousumer,IntFunction等

任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型

请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。

如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)

有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,程序员必须做出自己的选择

Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda。Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获局部变量this)

为什么局部变量有这些限制
第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个新线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后才去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制(指局部变量必须为final的限制)。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中解释,这种模式会阻碍很容易做到的并行处理)

你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖

如何构建方法引用方法引用主要有三类。
(1) 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
(2) 指向任意类型实例方法的方法引用(例如String的length方法,写作String::length)。
(3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensive-Transaction::getValue)。

第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式(String s)-> s.toUppeCase()可以写作String::toUpperCase

方法引用的签名必须和上下文类型匹配。

对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。

函数式接口就是仅仅定义一个抽象方法的接口。抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。

复合Lambda表达式的用法

比较器复合
逆序 reversed()
比较器链

第一个条件比较完后可以调用thenComparing()继续比较

谓词复合(Predicate)

谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词。

复合函数(Function)

Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例
andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。f.andthen(g)类似于数学函数中g(f(x)) ,而compse方法则类似于f(g(x))即f.compose(g)与数学函数中f(g(x))相似

Function<String, String> toUpperCase = String::toUpperCase; // 使用方法引用获取到function对象
Function<String, String> lowerCase = String::toLowerCase;
toUpperCase.andThen(lowerCase);
String test = "teest";
String apply = toUpperCase.apply(test);
System.out.println(apply);

Function<String, String> compose = lowerCase.compose(toUpperCase);
String apply1 = compose.apply(test);

        System.out.println(apply1);

只有在接受函数式接口的地方才可以使用Lambda表达式。

环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。

第4章 引入流

流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。

因为filter、sorted、map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构

Java 8中的Stream API可以让你写出这样的代码:
❑ 声明性——更简洁,更易读
❑ 可复合——更灵活
❑ 可并行——性能更好

集合与流之间的差异就在于什么时候进行计算

集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中

相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。

和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。所以要记得,流只能消费一次!

集合和流的另一个关键区别在于它们遍历数据的方式

使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。相反,Streams库使用内部迭代— 它帮你把迭代做了,还把得到的流值存在某个地方,你只要给出一个函数说要干什么即可

Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现

Stream流中的操作可以分为两大类:

❑ filter、map和limit可以连成一条流水线;
❑ collect触发流水线执行并关闭它。
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。**除非流水线上触发一个终端操作,否则中间操作不会执行任何处理---流的延迟性,**这是因为中间操作一般都可以合并起来,在终端操作时一次性执行

中间操作

尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并)

在既包含filter又包含map的流水线中,数据流中的元素会依次经过filter和map,即一次循环中元素既经过了filter也经过了map

操作操作参数函数描述
filterPredicateT->boolean接收一个Predicate对象,并返回符合该Predicate的流
mapFunction<T,R>T->R
limit
sortedComparator<T,T>->int
distinct 返回一个由hashcode和equals判断各不相同的流
skip 跳过n个元素,如果元素个数不足n,则返回空流
flatMap 把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流

终端操作

终端操作会从流的流水线生成结果,其结果可以是任何不是流的值

操作目的
foreach消费流中的元素并对其执行lambd,返回void
count返回流中元素的个数,这一操作返回long
collect把流归约成一个集合,比如List,Map,甚至可以是Integer

流的使用

总而言之,流的使用一般包括三件事:
❑ 一个数据源(如集合)来执行一个查询;
❑ 一个中间操作链,形成一条流的流水线;
❑ 一个终端操作,执行流水线,并能生成结果。

第5章 使用流

使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成统一的流中的内容。flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流

另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。

Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本

构建流

  1. 你可以使用静态方法Stream.of,通过显式值创建一个流
  1. 你可以使用静态方法Arrays.stream从数组创建一个流。
  1. API。java.nio.file.Files中的很多静态方法都会返回一个流。
  1. Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate,这两个api可以用来创建无限流

       // 输出偶数
        Stream.iterate(0,n-> n+2).limit(100).forEach(System.out::println);
        // 输出斐波那契数列
        Stream.iterate(new int[]{0,1},t -> new int[]{t[1],t[0]+t[1]}).limit(10).map(t-> t[0]).forEach(System.out::println);
   

但这里使用的匿名类和Lambda的区别在于,匿名类可以通过字段定义状态,而状态又可以用getAsInt方法来修改。这是一个副作用的例子。你迄今见过的所有Lambda都是没有副作用的;它们没有改变任何状态

filter和map等操作是无状态的,它们并不存储任何状态。reduce等操作要存储状态才能计算出一个值。sorted和distinct等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。

0

评论区