Lambda
来源:互联网 发布:淘宝美工和ui设计师 编辑:程序博客网 时间:2024/06/05 11:44
引入
我们都知道java是一门面向对象的语言,我们也清楚面向对象思想的强大。但是,这个概念,在java8出现之后,被推翻了。
java8引入了函数式开发,其实C语言就是标准的函数式开发。 那我们就更疑惑了,为什么如此好的一种思想,还会在后期被推翻了呢?
其实原因很简单,随着互联网的发展,越来越重视并行开发和基于事件的开发,而函数式开发,就特别擅长做这些事情。
什么是函数式编程
函数式编程的起源,是一门叫做范畴论(Category Theory)的数学分支。什么是范畴呢?
“范畴就是使用箭头连接的物体。”(In mathematics, a category is an algebraic structure that comprises “objects” that are linked by “arrows”. )
也就是说,彼此之间存在某种关系的概念、事物、对象等等,都构成”范畴”。随便什么东西,只要能找出它们之间的关系,就能定义一个”范畴”。
范畴论使用函数,表达范畴之间的关系。
伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的”函数式编程”。
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
函数式是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
环境准备
如果还没有安装Java 8,那么你应该先安装才能使用lambda和stream。 如果是使用Eclipse工具,那么需要安装4.4以上的版本。之后,就可以使用Java 8的新特性了,包括lambda表达式,可重复的注解,紧凑的概要文件和其他特性。
面向对象编程
我们先使用面向对象编程,来完成两个小案例。
1. 对数组中的对象进行排序
2. 创建一个线程,并开启线程。
class User{ public String name; public int score; public User(String name, int score) { super(); this.name = name; this.score = score; } @Override public String toString() { return "User [name=" + name + ", score=" + score + "]"; } } @Test public void arrayComparableTest(){ User[] us = new User[]{new User("张三", 10), new User("李四", 15), new User("王五", 12)}; Arrays.sort(us, new Comparator<User>() { // 对数组中的元素进行排序 @Override public int compare(User o1, User o2) { return Integer.compare(o1.score, o2.score); } }); System.out.println(Arrays.toString(us)); } @Testpublic void threadTest(){ Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println("hello lambda"); } }); t.start();}
其实,上面两个案例,都是体现的函数式编程。方法中需要另外一个方法来实现业务逻辑。因为java中之前没有函数式编程,所以想用另外一个方法来实现逻辑,只能通过匿名接口的方式。
现在,我们对这段代码进行分析:
User[] us = new User[]{new User("张三", 10), new User("李四", 15), new User("王五", 12)}; Arrays.sort(us, new Comparator<User>() { // 对数组中的元素进行排序 @Override public int compare(User o1, User o2) { return Integer.compare(o1.score, o2.score); } });
- sort方法,第二个参数,一定是一个Comparator接口。
- Comparator接口,一定需要实现一个compare方法。
compare方法,一定需要返回一个int类型的结果。
那我们就有疑问了,这些一定会发生的事情,编译器是否可以帮助我们做,帮助我们推导出来结果呢?
假设,编译器可以帮助我们推导,那么,我们就可以将代码改成下面的样子。
我们,将一定会发生的事情,删除掉,让编译器去推导。User[] us = new User[]{new User("张三", 10), new User("李四", 15), new User("王五", 12)};Arrays.sort(us, (User o1, User o2) { return Integer.compare(o1.score, o2.score);});System.out.println(Arrays.toString(us));
此时,语法不正确,代码当然编译不能通过。那我们该如何处理呢?之前有讲到一个概念,范畴论。使用箭头来连接物体。所以,这里提出一种新的语法: -> ,指向某处的意思(goes to)。
User[] us = new User[]{new User("张三", 10), new User("李四", 15), new User("王五", 12)}; Arrays.sort(us, (User o1, User o2) -> { return Integer.compare(o1.score, o2.score); }); System.out.println(Arrays.toString(us));
不负众望,编译通过了。编译通过,说明语法没问题。
那这种新的语法是什么呢?这个,就是Lambda表达式。
面向函数式编程
有没有觉得很厉害很神奇?
其实不然,回过头,我们去对比一下,发现,其实,使用Lambda表达式,就是将我们分析出来的,一定、必不可少的东西删除了,让编译器自己去推导出结果。从一定需要程序员来书写的东西,交给了编译器而已。
所以说,尽管我们使用了一种简洁的方式去写,但是,编译器也能推导出来,代码在编译的时候,也会推导成我们最开始的写法,其实Lambda表达式是新特性吗?不是,仅仅是编译器的新特性,解放程序员而已。
但是,有问题。我发现,感觉也没之前的写法方便多少呀,而且我还要记新的语法。
其实,我们还可以对上面的代码进行优化,优化的原则就是:将一定需要做的事情,简化掉。
Arrays.sort(us, (User o1, User o2) -> { return Integer.compare(o1.score, o2.score); });
这已经是一个Lambda表达式了。那我们再来分析,还有什么地方是一定要做的呢?
1. compare方法中只有一行代码,并且,这一行代码,一定会返回一个int类型的值。那么,我们去删除它。
Arrays.sort(us, (User o1, User o2)-> Integer.compare(o1.score, o2.score));
编译通过。
2. Compare方法,有两个参数,这两个参数的类型,可以通过传递的数组来推导元素类型,一定是User类型。那么,我们删除它。
Arrays.sort(us, (o1, o2)-> Integer.compare(o1.score, o2.score));
编译通过。
到这里,我们在来比较一下:
Arrays.sort(us, new Comparator<User>() { @Override public int compare(User o1, User o2) { return Integer.compare(o1.score, o2.score); } }); //Arrays.sort(us, (o1, o2)-> Integer.compare(o1.score, o2.score));
对于代码的简洁程度来说,高下立判。
好,我们快速的将第二个案例修改一下。
Thread t = new Thread(()-> System.out.println("hello lambda")); t.start();
Lambda表达式的含义
表达式的含义是什么?表达式就是一个组合,可以求得结果。Lambda表达式,首先是一个表达式,那么,代表着这个表达式在运算之后,也会有一个结果的。因为我们已经很清楚的为函数式定义过:函数式是一种数学运算,原始目的就是求值,不做其他事情。
所以,现在我们可以尝试,将Lambda表达式作为一个结果,赋值给一个变量。
Runnable r = new Runnable(){ public void run(){ System.out.println("hello lambda"); }}// 将上面的代码,用Lambda表达式去修改。Runnable r1 = () -> System.out.println("hello lambda");
当编译器看到这一段Lambda表达式的时候,就会去做推导,根据接受变量的类型,来推导出Lambda的常规写法。
Lambda表达式详解
概念
我们先看一下,百度百科对Lambda表达式的定义:
“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。
我们将带有参数变量的表达式,就称之为Lambda表达式。
Lambda表达式基本语法
Lambda表达式的基本语法:方法参数列表 -> 表达式; 参数列表,是接口的方法参数列表,表达式,是接口方法的具体逻辑实现。
方法参数列表
方法没有参数
这个案例其实我们已经做过了,创建一个线程,传递一个Runnable对象即可。
没有参数,需要注意的是,参数列表的 () 是一定不能省略的。因为括号才表示是方法的参数列表。new Thread(() -> System.out.println("没有参数")).start();
方法有一个参数
先引入一个案例:Frame f = new Frame("lambda");f.setBounds(100, 100, 100, 300);Button b = new Button("按钮");b.addActionListener((ActionEvent event) -> System.out.println("按钮被点击"));f.setVisible(true);
如果是一个参数,需要注意:
- 如果参数写类型,那么,必须写方法的()
如果参数不写类型(编译器可以推导出来参数的类型),那么,可以省略方法的()
所以,我们还可以这么写:Frame f = new Frame("lambda");f.setBounds(100, 100, 100, 300);Button b = new Button("按钮");b.addActionListener(event -> System.out.println("按钮被点击"));f.setVisible(true);
方法有两个或多个参数
如果是两个或者多个参数,那么需要注意,不管是否有参数类型,都必须有()Integer[] arr = new Integer[]{1,5,3,4};Arrays.sort(arr, (x, y) -> Integer.compare(x, y));System.out.println(Arrays.toString(arr));
如果有其他的修饰符修饰
比如,可以用final修饰局部变量,此时,如果要写额外的修饰符的时候,参数必须得有类型。不管是一个参数还是多个参数。Integer[] arr = new Integer[]{1,5,3,4};Arrays.sort(arr, (final Integer x, final Integer y) -> Integer.compare(x, y));System.out.println(Arrays.toString(arr));
表达式
- 如果表达式只有一行代码
可以不需要{},直接写,不能写return关键字。编译器帮助我们推导return。 - 如果表达式超过一行
那么这一段代码,就必须写{},就必须遵循方法的规则,并且如果方法有返回,就必须有返回语句。
变量作用域
在Lambda表达式中,可以存在三种变量。
1. 局部变量
2. 方法参数
3. 自由变量(不是局部变量,也不是方法参数)
我们分析这三种变量,其实对于局部变量和方法参数变量,跟我们普通的方法使用规则都是一样的。这里就不介绍了。我们要说两个比较特殊的情况:
比如下面的这一段代码
@Testpublic void test1(){ print("lambda", 5);}public void print(String content, int times){ new Thread(()-> { for (int i = 0; i < times; i++) { System.out.println(content); } }).start();}
- 自由变量,在Lambda表达式中,可以随意使用自由变量。比如,我们可以在run方法中使用到外层方法的参数。
其实这种写法,编译器在编译的时候,会推导成常规的写法,常规的我们会如何写?我们会使用final修饰content 和 times,为什么吗?因为print方法可能执行完毕之后,子线程才会执行,那么如果没有用final修饰content,方法执行完毕之后,这个局部变量就会被回收,相当于是线程中访问了一块已经被回收的内存,所以我们使用final修饰,将局部变量放置到常量池中保存起来。
那Lambda中,可以直接使用content变量,我们可以如何推断?其实编译器已经默认将自由变量用final修饰,保存起来了。我们只能使用这个变量,不能修改final变量。 - this,在匿名内部类中,如果我们使用this,那么,这个this其实是匿名内部类对自己的引用。但是,在Lambda表达式中的this,却是指向的该Lambda表达式所在方法,这个方法所在的类的引用。这一点需要跟以前的情况区分开。
- 操作自由变量的代码块,我们就称之为“闭包”。
函数式接口
想想我们写的几个案例,使用的都是接口,比如Runnable、Comparator、ActionListener等,发现,这些接口都只有一个抽象方法。那么,我们可以用Lambda写一个接口,接口中有多个方法吗?
不可以,我们能够写Lambda表达式的地方,只能是接口,并且这个接口中只能有一个抽象方法。这种接口,我们就称之为函数式接口。
思考两个问题:
1. 为什么Lambda表达式中,接口不能有多个方法?
其实,要回答这个问题,也很容易。Lambda主要是如何运行的?编译器推导出常规的写法来运行的。那么,大家可以试想一下,如果一个接口中包含多个方法,那么,编译器可以很容易的推导么?当然不可以!
2. 我们可不可以自定义一个接口,然后,只要在有接口参数的地方,就用Lambda表达式来写?可以!
@Test public void test5(){ this.test4(()->System.out.println("my work")); } public void test4(IWork work){ System.out.println("begin work"); work.doWork(); System.out.println("end work"); } interface IWork { void doWork(); }
- 在java中,如果接口中只有一个抽象方法,那么这个接口就是函数式接口。我们可以使用注解@FunctionalInterface 来检测这个接口是否是函数式接口。
- @FunctionalInterface注解只是用来检测,并不能定义某个接口是否是函数式接口,函数式接口主要是看接口的方法数。
- 简化函数式接口的使用,是Lambda表达式出现的唯一作用。
回过头,我们在看一下这一句代码。
this.test4(()->System.out.println("my work"));
好像,我们可以用一个接口变量来接收Lambda表达式的结果。所以,代码变成了这样:
IWork work = ()->System.out.println("my work");this.test4(work);
那么,问题来了,
Runnable r = ()->System.out.println("my work");
这样去申明一个Runnable接口,也是可以的。 那,如果我仅仅这么写
()->System.out.println("my work");
这个Lambda表达式,到底是用什么类型的接口去接收呢?
所以,结论是什么呢?结论就是,单靠Lambda表达式,是不能知道它的返回类型的,必须通过接收的变量类型,去推导这个Lambda表达式是否符合规范。而不能从Lambda表达式推导出变量类型。
- 函数式接口中,是可以写Object中的方法的,因为接口也是一个特殊的类,重写Object中的方法,不影响函数式接口。
- Lambda表达式中的异常处理, 要么就是在Lambda的代码块中自行try,要么是在接口中的方法中申明throws。
方法引用
引入
我们还是看之前写过的一个案例。
Integer[] arr = new Integer[]{1,5,3,4}; Arrays.sort(arr, (x, y) -> Integer.compare(x, y)); System.out.println(Arrays.toString(arr));
如何分析这一段代码呢?我们发现,方法需要两个参数 x/y,而这两个参数,原封不动的交给了Integer的compare方法的参数列表。那我们就有一个推论,既然这种情况出现,那么可不可以通过某种方式,让编译器自己去推导常规写法呢?
这里,我们引入一种新的语法结构 :: ,如果Lambda表达式中,接口方法的实现,只调用其他方法来真正完成逻辑,并且接口方法的参数原封不动的传递给了那个完成逻辑的方法,那么我们就可以使用这种语法。
我们还修正一下上面的代码。
Integer[] arr = new Integer[]{1,5,3,4}; Arrays.sort(arr, Integer::compare);// 两个参数,原封不动的传递给了compare方法 System.out.println(Arrays.toString(arr));
这种写法,我们就称之为方法引用。
方法引用详解
在Lambda表达式中,支持三种方法引用:
1. 类 :: 静态方法
2. 对象 :: 普通方法
3. 对象 :: 静态方法
对于静态方法,第一种和第三种其实是一样的,因为尽管用对象去调用静态方法,其实编译器还是使用的类去访问。
类 :: 静态方法, 我们讲解的案例,就是这种情况。
那现在我们来研究一下,对象 :: 普通方法
案例一:
我们对上面的代码进行改造:
@Testpublic void test3(){ Integer[] arr = new Integer[]{1,5,3,4}; LambdaTest test = new LambdaTest(); // 对象 :: 普通方法 Arrays.sort(arr, test::myCompare); System.out.println(Arrays.toString(arr));}public int myCompare(int x, int y){ return Integer.compare(x, y);}
代码写到这里,立马就会发现一个问题,为啥我们还要去创建一个对象呢?在类的普通方法里面,this就代表当前对象。所以,代码,我们还可以这么写:
@Testpublic void test3(){ Integer[] arr = new Integer[]{1,5,3,4}; // 对象 :: 普通方法 Arrays.sort(arr, this::myCompare); System.out.println(Arrays.toString(arr));}public int myCompare(int x, int y){ return Integer.compare(x, y);}
另外,我们花半分钟的时间,将这个普通方法改成静态方法,再用Lambda表达式写一遍。
@Testpublic void test3(){ Integer[] arr = new Integer[]{1,5,3,4}; // 类 :: 静态方法 Arrays.sort(arr, LambdaTest::myCompare); System.out.println(Arrays.toString(arr));}public static int myCompare(int x, int y){ System.out.println("一些其他的逻辑代码"); return Integer.compare(x, y);}
案例二:
疑问,我们都是调用的系统的方法,那么我们是否可以自己定义一个接口,用Lambda表达式来写呢?
// 定义的函数式接口@FunctionalInterfacepublic interface IWork { void work(int x, int y);// 接口方法没有返回值。}public class lambdatest { public void wrap(IWork work){ System.out.println("some work"); work.work(3, 4); } @Test public void test1(){ this.wrap(this::getSum);// 使用对象来调用普通方法,注意,这个方法是有返回值的。 } public int getSum(int x, int y){ System.out.println(x + y); return x + y; }}
对于这个案例,我们分析一下这个接口方法返回值和调用的普通方法返回值的问题。
1. 如果接口方法有返回值,我们使用方法引用,那么引用的方法,也必须有对应的返回值。因为接口方法需要return语句,仅仅是我们没有写出来而已,编译器会推导一个return语句。
2. 如果接口方法没有返回值,那么,引用的普通方法,返回值类型就随意定。
要想这个问题其实也很简单。比如
// 情况一public void run(){ // 如果这个是一个接口方法,这个接口方法里面可以调用有返回的普通方法,允许。 otherMethod();}// 情况二public int run1(){ // 如果这个是一个接口方法,这个接口方法调用普通方法,普通方法必须有对应的return语句才行 return otherMethod();}
案例三
java8中,List集合为我们提供了一个forEach方法,从字面意思,我们就能猜出,这个方法是用来遍历元素的,这个方法需要传递一个接口Consumer,Consumer就是一个函数式接口。所以,我们完成以下,遍历集合,打印元素。
@Testpublic void forEach(){ Integer[] arr = new Integer[]{1,2,3,4,5}; List<Integer> ls = Arrays.asList(arr); // 既然是函数式接口,那么我们就可以使用Lambda表达式来书写 ls.forEach(x -> System.out.println(x));}
我们分析这一个Lambda表达式
ls.forEach(x -> System.out.println(x));
这句表达式的含义是,Consumer接口的方法中需要一个参数,我们将这个参数直接传递给了打印操作。将参数原封不动的传递给某一个方法,我们也可以尝试的使用方法引用来改造代码。如何修改呢?
记住语法格式: 对象 :: 普通方法
println 是一个普通的方法,没有用static修饰,所以,我们需要用对象调用,那对象又是谁呢? System.out就是一个打印流对象。所以,我们可以将代码修改成这样:
ls.forEach(System.out::println);
这,也是我们以后使用Lambda表达式做打印操作,最常用的写法。
构造器引用
引入
其实,构造器,也是一个特殊的方法,但是这个方法,我们是结合关键字new一起使用的。比如,创建一个对象,我们就 new 类名(参数列表);
现在我们有这种使用场景:
当我们调用一个方法,返回的是一个接口对象的时候,一般,我们必须要返回这个接口对象的实现类对象,因为接口不能创建对象,那么,我们之前怎么做的呢?
// Arrays工具类中的as List 方法。public static <T> List<T> asList(T... a) { return new ArrayList<>(a);}
但是,这个方法是返回一个固定的ArrayList,如果我们期望,可以指定添加到一个集合对象中,比如这里,我希望可以将元素添加到LinkedList中,那我们怎么做?我们是不是应该在定义方法的时候,就要显示的将要创建的对象的类型传递给方法。所以,我们需要这么定义。
public static <T> List<T> asList(Class clz, T ... a){ // 通过反射机制,来创建传递过来的类的对象,然后再将元素添加到这个对象中,进行返回。}
但是这样写,代码耦合度太高,我们为了模块之间的解耦,可以使用一个接口来实现这种方式。
// 申明泛型为List 或者其子类类型public interface IMyCreator<T extends List<?>>{ T create();}// 测试类public class ListTest { // 传入一个接口类型,通过接口方法,返回一个类对象,再将元素添加到这个对象中,返回回去 @SuppressWarnings("unchecked") public <T> List<T> myAsList(IMyCreator<List<T>> create, T ... a){ List<T> list = create.create(); for (T t : a){ list.add(t); } return list; } @Test public void add(){ // 这里就使用了Lambda表达式,接口参数列表为空,接口中的方法具体逻辑就是返回一个ArrayList List<Integer> list = this.myAsList(()->new ArrayList<Integer>(), 1, 2, 3, 4); System.out.println(list); }}
分析这一段代码:
this.myAsList(()->new ArrayList<Integer>(), 1, 2, 3, 4);// 这已经是一个Lambda表达式了
这种场景,就是一个接口中定义一个方法,返回一个对象。那么,在Lambda表达式中,这种情况,我们就称之为构造器引用,语法格式是: 类 :: new
我们可以使用构造器引用的方式,来改造上面的Lambda表达式:
this.myAsList(ArrayList<Integer>::new, 1, 2, 3, 4);// 创建对象的时候,指定泛型
构造器引用细节
- 构造器引用的函数式接口,这个接口中的方法不能有参数。
Lambda表达式需要返回的对象,这个对象必须要有一个无参的构造器。
如果不满足这两点,我们就不能使用构造器引用,只能使用普通的Lambda表达式,比如:this.myAsList((x)->new MyArrayList<Integer>(x), 1, 2, 3, 4);// 只能使用简单的Lambda表达式
Lambda表达式的优点
Lambda表达式,看起来挺先进,其实经过我们的分析,还是由编译器来帮助我们推导成为常规代码。所以,很多人都觉得不建议使用Lambda表达式,因为他们觉得,如此写代码,感觉尽管简单,但是难懂,难以调试,不利于维护。
是吗?不是的,Lambda表达式,他的一些缺点,掩盖不了它的优点。
1. 代码简洁,开发快速
函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。
2. 接近自然语言,易于理解
函数式编程的自由度很高,可以写出很接近自然语言的代码。
3. 更方便的代码管理
函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
4. 易于”并发编程”
函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。
5. 代码的热升级
函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。
- Lambda
- lambda
- lambda
- lambda
- Lambda
- lambda
- lambda
- Lambda
- Lambda
- lambda
- lambda
- lambda
- lambda
- lambda
- lambda
- lambda
- lambda
- lambda
- Python爬煎蛋网的图片——老司机的第一步
- 06--MySQL自学教程:DML(Data Manipulation Language:数据库操作语言),只操作表
- SQL Server 2008 允许远程连接的解决方法
- Log4j 2使用教程一【简单使用】
- dinic+当前弧优化 模板
- Lambda
- 关于java调用http接口
- 关于 Metasploitable2 下的 unreal_ircd_3281_backdoor漏洞利用
- WinCE 中的 ListView怎么显示网格
- C语言也有大学问——大数相加问题
- Java 实现根据权重设置抽奖概率
- struts2,hibernate4,spring3配置时问题汇总及解决办法
- X86 LDS指令解析
- LeetCode——add two numbers