函数式接口:只包含一个抽象方法的接口
lambda表达式:是一段可以传递的代码
你最好将一个lambda表达式想象成一个函数,而不是一个对象,并记住它可以被转换为一个函数式接口。
事实上,函数式接口的转换是你在Java中使用lambda表达式能做的唯一一件事。
方法引用:又是要传递给其他代码的操作已经有实现的方法了,这时可以使用“方法引用”,等同于lambda表达式,有3种方式+1构造器引用:
对象::实例方法
类::静态方法
类::实例方法String::compareToIgnoreCase 等同于(x,y)->x.compareToIgnoreCase(y) 第一个参数会成为知心方法的对象
构造器引用:
Stream API
Stream是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,但是将执行操作的时间交给具体实现来决定。例如,如果你希望计算某个方法的平均值,你可以在每个元素上指定调用的方法,从而获得所有值的平均值。你可以使用StreamAPI来并行执行操作,使用多线程来计算每一段的总和与数量,再将结果汇总起来。
[原因]:Stream遵循“做什么,而不是怎么去做”的原则,迭代器意味着特定的遍历策略,禁止了高效的并发执行;
[特点]:1)Stream自己不会存储元素;2)不会改变源对象;3)可能是延迟执行的
[使用]:1)创建一个Stream;2)在一个或多个步骤中,指定将初始Stream转换为另一个Stream的中间操作;3)使用一个终止操作来产生一个结果。该操作会强制它之前的延迟操作立即执行。在这之后,该Steam就不会再被使用了。
使用lambda编程
使用lambda表达式的主要原因:将代码的执行延迟到一个合适的时间点。
所有lambda表达式都是延迟执行的,毕竟,如果你希望立即执行一段代码,那就没必要使用lambda表达式。延迟执行代码的原因有很多,例如
1)在另外一个线程中运行代码:异步
2)多次运行代码:封装
3)在某个算法的正确时间点上运行代码(比如排序中的比较操作):回调/模板
4)当有些情况发生时运行代码(按钮被点击、数据到达等)
5)只有在需要的时候才允许代码
(封装、回调、模板、延迟执行优化)
例子:
如果日志级别设置为忽略INFO消息时,会发生什么呢?该字符串会被计算并传递给info方法,然后再确定是否真的要执行。为什么不能在确定需要打印时,再将字符串合并起来呢?
只有在需要的时候才运行代码,这是使用lambda表达式的一种情况。惯用的办法是将这段代码包装成一个无参数的lambda表达式:
()->"x:"+x+",y:"+y;
现在我们需要编写一个这样的方法:
1)接受lambda表达式。
2)检查它是否应该被调用
3)在需要时调用它
要接受lambda表达式,我们需要选择(在某些极少情况下需要提供)一个函数式接口。我们会在第3.3节中详细讨论如何选择一个接口。在这里我们选择Supplier<String>。以下方法提供了延迟记录日志的能力:
publicstaticvoid inf(Logger logger,Supplier<String> message)
{
if(logger.isLoggable(Level.INFO))
logger.info(message.get());
}
我们使用Logger类的isLoggable方法来决定什么时候记录INFO消息。当需要记录时,我们通过调用抽象方法来调用lambda表达式。
如果可以,请选择已有的函数式接口
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 |
Runnable | 无 | void | run | 执行一个没有参数和返回值的操作 |
|
Supplier<T> | 无 | T | get | 提供一个T类型的值 |
|
Consumer<T> | T | void | accept | 处理一个T类型的值 | andThen |
BiConsumer<T,U> | T, U | void | accept | 处理T类型和U类型的值 | andThen |
Function<T,R> | T | R | apply | 一个参数类型为T的函数 | compose, andThen, identity |
BiFunction<T,U,R> | T,U | R | apply | 一个参数类型为T和U的函数 | andThen |
UnaryOperator<T> | T | T | apply | 对类型T进行的一元操作 | compose, andThen, identity |
BinaryOperator<T> | T,T | T | apply | 对类型T进行二元操作 | addThen |
Predicate<T> | T | Boolean | test | 一个计算Boolean值的函数 | and, or, negate, isEqual |
BiPredicate<T,U> | T,U | Boolean | test | 一个含有两个参数、计算boolean值得函数 | and, or, negate |
接口的默认方法:
在接口中定义方法的诸多变化引起了一系列问题,既然可用代码主体定义方法,那Java8种的接口还是旧有版本中界定的代码吗?现在的接口提供了某种形式上的多重继承功能,然而多重继承在以前饱受诟病,Java因此舍弃了该语言特性,这也正是Java在易用性方面由于C++的原因之一。
语言特定的利弊在不断演化。很多人认为多重继承的问题在于对象状态的继承,而不是代码块的继承,默认方法避免了状态的继承,也因此避免了C++中多重继承的最大缺点。
接口的静态方法:
人们在编程过程中积累了这一一条经验,那就是一个包含很多静态方法的类。有时,类是一个放置工具方法的好地方,比如Java7中引入的Objects类,就包含了很多工具方法,这些方法不是具体属于某个类的。
当然,如果一个方法有充分的语义原因和某个概念相关,那么就应该将该方法和相关的类或接口放在一起,而不是放到另一个工具类中。这有助于更好地组织代码,阅读代码的也更容易找到相关方法。
比如,如果想创建一个简单值组成的Stream,自然希望Stream中能有一个这样的方法。在这再以前很难达成,引入重载接口的Stream对象,最后促使Java为接口加入了静态方法。
Optional
Optional对象有2个目的:1)Optional对象鼓励程序员适时检查变量是否为空,以避免代码缺陷;2)它将一个类的API中可能为空的值文档化,这比阅读实现代码要简单很多???
Optional对象相当于值得容器,而该值可以通过get方法提取。
并行流性能
影响:
1)数据大小:输入的数据的大小会影响并行化处理对性能的提升。将问题分解之后并行化处理,再将结果合并会带来额外的开销。因此只有数据足够大、每个数据处理管道花费的时间足够多时,并行化处理才有意义;
2)源数据结构:每个管道的操作都基于一些原始数据源,通常是集合。将不同的数据源分割相对容易,这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升;
3)装箱:处理基本类型比处理装箱类型要快;
4)核的数量:极端情况下,只有一个核,因此完全没有必要并行化。显然拥有的核越多,获得潜在性能提升的幅度就越大;
5)单元处理开销:比如数据大小,这个一场并行执行花费时间和分解合并操作开销之间的战争。花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显。
核心类库提供的通用数据结构:
1)性能好:ArrayList、数组或IntStream.range,这些数据结构支持随机读取,也就是说他们能轻而易举地被任意分解;
2)性能一般:HashSet、TreeSet这些数据结构不易公平地被分解,但是大多数时间分解是可能的
3)性能差:有些数据结构难以分解,比如,可能要花O(N)的时间复杂度来分解问题。其中包括LinkedList,对半分解太难。还有Streams.iterate和BufferedReader.lines,它们长度未知,因此很难预测该在哪里分解。
流中单独操作每一块的种类时,可以分成两种不同的操作:无状态和有状态。无状态操作整个过程中不必维护状态,有状态操作则是由维护状态所需的开销和限制。如果能避开有状态,选用无状态操作就能获得更好的并行性能。。无状态操作包括map、filter、flatMap,有状态操作包括sorted、distinct、limit
以下内容也不是原创,网上看到忘记原文地址
函数式编程语言是什么?
1)核心是以处理数据的方式处理代码——函数应该是第一等级(First-class)的值,并且能够被赋值给变量,传递给函数等等。[代码/行为即数据,面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象]
2)更有甚者:将计算和算法看得比它们操作的数据更重要。其中有些语言想分离程序状态和函数(以一种看起来有点对立的方式,使用面向对象的语言,这通常会将它们联系得更紧密)。
非函数语言的函数式编程
1)Java 8没有创建新的类型,而是通过编译器将Lambda表达式自动转换成一个类的实例。
2)支持面向对象编程是Java一个主要的优点。函数式编程和面向对象编程并不排斥。真正的风格变化是从命令行编程转到声明式编程。在Java 8里,函数式和面向对象可以有效的融合到一起。我们可以继续用OOP的风格来对领域实体以及它们的状态,关系进行建模。除此之外,我们还可以对行为或者状态的转变,工作流和数据处理用函数来进行建模,建立复合函数。
3)命令式到声明式
命令式编程的核心就是可变性和命令驱动的编程。我们创建变量,然后不断修改它们的值。我们还提供了要执行的详细的指令,比如生成迭代的索引标志,增加它的值,检查循环是否结束,更新数组的第N个元素等。在过去由于工具的特性和硬件的限制,我们只能这么写代码。我们也看到了,在一个不可变集合上,声明式的contains方法比命令式的更容易使用。所有的难题和低级的操作都在库函数里来实现了,我们不用再关心这些细节。
函数式编程
精髓
1)不可变性(不可变值)
2)声明式编程(描述问题,函数)
三大特性
immutable data 不可变数据
1)变量可变不好:a)变量可变的代码会有很多活动路径。改的东西越多,越容易破坏原有的结构,并引入更多的错误;b)有多个变量被修改的代码难于理解也很难进行并行化。不可变性从根本上消除了这些烦恼。Java支持不可变性但没有强制要求——但我们可以。我们需要改变修改对象状态这个旧习惯。我们要尽可能的使用不可变的对象。声明变量,成员和参数的时候,尽量声明为final的,就像Joshua Bloch在” Effective Java“里说的那句名言那样,“把对象当成不可变的吧”。当创建对象的时候,尽量创建不可变的对象,比如String这样的。创建集合的时候,也尽量创建不可变或者无法修改的集合,比如用Arrays.asList()和Collections的unmodifiableList()这样的方法。
2)没有副作用的函数:避免了可变性我们才可以写出纯粹的函数。没有副作用的函数推崇的是不可变性,在它的作用域内不会修改任何输入或者别的东西。好处:a)可读性强,错误少,容易优化;b)由于没有副作用,也不用再担心什么竞争条件或者并发修改了。可以很容易并行执行这些函数。
3)优先使用表达式:语句是个烫手的山芋,因为它强制进行修改。表达式提升了不可变性和函数组合的能力。比如,我们先用for语句计算折扣后的总价。这样的代码导致了可变性以及冗长的代码。使用map和sum方法的表达性更强的声明式的版本后,不仅避免了修改操作,同时还能把函数串联起来。写代码的时候应该尽量使用表达式,而不是语句。这样使得代码更简洁易懂。代码会顺着业务逻辑执行,就像我们描述问题的时候那样。如果需求变动,简洁的版本无疑更容易修改。
first class functions
使函数就像变量一样来使用。也就是说,你的函数可以像变量一样被创建,修改,并当成变量一样传递,返回或是在函数中嵌套函数。
尾递归优化
如果递归很深的话,stack受不了,并会导致性能大幅度下降。而尾递归优化技术——每次递归时都会重用stack来提升性能,这需要语言或编译器的支持。
几个技术
1)map&reduce:对一个集合做Map和Reduce操作。这比起过程式的语言来说,在代码上要更容易阅读。(传统过程式的语言需要使用for/while循环,然后在各种变量中把数据倒过来倒过去的)
2)pipeline:把函数实例成一个一个的action,然后,把一组action放到一个数组或是列表中,然后把数据传给这个action list,数据就像一个pipeline一样顺序地被各个函数所操作,最终得到我们想要的结果。
3)recursing 递归 :递归最大的好处就简化代码,把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。
4)currying:把一个函数的多个参数分解成多个函数,然后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数这样,可以简化函数的多个参数。
5)higher order function 高阶函数:所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出,就像面向对象对象满天飞一样。
6)lazy evaluation 惰性求值:需要编译器的支持。表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值,也就是说,语句如x:=expression; (把一个表达式的结果赋值给一个变量)明显的调用这个表达式被计算并把结果放置到 x 中,但是先不管实际在 x 中的是什么,直到通过后面的表达式中到 x 的引用而有了对它的值的需求的时候,而后面表达式自身的求值也可以被延迟,最终为了生成让外界看到的某个符号而计算这个快速增长的依赖树。