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);          }      });    
  1. sort方法,第二个参数,一定是一个Comparator接口。
  2. Comparator接口,一定需要实现一个compare方法。
  3. 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表达式的基本语法:方法参数列表 -> 表达式; 参数列表,是接口的方法参数列表,表达式,是接口方法的具体逻辑实现。

方法参数列表

  1. 方法没有参数
    这个案例其实我们已经做过了,创建一个线程,传递一个Runnable对象即可。
    没有参数,需要注意的是,参数列表的 () 是一定不能省略的。因为括号才表示是方法的参数列表。

    new Thread(() -> System.out.println("没有参数")).start();
  2. 方法有一个参数
    先引入一个案例:

    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);

    如果是一个参数,需要注意:

    1. 如果参数写类型,那么,必须写方法的()
    2. 如果参数不写类型(编译器可以推导出来参数的类型),那么,可以省略方法的()
      所以,我们还可以这么写:

      Frame f = new Frame("lambda");f.setBounds(100, 100, 100, 300);Button b = new Button("按钮");b.addActionListener(event -> System.out.println("按钮被点击"));f.setVisible(true);
  3. 方法有两个或多个参数
    如果是两个或者多个参数,那么需要注意,不管是否有参数类型,都必须有()

    Integer[] arr = new Integer[]{1,5,3,4};Arrays.sort(arr, (x, y) -> Integer.compare(x, y));System.out.println(Arrays.toString(arr));
  4. 如果有其他的修饰符修饰
    比如,可以用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));

表达式

  1. 如果表达式只有一行代码
    可以不需要{},直接写,不能写return关键字。编译器帮助我们推导return。
  2. 如果表达式超过一行
    那么这一段代码,就必须写{},就必须遵循方法的规则,并且如果方法有返回,就必须有返回语句。

变量作用域

在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();}
  1. 自由变量,在Lambda表达式中,可以随意使用自由变量。比如,我们可以在run方法中使用到外层方法的参数。
    其实这种写法,编译器在编译的时候,会推导成常规的写法,常规的我们会如何写?我们会使用final修饰content 和 times,为什么吗?因为print方法可能执行完毕之后,子线程才会执行,那么如果没有用final修饰content,方法执行完毕之后,这个局部变量就会被回收,相当于是线程中访问了一块已经被回收的内存,所以我们使用final修饰,将局部变量放置到常量池中保存起来。
    那Lambda中,可以直接使用content变量,我们可以如何推断?其实编译器已经默认将自由变量用final修饰,保存起来了。我们只能使用这个变量,不能修改final变量。
  2. this,在匿名内部类中,如果我们使用this,那么,这个this其实是匿名内部类对自己的引用。但是,在Lambda表达式中的this,却是指向的该Lambda表达式所在方法,这个方法所在的类的引用。这一点需要跟以前的情况区分开。
  3. 操作自由变量的代码块,我们就称之为“闭包”

函数式接口

想想我们写的几个案例,使用的都是接口,比如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();    }
  1. 在java中,如果接口中只有一个抽象方法,那么这个接口就是函数式接口。我们可以使用注解@FunctionalInterface 来检测这个接口是否是函数式接口。
  2. @FunctionalInterface注解只是用来检测,并不能定义某个接口是否是函数式接口,函数式接口主要是看接口的方法数。
  3. 简化函数式接口的使用,是Lambda表达式出现的唯一作用
  4. 回过头,我们在看一下这一句代码。

    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表达式推导出变量类型

  1. 函数式接口中,是可以写Object中的方法的,因为接口也是一个特殊的类,重写Object中的方法,不影响函数式接口
  2. 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);// 创建对象的时候,指定泛型

构造器引用细节

  1. 构造器引用的函数式接口,这个接口中的方法不能有参数
  2. 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. 代码的热升级
函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。

原创粉丝点击