本期的话题是ModernJava
目录
前言
一、重新认识JAVA
二、 Lambda表达式
2-1 匿名内部类的问题
三、常用函数式接口
四、组合异步编程与新API
五、总结
前言
因有感直播中接收知识点过于密集,单纯听一听吸收下来的并不多,遂综合直播回放和文字将其作为学习笔记,以慢慢消化。下文主要内容为老师讲解的内容,我做资料查找和补充工作。
本期嘉宾:湘王(下文简称XW)、苍狼、团子
视频回放:http://s5j.net/bc9mm
推荐结合视频回放和笔记内容,会有较好的学习效果。
以下是正文部分
一、重新认识JAVA
XW:
Modern Java这个概念是这几年才提的比较多的一个概念,以及与之相关的函数式编程接口和Lambda表达式,在Java中新出现的组合异步编程和一些新的API。
Java和大数据其实也有自己的「行话」,但并不是像网络安全那样搞三个人出来,而是通过另一种方式。
比如如果我说juc,稍微资深一点的Java工程师一听就知道我说的是什么。再比如S0S1,还有Eden、Stop The World等等,Java高玩也肯定不陌生。
👇为笔者注:
1. JUC 简介(注:可直接点击,会跳转至我查找到的解释原文)
- 在 Java 5.0 提供了
java.util.concurrent
(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,
用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中
的 Collection 实现等;2.S0S1
- S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
- S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
3.Eden
- 因为采用复制算法,所以年轻代分为三部分:1个Eden区和2个Survivor区(分别叫From和To),默认比例为8:1。
- 所谓的Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。
在大数据领域,WC、MR、算子,这些只要是稍稍搞过一点大数据的童鞋,应该一听就懂是什么。
前面这两个可能浅一点,但是如果我说并行度、流分组、数据倾斜,这个就需要一点功底才知道了。
或者就像搞过电商的童鞋一听SKU、SPU就知道是啥。
👇为笔者注:
1.WC
- 统计文件里面有多少单词,多少行,多少字符。
2.MR
- MR理论:MapTask & ReduceTask
3.算子
- 在数学上可以解释为一个函数空间到函数空间上的映射O:X->X,其实就是一个处理单元,往往是指一个函数,在使用算子时往往会有输入和输出,算子则完成相应数据的转化,比如:Group、Sort等都是算子。
Java是一个静态类型的、强类型和解释型的语言,这些都代表什么意呢?
解释型就是说想Java源代码其实是没有被计算机转换成可执行文件的,也就是字节码。
像之前学过的C语言,源代码就必须通过make编译成目标文件以后才能执行,但是Java就不用先编译之后才执行,因为Java是跨平台的,有JVM,所以可以在运行的时候再去解释。
👇为笔者注:
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
强类型比较好理解,例如javascript,比如我用var定义一个变量,虽然之前存的是数字,但是后面还可以存字符串或者是其他类型。
所以js就是一个弱类型语言,但是像Java是需要明确定义数据类型的,而且一旦定义之后就不能改成其他数据类型了
举个例子:这有点像结婚证~没拿之前,你交男女朋友都没有约束力,想和谁做朋友都可以(笔者注:可以,但不提倡),这就是弱类型。
但是一旦拿了结婚证办了酒呢,就不能想换个人就换个人了,这就是强类型
静态语言是你在写代码的时候就能知道数据类型有没有报错,就像你用idea写java代码的时候,就能检查出来语法错误,但是你用webstorm写js代码,默认情况下它是不会检查你的语法错误的,即使是选了JavaScript语法,也不会检查的。
Modern Java中的Modern又是什么意思呢?
Modern的字面意思就是现代,更深一层的意思就是先进的,Modern Java就是更先进的Java的意思。而且也主要是从Java8之后才开始叫起来的。
那Java8之前就不叫Modern Java了吗?
因为先进不仅意味着能够很好地解决当前的问题,还能够解决未来可能遇到的问题,例如更稳定、更简单、可扩展,等等等等。
而Java8就达到了这样的标准。而Java8之所以被称之为Modern Java,不是因为它仅仅是Java7的升级版,而是它还是Java7的重生版
请注意,是「重生版」!
为什么这么说呢?
因为其实「函数」这个概念可能从编程语言诞生的那天起就有了,很多同学第一次学习编程的时候,例如C语言里面的函数,和Java里面的类/对象方法本质上是一回事。
但是和C语言、JavaScript等语言不同的是,在Java8之前的版本中,函数只是Java的附属品,根本没有独立存在的资格。
如果大数据技术没有出现,不知道这种状况还会持续多久
难道Modern和大数据也有关系吗?
肯定有关系的。Java是一门纯面向对象语言,这个在OOD和OOP的时代,OOD呢就是面向对象的设计,OOP就是面向对象编程,也就是在十多年前吧,因为那时候纯面向对象的编程语言不多,所以那时候的Java完全是巨星闪耀,一枝独秀的感觉。
但是时过境迁,云计算和大数据技术出现了,在这种情况下,如果只是要处理大量的数据而业务要求非常简单的情况下,面向对象不仅没有任何好处,反而还会造成拖累。
因为大量的代码都和数据处理这件事情关系不大,但按照Java的语法规定却又不得不写,比如Java中大量的数据操作,其实都是通过集合来完成的,比如List、比如Map和Set,这些工具在大数据技术到来之前倒并没有觉得特别不方便的地方。
但是大数据里面的MR数据处理过程,就把这个完全颠覆了,也不是颠覆吧,就是使用方式感觉就不一样了。
比如一个简单的WC,就是wordcount,对集合的使用就和以前的代码完全不一样,尤其是像Spark、Flink这种流式及流批一体的大数据计算引擎中对各类算子的使用,异常复杂,用传统Java的集合处理API完全无法胜任,这是一方面。
另一方面,像Python、Scala等脚本类编程语言在大数据环境下,由于编码方式简单,只需要写点简单的功能函数,就能马上看到结果了,简直是如鱼得水,天然就适合做大数据功能的开发。
所以这样一来二去,两相对比之下,Java的在这方面的弊端就太明显了。
所以,开发出Java的工程师们也看到了这些情况,就干脆给Java来了个大手术,把以前类和对象里面的方法,或者叫函数,直接提升成了一等公民。
所以能够处理大数据的Java就是Modern Java了吗?
部分原因吧。但是却是影响最为深远的决定性的原因。它直接促成了Modern Java函数式编程的诞生。
也就是说,Java8之后的Java,不但依旧可以用OOD、OOP的方式编程,而且还多了一个新的,也非常强大的功能特性,那就是面向函数编程OFP,或者也叫函数式编程。
不过,还有另一个可能是同样重要而且同样影响深远的决定性因素,只是目前还不太明显而已。
如果就是这样的要求,确实很难玩出新花样,毕竟需求太简单,体现不出水平来。好,那这样,我问一下各位在线的小伙伴,如果需求复杂一点,比如现在有几家大厂的员工之间不定期搞一下联谊聚会活动,解决一下单身问题。但是只有通过公司才能找到员工,然后员工信息有年龄、是否已婚等内容,如果让你来做一个未婚员工的筛选,再根据年龄排序,然后全部放到一个列表里面,你该怎么实现呢?再加个变态的要求:用一行代码实现。
在实际开发中,这种对集合的操作是每个Java工程师每天都要处理的事情,但是大多数工程师,至少我曾经面试过的Java工程师,或者说程序员吧,大部分都没用过Java8,有的甚至听都没听说过,真的有点让人震惊。
Java8最主要的变化就是函数式编程,而这其中最为主要和最为核心的部分,可能也是大多数人都觉得比较难理解地方,就是我刚才说的,用一行代码解决需求的Lambda表达式。
二、 Lambda表达式
Lambda表达式之所以叫Lambda,是因为λ演算在计算机存在之前就已经存在了,它是由数学家阿隆佐·邱奇从数理逻辑中发展演变出来的,并且任何可计算函数都能用这种形式来表达和求值。
而且Lambda演算强调的是变量、谓词(动词)、量词和规则的运用,而非实现它们的具体机器。
👇为笔者注:知识点补充Lambda
Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。
不采用Lambda的老方法:
Runnable runnable1=new Runnable(){
@Override
public void run(){
System.out.println("Running without Lambda");
}
};
采用Lambda的新方法:
Runnable runnable2=()->System.out.println("Running from Lambda");
这种思想和开发出Java8的工程师们不谋而合,而且也为了纪念阿隆佐·邱奇这位数学家的天才发现,不光Java8里面,现在只要某个语言中涉及到函数式编程,统一都叫Lambda表达式。比如,Python里面也有Lambda表达式。
计算机科学里面其实有很多很有意思的趣事,大家可以当作学习之余的一点谈论交流话题吧。
IT行业里面有一句话:Talk is cheap,show me the code,翻译成白话过来说就是,能动手咱就别BB。所以现在团子给大家展示一下,看看Java8之前,工程师们都是怎么开发的。
(演示建议观看直播回放)
感谢团子的演示,咱们可以简单看看代码,这段代码,不管Modern不Modern,肯定是都能运行的。不过按照Java标准语法来看,这里面有几行代码是没有意义的,也就是不够优雅~
对,就是优雅,也就是太笨重,而且不灵活。
为什么这么说呢,因为这些代码是和实际功能没有很强的关联关系,这些代码已经不可能再去优化它了,否则功能都不完整。
比如代码里面的setLayout、setTtitle等等set方法,还有一些创建jbutton的方法,以及给按钮增加点击事件的方法,就是addActionListern()方法。
这其实就是当初Java8的开发者所面临的问题,该从哪里开始优化代码呢?
大家可以把自己想像成Java8项目开发组的成员,就是由你来开发Java8这门语言的编译器,想想如果是你,你该怎么优化Java,让他变得更好更强大,然后可以对别人说:「我会Python,我会C,(唉~)就是不用,(就是玩~)」。(括号内内容是笔者瞎写的,略)
既然我们已经知道,业务代码,就是那些创建环境、给对象赋值、然后调整大小、位置的代码是不能被优化的,那么很明显,真相只有一个~
2-1 匿名内部类的问题
做Java时间长一点的同学都知道,在Java中,有一种类叫「匿名内部类」,就是刚才代码里面给按钮1和按钮2增加点击事件的时候用到的那个ActionListener,还有创建线程的时候用到的Runnable,这种匿名内部类的特点就是没有赋值给一个明确的变量,比如创建按钮时就通过代码new JButton("button1")明确地把对象的引用赋值给了jb1和jb2,而像匿名内部类这种语法的设计初衷,就是为了方便Java程序员将代码作为数据传递进去,其实就是想达到「代码即数据」的目的。
说的直白一点,就是把new ActionListener() {}这段代码块作为数据,传给jb1或者jb2对象。
如果是我,我会觉得最外面那个new ActionListener() {}有点多余,原因有几点:
1、因为ActionListener是以匿名类的方式实现的,既然它都已经不想让人知道自己干了什么,退出江湖了,又何必再露脸呢,对吧。
2、因为button的addActionListener里面已经要求了ActionListener作为参数,所以其实只要是个稍微聪明点的编译器,它自己就应该能够推断出来需要啥玩意,不用我再来告诉它一次,有点多此一举了。
3、第三点就是,其实咱们写代码,最终是只需要两个东西的,也就是数据和结果,而且这个类本身也是匿名的,所以它完全可以去掉,去掉以后应该是这样的。
public是多余的,因为只是作为参数了,有没有public没啥区别,所以去掉;
然后void也是多余的,编译器可以推断出来有没有返回值,以及它的返回值是啥;
然后方法名actionPerformed也是多余的,因为这段代码已经被赋值给「代码块」了,所以它不需要,也不配拥有自己的名字了。
删光不是目的,目的是要达到咱们优化的目的。
进行到这一步,我想Java8开发组的工程师们已经知道自己在干嘛了。
刚开始只是尝试着找到优化的方向,另外说一句,咱们可能用了不到几分钟就做到了这一步,那些工程师们可是呕心沥血了几年才达到了这一步的,所以在这里,我们需要向那些优秀的工程师们致敬,感谢他们为我们这些程序员所做出的付出,为整个Java开发社区和生态所做出的贡献。
然后,和返回类型一样,需要什么类型的数据编译器也应该是能够自己推断出来的,所以括号里那个ActionEvent也可以省略了。
有些像我这样追求完美的工程师会觉得这还不行,太丑了,这种形式看起来真的特别别扭,不知道其他人会不会这么觉得,反正我是这么觉得。如果是我的话,怎么能写出这么丑陋的代码来呢,我会继续把它打扮漂亮一点。
大功告成。大家可以看团子的桌面,对比一下之前的代码和现在精简的代码。这就是Java8中标准的Lambda表达式。也许之前的那些工程师经历了和我们一样的过程。
Lmabda表达式改造完了,但是咱们改造的目的不就是为了运用吗,对吧。所以,咱们应该马上趁热打铁,让团子把第二个按钮再按第一遍的形式改一下。
这段代码其实还有点小尾巴,就是多线程那一块,留着给大家用Lambda的方式去优化的哈。还是老规矩,如果看完咱们今天的直播以后,有小伙伴需要源码的,就在微信群里打0.618,如果统计出来有超过50位小伙伴,就让团子把源码发到群里。
大家可以看到哈,从去掉new ActionListener() {}开始,到去掉public,去掉void,去掉方法名actionPerformed(),去掉参数ActionEvent,最后把为了美化代码,增加了「->」符号,最终,咱们实现了将代码作为数据传递的目的,而且是以一种非常优雅的方式完成的。所以总结一下,Lambda表达式的特点:
一是匿名,没有名字,只有代码块;
二是它基于接口函数执行,而不是类;三是它传递的是行为和数据,而不是代码,这几点,只有在理解的基础上才能记住。
这种带个箭头的语法是不是可以到处用?或者说我想在哪加都行呢?
这其实就是函数式编程中比较重要的另一块了,Lambda可不是你想写,想写就能写的。刚才咱们之所以能够省掉那么多的代码,其实是开发Java8的工程师们给咱们提前就准备好的语法规范。
这个规范就叫函数式接口。说白了,就是专门用来写lambda表达式的接口,这种接口里面只有一个方法,记住,只能有一个。
虽然Java8之后的版本里面加入了默认方法,但是函数式接口里面只应该有一个待实现的非默认方法,这一点很关键,各位同学可以记下来,这是一条函数式编程的语法约束。
大家可以看到,比如有的函数式接口只有一个参数,那么就是arg1,如果有两个参数就是arg2,依此类推。如果有返回值的话,也要根据情况来区分。
这就是按照上面的「形式」列表归纳的几个常见的函数式接口,而且有些接口其实在Java8之前就已经存在了,这样也极大地平缓了程序员学习函数式编程的曲线,所以有时候真的是不得不佩服和感谢那些天才的工程师们,没有他们,我们不可能像今天这么方便。
刚才咱们的代码里面就有和第一个Runnable相关的函数式接口吧,就是「() -> void」,而且之前给大家留的一个小任务,多线程那块改成lambda表达式的,也是这个。至于后面两个,其实大家看看JDK的源码就知道了。
三、常用函数式接口
虽然可以自己定义函数式接口,但是开发Java8的工程师们已经预先替咱们程序员考虑到了使用Lambda表达式的若干种场景。所以,其实大部分情况下,都不用自己再去定义函数式接口了。
大家看着这张表格,这些都是Java8里面已经事先定义好的函数式接口,这些接口都在Java8的juf包里面。
方法名就是函数中的那唯一一个接口方法,然后描述符就是Lambda表达式的抽象描述,比如对于咱们自定义的MyFirstFunctionInterface函数式接口,它的Lambda表达式是(content) -> System.out.println(content);
那么它的抽象描述就是(T) -> void,就是这个意思,然后按照Lambda的类别来分的话,它属于消费型,因为它接收了一个参数却没有返回任何数据;相对应的,第一行的() -> T就属于供给型的,因为它不需要任何参数,但却返回了一个泛型T,依此类推。
四、组合异步编程与新API
接下来我们来说说组合异步编程与新API。
关于咱们这次的Modern Java函数式编程的技术分享中,还有最后一个部分,那就是Java8中新出来的一些特性,其中比较重要的就是两个部分:
一是组合异步编程,这个在Java8中是新的CompletableFuture API;
二是新的时间日期API。
其实组合异步编程的知识更多适合在更高级的部分来讲解,至于什么是更高阶的部分,这里先暂时保密。今天只把组合异步编程的基础部分说一下。
在开发的时候也会遇到这种场景,比如当有用户请求时,服务器如果一直保持连接,等用户输入完信息并且发送以后才去干别的事情,那么当有其他用户请求时,肯定是无法及时处理的,那么这个新用户的体验就会非常差,这个就是传统的阻塞式I/O干的事情,然后为了改善用户体验,服务器端只要知道有用户请求就会立即响应请求新的请求,不管现在的用户是不是正在输入数据还是在干别的。
等用户输入完要发送了,再通知服务器说我输完了,你发吧,这时候服务器再来发,就不用干等了。这样一来,响应速度和处理效率就会大大提升,这就是非阻塞I/O,在Java中也叫NIO。这是一种典型的并行处理方式。就是客户端输入数据和服务端处理数据其实是并行着的。
所谓并发,就是指一瞬间,资源被大量请求占用,比如CPU内核和内存或者磁盘上的数据,这些请求虽然在宏观上感觉是同时发生的,但在微观上仍然是串行顺序执行的。
然后并行就完全是同时进行的了,不存在微观上顺序的问题,不管宏观微观,都是同时发生,就比如当用户在输入数据时,服务器在处理其他任务。所以如果再看到有资料或其他人说什么提高并发访问量,或者说提高并行度,您就应该理解这后面是什么意思了吧。
并发可以通过多线程去控制它,而并行可以通过回调进行控制。这两点单独讲都是很大的主题,内容都特别多,所以在这里不可能把它们讲的特别清楚,只能点到为止,也算是给大家做个科普。
其实现在计算机硬件成本已经很低了,像我现在用的苹果笔记本macbook pro就是i9八核、64G的内存,几乎已经感觉不到什么并行和并发的区别了。
这是一方面,另一方面,其实并发是需要尽量控制或避免线程阻塞对资源造成的浪费,从这个角度来说,程序也是需要某种方法来控制响应的。比如,Java从JDK1.5开始就提供了Future类。
只不过这里面有些问题,举个例子来说,在物联网开发领域,如果我们要采集大楼内所有烟感设备的感应数据,那么就需要持续不断地采集,如果采集一次就不能再次采集了,肯定是不能满足要求的,如果用Future实现这个功能,会有严重的缺陷,因为Future是一个「一次性」的对象。
这里的一次性并不是说这个对象只能用一次,然后要销毁重新生成,而是说这个对象里面的获取结果的方法,只能调用一次,之后再去调用就拿不到任何数据了。
很明显哈,我用Future第一查询数据的时候,是能够得到数据的,然后第二次问,就得不到数据了。但是用CompletableFuture就不一样,它可以反复得到数据,大家可以细品其中的区别。
其实CompletableFuture对Future来说,不止是加了个Completable这么简单,而是一些根本性的变革。至于为什么,咱们再下一期继续来说,这里先卖个关子。
然后Java8里面也增加了一些新的时间日期API,主要是由于一些历史遗留原因,导致Java中的Date和Calendar类对时间日期的处理,包括像一些时区、格式化、转换、线程安全性考虑以及日期计算方面都比较麻烦,所以从Java8开始,JDK提供了一些新的工具来解决这些问题,例如像什么LocalDate、LocalTime、LocalDateTime、Instant、Duration以及Period等等。
除了时间日期,还有一个比较独特的新API,就是Optional这个全新的类,这个类其实就是为了解决大量的防御性代码问题的,什么是防御性代码呢?
比如有个接口,用户会输入手机号、密码、验证码等等信息,OK,咱们为了保证这个接口不报错,那肯定要先保证数据不会出问题对吧,行,那就每个数据都来个检查好了。
比如手机号,要判断有没有输入对吧,输入了,是不是一个正确的手机号,长度、正则啊什么的,都要检查一遍,加几个判断条件就行了,也就是if...else把,好,然后密码什么的也这样搞一遍,其实我们可以看到,真正实现业务功能的代码可能就一两行,但是这种防御性代码,像什么if...else可能会有一大堆,完全把业务部分都淹没了,这就是防御性代码。
Optional虽然不解决业务数据是否合法,但是却可以从技术上解决NPE问题,什么是NPE?搞Java的同学不可能不遇到NPE。不过有些童鞋刚开始可能会不习惯,没关系,像Lambda一样,慢慢适应就好了。
👇为笔者注
NPE是指编程语言中的空指针异常
五、总结
当然也可以不总结的,但整个直播讲的对我来说新知识量大于旧知识量,即使把这个笔记梳理后,也没有完全消化。但同时确确实实对一些java的历史和概念性问题,如,Java是一个静态类型的、强类型和解释型的语言、Modern Java的含义和由来以及指向的问题,有了一些认识。再如Lambda,当初自学Python的时候就已经遇到这个概念了,但当时并没有搞懂,一晃而过了。现在算是再次理解这个概念了,不过仍需要在代码中多实践几次,去体会理解。并发和并行的概念,老师上课时曾经讲过,这里做重温,也算有一番新的体会。以及比较晚了,大概就说这样多。