Java 帝国之函数式编程(下)

Posted 码农翻身

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 帝国之函数式编程(下)相关的知识,希望对你有一定的参考价值。

上一篇文章《 》说到Java帝国为了迎合函数式编程的狂热分子,决定为Java语言加上Labmda表达式。

同时为了展示“声明式”编程的魅力, 小码哥决定加上一个新的概念: Stream。

等到一切准备停当,  我们召开了一场开发者大会, 决定隆重推出Java 函数式编程。 
1. Stream
小码哥首先展示了Labmda表达式 和 类型推断, 大家还比较满意, 台下有人说:

“嗯, 和微软的C#长的挺像的, 人家用 =>   ,我们用 ->   ”

等到小码哥开始介绍Stream的时候, 下面就有人发难了:

“我们不是已经有IO流了吗, 字节流,字符流, 怎么又搞个流出来?”

小码哥赶紧解释: “这个Stream和IO流是不一样的, 这是个新的概念, 主要是为了实现LISP中常见的延迟计算(或者惰性求值)的功能”

看到下面的人开始交头接耳, 小码哥说: “大家别着急,先看看这一段代码:”

public class EvenNumber implements 
                    Supplier<Long>{
    long num = 0;
    @Override
    public Long get() {
        num += 2;
        return num ;
    }    
}

如果持续调用这个类的get()方法, 就可以得到所有的偶数, 现在我们就可以用它产生一个流:

Stream<Long> numbers = Stream.generate(
new EvenNumber());

这个Stream 其实就代表了一个无穷无尽的偶数序列, 只是它还没有计算出来而已( 惰性的/延迟的)
如果试图打印每个元素,那就开始计算了,  你就发现它会一直运行下去

numbers.forEach(x->System.out.println(x));

当然,无限的序列是无意义的, 各位在编程中肯定会限定长度的: 

numbers.limit(5).forEach(x->System.out.println(x));
输出: 2 4 6 8 10
2. 延迟计算
现在台下的人安静了,  小码哥有了信心, 开始介绍对Java 集合框架的改进:

“为了方便大家使用函数式风格,我们对集合框架中的类库做了极大的增强, 每个集合都可以变成一个stream , 然后就可以使用那些著名的map , reduce , filter 等函数了”

Arrays.asList("Hello","Java8","Java7"). stream()
            . map(s -> s.toUpperCase())         

map 是个高阶函数, 它接受了一个Labmda表达式(匿名函数)作为参数, 把Stream中的元素做了变换: 字符串变成了大写 , 然后返回了一个新的Stream 

这也是延迟计算, 即使你加了一个打印语句, 也不会有任何任何输出:

 Arrays.asList("Hello","Java8","Java7").stream()
    .map(s -> {
            System.out.println(s);
            return s.toUpperCase();
            });

由于map 返回了一个新的Stream, 可以在新的Stream上继续操作, 例如filter :  把以J开头的字符串找出来,  filter 的结果仍然是个Stream.

Arrays.asList("Hello","Java8","Java7").stream()
     . map(s ->s.toUpperCase())
     . filter(s -> s.startsWith("J"));

最后你就可以用forEach 输出了,和惰性求值相反, forEach 是个立即求值的函数:

Arrays.asList("Hello","Java8","Java7").stream()
    . map(s ->s.toUpperCase())
    . filter(s -> s.startsWith("J"))
    . forEach(s -> System.out.println(s));

如你所料,  这里的输出是:
JAVA 8
JAVA 7

台下有人举手问到: “既然是延迟计算, 那列表中的元素在流中到底是怎么处理的?”

小码哥回答:“这是个好问题, 我们加点打印语句看看:”

Arrays.asList("Hello","Java8","Java7").stream()
        .map(s -> {
             System.out.println("map: "+ s);
            return s.toUpperCase();})
        .filter(s -> {
             System.out.println("filter:"+ s);
            return s.startsWith("J");})
        .forEach(s -> System.out.println(s));

系统的输出是这样的:
map: Hello
filter:HELLO
map: Java8
filter:JAVA8
JAVA8
map: Java7
filter:JAVA7
JAVA7

由此可以看出, 系统先取到初始Stream 中的第一个元素“Hello” , 做map操作,变成 "HELLO",  然后传递给 filter , filter 做了判断, 发现不是以"J" 开头的字符串, 立刻停止, 不会走到forEach那里。

接下来取第二个元素"Java8 ", 再经过map, filter, 这时候发现符合规则, 就走到了forEach 那里, 打印出来了。 

对第三个元素“Java 7” 也是类似处理

还有人问道: “s ->s.toUpperCase()  是不是和java.util.function.Function  这个接口相匹配?

小码哥说: “是的, 这是JDK内置的一个接口, 还有那个s->s.startsWith("J")  和 java.util.function.Predicate  匹配,  这是为了方便大家编程, 不用自己写函数接口了”

