Lambda -- 01基础

目录

静态代理模式

策略模式

lambda思想

匿名内部类

lambda语法糖

行为参数化

函数名

就一个抽象方法的接口:函数式接口

lambda表达式


谈Lambda之前,先讲讲静态代理模式,以及策略模式,然后我们在逐步进入正题,lambda思想是怎么一步步的由传统的java编程演变而来的。

静态代理模式

看示例代码:

class MyTask implements Runnable 
{
	public void run(){
		System.out.println("hello world");
	}
}

public class TestStaticProxy
{
	public static void main(String[] args){
		Runnable task = new MyTask();
		Thread t = new Thread(task,"myThread");
		t.start(); //这里仅仅调用了线程t启动,怎么就能执行MyTask类的run()方法了呢?
	}
}

以上示例,运行,输出 hello world

程序的执行,从上至下执行的顺序结构,来看问题。main()方法是java程序运行的入口,程序执行到 t.start();这一句,表示一个新线程启动了,然后主线程main就执行完退出了,那么这个MyTask类的run()方法是什么时候被调用,又是被谁调用的呢?答:这就是静态代码模式的运用了,线程t启动后,t的start()会内部调用task对象(MyTask类类型)的run()方法,可以理解为t.start(){ task.run(); }。我们可以说,task对象的run()方法托管给了线程t的start()方法内部来调用,专业术语:这就叫“静态代理模式”---执行的是A方法,A方法的内部会调用X对象的B方法,X对象的B方法被静态代理。

策略模式

 看示例代码:

这里我举例数据库的一张表来描述策略模式思想,感觉更贴切些,假定有这么一张表,表示食谱表,食谱表中的每行记录,代表每道菜。对应到java里面,就是表中的每行记录(/每道菜)都是一个java的值vo对象,也即所谓的普通POJO对象。整张表呢就是一个List集合。让我们看看示例:

数据库表Dish表中的每行记录,都是一道菜

//数据库表Dish表中的每行记录,都是一道菜
public class Dish { 
    private final String name;         //菜名
    private final boolean vegetarian;  //是否素菜
    private final int calories;        //菜还有的热量
    private final Type type;           //菜的类型
 
    public Dish(String name, boolean vegetarian, int calories, Type type) { 
        this.name = name; 
        this.vegetarian = vegetarian; 
        this.calories = calories; 
        this.type = type; 
    } 

    public String getName() { return name; } 
    public boolean isVegetarian() { return vegetarian; } 
    public int getCalories() { return calories; } 
    public Type getType() { return type; } 

    public String toString() { return name; }

    public enum Type { MEAT, FISH, OTHER } 
} 

Dish表中的全部记录,构成了一份食谱,也即所有菜的集合

import java.util.*;

//Dish表的所有记录,构成了一份食谱,也即菜的集合
List dishes = 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) 
);

好,现在假定我们有了上方的食谱(/也即我们去吃饭时的菜单),我们接着引入如下诉求:

  • 区分出来哪些是素菜
  • 区分出来哪些菜含的热量>400
  • 区分出来哪些菜是FISH类别的
  • ......如果菜对象携带有更多的属性信息,客户无穷无尽的针对属性要求区分

让我们先用传统的手法针对客户的诉求来实现:

//区分出哪些是素菜
public static List filterIsVegetarianDishes(List dishes) { 
    List result = new ArrayList(); 
    for(Dish dish: dishes)
        if( dish.isVegetarian() )
            result.add(dish); 
    return result; 
} 
//区分出哪些菜是含热量>400的
public static List filterCalories400Dishes(List dishes) { 
    List result = new ArrayList(); 
    for(Dish dish: dishes)
        if( dish.getCalories() > 400 )
            result.add(dish); 
    return result; 
}
//区分出哪些菜是FISH类别的
public static List filterTypeFISHDishes(List dishes) { 
    List result = new ArrayList(); 
    for(Dish dish: dishes)
        if( dish.getType().equals(Dish.Type.FISH) )
            result.add(dish); 
    return result; 
}

 。。。。。。客户无穷无尽的区分诉求,难道我们也要添加无穷无尽的《长相非常类似的方法》嘛?答案:是的,但我们的主体代码组织结构可以变通下,使其主体代码结构看起来更优雅一些。我们可以引入策略模式来创建主体结构代码,细微的“客户区分变化诉求”抽象出来,抽象成一个接口。这样,主体结构代码就具备了通用性,至于以后客户在来的无穷无尽的区分诉求,只要我们搞好细微的区分变化诉求即可。

