参考URL
目录
一、Lambda表达式
二、接口的默认方法与静态方法
三、Optional
四、方法引用
五、注解相关改变
六、Stream API
七、Date/Time API (JSR 310)
八、Base64
九、并行(parallel)数组
十、并发(Concurrency)
十一、JavaScript引擎Nashorn
参考URL
最广泛的新特性是Lambda表达式和Stream API。
Lambda表达式是一个匿名函数,使用Lambda表达式能够使Java的语言表达能力得到提升。
匿名内部类:
Comparator com = new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
在上述代码中,我们使用匿名内部类实现了比较两个Integer类型数据的大小。接下来,我们就可以将上述匿名内部类的实例作为参数,传递到其他方法中了,如下所示:
Comparator com = new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
TreeSet treeSet = new TreeSet<>(com);
我们分析下上述代码,在整个匿名内部类中,实际上真正有用的就是下面一行代码。
return Integer.compare(o1, o2);
其他的代码本质上都是“冗余”的。但是为了书写上面的一行代码,我们不得不在匿名内部类中书写更多的代码。如果使用Lambda表达式完成两个Integer类型数据的比较,我们该如何实现呢?
Comparator com = (x, y) -> Integer.compare(x, y);
看到没,使用Lambda表达式,我们只需要使用一行代码就能够实现两个Integer类型数据的比较。
我们也可以将Lambda表达式传递到TreeSet的构造方法中,如下所示。
TreeSet treeSet = new treeSet<>((x,y) -> Integer.compare(x,y));
直观的感受就是使用Lambda表达式一行代码就能搞定匿名内部类多行代码的功能。
看到这,不少读者会问:我使用匿名内部类的方式实现比较两个整数类型的数据大小并不复杂啊!我为啥还要学习一种新的语法呢?
其实,我想说的是:上面咱们只是简单的列举了一个示例,接下来,咱们写一个稍微复杂一点的例子,来对比下使用匿名内部类与Lambda表达式哪种方式更加简洁。
对比常规方法和Lambda表达式
例如,现在有这样一个需求:获取当前公司中员工年龄大于30岁的员工信息。
首先,我们需要创建一个Employee实体类来存储员工的信息。
@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
private static final long serialVersionUID = -9079722457749166858L;
private String name;
private Integer age;
private Double salary;
}
在Employee中,我们简单存储了员工的姓名、年龄和薪资。接下来,我们创建一个存储多个员工的List集合,如下所示:
protected List employees = Arrays.asList(
new Employee("张三", 18, 9999.99),
new Employee("李四", 38, 5555.55),
new Employee("王五", 60, 6666.66),
new Employee("赵六", 16, 7777.77),
new Employee("田七", 18, 3333.33));
1)常规遍历集合
我们先使用常规遍历集合的方式来查找年龄大于等于30的员工信息。
public List filterEmployeesByAge(List list){
List employees = new ArrayList<>();
for(Employee e : list) {
if(e.getAge() >= 30) {
employees.add(e);
}
}
return employees;
}
总体来说,查找年龄大于或者等于30的员工信息,使用常规遍历集合的方式稍显复杂了。
例如,需求发生了变化:获取当前公司中员工工资大于或者等于5000的员工信息。
此时,我们不得不再次创建一个按照工资过滤的方法。
public List filterEmployeesBySalary(List list) {
List employees = new ArrayList<>();
for(Employee e : list){
if(e.getSalary() >= 5000){
employees.add(e);
}
}
return employees;
}
对比filterEmployeesByAge()方法和filterEmployeesBySalary方法后,我们发现,大部分的方法体是相同的,只是for循环中对于条件的判断不同。
如果此时我们再来一个需求,查找当前公司中年龄小于或者等于20的员工信息,那我们又要创建一个过滤方法了。看来使用常规方法是真的不方便啊!那么如何优化代码咧?
2)使用设计模式优化代码
我们先定义一个泛型接口 MyPredicate,对传递过来的数据进行过滤,符合规则返回true,不符合规则返回false。
public interface MyPredicate {
/*对传递过来的T类型的数据进行过滤/符合规则返回true/不符合规则返false*/
booleanfilter(T t);
}
接下来,我们创建MyPredicate接口的实现类FilterEmployeeByAge来过滤年龄大于或者等于30的员工信息。
public class Filter EmployeeByAge implements MyPredicate= 30;
}
}
我们定义一个过滤员工信息的方法,此时传递的参数不仅有员工的信息集合,同时还有一个我们定义的接口实例,在遍历员工集合时将符合过滤条件的员工信息返回。
public List filterEmployee(List list, MyPredicate myPredicate){
List employees = new ArrayList<>();
for(Employee e : list) {
if(myPredicate.filter(e)){
employees.add(e);
}
}
return employees;
}
写到这里,大家是否有一种豁然开朗的感觉呢?
没错,这就是设计模式的魅力!
我们继续获取当前公司中工资大于或者等于5000的员工信息,此时,我们只需要创建一个FilterEmployeeBySalary类实现MyPredicate接口,如下所示:
public class FilterEmployeeBySalary implements MyPredicate{
@Override
public boolean filter(Employee employee) {
return employee.getSalary() >= 5000;
}
}
可以看到,使用设计模式对代码进行优化后,无论过滤员工信息的需求如何变化,我们只需要创建MyPredicate接口的实现类来实现具体的过滤逻辑,然后在测试方法中调用filterEmployee(List
使用设计模式优化代码也有不好的地方:每次定义一个过滤策略的时候,我们都要单独创建一个过滤类!!
那还有没有更加简化的方式呢?
4)重头戏:Lambda表达式
在使用Lambda表达式时,我们还是要调用之前写的filterEmployee(List
@Test
public void test() {
filterEmployee(this.employees, (e) -> e.getAge() >=30)
.forEach(System.out::println);
}
看到没,使用Lambda表达式只需要一行代码就完成了员工信息的过滤和输出。是不是很6呢。
@Test
public void test() {
filterEmployee(this.employees, (e) -> e.getSalary() >= 5000)
.forEach(System.out::println);
}
没错,使用Lambda表达式,又是一行代码就搞定了!!另外,使用Lambda表达式时,只需要给出需要过滤的集合,我们就能够实现从集合中过滤指定规则的元素,并输出结果信息。
5)重头戏:Stream API
使用Lambda表达式结合Stream API,只要给出相应的集合,我们就可以完成对集合的各种过滤并输出结果信息。例如,此时只要有一个employees集合,我们使用Lambda表达式来获取工资大于或者等于5000的员工信息。
@Test
public void test() {
employees.stream().filter((e) -> e.getSalary() >= 5000)
.forEach(System.out::println);
}
没错,只给出一个集合,使用Lambda表达式和Stream API,一行代码就能够过滤出想要的元素并进行输出。
@Test
public void test(){
employees.stream().filter((e) -> e.getSalary() >= 5000)
.map(Employee::getName)
.forEach(System.out::println);
}
可以看到,使用map过滤出了工资大于或者等于5000的员工姓名。
6)匿名类到Lambda表达式
我们先来看看从匿名类如何转换到Lambda表达式呢?
这里,我们可以使用两个示例来说明如何从匿名内部类转换为Lambda表达式。
匿名内部类到Lambda表达式使用匿名内部类如下所示。
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("Hello Lambda");
}
}
转化为Lambda表达式如下所示。
Runnable r = () -> System.out.println("Hello Lambda");
TreeSet ts = new TreeSet<>( (o1, o2) -> Integer.compare(o1, o2););
从直观上看,Lambda表达式要比常规的语法简洁的多。
Lambda表达式在Java语言中引入了 “->” 操作符, “->” 操作符被称为Lambda表达式的操作符或者箭头操作符,它将Lambda表达式分为两部分:
我们可以将Lambda表达式的语法总结如下。
语法格式一:无参,无返回值,Lambda体只有一条语句
Runnable r = () -> System.out.println("Hello Lambda");
具体示例如下所示。
@Testpublicvoidtest1(){ Runnable r = () -> System.out.println("Hello Lambda");new Thread(r).start();}
语法格式二:Lambda表达式需要一个参数,并且无返回值
Consumer
func = (s) -> System.out.println(s);
具体示例如下所示。
@Testpublicvoidtest2(){ Consumer
consumer = (x) -> System.out.println(x); consumer.accept("Hello Lambda");}
语法格式三:Lambda只需要一个参数时,参数的小括号可以省略
Consumer
func = s -> System.out.println(s);
具体示例如下所示。
@Test
public void test3() {
Consumer consumer = x -> System.out.println(x); consumer.accept("Hello Lambda");
}
语法格式四:Lambda需要两个参数,并且有返回值
BinaryOperator
bo = (a, b) -> { System.out.println("函数式接口");return a + b;};
具体示例如下所示:
@Test
public void test4() {
Comparator comparator = (x, y) -> { System.out.println("函数式接口");
return Integer.compare(x, y); };
}
语法格式五:当Lambda体只有一条语句时,return和大括号可以省略
BinaryOperator
bo = (a, b) -> a + b;
具体示例如下所示:
@Test
public void test5(){
Comparator comparator = (x, y) -> Integer.compare(x, y);
}
语法格式六:Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器能够通过上下文推断出数据类型,这就是“类型推断”
BinaryOperator
等同于
BinaryOperator
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。
Lambda表达式需要函数式接口的支持,所以,我们有必要来说说什么是函数式接口。
只包含一个抽象方法的接口,称为函数式接口。可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。可以在任意函数式接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
我们可以自定义函数式接口,并使用Lambda表达式来实现相应的功能。
例如,使用函数式接口和Lambda表达式实现对字符串的处理功能。
首先,我们定义一个函数式接口MyFunc,如下所示:
@FunctionalInterface
public interfaceMyFunc {public T getValue(T t);}
接下来,我们定义一个操作字符串的方法,其中参数为MyFunc接口实例和需要转换的字符串。
public String handlerString(MyFunc myFunc, String str) {
return myFunc.getValue(str);
}
接下来,我们对自定义的函数式接口进行测试,此时我们传递的函数式接口的参数为Lambda表达式,并且将字符串转化为大写。
@Test
public void test6() {
String str = handlerString((s) -> s.toUpperCase(), "binghe");
System.out.println(str);
}
运行test6方法,得出的结果信息:“BINGHE”。
我们也可以截取字符串的某一部分,如:
@Test
public void test7() {
String str = handlerString((s) -> s.substring(0,4), "binghe");
System.out.println(str);
}
可以看到,我们可以通过handlerString(MyFunc
需求
调用Collections.sort()方法,通过定制排序比较两个Employee(先比较年龄,年龄相同按姓名比较),使用Lambda表达式作为参数传递。
实现
这里,我们先创建一个Employee类,为了满足需求,我们在Employee类中定义了姓名、年龄和工资三个字段,如下:
@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
private static final long serialVersionUID = -9079722457749166858L;
private String name;private Integer age;
private Double salary;
}
接下来,我们在TestLambda类中定义一个成员变量employees,employees变量是一个List集合,存储了Employee的一个列表,如下:
protected List employees = Arrays.asList(
new Employee("张三", 18, 9999.99),
new Employee("李四", 38, 5555.55),
new Employee("王五", 60, 6666.66),
new Employee("赵六", 8, 7777.77),
new Employee("田七", 58, 3333.33)
);
前期的准备工作完成了,接下来,我们就可以实现具体的业务逻辑了。
@Test
public void test1() {
Collections.sort(employees, (e1, e2) -> {if(e1.getAge() == e2.getAge()) {
return e1.getName().compareTo(e2.getName());
}
return Integer.compare(e1.getAge(), e2.getAge());
});
employees.stream().forEach(System.out::println);
}
上述代码比较简单,我就不赘述具体逻辑了。运行test1方法,得出的结果信息如下所示:
Employee(name=赵六, age=8, salary=7777.77)
Employee(name=张三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=田七, age=58, salary=3333.33)
Employee(name=王五, age=60, salary=6666.66)
如果想倒叙输出如何处理呢,只需要在将
return Integer.compare(e1.getAge(), e2.getAge());
修改成
return -Integer.compare(e1.getAge(), e2.getAge());
即可,如下所示:
@Test
public void test1() {
Collections.sort(employees, (e1, e2) -> {if(e1.getAge() == e2.getAge()) {
return e1.getName().compareTo(e2.getName()); }
return -Integer.compare(e1.getAge(), e2.getAge()); });
employees.stream().forEach(System.out::println);}
再次运行test1方法,得出的结果信息如下所示:
Employee(name=王五, age=60, salary=6666.66)
Employee(name=田七, age=58, salary=3333.33)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=张三, age=18, salary=9999.99)
Employee(name=赵六, age=8, salary=7777.77)
结果符合我们的需求!
Java 8用默认方法与静态方法这两个新概念来扩展接口的声明。
我们可以在接口中定义默认方法,使用default关键字,并提供默认的实现。所有实现这个接口的类都会接受默认方法的实现,除非子类覆盖重写接口的默认方法。
public interface Vehicle
{
default void print(){
System.out.println("我是一辆车!");
}
}
我们还可以在接口中定义静态方法,使用static关键字,也可以提供实现。例如:
public interface StaticFunctionInterface {
static String staticFunction() {
return "static function";
}
}
接口的默认方法和静态方法的引入,以后我们再也不用在每个实现类中都写重复的代码了。
Java 8 引入一个很有趣的特性是 Optional 类,Optional 类主要解决的问题是臭名昭著的空指针异(NullPointerException)
Optional 是 Java 实现函数式编程的强劲一步,并且帮助在范式中实现。
Java 8引入Optional类来防止空指针异常,Optional类最先是由Google的Guava项目引入的。Optional类实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional 类的引入很好的解决空指针异常,Optional提供很多有用的方法,这样我们就不用显式进行null值判断(null的防御性检查)。
我们来看一段代码:
public static String getGender(Student student)
{
if(null == student)
{
return "Unkown";
}
return student.getGender();
}
这是一个获取学生性别的方法,方法入参为一个Student对象,为了防止student对象为null, 做了防御性检查:如果值为null,返回"Unkown"。
再看使用Optional优化后的方法:
public static String getGender(Student student)
{
return Optional.ofNullable(student).map(u -> u.getGender()).orElse("Unkown");
}
可以看到,Optional类结合lambda表达式的使用能够让开发出的代码更简洁和优雅。
Optional 类相关API
尽量避免在程序中直接调用Optional对象的get()和isPresent()方法,避免使用Optional类型声明实体类的属性。
(1)Optional.of(T t) : 创建一个 Optional 实例
(2)Optional.empty() : 创建一个空的 Optional 实例
(3)Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
(4)isPresent() : 判断是否包含值 orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
(5)orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
(6)map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()
(7)flatMap(Function mapper):与 map 类似,要求返回值必须是Optional
1.创建optional对象,一般用ofNullable()而不用of():
(1)empty() :用于创建一个没有值的Optional对象:Optional
emptyOpt = Optional.empty(); (2)of() :使用一个非空的值创建Optional对象:Optional
notNullOpt = Optional.of(str); (3)ofNullable() :接收一个可以为null的值:Optional
nullableOpt = Optional.ofNullable(str); 2.判断Null:
(1)isPresent():如果创建的对象实例为非空值的话,isPresent()返回true,调用get()方法会返回该对象,如果没有值,调用isPresent()方法会返回false,调用get()方法抛出NullPointerException异常。
3.获取对象:
(1)get()
4.使用map提取对象的值,如果我们要获取User对象中的roleId属性值,常见的方式是先判断是否为null然后直接获取,但使用Optional中提供的map()方法可以以更简单的方式实现
5.使用orElse方法设置默认值,Optional类还包含其他方法用于获取值,这些方法分别为:
(1)orElse():如果有值就返回,否则返回一个给定的值作为默认值;
(2)orElseGet():与orElse()方法作用类似,区别在于生成默认值的方式不同。该方法接受一个Supplier extends T>函数式接口参数,用于生成默认值;
(3)orElseThrow():与前面介绍的get()方法类似,当值为null时调用这两个方法都会抛出NullPointerException异常,区别在于该方法可以指定抛出的异常类型。
6.使用filter()方法过滤,filter()方法可用于判断Optional对象是否满足给定条件,一般用于条件过滤,在代码中,如果filter()方法中的Lambda表达式成立,filter()方法会返回当前Optional对象值,否则,返回一个值为空的Optional对象。
方法引用提供了非常有用的语法,可以直接引用已有Java类或对象的方法或构造器。方法引用通常与Lambda表达式联合使用,可以使语言的构造更紧凑简洁,减少冗余代码。
举例:
定义了4个方法的Car这个类作为例子,区分Java中支持的4种不同的方法引用。
public static class Car {
public static Car create( final Supplier< Car > supplier ) {
return supplier.get();
}
public static void collide( final Car car ) {
System.out.println( "Collided " + car.toString() );
}
public void follow( final Car another ) {
System.out.println( "Following the " + another.toString() );
}
public void repair() {
System.out.println( "Repaired " + this.toString() );
}
}
第一种方法引用是构造器引用,它的语法是Class::new,或者更一般的Class< T >::new。请注意构造器没有参数。
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
第二种方法引用是静态方法引用,它的语法是Class::static_method。请注意这个方法接受一个Car类型的参数
cars.forEach( Car::collide );
第三种方法引用是特定类的任意对象的方法引用,它的语法是Class::method。请注意,这个方法没有参数。
cars.forEach( Car::repair );
第四种方法引用是特定对象的方法引用,它的语法是instance::method。请注意,这个方法接受一个Car类型的参数
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
1、引入重复注解@Repeatable
自从Java 5引入了注解机制,这一特性就变得非常流行并且广为使用。但是在Java 5中使用注解有一个限制,即相同的注解在同一位置只能声明一次。Java 8引入重复注解,这样相同的注解在同一地方也可以声明多次。
重复注解机制本身必须用@Repeatable注解。事实上,这并不是语言层面上的改变,更多的是编译器的技巧,Java 8在编译器层做了优化,相同注解会以集合的方式保存,因此底层的原理并没有变化。
2、扩展注解的支持
Java 8扩展了注解的上下文。现在几乎可以为任何东西添加注解:局部变量、泛型类、父类与接口的实现,就连方法的异常也能添加注解。
Java 8 API添加了一个新的抽象称为流。Stream把真正的函数式编程风格引入到Java中,可以让你以一种声明的方式处理数据。Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。Stream API极大简化了集合框架的处理,这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
Stream API是把真正的函数式编程风格引入到Java中。其实简单来说可以把Stream理解为MapReduce,当然Google的MapReduce的灵感也是来自函数式编程。她其实是一连串支持连续、并行聚集操作的元素。从语法上看,也很像linux的管道、或者链式编程,代码写起来简洁明了。
Stream详解参考这位前辈:【Java8新特性】Stream API详解
Java 8新的Date-Time API (JSR 310)受开源的Joda-Time库的影响,提供了新的java.time包,可以用来替代 java.util.Date和java.util.Calendar。一般会用到Clock、LocaleDate、LocalTime、LocaleDateTime、ZonedDateTime、Duration这些类,对于时间日期的改进还是非常不错的。
时间大致可以分为三个部分:日期、时间、时区
其中日期又细分为年、月、日;时间又细分为时、分、秒
一般机器时间用从 1970-01-01T00:00 到现在的秒数来表示时间; 这里纠正大部分人犯的一个错误概念,时间戳指的是秒数,而不是毫秒数。
几乎所有的时间对象都实现了 Temporal 接口,所以接口参数一般都是 Temporal
Instant: 表示时间线上的一个点,参考点是标准的Java纪元(epoch),即1970-01-01T00:00:00Z(1970年1月1日00:00 GMT)
LocalDate: 日期值对象如 2019-09-22
LocalTime: 时间值对象如 21:25:36
LocalDateTime: 日期 时间值对象
ZoneId: 时区
ZonedDateTime: 日期 时间 时区值对象
DateTimeFormatter: 用于日期时间的格式化
Period: 用于计算日期间隔
Duration: 用于计算时间间隔
在Java 8中,Base64编码成为了Java类库的标准。Base64类同时还提供了对URL、MIME友好的编码器与解码器。
对于 Base64 终于不用引用第三方包了,使用 java 库就可以完成
// 编码
final String encoded = Base64.getEncoder().encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
// 解码
final String decoded = new String( Base64.getDecoder().decode( encoded ),StandardCharsets.UTF_8 );
并行(parallel)数组:支持对数组进行并行处理,主要是parallelSort()方法,它可以在多核机器上极大提高数组排序的速度。
Java 8增加了大量的新方法来对数组进行并行处理。可以说,最重要的是parallelSort()方法,因为它可以在多核机器上极大提高数组排序的速度。下面的例子展示了新方法(parallelXxx)的使用。
上面的代码片段使用了parallelSetAll()方法来对一个有20000个元素的数组进行随机赋值。然后,调用parallelSort方法。这个程序首先打印出前10个元素的值,之后对整个数组排序。这个程序在控制台上的输出如下(请注意数组元素是随机生产的):
并发(Concurrency):在新增Stream机制与Lambda的基础之上,加入了一些新方法来支持聚集操作。
在新增Stream机制与lambda的基础之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法来支持聚集操作。同时也在java.util.concurrent.ForkJoinPool类中加入了一些新方法来支持共有资源池(common pool)(请查看我们关于Java 并发的免费课程)。
新增的java.util.concurrent.locks.StampedLock类提供一直基于容量的锁,这种锁有三个模型来控制读写操作(它被认为是不太有名的java.util.concurrent.locks.ReadWriteLock类的替代者)。
在java.util.concurrent.atomic包中还增加了下面这些类:
1.DoubleAccumulator
2.DoubleAdder
3.LongAccumulator
4.LongAdder
Nashorn引擎jjs:基于Nashorn引擎的命令行工具。它接受一些JavaScript源代码为参数,并且执行这些源代码。
Nashorn允许在JVM上开发运行JavaScript应用,允许Java与JavaScript相互调用。
除了这十大新特性之外,还有另外的一些新特性:
更好的类型推测机制:Java 8在类型推测方面有了很大的提高,这就使代码更整洁,不需要太多的强制类型转换了。
编译器优化:Java 8将方法的参数名加入了字节码中,这样在运行时通过反射就能获取到参数名,只需要在编译时使用-parameters参数。
类依赖分析器jdeps:可以显示Java类的包级别或类级别的依赖。
JVM的PermGen空间被移除:取代它的是Metaspace(JEP 122)。
当前2020年6月19日:Java最新版本为Java14,于2020年3月17日正式发布。