java 8引入lambda迫切需求是因为lambda 表达式能简化集合上数据的多线程或者多核的处理,提供更快的集合处理速度
函数式接口
Java 8 引入的一个核心概念是函数式接口(Functional Interfaces)。通过在接口里面添加一个抽象方法,这些方法可以直接从接口中运行。如果一个接口定义个唯一一个抽象方法,那么这个接口就成为函数式接口。同时,引入了一个新的注解:@FunctionalInterface。可以把他它放在一个接口前,表示这个接口是一个函数式接口。这个注解是非必须的,只要接口只包含一个方法的接口,虚拟机会自动判断,不过最好在接口上使用注解 @FunctionalInterface 进行声明。在接口中添加了 @FunctionalInterface 的接口,只允许有一个抽象方法,否则编译器也会报错,可以拥有若干个默认方法。
java.lang.Runnable 就是一个函数式接口。 @FunctionalInterface public interface Runnable { public abstract void run(); }
lambda表达式
Java中的lambda无法单独出现,它需要一个函数式接口来盛放,lambda表达式方法体其实就是函数接口的实现。
lambda语法包含三个部分:
1)一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数
2)一个箭头符号: ->
3)方法体,可以是表达式和代码块,方法体函数式接口里面的方法实现,如果是代码块要用{ }包起来,并且需要 return 返回值,如果方法返回值是 void 则不需要 { }
/** * @description: lambda表达式 * @author: liuxin * @create: 2019-01-26 18:39 **/ public class Test { private static void runNew(){ /* * Runnable是一个函数接口,只包含一个无参数返回void的run方法 * 所以lambda表达式没有参数也没有return * */ new Thread(()-> System.out.println("lambda方法")).start(); } private static void runOld(){ new Thread(new Runnable() { @Override public void run() { System.out.println("内部类实现方法"); } }).start(); } public static void main(String[] args) { Test.runNew(); Test.runOld(); } }
使用lambda表达式可以使代码更加简洁的同时保持可读性。
方法引用
方法引用是lambda表达式的一个简化写法,引用的方法其实是表达式的方法体实现,语法非常简单,左边是容器(类名,实例名)中间是“” :: “”,右边则是相应的方法名。
ObjectReference::methodName
引用格式:
1)静态方法:ClassName::methodName。例如 Object::equals
2)实例方法:Instance::menthodName。例如 Object obj =new Object();obj::equals
3)构造函数:ClassName::new
/** * @description: 方法引用 * @author: liuxin * @create: 2019-01-26 18:58 **/ public class TestMethod { public static void main(String[] args) { ArrayList<Integer> a =new ArrayList<>(); a.add(1); a.add(2); //这里addActionListener方法的参数是ActionListener,是一个函数式接口 //使用lambda表达式方式 a.forEach(e -> { System.out.println("Lambda实现方式"); }); //使用方法引用方式 a.forEach(TestMethod::sys); } public static void sys(Integer e) { System.out.println("方法引用实现方式"); } }
这里的sys()方法就是lambda表达式的实现,这样的好处是,当方法体过长影响代码可读性时,可以使用方法引用。
默认方法和静态方法
默认方法:
Java 8 还允许我们给接口添加一个非抽象的方法实现,就是接口可以有实现方法,而且不需要实现类去实现其方法,只需要使用 default 关键字即可,这个特征又叫做扩展方法。在实现该接口时,该默认扩展方法在子类上可以直接使用,它的使用方式类似于抽象类中非抽象成员方法。为什么要有这个特性?首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。但扩展方法不能够重载 Object 中的方法。例如:toString、equals、 hashCode 不能在接口中被重载。
public interface TestA { default void testA(){ System.out.println("这是默认方法!!!"); } } /** * @description: 默认方法 * @author: liuxin * @create: 2019-01-27 10:30 **/ public class TestDefult implements TestA{ public static void main(String[] args) { TestDefult testDefult =new TestDefult(); //调用testA()方法 testDefult.testA(); } }
静态方法:在接口中,还允许定义静态的方法。接口中的静态方法可以直接用接口来调用。
public interface TestA { static void TestB(){ System.out.println("这是静态方法!!!"); } } public class TestDefult { public static void main(String[] args) { TestA.TestB(); } }
java 8抽象类与接口对比
这一个功能特性出来后,很多同学都反应了,java 8的接口都有实现方法了,跟抽象类还有什么区别?其实还是有的,请看下表对比。。
多继承冲突
public interface TestA { default void testA(){ System.out.println("这是默认方法!!!"); } } public interface TestB extends TestA{ default void testA(){ System.out.println("这是默认方法B!!!"); } } public class TestDefult implements TestB,TestA{ public static void main(String[] args) { TestDefult testDefult =new TestDefult(); testDefult.testA(); } } 输出结果为:这是默认方法B!!!
如果想调用testA的默认函数,则要用X.super.m(。。。。。)
public class TestDefult implements TestA{ @Override public void testA(){ TestA.super.testA(); } public static void main(String[] args) { TestDefult testDefult =new TestDefult(); testDefult.testA(); } }
默认方法给予我们修改接口而不破坏原来的实现类的结构提供了便利,目前java 8的集合框架已经大量使用了默认方法来改进了,当我们最终开始使用Java 8的lambdas表达式时,提供给我们一个平滑的过渡体验。
JSR335
JSR是Java Specification Requests的缩写,意思是Java 规范请求,Java 8 版本的主要改进是 Lambda 项目(JSR 335),其目的是使 Java 更易于为多核处理器编写代码。JSR 335=lambda表达式+接口改进(默认方法)+批量数据操作。前面我们已是完整的学习了JSR335的相关内容了。
外部VS内部迭代
外部迭代就是我们常用的for循环和while循环 public static void main(String[] args) { ArrayList<Integer> a =new ArrayList<>(); a.add(1); a.add(2); for (Integer i :a){ System.out.println(i); } }
在现在多核的时代,如果我们想并行循环,不得不修改以上代码。效率能有多大提升还说定,且会带来一定的风险(线程安全问题等等)。
要描述内部迭代,我们需要用到Lambda这样的类库,下面利用lambda和Collection.forEach重写上面的循环
public static void main(String[] args) { ArrayList<Integer> a =new ArrayList<>(); a.add(1); a.add(2);
a.forEach(i-> System.out.println(i)); }
现在是由jdk 库来控制循环了,库可以根据运行环境来决定怎么做,并行,乱序或者懒加载方式。这就是内部迭代
Stream API
流(Stream)仅仅代表着数据流,并没有数据结构,所以他遍历完一次之后便再也无法遍历(这点在编程时候需要注意,不像Collection,遍历多少次里面都还有数据),它的来源可以是Collection、array、io等等。
1)中间与终点方法:
流作用是提供了一种操作大数据接口,让数据操作更容易和更快。它具有过滤、映射以及减少遍历数等方法,这些方法分两种:中间方法和终点方法,中间方法返回的是Stream,允许更多的链式操作,如果我们要获取最终结果的话,必须使用终点操作才能收集流产生的最终结果。区分这两个方法是看他的返回值,如果是Stream则是中间方法,否则是终点方法。具体请参照Stream的api。
中间方法:
filter():对元素进行过滤;
sorted():对元素排序;
map():元素的映射;
distinct():去除重复元素;
subStream():获取子 Stream 等。
//过滤大于1的结果 public static void main(String[] args) { ArrayList<Integer> a =new ArrayList<>(); a.add(1); a.add(2); a.add(2); a.add(2); a.stream().filter(i->i>1).forEach(System.out::println); } 输出结果为2 2 2
//去重 public static void main(String[] args) { ArrayList<Integer> a =new ArrayList<>(); a.add(1); a.add(2); a.add(2); a.add(2); a.stream().distinct().forEach(System.out::println); } 输出结果为 1 2
终点方法:
forEach():对每个元素做处理;
toArray():把元素导出到数组;
findFirst():返回第一个匹配的元素;
anyMatch():是否有匹配的元素等。
2)顺序流与并行流
流有串行和并行两种,串行流上的操作是在一个线程中依次完成,而并行流则是在多个线程上同时执行。并行与串行的流可以相互切换:通过 stream.sequential() ( .sequential() 可以省略)返回串行的流,通过 stream.parallel() 返回并行的流。相比较串行的流,并行的流可以很大程度上提高程序的执行效率。
public static void main(String[] args) { //串行流计算一个范围100万整数流,求能被2整除的数字 int a[]= IntStream.range(0, 1_000_000).sequential() .filter(p -> p % 2==0).toArray(); long time1 = System.nanoTime(); //并行流来计算 int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray(); long time2 = System.nanoTime(); System.out.printf("serial: %.2fs, parallel %.2fs%n", (time1 - time0) * 1e-9, (time2 - time1) * 1e-9); } 结果为 0.07s 和 0.02s,可见,并行排序的时间相比较串行排序时间要少很多。
如果没有lambda,Stream用起来相当别扭,他会产生大量的匿名内部类,如果没有default method,集合框架更改势必会引起大量的改动,所以lambda+default method使得jdk库更加强大,以及灵活,Stream以及集合框架的改进便是最好的证明。