通过以上三个小示例,不难发现,为什么这三个方法称作《长相非常类似的方法》,除了

  • 区分素菜:if( dish.isVegetarian() )
  • 区分热量>400:if( dish.getCalories() > 400 )
  • 区分FISH类别:if( dish.getType().equals(Dish.Type.FISH) )

这三句有别外,剩下的三个方法内的其它代码,堪称一模一样的,所以称之为《长相非常类似的方法》。好,那么现在就抽象出来这三个细微的区分诉求,抽象为一个接口:DishPredicate,在结合静态代理模式,我们就领略了策略模式的由来,看代码:

//策略模式的主体结构代码组成:
interface MyPredicate(
    boolean test(Dish d);
)

public static List filterDishes(List dishes, MyPredicate p) { 
    List result = new ArrayList(); 
    for(Dish dish: dishes)
        //if( dish.isVegetarian() ) 
        //if( dish.getCalories() > 400 )
        //if( dish.getType().equals(Dish.Type.FISH) )
        if( p.test(dish) )    //主体结构代码,高度抽象出来了不同的区分诉求为一个接口
            result.add(dish); 
    return result; 
}

主体结构代码是整洁了,但是方法filterDishes(List dishes, MyPredicate p)的入参p,仍然是需要我们书写不同的策略实现方案的:看代码:

//策略模式,不同的策略实现方案

//素菜策略,实现方案
class VegetarianImpl implements MyPredicate{
    public boolean test(Dish dish){
        return dish.isVegetarian() ;
    }
}

//热量400策略,实现方案
class Calories400Impl implements MyPredicate{
    public boolean test(Dish dish){
        return dish.getCalories() > 400 ;
    }
}

//FISH类别策略,实现方案
class TypeFISHImpl implements MyPredicate{
    public boolean test(Dish dish){
        return dish.getType().equals(Dish.Type.FISH) ;
    }
}

现在有了策略模式的主体结构代码,有了各种策略的实现方案,那么我们可以写测试类来运用策略模式:(说白了,还是面向接口编程,请注意,我的讲解中,始终讲的是面向接口编程,因为面向接口编程是体现Java多态性的很重要的一个方面,所有的框架都是面向接口的)

//测试类 测试策略模式
public class Test{
    public static void main(String[] args){
        filterDishes(dishes, new VegetarianImpl());
	filterDishes(dishes, new Calories400Impl());
	filterDishes(dishes, new TypeFISHImpl());
    }
}

好:我们现在总结下:

策略模式:将《长相非常类似的方法》抽离出来抽象接口作为策略,作为入参传递给策略主体结构,策略主体结构中的主体代码利用静态代理模式,代理了策略实现方案,不同的策略实现方案就是一个个的抽象接口的实现类。

  • 抽象接口
  • 策略主体结构
  • 不同的策略实现方案

用图来表达策略模式

Lambda -- 01基础_第1张图片

Lambda -- 01基础_第2张图片

策略模式,如果单单看策略主体结构代码以及测试类的话,的确很爽,感觉代码主体整洁,言简意赅。但是,别忘记了,我们还不得不为了不同的策略实现方案书写一个又一个的方案,书写的策略实现方案,是个类,我们想用这个类干嘛呢,不过是想要我上边图示的//具体策略方案1 //具体策略方案2 //具体策略方案3 这里的代码逻辑而已,仅次而已,我们就要繁琐的一遍遍的书写本不需要的class MyStraImpl implements MyStrategy{}死模板式的代码,那么如果能有一种技术,我传递给策略主体结构代码的入参p就是 //策略方案实现 的表现手法,岂不是很完美嘛?于是乎,lambda表达式随着java8应运而生。

lambda思想

匿名内部类

让我们继续策略模式话题:有了上面的分析后,策略主体结构代码的xxxMethod(MyStrategy strategy)的入参,传进来的是某个策略实现类的对象,这个对象我们仅仅是想调用它的具体策略方案代码而已,基于传统的java编程手段,方法的入参只能是基本类型或者类类型,所以哪怕仅仅只是想用具体策略方案,也不得不传递策略方案对象进来当成入参。当然,匿名内部类是一种手法,让我们可以不用具体的策略实现类,临时new 一个策略对象,用完即消亡。例如代码如下:

