【闲聊杂谈】深入剖析Java8新特性
0、初尝新特性
在没有接触Java8新特性之前,要新开一个线程,可以这么做:
Thread类需要一个Runnable接口作为参数,其中的抽象方法run方法是用来指定线程任务内容的核心。为了指定run方法体,不得不需要Runnable的实现类。为了省去定义一个Runnable 的实现类,不得不使用匿名内部类。必须覆盖重写抽象的run方法,所有的方法名称,方法参数,方法返回值不得不都重写一遍,而且不能出错。而实际上,我们只在乎方法体中的代码。为了达到这一目的,Java8提供了Lambda表达式这种操作:
而实际上,Lambda表达式是一个匿名函数,可以理解为一段可以传递的代码。Lambda表达式的重点是简化了匿名内部类的使用,语法更加简单。匿名内部类语法冗余,体验了Lambda表达式后,发现Lambda表达式是简化匿名内部类的一种方式。
1、Lambda表达式
基本语法
Lambda表达式省去了面向对象的条条框框,Lambda表达式的标准格式由3个部分组成:
(参数类型 参数名称) -> {代码体; }
格式说明:
(参数类型 参数名称):参数列表
{代码体;}:方法体
->:箭头,分割参数列表和方法体
完成一个无参无返回值的Lambda表达式
完成一个有参有返回值的Lambda表达式在sort方法的第二个参数是一个Comparator接口的匿名内部类,且执行的方法有参数和返回值,那么就可以改写为Lambda表达式形式。
@FunctionalInterface注解
如果在上例的UserService接口中再定义另外一个抽象方法,那么Lambda表达式就会报错:接口中有两个抽象方法,但是通过Lambda表达式只能去实现其中一个,Lambda表达式就懵逼了,到底要实现哪一个?所以只能有一个抽象方法才可以。
那要怎么保证这个接口中只能有一个抽象方法?使用@FunctionalInterface注解来保证。这是一个标志注解,被该注解修饰的接口只能声明一个抽象方法。有了这个注解之后,定义超过一个抽象方法就会给出报错提示:
Lambda表达式的原理
匿名内部类的本质是在编译时生成一个Class文件:XXXXX$1.class
还可以通过反编译工具来查看生成的代码XJad工具来查看:
同样的也可以使用反编译工具来看Lambda表达式生成的class文件,但是发现使用XJad查看报错。其实可以通过JDK自带的一个工具:javap 对字节码进行反汇编操作:
javap -c -p 文件名.class,-c:表示对代码进行反汇编,-p:显示所有的类和成员
在这个反编译的源码中我们看到了一个静态方法 lambda$main$0(),这个方法里面做了什么事情呢?可以通过debug的方式来查看下:
可以看到,【System.out.println("通过Lambda表达式实现show方法");】这个方法是在【lambda$main$0()】中执行的,简单点可以理解为:
为了更加直观的理解这个内容,我们可以在运行的时候添加参数:
java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名,加上这个参数会将内部class码输出到一个文件中:
将这个文件反编译后看到:
可以看到这个匿名的内部类实现了UserService接口,并重写了show()方法。在show()方法中调用了Demo03Lambda.lambda$main$0(),也就是调用了Lambda表达式中的内容。
总结下来就是:匿名内部类在编译的时候会产生一个class文件,Lambda表达式在程序运行的时候会形成一个类。在类中新增了一个方法,这个方法的方法体就是Lambda表达式中的代码。还会形成一个匿名内部类,实现接口,重写抽象方法,在接口中重写方法会调用新生成的方法。
Lambda表达式的省略写法
在lambda表达式的标准写法基础上,可以使用省略写法的规则为:
1、小括号内的参数类型可以省略;
2、如果小括号内有且仅有一个参数,则小括号可以省略;
3、如果大括号内有且仅有一个语句,可以同时省略大括号,return 关键字及语句分号;
Lambda表达式的使用前提
Lambda表达式的语法是非常简洁的,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
1、方法的参数或局部变量类型必须为接口才能使用Lambda
2、接口中有且仅有一个抽象方法(@FunctionalInterface)
Lambda和匿名内部类的对比
1、所需类型不一样
- 匿名内部类的类型可以是 类,抽象类,接口
- Lambda表达式需要的类型必须是接口
2、抽象方法的数量不一样
- 匿名内部类所需的接口中的抽象方法的数量是随意的
- Lambda表达式所需的接口中只能有一个抽象方法
3、实现原理不一样
- 匿名内部类是在编译后形成一个class
- Lambda表达式是在程序运行的时候动态生成class
2、接口增强
在Java8之前:
interface 接口名{
静态常量;
抽象方法;
}
在Java8之后:
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
默认方法
在JDK8以前接口中只能有抽象方法和静态常量,会存在一个问题:如果接口中新增抽象方法,那么实现类都必须要抽象这个抽象方法,非常不利于接口的扩展的。
接口默认方法的语法
interface 接口名{
修饰符 default 返回值类型 方法名{
方法体;
}
}
接口中的默认方法有两种使用方式:
1、实现类直接调用接口的默认方法
2、实现类重写接口的默认方法
静态方法
接口中的静态方法在实现类中是不能被重写的,调用的话只能通过接口类型来实现:
接口名.静态方法名();
接口静态方法的语法
interface 接口名{
修饰符 static 返回值类型 方法名{
方法体;
}
}
默认方法和静态方法的区别
1、默认方法通过实例调用,静态方法通过接口名调用;
2、默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法;
3、静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用;
3、函数式接口
Supplier接口
无参有返回值的接口,对于的Lambda表达式需要提供一个返回数据的类型:
Consumer接口
有参无返回值得接口。前面介绍的Supplier接口是用来生产数据的,而Consumer接口是用来消费数据的,使用的时候需要指定一个泛型来定义参数类型:
除了抽象方法以外,还有一个默认方法:andThen。如果一个方法的参数和返回值全部是Consumer类型,那么就可以实现消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合,而这个方法就是Consumer接口中的default andThen方法:
Function接口
有参有返回值的接口,Function接口是根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
default andThen方法:
这个接口中还包含另外两个方法,默认的compose方法的作用顺序和andThen方法刚好相反,而静态方法identity则是,输入什么参数就返回什么参数。
Predicate接口
有参且返回值为Boolean的接口。
在Predicate中的默认方法提供了逻辑关系操作 and、or、negate、isEquals等方法
4、方法引用
在使用Lambda表达式的时候,也会出现代码冗余的情况,比如:用Lambda表达式求一个数组的和。
因为在Lambda表达式中要执行的代码和另一个方法中的代码是一样的,这时就没有必要重写一份逻辑了,可以直接 “引用” 重复代码。
方法引用的语法
符号表示:【::】
符号说明:双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
应用场景:如果Lambda表达式所要实现的方案,已经有其他方法存在相同的方案,那么则可以使用方法引用。
方法引用在Java8中使用是相当灵活的,有以下几种形式:
1、instanceName::methodName 对象::方法名
2、ClassName::staticMethodName 类名::静态方法
3、ClassName::methodName 类名::普通方法
4、ClassName::new 类名::new 调用的构造器
5、TypeName[]::new String[]::new 调用数组的构造器
instanceName::methodName 对象::方法名
这是最常见的一种用法。如果一个类中的已经存在了一个成员方法,则可以通过对象名引用成员方法。方法引用的注意事项:
1、被引用的方法,参数要和接口中的抽象方法的参数一样;
2、当接口抽象方法有返回值时,被引用的方法也必须有返回值;
ClassName::staticMethodName 类名::静态方法
ClassName::methodName 类名::普通方法
Java面向对象中,类名只能调用静态方法,类名引用实例方法是用前提的,实际上是拿第一个参数作为方法的调用者。
ClassName::new 类名::new 调用的构造器
由于构造器的名称和类名完全一致,所以构造器引用使用"::new"的格式使用。
TypeName[]::new String[]::new 调用数组的构造器
方法引用是对Lambda表达式符合特定情况下的一种缩写方式,它使得Lambda表达式更加的精简,也可以理解为lambda表达式的缩写形式,不过要注意的是方法引用只能引用已经存在的方法。
5、Stream API
当需要对集合中的元素进行操作的时候,除了必需的添加,删除,获取外,最典型的操作就是集合遍历。很多时候针对不同的需求总是一次次的循环循环循环,这时就非常希望有更加高效的处理方式,现在可以通过JDK8中提供的Stream API来解决这个问题。
Stream流式思想
首先先明确一个概念:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象。Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去重、统计、匹配和归约。
Stream流获取方式
Collection接口获取
java.util.Collection 接口中加入了default方法stream,也就是说Collection接口下的所有的实现都可以通过steam方法来获取Stream流。
但是Map接口别没有实现Collection接口,那这时怎么办呢?可以根据Map获取对应的key-value的集合,在通过集合去获取Stream流。
Stream的of方法获取
在实际开发中不可避免的还是会操作到数组中的数据,由于数组对象不可能添加默认方法,所有Stream接口中提供了of静态方法。
Stream流常用方法
Stream流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。终结方法包括 count 和 forEach 方法;
非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。除了终结方法外,其余方法均为非终结方法;
Stream流需要注意:
1、Stream只能操作一次;
2、Stream方法返回的是新的流;
3、Stream不调用终结方法,中间的操作不会执行;
方法名 | 方法作用 | 返回值类型 | 方法种类 |
---|---|---|---|
count | 统计个数 | long | 终结 |
forEach | 逐一处理 | void | 终结 |
filter | 过滤 | Stream | 函数拼接 |
limit | 取用前几个 | Stream | 函数拼接 |
skip | 跳过前几个 | Stream | 函数拼接 |
map | 映射 | Stream | 函数拼接 |
concat | 组合 | Stream | 函数拼接 |
forEach用来遍历流中的数据的,该方法接受一个Consumer接口,会将每一个流元素交给函数处理。
count
Stream流中的count方法用来统计其中的元素个数的,该方法返回一个long值,代表元素的个数。
filter
filter方法的作用是用来过滤数据的,返回符合条件的数据,可以通过filter方法将一个流转换成另一个子集流。
可以看到,该接口接收一个Predicate函数式接口参数作为筛选条件。
limit
limit方法可以对流进行截取处理,只取前n个数据。
参数是一个long类型的数值,如果集合当前长度大于参数就进行截取,否则不操作。
skip
如果希望跳过前面几个元素,可以使用skip方法获取一个截取之后的新流。
map
如果我们需要将流中的元素映射到另一个流中,可以使用map方法。
该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的数据。
sorted
如果需要将数据排序,可以使用sorted方法。
在使用的时候可以根据自然规则排序,也可以通过比较强来指定对应的排序规则。
distinct
如果要去掉重复数据,可以使用distinct方法。
Stream流中的distinct方法对于基本数据类型是可以直接出重的,但是对于自定义类型,需要重写hashCode和equals方法来移除重复元素。
match
如果需要判断数据是否匹配指定的条件,可以使用match相关的方法。
anyMatch:元素是否有任意一个满足条件
allMatch:元素是否都满足条件
noneMatch:元素是否都不满足条件
find
如果我们需要找到某些数据,可以使用find方法来实现。
max / min
reduce
如果需要将所有数据归纳得到一个数据,可以使用reduce方法。
而在实际开发中,经常会将map和reduce一块来使用。
mapToInt
如果需要将Stream中的Integer类型转换成int类型,可以使用mapToInt方法来实现。
concat
如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat。
Stream流综合案例
定义两个集合,然后在集合中存储多个用户名称。然后完成如下的操作:
1、第一个队伍只保留姓名长度为3的成员;
2、第一个队伍筛选之后只要前3个人;
3、第二个队伍只要姓张的成员;
4、第二个队伍筛选之后不要前一个人;
5、将两个队伍合并为一个队伍;
6、根据姓名创建Person对象;
7、打印整个队伍的Person信息;
Stream结果收集
结果收集到集合中
结果收集到数组中
Stream流中数据进行聚合计算
在使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作,比如:获得最大值,最小值,求和,平均值,统计数量。
Stream流中数据进行分组
在使用Stream流处理数据后,可以根据某个属性将数据分组。
还可以根据多个字段进行多级组合分组。
Stream流中数据进行分区
Collectors.partitioningBy会根据值是否为true,把集合中的数据分割为两个列表,一个true列表,一个false列表。
Stream流中数据进行拼接
Collectors.joining会根据指定的连接符,将所有的元素连接成一个字符串。
Stream并行流
并行流与串行流
上面提到的都是Stream串行流,也就是在一个线程上面执行。为了提高在大数据量方面的效率,引入了并行流的概念。parallelStream其实就是一个并行执行的流,它通过默认的ForkJoinPool,可以提高多线程任务的速度。可以通过两种方式来获取并行流:
1、通过List接口中的parallelStream方法来获取;
2、通过已有的串行流转换为并行流(parallel);
并行流的操作和串行流没有任何区别
通过for循环,串行Stream流,并行Stream流来对500000000亿个数字求和,来看消耗时间
可以看到parallelStream的效率是最高的,Stream并行处理的过程会分而治之,也就是将一个大的任务切分成了多个小任务,这表示每个任务都是一个线程操作。但需要注意的是,使用并行流是在数据量较大的情况下才会占据优势,如果是较小的数据量情况下,并行流的效率可能还不如串行流。
并行流的线程安全问题
加锁
使用线程安全容器
使用Stream中的toArray/collect
6、Optional
Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象,主要就是解决空指针的问题,防止NullpointerException。
不使用Optional对null的处理显得代码非常的繁琐和臃肿
Optional对象的三种创建方式
Optional常用方法
get
如果Optional有值则返回,否则抛出NoSuchElementException异常
isPresent
判断是否包含值,包含值返回true,不包含值返回false,常和get方法一起搭配使用
orElse
如果调用对象包含值,就返回该值,否则返回t
orElseGet
如果调用对象包含值,就返回该值,否则返回Lambda表达式的返回值
7、LocalDateTime
为什么引入LocalDateTime
在旧版本中JDK对于日期和时间这块的支持是非常差的:
1、设计不合理,在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间的,而java.sql.Date仅仅包含日期,此外用于格式化和解析的类在java.text包下;
2、非线程安全,java.util.Date是非线程安全的,所有的日期类都是可变的,这是java日期类最大的问题之一;
3、时区处理麻烦,日期类并不提供国际化,没有时区支持;
针对以上种种问题,Java8中增加了一套全新的日期时间API,这套API设计合理,并且支持线程安全。新的日期及时间API位于 java.time 包
中,下面是一些关键类:
- LocalDate:表示日期,包含年月日,格式为【2019-10-16】;
- LocalTime:表示时间,包含时分秒,格式为【16:38:54.158549300】;
- LocalDateTime:表示日期时间,包含年月日,时分秒,格式为【2018-09-06T15:33:56.750】;
- DateTimeFormatter:日期时间格式化类;
- Instant:时间戳,表示一个特定的时间瞬间;
- Duration:用于计算2个时间(LocalTime,时分秒)的距离;
- Period:用于计算2个日期(LocalDate,年月日)的距离;
- ZonedDateTime :包含时区的时间;
Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是所说的公历。平年有365天,闰年是366天。此外Java8还提供了4套其他历法,分别是:
- ThaiBuddhistDate:泰国佛教历
- MinguoDate:中华民国历
- JapaneseDate:日本历
- HijrahDate:伊斯兰历
LocalDate 常用方法
LocalTime常用方法
LocalDateTime常用方法
日期时间的修改和比较
LocalDateTime中对日期时间的修改,对已存在的对象创建了它模板,并不会修改原来对象本身的时间,自然也就不会牵涉到数据安全的问题。
日期时间的格式化和解析
在JDK8中可以通过java.time.format.DateTimeFormatter
类可以进行日期的解析和格式化操作
日期时间差
Java8中提供了两个工具类来计算日期时间差:
1、Duration:用来计算两个时间差(LocalTime);
2、Period:用来计算两个日期差(LocalDate);
时间矫正器
有时候对时间可能需要如下调整:将日期调整到"下个月的第一天"这种操作,虽然通过前面介绍的plus和with也可以实现,不过使用时间校正器效果可能会更好。
- TemporalAdjuster:时间校正器
- TemporalAdjusters:通过该类静态方法提供了大量的常用TemporalAdjuster的实现
时区
Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。 ZoneId:该类中包含了所有的时区信息。
JDK新的日期和时间API的优势:
1、新版日期时间API中,日期和时间对象是不可变,操作日期不会影响原来的值,而是生成一个新的实例;
2、提供不同的两种方式,有效的区分了人和机器的操作;
3、TemporalAdjuster可以更精确的操作日期,还可以自定义日期调整期;
4、线程安全;
8、重复注解和类型注解
重复注解
自从Java5中引入注解以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限制是,在同一个地方不能多次使用同一个注解。Java8引入了重复注解的概念,允许在同一个地方多次使用同一个注解。在Java8中使用@Repeatable注解定义重复注解。
定义一个可以重复的注解
定义一个重复注解的容器
使用重复注解
解析得到指定的注解
类型注解
Java8为@Target元注解新增了两种类型: TYPE_PARAMETER、TYPE_USE
- TYPE_PARAMETER:表示该注解能写在类型参数的声明语句中。 类型参数声明如:<T>
- TYPE_USE:表示注解可以再任何用到类型的地方使用。
TYPE_PARAMETER
TYPE_USE
【闲聊杂谈】深入剖析Java8新特性相关推荐
- 【Java8新特性】关于Java8的Stream API,看这一篇就够了!!
写在前面 Java8中有两大最为重要的改变.第一个是 Lambda 表达式:另外一个则是 Stream API(java.util.stream.*) ,那什么是Stream API呢?Java8中 ...
- 【Java8新特性】浅谈方法引用和构造器引用
写在前面 Java8中一个很牛逼的新特性就是方法引用和构造器引用,为什么说它很牛逼呢?往下看! 方法引用 当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!这里需要注意的是:实现抽 ...
- java8新特性_Java8新特性之Date API|乐字节
大家好,我是乐字节的小乐,上篇文章讲述了<Java8新特性之Optional>,接下来,小乐将接着讲述Java8新特性之Date API 2019日历 Java8之Date API Jav ...
- Java8 新特性之流式数据处理(转)
转自:https://www.cnblogs.com/shenlanzhizun/p/6027042.html 一. 流式处理简介 在我接触到java8流式处理的时候,我的第一感觉是流式处理让集合操作 ...
- java8新特性_乐字节-Java8新特性-接口默认方法
总概 JAVA8 已经发布很久,而且毫无疑问,java8是自java5(2004年发布)之后的最重要的版本.其中包括语言.编译器.库.工具和JVM等诸多方面的新特性. Java8 新特性列表如下: 接 ...
- java8新特性: lambda表达式:直接获得某个list/array/对象里面的字段集合
java8新特性: lambda表达式:直接获得某个list/array/对象里面的字段集合 比如,我有一张表: entity Category.java service CategoryServic ...
- java8新特性_乐字节-Java8新特性-函数式接口
上一篇小乐带大家学过 Java8新特性-Lambda表达式,那什么时候可以使用Lambda?通常Lambda表达式是用在函数式接口上使用的.从Java8开始引入了函数式接口,其说明比较简单:函数式接口 ...
- 使用Java8新特性(stream流、Lambda表达式)实现多个List 的笛卡尔乘积 返回需要的List<JavaBean>
需求分析: 有两个Long类型的集合 : List<Long> tagsIds; List<Long> attributesIds; 现在需要将这两个Long类型的集合进行组合 ...
- java8新特性简述
Java8发布时间是2014年3月19日,距离今日已经很久了,那么Java8新特性你了解吗? java8是Java的一次重大升级,巨大的里程碑式的改进!! Java语言新特性: 1.与传统结合 -- ...
最新文章
- UpdateData使用简介
- 如何用php新增税金一列_PHP计算个人所得税步骤详解(附代码)
- 一道题,最小操作次数使数组元素相等引发的思考
- 三星sec.android.soagent,3.0降级2.5教程
- 【HDU - 1561】The more, The Better(树形背包,dp,依赖背包问题与空间优化,tricks)
- TabHost和ActivityGroup用法
- 最牛逼android上的图表库MpChart(二) 折线图
- 【图像处理基础】基于matlab GUI图片浏览器【含Matlab源码 1015期】
- 点集凸包算法python实现
- 所有文件夹都变成1KB文件夹快捷方式病毒的手动清除方法
- 计算机制作贺卡教案,三八爱心节贺卡教案
- 计算机excel感叹号,excel的文件上有个的感叹号是什么意思?
- 浅层砂过滤器的原理是什么,滤料是什么,需要不需要定期?
- Android内存泄漏leakcanary2.7
- Android屏幕适配概论:
- swift 设置 pickerView 为黑底白字
- 华为硬件工程师手册_华为,英飞凌,中兴硬件工程师面试题
- NOMAO软件测试工资,基于混合遗传算法的测试数据自动生成研究
- node中全局对象一 --- __dirname和__filename
- EXCEL---一个简单的查询--对比大佬的方法,学习几个函数 mmult sign find subtitute lookup( , 0/) 等用法