Java8发生的变化比历史上任何一次变化都要影响深远,而且让你的编程更加容易。
因为编程语言千千万,他们就像一个生态系统一样,新的语言会出现,旧的语言会被取代,除非它不断地演变,能跟上节奏;同理,java也是取代了竞争对手语言,且根据编程市场不断演变才能一直存活的。
Java的天资很好,这个面向对象语言一开始就被精心设计:
(1)有许多有用的库;
(2)集成了线程和锁的支持,一开始就支持小规模的并发;
(3)一开始就设计成将java编译成JVM字节码,这种虚拟机代码可以更快更好的被其他浏览器支持。而JVM的更新也在旨在帮助竞争对手语言在JVM上顺利运行,与java交互操作。
BUT,生态系统中会出现不同的影响因素,比如,随着市场的变化,硬件发生新的变化,也会出现新的编程影响因素。
举例:程序越来越要对大数据进行处理,并希望利用多核或集群有效的处理数据集。这就意味着程序并行处理的需求,这也就是此次java8的一大变革。
所以java要想继续生存下去,不得不做出演变,以适应新市场,所以,演变后的java8为我们提供了新的编程工具和编程概念,帮助程序员更快的编写出更简洁、更容易维护的代码有解决现有的或新的的编程问题。
这里突出介绍java8的三大编程概念:行为参数化、流处理、并行
Java8第一个重要的变成概念就是行为参数化传递代码
在过去,只有原始类型值或对象类型值才可以被传递(这些都叫做一等值),java8引入了方法也可以作为参数被传递。简而言之,方法(或后面说的lambda)被提升到原始类型和对象类型值一样的高度,变成一等值,用来作为参数传递行为代码。
怎么理解方法行为参数化呢?
举个例子,大背景如下:一个菜单,菜单上有几十个菜,每个菜的属性为如下的Dish类:
/**
* 菜品
*/
@Data
@AllArgsConstructor
public class Dish {
private final String name; //菜名
private final boolean vegetarian; //是否为素菜
private int calories; //菜的热量
private final Type type; //菜的类型
public enum Type{ MEAT,FISH,OTHER}
}
public List menu = Arrays.asList(
new Dish("pork",false,800,Dish.Type.MEAT),
new Dish("beef",false,700,Dish.Type.MEAT),
new Dish("chicken",false,400,Dish.Type.MEAT),
new Dish("french fries",true,530,Dish.Type.OTHER),
new Dish("rice",true,350,Dish.Type.OTHER),
new Dish("season fruit",true,120,Dish.Type.OTHER),
new Dish("pizza",true,550,Dish.Type.OTHER),
new Dish("prawns",false,300,Dish.Type.FISH),
new Dish("salmon",false,450,Dish.Type.FISH)
);
需求:对菜单menu数据集中的菜品Dish进行运算。
1.找出卡路里在300以上的菜品Dish。
(解决办法:java8以前你可能写一个方法1,直接遍历menu集合,按照条件得到卡路里大于300的菜品,存放在list中)
2.又增加了需求,需要找出鱼肉类蔬菜。
(又写了一个方法2,采用上面同样的思想遍历一遍,找到符合条件的菜品,存放在list中)
3.又增加了需求,找到符合上述两个条件的菜品?
(同样,又写了一个方法3遍历,选择菜品,存放在list,或者直接修改上面的方法1或者2)
4.其他需求……
纵观上述的需求应对办法,他们都在反复的遍历数据集,选择符合条件的东西,存放在list,其实只有一步是不同的,那就是选择菜品的条件,结果却重写了其他的代码!
有没有优化办法呢?
java8引入了行为参数化的思想,我们可以将选择菜品的代码抽取出来作为一个方法(行为)传递给这个选择菜品的方法即可。如下做法:
第一步:定义函数式接口,用于接收具体的行为方法。
/**
* 函数式接口
*/
@FunctionalInterface //函数式接口声明注解,可省略
public interface Predicate {
boolean pick(T t);
}
第二步:定义业务方法,并将行为代码作为参数传递给业务方法
/**
* 业务方法
*/
public List FilterMenu(List menu, Predicate pre){
List dishes=new ArrayList<>();
for(Dish dish:menu){
if(pre.pick(dish)){ //利用参数化的行为作为筛选条件
dishes.add(dish);
}
}
return dishes;
}
第三步:使用业务方法时,传递具体的行为代码
@Test
public void test(){
//第一种方式:传入匿名函数类,实现其方法
List dishes= FilterMenu(menu, new Predicate(){
@Override
public boolean pick(Dish dish) {
return dish.getCalories()>500;
}
});
//第二种方式:传递方法(要写行为方法以便作为参数传递)
List dishes_2= FilterMenu(menu,Strategy::isHighCalories );
List dishes_3= FilterMenu(menu,Strategy::isHighCalories );
//第三种方式:传递lambda表达式,将传递的方法转换为lambda表达式,随用随写
List dishes_4= FilterMenu(menu,(Dish dish)->dish.isVegetarian() ); //选择蔬菜
dishes_4.forEach(System.out::println);
List dishes_5= FilterMenu(menu,
(Dish dish)->dish.isVegetarian()&&dish.getCalories()>500 ); //蔬菜且热量大于500
dishes_5.forEach(System.out::println);
}
说明:
第一种传入方式:直接传入一个参数,这个参数同时声明并实例化一个匿名类,从而将匿名类的行为。(是不是很熟悉,类同于new Runnable(){public void run(){}})
第二种传入方式:将行为代码抽取出来形成一个算法类,向业务方法传递方法即可,“Strategy::isHighCalories”叫做“方法引用”,表示传递Strategy类下的isHighCalories()方法,本质上是将函数式接口指向Strategy类的isHighCalories()方法。
public class Strategy {
public static boolean isHighCalories(Dish dish){
return dish.getCalories()>400;
}
public static boolean isHighCaloriesAndFish(Dish dish){
return dish.getType().equals(Dish.Type.FISH);
}
}
第三种传入方式:直接传递行为代码,即以 lambda表达式的形式传递行为代码,将行为代码和逻辑紧绑在一起,简洁易懂。
行为参数化的好处:这几种方式都属于行为参数化,他们都可以将要筛选的逻辑和元素行为分离开来,这样就可以重复使用同一个方法,给他不同的行为来达到不同的目的,从而避免 因为需求变化不断而重复部分代码。即“多种行为,一个参数”
使用场景:java8的这种将方法(代码)作为参数传递给另一个方法的能力,在应对频繁的需求变化时尤其方便,可以减轻未来的工作量,我们把这种概念叫做行为参数化。
(行为参数化、lambda表达式,方法引用等更多介绍参看其他章节。)
Java8的java.util.stream提供了一个Stream API,它提供了很多方法,这些方法可以连接起来形成一个复杂的流水线,高效处理数据集。并且可以透明的将输入的数据(彼此不相关,即数据集中的数据没有共享变量)拿到几个CPU分别执行Stream流水线,这种免费的并行可以轻松替代Thread线程了。
举个例子,有需求:现在想要遍历菜品,找到前三个高热量的菜品名?
(以前的老办法大概就是使用先对菜单进行排序,保存在另一个列表中,然后取出前三个热量较大的菜品,把名称保存在另一个list中。---最少需要两行代码,甚至需要四五行代码,如果没有合适的注释,有的人可能一眼看不出来这些代码想要干什么)
现在看看Java8解决办法:对数据集 引入流操作
@Test
public void streamAPITest(){
List menuNames=menu.stream() //1:从menu获取流
.filter(d->d.getCalories()>300) //2:筛选出高热量的菜品
.map(Dish::getName) //3:使用映射获取菜品名称
.limit(3) //4:截短只取前三个符合条件的结果
.collect(Collectors.toList()); //5:结束流操作,将流保存到另外一个List中
menuNames.forEach(System.out::println); //输出流水线操作的结果,即:pork beef chicken
}
引入流之后,用很简单的操作流水线即可解决(参考上面五步注释);
从上面代码可以看出流操作具有的特点:
新的StreamAPI和现有的集合API的行为差不多,都能访问数据项目序列,他们有什么区别么?
Collections主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算,且流允许也提倡并行处理一个Stream中的元素。
Java8引入流后,流实现并行比Java现有的线程API更容易(在多个处理器内核之间使用synchronized来打破“不能有共享可变数据”这一规则的代价比预期的要大的多)
实现并行流的代价很小,使用parallelStream()获取并行流即可:
List menuNames=menu.parallelStream() //从menu获取并行流
.filter(d->d.getCalories()>300)
.map(Dish::getName)
.limit(3)
.collect(Collectors.toList());
并行流处理过程图例:
执行时:
首先,java8的库会负责分块流水线同时执行,把大的流分成小的流,以便并行处理。
其次,流提供的并行,只有在传递给不会发生元素互动(如有共享变量)的方式时才会生效,比如filter方法。
所以我们常说的函数式编程不止包含了方法提升到一等值可作为参数的意思,还包括第二层一次:“执行时元素之间无互动”;由此可以得出结论:
结论:没有共享的可变数据和将方法(函数或代码)传递给其他方法的能力,是函数式编程的基石。
上面的并行流在处理大量数据时,可以更高效地选出符合条件的菜品名称。
但需要明白几点:
java8主要的变化反映了他开始偏离经典面向对象思想,开始侧重于函数式编程领域,两者看起来是冲突的,但是java的演变理念是获取两种编程范式中最好的东西,相互融合。
而语言也只有不断改进以跟进硬件的更新或者满足程序员的需求和期待,他才能更具竞争力。而作为程序员的我们,熟悉并使用java8的新功能,其实也是在保护我们自己的java职业生涯。
除了上述变化,java8还有其他好的思想:
主要是为了支持库设计师和向后兼容—接口后续新增方法签名,会同时提供默认方法,无需劳烦所有接口实现类兴师动众的添加实现方法,以保证编译通过。
例如:java8的List接口提供了sort方法,也是一个默认方法,并且工具类Collections.sort()也是调用了该方法。
default void sort(Comparator super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
使用时,传入行为即可,相当于实现了Comparator接口的compare方法。
dishes.sort((Dish d1,Dish d2)->d1.getCalories()-d2.getCalories());
Collections.sort(dishes,(Dish d1,Dish d2)->d1.getCalories()-d2.getCalories());
Optional
isPresent():判断容器中是否有值。
ifPresent(Consume lambda):容器若不为空则执行括号中的Lambda表达式。
T get():获取容器中的元素,若容器为空则抛出NoSuchElement异常。
T orElse(T other):获取容器中的元素,若容器为空则返回括号中的默认值。
提供了新的java.time包,可用来替代老的 java.util.Date和java.util.Calendar。常用的有Clock、LocaleDate、LocalTime、LocaleDateTime、ZonedDateTime、Duration这些类,对于时间日期的改进还是非常不错的
可以利用java8改善代码可读性,下面列出几个重构方向:
(1)匿名类->lambda表达式
(2)lambda表达式->方法引用
(3)命令式语句->stream声明式语句
(4)采用函数式接口,有条件的延迟执行
(5)重构设计模式:策略模式、观察者模式、模板模式、责任链模式、工厂模式