filterDishes( dishes,
            new MyPredicate(){
                public boolean test(Dish dish){
                    return dish.getType().equals(Dish.Type.FISH) ;
                }
            }
);

上面虽然采用了匿名内部类,不用我们具体去一个个的策略实现方案的实现类了,其实本质和策略实现类一样的道理,并没有改变死模板代码的书写。

filterDishes( dishes,
            new MyPredicate(){                        //死模板代码
                public boolean test(Dish dish){   //死模板代码
                    return dish.getType().equals(Dish.Type.FISH) ;
                } //死模板代码
            } //死模板代码
);


lambda语法糖

我们的策略主体结构代码public static List filterDishes(List dishes, MyPredicate p),入参p,不过是想利用它的行为动作“具体的策略实现方案”而已,有那么点意思了,也就是说,你要是能传递给我行为动作就可以了,其它的我都不关心,于是java8引入了概念:行为参数化


  • 行为参数化

“行为参数化”,什么意思呢,就是你把行为可以当成一个《值》一样来传递给方法了,这个《值》要描述清楚,行为(/行为也就是函数,也就是方法)的入参是什么,行为的具体逻辑是什么,行为有什么样的返回值,这三点不正是一个函数的四大要素之三嘛?函数:函数入参函数体函数返回值,函数名。而我这里为什么只说了四大要素之三,没说全呢?有人读到这里,不禁要问,函数名称呢,你不告知函数名称,传递进来一个行为,鬼知道你这个行为是哪个函数呢?不同名称的函数,但是函数入参,函数体,函数返回值都一样的,多了去了,到底调用哪个函数,难道JVM靠猜的?答:不是靠猜的,我接下来讲函数名是怎么被JVM正确识别出的。


  • 函数名

JVM根据策略主体结构代码,可以自动推导出来的。再看filterDishes(List dishes, MyPredicate p)方法,方法的入参p是什么类型的,是MyPredicate类类型的,所以你传递进来的这个《行为值》就铁定是MyPredicate的一个实例,否则编译是无法通过的,这个《行为值》在jvm虚拟机内部有个名称的,这个名称就是函数名,我们不用关心这个名称的,我们所要描述清楚的这个行为,只要给出来函数四大要素之三:函数入参函数体函数返回值,就行,因为函数名是jvm内部自己通过编译后自动给的,你给出来的这个《行为值》也是有具体类类型的,就是MyPredicate类类型。也许此处有人有疑问,根据函数四大要素之三,JVM去推理这个《行为值》的具体类型,那不会有推断出错,模棱两可的时候吗?答案:有模棱两可的时候的,请看我下方的小示例:

import java.util.function.*;

//我们自己定义的函数式接口
interface MyPredicate{
    boolean test(Character d);
}


//我们现在写个lambda表达式(也就是我一直说的《行为值》)
public class TestUser
{
    //多态:重载的同名方法test    
    static boolean test(MyPredicate p){
	return p.test('a');
    }

    //多态:重载的同名方法test
    static boolean test(Predicate p){
	Predicate p0 = p.negate(); //p的非,也就是p求反,原来为true,求反后为false
	return p0.test('a');
    }

    public static void main(String[] args){
	/** System.out.println( test( (d)->{return true;} ) );
	*
	* 该句无法编译:
	* TestUser.java:32: 错误: 对test的引用不明确
	* TestUser 中的方法 test(MyPredicate) 和 test(Predicate) 都匹配 (d)->{return true;}
	*/

	System.out.println( test((MyPredicate)d->true) ); //输出true
	System.out.println( test((Predicate)d->true) ); //输出false
    }
}