“除了map, filter, 还有哪些可以用的内置函数?”

小码哥说: “我们内置了很多, 像reduce , max ,min , collect, flatMap, 大家可以看看我们会议分发的手册, 使用这些函数, 我们就不用考虑集合处理的细节了, 基本上能做到声明式编程了。”

我看出大家有点小失望, 毕竟和纯正的函数式编程相差还比较远, 只能说是给面向对象的Java 增加了一点函数式的特性。 
3. 函数式编程的好处
这时候台下有个热爱Ruby 的家伙叫了起来: “啊 !  我知道怎么利用Java函数式编程写出Ruby 风格的代码了,举个例子”

public class Connection {
    public void  open(){
        System.out.println("Open connection");
    }
    public void  close(){
        System.out.println("Close connection");
    }
    public void  read(){
        System.out.println("Read from connection");
    }
}

正常的使用是这样的, 调用方很麻烦, 得用try finally 确保连接关闭。

Connection conn = new Connection();
conn.open();
try{
    conn.read();
 }finally{
     // 一定得确保连接在finally中关闭
    conn.close();    
}

有了函数式编程, 我们可以在Connection 添加这么一个函数:
public class Connection {
    ......

 注意这个方法, 它已经把打开连接,关闭连接搞定了    

public static void open(

       Consumer<Connection> consumer){

        Connection conn = new Connection();
        conn.open();
        try{
             consumer.accept(conn);
        }finally{
            conn.close();
        }
    }
}

然后调用方就简单了: 
Connection.open(conn -> conn.read());

用这种方式, 调用方根本不用处理连接的打开和关闭问题了 !  只需要关注自己想要调用的逻辑
实在是太爽了!
 
这和Ruby 风格非常像 :
Connection.open do |conn|
    conn.read
end

我和小码哥都大喜过望: 没有想到还有这么一个同盟军 ! 

另外一个家伙说:“这的确是个好例子, 其实大家想想在java 世界大行其道的设计模式, 很多时候设计模式就是想把一个行为封装起来, 到处传递而已, 只是当时我们没有函数式编程的概念, 只好用个类来封装一个行为,显得很笨拙, 最典型的就是策略模式。”

小码哥回应说: “对的, 大家可以充分的发掘一下函数式编程的威力, 不但可以简化集合框架的操作, 还能简化代码,  简化设计模式, 甚至也能向Ruby那样,开发出领域特定语言(DSL)出来 。 ”
4. 并行
我看发布会临近尾声, 小码哥竟然还没有介绍并行化, 赶紧提醒他: 小码,快讲讲数据并行化啊。
 
“对了, Java 函数式编程还给大家提供了另外一件福利:并行化“ 小码哥终于要补上这一项了

“把代码变成并行化代码异常简单, 只需要把stream()操作改成 parallelStream() 就可以了, 例如下面这个计算素数的程序”

List<Integer> numbers= .....
List<Integer> prims = numbers. parallelStream()
    .filter(i -> Util.isPrim(i) )
    .collect(Collectors.toList())

“再比如对数组的并行排序:Arrays. parallelSort(), 只需要做一点点改动, 剩下的工作就交由Java 来完成了, 我们会把数据自动进行分块, 分配到各个CPU核心上去运行, 最后把结果收集回来, 一个简单的变化就能极大的提升性能。”

我听到台下响起了欢呼声!

但是小码哥接着就泼了一盆冷水: ”使用并行stream的时候要注意,它不一定100%能提高性能,因为这依赖很多因素 ,例如输入的数据是否容易分解成块, 是不是CPU密集型的任务, 有没有IO等待操作等等...... ”

我就知道技术人员太老实 ,不会忽悠, 眼瞅着热烈的气氛要冷却下来,  我赶紧上台, 抢过话筒说: “这些细节大家下来再和小码哥聊吧, 我们今天的发布会就到这里, 再见。” 

Java 的函数式编程就这么发布了, 帝国程序员的工具箱里又多了一件工具,  虽然不是纯正的函数式编程, 但我们确实可以用它来写出更简洁的代码, 希望大家能够喜欢它。 

声明:原创文章,未经授权,禁止转载

你看到的只是冰山一角, 更多精彩文章,尽在“码农翻身” 微信公共号, 回复消息“m” 或者“目录” 查看更多文章。


公共号:码农翻身
“码农翻身”公众号由工作15年的前IBM架构师创建,分享编程和职场的经验教训。


以上是关于Java 帝国之函数式编程(下)的主要内容,如果未能解决你的问题,请参考以下文章

java之Lambda函数式编程最佳应用举例,链式语法「真干货来拿走」

Java 之 函数式编程

Java技术分享之函数式编程

函数式编程之实践

java函数式编程之Supplier

优雅编程之函数式接口