在没有接触Java8新特性之前,要新开一个线程,可以这么做:
Thread类需要一个Runnable接口作为参数,其中的抽象方法run方法是用来指定线程任务内容的核心。为了指定run方法体,不得不需要Runnable的实现类。为了省去定义一个Runnable 的实现类,不得不使用匿名内部类。必须覆盖重写抽象的run方法,所有的方法名称,方法参数,方法返回值不得不都重写一遍,而且不能出错。而实际上,我们只在乎方法体中的代码。为了达到这一目的,Java8提供了Lambda表达式这种操作:
而实际上,Lambda表达式是一个匿名函数,可以理解为一段可以传递的代码。Lambda表达式的重点是简化了匿名内部类的使用,语法更加简单。匿名内部类语法冗余,体验了Lambda表达式后,发现Lambda表达式是简化匿名内部类的一种方式。
Lambda表达式省去了面向对象的条条框框,Lambda表达式的标准格式由3个部分组成:
(参数类型 参数名称) -> { 代码体; }
格式说明:
(参数类型 参数名称):参数列表
{代码体;}:方法体
->:箭头,分割参数列表和方法体
完成一个有参有返回值的Lambda表达式在sort方法的第二个参数是一个Comparator接口的匿名内部类,且执行的方法有参数和返回值,那么就可以改写为Lambda表达式形式。
如果在上例的UserService接口中再定义另外一个抽象方法,那么Lambda表达式就会报错:接口中有两个抽象方法,但是通过Lambda表达式只能去实现其中一个,Lambda表达式就懵逼了,到底要实现哪一个?所以只能有一个抽象方法才可以。
那要怎么保证这个接口中只能有一个抽象方法?使用@FunctionalInterface注解来保证。这是一个标志注解,被该注解修饰的接口只能声明一个抽象方法。有了这个注解之后,定义超过一个抽象方法就会给出报错提示:
匿名内部类的本质是在编译时生成一个Class文件:XXXXX$1.class
同样的也可以使用反编译工具来看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表达式的标准写法基础上,可以使用省略写法的规则为:
1、小括号内的参数类型可以省略;
2、如果小括号内有且仅有一个参数,则小括号可以省略;
3、如果大括号内有且仅有一个语句,可以同时省略大括号,return 关键字及语句分号;
Lambda表达式的语法是非常简洁的,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
1、方法的参数或局部变量类型必须为接口才能使用Lambda
2、接口中有且仅有一个抽象方法(@FunctionalInterface)
1、所需类型不一样
- 匿名内部类的类型可以是 类,抽象类,接口
- Lambda表达式需要的类型必须是接口
2、抽象方法的数量不一样
- 匿名内部类所需的接口中的抽象方法的数量是随意的
- Lambda表达式所需的接口中只能有一个抽象方法
3、实现原理不一样
- 匿名内部类是在编译后形成一个class
- Lambda表达式是在程序运行的时候动态生成class
在Java8之前:
interface 接口名{
静态常量;
抽象方法;
}
在Java8之后:
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
在JDK8以前接口中只能有抽象方法和静态常量,会存在一个问题:如果接口中新增抽象方法,那么实现类都必须要抽象这个抽象方法,非常不利于接口的扩展的。
interface 接口名{
修饰符 default 返回值类型 方法名{
方法体;
}
}
接口中的默认方法有两种使用方式:
1、实现类直接调用接口的默认方法
2、实现类重写接口的默认方法
接口中的静态方法在实现类中是不能被重写的,调用的话只能通过接口类型来实现:
接口名.静态方法名();
interface 接口名{
修饰符 static 返回值类型 方法名{
方法体;
}
}
1、默认方法通过实例调用,静态方法通过接口名调用;
2、默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法;
3、静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用;
无参有返回值的接口,对于的Lambda表达式需要提供一个返回数据的类型:
有参无返回值得接口。前面介绍的Supplier接口是用来生产数据的,而Consumer接口是用来消费数据的,使用的时候需要指定一个泛型来定义参数类型:
除了抽象方法以外,还有一个默认方法:andThen。如果一个方法的参数和返回值全部是Consumer类型,那么就可以实现消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合,而这个方法就是Consumer接口中的default andThen方法:
有参有返回值的接口,Function接口是根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
这个接口中还包含另外两个方法,默认的compose方法的作用顺序和andThen方法刚好相反,而静态方法identity则是,输入什么参数就返回什么参数。
在Predicate中的默认方法提供了逻辑关系操作 and、or、negate、isEquals等方法
在使用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表达式的缩写形式,不过要注意的是方法引用只能引用已经存在的方法。
当需要对集合中的元素进行操作的时候,除了必需的添加,删除,获取外,最典型的操作就是集合遍历。很多时候针对不同的需求总是一次次的循环循环循环,这时就非常希望有更加高效的处理方式,现在可以通过JDK8中提供的Stream API来解决这个问题。
首先先明确一个概念:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象。Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去重、统计、匹配和归约。
java.util.Collection 接口中加入了default方法stream,也就是说Collection接口下的所有的实现都可以通过steam方法来获取Stream流。
但是Map接口别没有实现Collection接口,那这时怎么办呢?可以根据Map获取对应的key-value的集合,在通过集合去获取Stream流。
在实际开发中不可避免的还是会操作到数组中的数据,由于数组对象不可能添加默认方法,所有Stream接口中提供了of静态方法。
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接口,会将每一个流元素交给函数处理。
Stream流中的count方法用来统计其中的元素个数的,该方法返回一个long值,代表元素的个数。
filter方法的作用是用来过滤数据的,返回符合条件的数据,可以通过filter方法将一个流转换成另一个子集流。
可以看到,该接口接收一个Predicate函数式接口参数作为筛选条件。
limit方法可以对流进行截取处理,只取前n个数据。
参数是一个long类型的数值,如果集合当前长度大于参数就进行截取,否则不操作。
如果希望跳过前面几个元素,可以使用skip方法获取一个截取之后的新流。
如果我们需要将流中的元素映射到另一个流中,可以使用map方法。
该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的数据。
如果需要将数据排序,可以使用sorted方法。
在使用的时候可以根据自然规则排序,也可以通过比较强来指定对应的排序规则。
如果要去掉重复数据,可以使用distinct方法。
Stream流中的distinct方法对于基本数据类型是可以直接出重的,但是对于自定义类型,需要重写hashCode和equals方法来移除重复元素。
如果需要判断数据是否匹配指定的条件,可以使用match相关的方法。
如果我们需要找到某些数据,可以使用find方法来实现。
如果需要将所有数据归纳得到一个数据,可以使用reduce方法。
而在实际开发中,经常会将map和reduce一块来使用。
如果需要将Stream中的Integer类型转换成int类型,可以使用mapToInt方法来实现。
如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat。
定义两个集合,然后在集合中存储多个用户名称。然后完成如下的操作:
1、第一个队伍只保留姓名长度为3的成员;
2、第一个队伍筛选之后只要前3个人;
3、第二个队伍只要姓张的成员;
4、第二个队伍筛选之后不要前一个人;
5、将两个队伍合并为一个队伍;
6、根据姓名创建Person对象;
7、打印整个队伍的Person信息;
在使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作,比如:获得最大值,最小值,求和,平均值,统计数量。
在使用Stream流处理数据后,可以根据某个属性将数据分组。
还可以根据多个字段进行多级组合分组。
Collectors.partitioningBy会根据值是否为true,把集合中的数据分割为两个列表,一个true列表,一个false列表。
Collectors.joining会根据指定的连接符,将所有的元素连接成一个字符串。
上面提到的都是Stream串行流,也就是在一个线程上面执行。为了提高在大数据量方面的效率,引入了并行流的概念。parallelStream其实就是一个并行执行的流,它通过默认的ForkJoinPool,可以提高多线程任务的速度。可以通过两种方式来获取并行流:
1、通过List接口中的parallelStream方法来获取;
2、通过已有的串行流转换为并行流(parallel);
并行流的操作和串行流没有任何区别
通过for循环,串行Stream流,并行Stream流来对500000000亿个数字求和,来看消耗时间
可以看到parallelStream的效率是最高的,Stream并行处理的过程会分而治之,也就是将一个大的任务切分成了多个小任务,这表示每个任务都是一个线程操作。但需要注意的是,使用并行流是在数据量较大的情况下才会占据优势,如果是较小的数据量情况下,并行流的效率可能还不如串行流。
Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象,主要就是解决空指针的问题,防止NullpointerException。
不使用Optional对null的处理显得代码非常的繁琐和臃肿
如果Optional有值则返回,否则抛出NoSuchElementException异常
判断是否包含值,包含值返回true,不包含值返回false,常和get方法一起搭配使用
如果调用对象包含值,就返回该值,否则返回Lambda表达式的返回值
在旧版本中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:伊斯兰历
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、线程安全;
自从Java5中引入注解以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限制是,在同一个地方不能多次使用同一个注解。Java8引入了重复注解的概念,允许在同一个地方多次使用同一个注解。在Java8中使用@Repeatable注解定义重复注解。
Java8为@Target元注解新增了两种类型: TYPE_PARAMETER、TYPE_USE
- TYPE_PARAMETER:表示该注解能写在类型参数的声明语句中。 类型参数声明如:
- TYPE_USE:表示注解可以再任何用到类型的地方使用。