上述小示例,说明,当我们自定义函数式接口时,如果有和jdk内置的函数式接口雷同,并且此时有多态的方法重载的现象存在的话,那么我们的lambda表达式在作为《行为值》传递的时候,就要明确强制类型转换才行。请注意标红的这句话,很重要,这是多态产生的毛病。这里不仅要问,jdk内置了哪些函数式接口,难道我们写程序之前,还要滚瓜烂熟的背下来,否则我哪还敢私自定义函数式接口呢,因为看到几乎所有使用lambda表达式的地方,从来没在前面搞个强制类型转换。那我们小问一下,如果没有多态的方法重载,我们就私自定义了一个函数式接口,虽然雷同了jdk内置的函数式接口的抽象方法的签名信息(方法名,入参,返回值类型),是不是编写应用程序代码,不用担心jdk的推导时模棱两可的问题呢?答案,是的,只要没有多态的方法重载,你想怎么定义函数式接口,随便你,请看上方小示例的改造版本:

import java.util.function.*;

//我们自己定义的函数式接口
interface MyPredicate{
    boolean test(Character d);
}


//我们现在写个lambda表达式(也就是我一直说的《行为值》)
public class TestUser
{
    //多态:重载的同名方法test    
    //static boolean test(MyPredicate p){
	//return p.test('a');
    //}

    //多态:重载的同名方法test
    static boolean test(Predicate p){
	Predicate p0 = p.negate(); //p的非,也就是p求反,原来为true,求反后为false
	return p0.test('a');
    }

    public static void main(String[] args){
	System.out.println( test( (d)->{return true;} ) ); //编译OK,输出false

	//System.out.println( test((MyPredicate)d->true) ); //无法编译
	System.out.println( test((Predicate)d->true) ); //输出false
    }
}
import java.util.function.*;

//我们自己定义的函数式接口
interface MyPredicate{
    boolean test(Character d);
}


//我们现在写个lambda表达式(也就是我一直说的《行为值》)
public class TestUser
{
    多态:重载的同名方法test    
    static boolean test(MyPredicate p){
	return p.test('a');
    }

    //多态:重载的同名方法test
    //static boolean test(Predicate p){
	//Predicate p0 = p.negate(); //p的非,也就是p求反,原来为true,求反后为false
	//return p0.test('a');
    //}

    public static void main(String[] args){
	System.out.println( test( (d)->{return true;} ) ); //编译OK,输出true

	System.out.println( test((MyPredicate)d->true) ); //输出true
	System.out.println( test((Predicate)d->true) ); //输出false
    }
}

上面只是讲解《行为值》碰到多态重载方法,jdk自动类型推导时的一个小插曲。对于程序来说,其实呢,凡是编译不能通过的,我们都只是了解下原理即可,没必要纠结太深。为什么呢?因为编译不通过,往下你啥都干不了了啊,哈哈。。。

说了半天,函数在哪呢?java编程语言里,从来没有函数的概念(这玩意在javascript语言中倒是会频繁的出现,函数就是一个方法),java编程语言里,有类、接口、对象的概念,就是独独没有函数的概念,那java怎么解决函数这个概念的呢?好,接下来,看java是如何引入“函数”概念的。


  • 就一个抽象方法的接口:函数式接口

java中,为了兼容以前的老版本,也为了不停的更新支持更多的编程手法,于是,java中,把就一个抽象方法的接口,叫做“函数式接口”,也叫“函数”,这就是java8中大家会发现多了一个java.util.function包,这个包中的所有接口,都是只有一个抽象方法的,所以这个包的包名为什么叫function相信大家都该明白了。这里强调一下:jdk8以后,接口扩容了,不仅可以有静态不可变的变量,还能有默认的方法,还能有静态方法,如果一个接口中,就一个抽象方法,其它哪怕拥有再多的默认方法/静态方法/变量,那么此接口仍然归为“函数式接口”。

jdk8为什么对接口进行扩容改造呢?原因就是:为了兼容老的jdk版本,比如老的接口Collection接口,现在想加入个方法,如果此方法是抽象的,而你的老代码中,直接实现了这个接口的程序正在生产运行着,现在由于升级了高版本的jdk,导致你的程序无法运行(因为你的程序没有实现Collection接口中的新增方法),那这是致命的,试问,jdk的高版本再好的垃圾回收机制,再好的性能优化,我也不敢升级啊,那jdk高版本还怎么推向市场呢?所以jdk8对接口进行了扩容,允许静态方法、默认方法、变量。关于接口中的默认方法,其实本质是适配器模式;关于接口中的变量,其实本质是include头信息的思想。

好,现在知道了函数式接口了,函数式接口的这独一份的抽象方法,就是java中引入的函数概念。那怎么样给出这样的一个《行为值》来描述清楚函数四大要素之三呢?我们总算可以步入正题,lambda表达式的由来!


  • lambda表达式

