JDK1.8新特性中最值得注意的当然就是Lambda表达式了,还有新增了很多关于Lambda表达式的新特性,比如函数式接口、方法引用和构造器调用。处理之外还有Stream API、接口中的默认方法和静态方法、新的时间日期API,HashMap以及ConcurrentHashMap结构的更改等等。
1、Lambda表达式
(1)什么是Lambda表达式呢?Lambda表达式本质上就是一段匿名内部类,也可以是一段可以传递的代码。
语法如下:
(2)Lambda表达式到底方便在哪里?举例说明
我们发现实际上这些过滤方法的核心就只有if语句中的条件判断,其他均为模版代码,每次变更一下需求,都需要新增一个方法,然后复制黏贴,假设这个过滤方法有几百行,那么这样的做法难免笨拙了一点。如何进行优化呢?
a.使用设计模式
看起来好像代码更多了一点,不过这是在只有两个例子的情况下,如果有100个过滤那么会发现还是设计模式比较简洁一点,而且设计模式好在的地方是对代码进行了整合,看起来逻辑更简洁明了。不过对于程序员来说,还是要搬运很多代码,接下来进一步优化
b.使用匿名内部类
匿名内部类:没有显示名字的内部类
本质:匿名内部类会隐式得继承一个类或实现一个接口,或者说,匿名内部类时一个继承了该类或者实现了该接口的子类匿名对象
格式:new 类名/接口/抽象类(){}
使用匿名内部类,就不需要每次都新建一个实现类,直接在方法内部实现。看起来是不是更简洁明了一点。
c.使用Lambda表达式
Lambda表达式就是一段更加简洁明了的匿名内部类。
d.还可以使用Stream API
2、函数式接口
问题一:为什么要新增函数式接口?
答案是为了配合lambda表达式使用,当接口中存在多个抽象方法的时候,单纯使用lambda表达式并不能智能匹配对应的抽象方法,因此引入了函数式接口的概念
问题二:什么是函数式接口
概念:只定义了一个抽象方法的接口(Object类的public方法除外),就是函数式接口,并且还提供了注解:@FunctionallInterface
理解:首先要理解什么是函数,这里的函数跟编程中的函数并不一样,而是与数学中函数的概念一样——一种映射的关系,接下来看看分类就就很明了了。
常见的四大函数式接口:
a.Consumer
b.Supplier
c.Function
d.Predicate
在四大核心函数式接口基础上,还提供了诸如BiFunction、BinaryOperation、toIntFunction等扩展式的函数式接口,都是在这四种函数式接口上扩展而来的
三、方法引用和构造器调用
方法引用和构造器调用是在基于Lambda表达式的基础之上的:若lambda体中的内容有方法已经实现了,那么可以使用"方法引用"。可以理解为方法引用是lambda表达式的另外一种表现形式并且其语法比lambda表达式更加简单
1、方法引用
三种表现形式:
a.对象::实例方法名
b.类::静态方法名
c.类::实例方法名(lambda参数列表中的第一个参数是实例方法的调用这,第二个参数是实例方法时可用)
2、构造器调用
格式:ClassName::new
还有一种数组引用
格式:Type[]::new
4、Stream API
Stream操作的三个步骤:
(1)创建stream
(2)中间操作(过滤、map)
(3)终止操作
还有功能比较强大的两个终止操作 reduce和collect
reduce操作: reduce:(T identity,BinaryOperator)/reduce(BinaryOperator)-可以将流中元素反复结合起来,得到一个值
并行流和串行流:在jdk1.8新的stream包中针对集合的操作也提供了并行操作流和串行操作流。并行流就是把内容切割成多个数据块,并且使用多个线程分别处理每个数据块的内容。Stream api中声明可以通过parallel()与sequential()方法在并行流和串行流之间进行切换。
jkd1.8并行流使用的是fork/join框架进行并行操作
ForkJoin框架:在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join汇总
关键:递归分合、分而治之
采用”工作窃取“工作模式(work-stealing):当执行新的任务时,它可以将其拆分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放到自己的队列中
相对于一般的线程池实现,fork/join框架的优势体现在对其包含的任务的处理方式上
一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续,那么该线程会处于等待状态
fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行,那么处理该子问题的线程会主动寻找其它尚未运行的子问题来执行,这个方式减少了线程的等待时间,提高了性能
五、接口中的默认方法和静态方法
在接口中可以使用default和static关键字来修饰接口中定义的普通方法
在JDK1.8中很多接口会新增方法,为了保证1.8向下兼容,1.7版本中的接口实现类不用每个都重新实现新添加的接口方法,引入了default默认实现,static的用法是直接用借口名去调方法既可。当一个类继承父类又实现接口时,若后两者方法名相同,则有限继承父类中的同名方法,即”类优先“,如果实现两个同名方法的接口,则要求实现类必须手动声明默认实现哪个接口中的方法
六、新的时间日期API
1、使用过时间日期API的都知道,之前使用的java.util.Date月份从0开始,我们一般会+1使用,很不方便,新的java.time.LocalDate月份和星期都改成了enum;
2、java.util.Date和SimpleDateFormat都不是线程安全的,而LocalDate和LocalTime和最基本的String一样,是不变类型,不但线程安全,而且不能修改。
之前SimpleDateFormat多线程环境下一般都是配合ThreadLocal使用,新的LocalDate显然更加简洁明了。
3、java.util.Date是一个“万能接口”,它包含日期、时间,还有毫秒数,更加明确需求取舍
4、新接口更好用的原因是考虑到了日期时间的操作,经常发生往前推或往后推几天的情况。用java.util.Date配合Calendar要写好多代码,而且一般的开发人员还不一定能写对。
七、HashMap、ConcurrentHashMap数据结构优化(面试重点)
面试问题:你能说说HashMap/ConcurrentHashMap的结构吗?
(一)JDK1.8之前
hashMap采用的是哈希表(数组+链表)
ConcurrentHashMap采用的是哈希表(数组+链表),通过分段锁机制来实现线程安全。
1、hashMap默认大小16,一个0-15索引的数组。
2、存储方式:
(1)首先调用元素的hashcode方法计算出哈希码值,经过哈希算法算成数组的索引值;
(2)如果对应的索引处没有元素,直接存放,如果有对象在,那么比较它们的equals方法比较内容
a.如果内容一样,后一个value值会将前一个value值覆盖
b.如果不一样,后加的会放在前面,形成一个链表,形成了碰撞
3、缺点:加载因子:0.75,数组扩容,达到总容量的75%,就进行扩容,但是无法避免碰撞的情况发生
(二)JDK1.8之后
hashMap采用的是数组+链表+红黑树
ConcurrentHashMap采用的是数组+链表+红黑树,通过CAS+Synchronized来实现线程安全
1、当碰撞的个数>8时&&总容量>64,会有红黑树的引入,除了添加之后,效率都比链表高
2、链表新进元素加到末尾
3、ConcurrentHashMap(锁分段机制),concurrentLevel,JDK1.8采用CAS算法(无锁算法,不再使用锁分段)
4、红黑树(自平衡的二叉查找树,基于二叉查找树、完美平衡二叉树)
问题二:HashMap为什么不是线程安全的?
答:HashMap中的变量没有用volatile关键字修饰来保证可见性和有序性,也没有用任何锁来保证原子性,在多线程运行的环境下,可能会产生并发问题,如多个线程同时插入时,可能会导致闭环。
问题三:ConcurrentHashMap怎么保证线程安全?
答:JDK1.7中,ConcurrentHashMap采用锁分段机制来保证线程安全同时提高并发性;JDK1.8中,ConcurrentHashMap采用的是CAS加上Synchronized来保证线程安全,当没有发生冲突的使用使用CAS来保证线程安全,当发生冲突的时候用Synchronized来锁住。
PS:有关锁可以参考我之前写的JUC之Locks锁面经整理