filterDishes( dishes,
            new MyPredicate(){                        //死模板代码
                public boolean test(Dish dish){   //死模板代码

                    return dish.getType().equals(Dish.Type.FISH) ;
                } //死模板代码
            }     //死模板代码

);

lambda表达式,就是解决上述问题,如何去掉 //死模板代码 的。上面我已分析过,lambda表达式,就是要传递策略主体结构方法filterDishes(List dishes, MyPredicate p) 代表p对象的一个《值》,这个《值》需要描述出来函数的四大要素之三,函数入参,函数体,函数返回值。

lambda表达式语法:() -> {}

让我们在看 filterDishes(List dishes, MyPredicate p)的具体策略方案的写法

  • 老的写法:
filterDishes( dishes,
    new MyPredicate(){                    //死模板代码
        public boolean test(Dish dish){   //死模板代码
            return dish.getType().equals(Dish.Type.FISH) ;
        } //死模板代码
    }     //死模板代码
);
  • lambda的写法:
/**
* lambda表达式一句话搞定,行为参数化(函数四大要素之三),传递《行为值》
*
* 可以认为lambda表达式就是入参p对象,是MyPredicate类类型的,
* 描述了函数式接口中的抽象方法 boolean test(Dish d)
*
* 抽象方法boolean test(Dish d) 的函数入参 dish
* 抽象方法boolean test(Dish d) 的函数体 dish.getCalories() > 400 ;
* 抽象方法boolean test(Dish d) 的函数返回值 return dish.getCalories() > 400 ;
*/

//为什么不关心抽象方法本身的名称test呢?原因很简单,主体结构代码filterDishes方法内部的逻辑中要利用p.test()来调用的,调用的就是抽象接口方法test
filterDishes(dishes, (dish) -> {return dish.getCalories() > 400;} );

filterDishes(dishes, (dish) -> {return dish.getCalories() > 400;} );如果单单看这一句话,的确够精简,但是也够蒙圈的,我们在来结合策略主体结构代码实现体一起来看看呢?

//策略模式的主体结构代码组成:
interface MyPredicate(
    boolean test(Dish dish);
)

public static List filterDishes(List dishes, MyPredicate p) {
    List result = new ArrayList();
    for(Dish dish: dishes)
        if( p.test(dish) )    //主体结构代码,高度抽象出来了不同的区分诉求为一个接口
            result.add(dish);
    return result;
}

当然:可以认为(dish) -> {return dish.getCalories() > 400;} 是接口MyPredicate的一个具体实现类的对象p,可以认为:

MyPredicate p = new MyPredicateImpl(){ //【本质】
    public boolean test(Dish dish){
        return dish.getCalories() > 400;
    }
}

和下面这一句是一个意思:

MyPredicate p = (dish) -> { return dish.getCalories() > 400; } ;

我来个图吧:

Lambda -- 01基础_第3张图片

解释到这里,相信大家都明白lambda的由来了,以上也能很好的解释了为什么说lambda表达式是个“语法糖”,是个简写形式而已。因为背后的本质,就是我上方写的 //【本质】。所以我更喜欢给lambda表达式,把它叫做《行为值》,也叫做《函数式接口的实例对象》。

好,让我们继续,这里我写的MyPredicate抽象接口,只能处理Dish类类型的,我如果有其它的表结构,也类似的客户不停的要区分诉求怎么办呢?难道我还要写MyPredicate2抽象接口,MyPredicate3抽象接口嘛......?答:不用这样的。谈到这里,大家都能猜到,接下来,要讲的就是泛型的引入。针对上方诉求,另外很多表都有这样的诉求,要求不同的属性区分,其实一个泛型接口就足够,这正是泛型的模具化思路。泛型如果不懂,请参考我的博客 java基础--04泛型

interface MyPredicate{
    boolean test(T t); //具体对象t时,你传啥真实的类类型,这里的T就是你传的那个真实的类类型
}

由此我们就知道,为什么java.util.function包中的很多函数式接口都是泛型的,关于系统预留的函数式接口,我放在另外的篇章里讲解!

配套小示例:

 

 

你可能感兴趣的:(Java,jdk8,Java8,Stream)