JDK 8 Lambda表达式基础知识汇总

前言

最近我在学习的时候记得看到一篇文章
对HashMap类进行了大约这个样子的遍历方式:
Map<String, Integer> map = new HashMap<>();
map.forEach((k,v)-> System.out.println(k+"—"+v));
后面这个代码是真难懂,不过也挺好用的,可以按自己的方式输出k,v
然后我就去找forEach方法的源码
如下:

default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }

参数action是BiConsumer<T, U>类型的,我就又去找源码了

public interface BiConsumer<T, U> {
    void accept(T t, U u);
    default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
        Objects.requireNonNull(after);
        return (l, r) -> {
            accept(l, r);
            after.accept(l, r);
        };
    }
}

可以看出BiConsumer<T,U>是一个接口
然后这个接口里面又两个方法,一个是抽象方法
我们知道
1 接口中的域默认都是static和final的
2 接口中的方法 默认都是public和abstract的
因此我们的accept方法就是一个抽象方法,
那接口的存在不就是为了有一个类去implements这个接口并且重写里面的抽象方法吗
还有一个方法是default方法,奇怪的是这个方法居然有方法体?!
在这里我先告诉大家,这个是default这个JDK 8的新关键字,在接口中编写default修饰的方法时,不是可以有方法体,而是必须有方法体!!
default不会破坏Java多态的特性,default关键字可以让接口中的方法可以有默认的函数体,当一个类实现这个接口时,可以不用去实现这个方法,当然,这个类若实现这个方法,就等于子类覆盖了这个方法,最终运行结果符合Java多态特性。

好,基本上意思就是,我们写一个类继承,重写accept方法就可以用啦。
public class TestBiconsumer implements BiConsumer<String,Integer> {
@Override
public void accept(String string, Integer integer) {
System.out.println(string+"—"+integer);
}
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put(“1”,1);
map.put(“2”,2);
map.put(“3”,3);
map.put(“4”,4);
map.put(“5”,5);
TestBiconsumer t=new TestBiconsumer();
map.forEach(t);
}
}
就像这样!可能是麻烦了点,但是用匿名内部类就算用匿名内部类也应该这样吧:
map.forEach(new BiConsumer<String, Integer>() {
@Override
public void accept(String string, Integer integer) {
System.out.println(string+"—"+integer);
}
});
不管怎么样都跟人家的map.forEach((k,v)-> System.out.println(k+"—"+v)); 简直没法比好吧,人家的明显高大上许多多好不好。
这就是Lambda表达式!
接下来让我从头跟大家说起。

函数式编程基本概念

早在2014年oracle发布了jdk 8,在里面增加了lambda模块。于是java程序员们又多了一种新的编程方式:函数式编程,也就是lambda表达式。
函数式编程是什么?
函数式编程(英语:functional programming)或称函数程序设计,又称泛函编程,是一种编程典范,它将电脑运算视为数学上的函数(cos sin tan)计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。
比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。这是维基百科给出的定义。从这个我们知道函数式编程是相对于指令式编程的一种编程典范,并且对而言具有一些优点。

什么是函数式编程呢?

函数式编程的特性

1、函数是"第一等公民"
什么是"第一等公民"?所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,它不仅拥有一切传统函数的使用方式(声明和调用),可以赋值给其他变量(赋值),也可以作为参数,传入另一个函数(传参),或者作为别的函数的返回值(返回)。函数可以作为参数进行传递,意味我们可以把行为"参数化",处理逻辑可以从外部传入,这样程序就可以设计得更灵活。
2、没有"副作用"
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
3、引用透明
引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。这里强调了一点"输入"不变则"输出"也不变,就像数学函数里面的f(x),只要输入的x一样那得到的结果也肯定定是一样的。

函数式编程的优点

1、代码简洁,开发快速。
函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。Paul Graham在《黑客与画家》一书中写道:同样功能的程序,极端情况下,Lisp代码的长度可能是C代码的二十分之一。如果程序员每天所写的代码行数基本相同,这就意味着,“C语言需要一年时间完成开发某个功能,Lisp语言只需要不到三星期。反过来说,如果某个新功能,Lisp语言完成开发需要三个月,C语言需要写五年。“当然,这样的对比故意夸大了差异,但是"在一个高度竞争的市场中,即使开发速度只相差两三倍,也足以使得你永远处在落后的位置。”
2. 接近自然语言,易于理解
函数式编程的自由度很高,可以写出很接近自然语言的代码。以java为例把学生以性别分组:
没用labmda表达式:
Map<String,List> studentsMap = new HashMap<>();
for(Student student : students){
List studentList = studentsMap.getOrDefault(student.getSex(), new ArrayList<>());
studentList.add(student);
studentsMap.put(student.getSex(),studentList);
}
  用了lambda表达式:
Map<String,List> studentsMap = students.stream().collect(Collectors.groupingBy(Student::getSex));
这基本就是自然语言的表达了,大家应该一眼就能明白它的意思吧。
3. 更方便的代码管理
函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元unit,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
4. 易于"并发编程”
函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
请看下面的代码:
var s1 = Op1();
var s2 = Op2();
var s3 = concat(s1, s2);
由于s1和s2互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分配在两个线程上完成。其他类型的语言就做不到这一点,因为s1可能会修改系统状态,而s2可能会用到这些状态,所以必须保证s2在s1之后运行,自然也就不能部署到其他线程上了。多核CPU是将来的潮流,所以函数式编程的这个特性非常重要。
5. 代码的热升级
函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。

函数式编程的缺点

1.函数式编程常被认为严重耗费在CPU和存储器资源。主因有二:
①早期的函数式编程语言实现时并无考虑过效率问题。
②有些非函数式编程语言为求提升速度,不提供自动边界检查或自动垃圾回收等功能。
惰性求值亦为语言如Haskell增加了额外的管理工作。
2.语言学习曲线陡峭,难度高
函数式语言对开发者的要求比较高,学习曲线比较陡,而且很容易因为其灵活的语法控制不好程序的结构。

以上就是Lambda的故事背景和一些你应该知道的特性及优缺点,接下来我们进入Lambda的正式学习了

1.Lambda的基本语法

java 8 中Lambda 表达式由三个部分组成:第一部分为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;
你肯定会疑问:接口里方法?接口里方法可能多了去了,我知道是哪个?现在先告诉你,其实只有一个方法!后面我们会再说这个点的,认真往下看。
第二部分为一个箭头符号:->;第三部分为方法体,可以是表达式和代码块。语法如下

(parameters)->expression
栗子:(int a,int b) -> return a + b; 或者(a, b) -> return a + b; 的这种形式

(parameters)->{statements;}
栗子:(int a) -> {
a=(a+4)/2;
System.out.println("a = " + a);
}
或者直接( a) -> {a=(a+4)/2;System.out.println("a = " + a);}

2.Lambda表达式的特征
①可选类型声明:不需要声明参数类型,编译器可以统一识别参数值
②可选的参数圆括号:一个参数(不声明参数类型,声明参数类型的话就需要圆括号了)无需定义圆括号,但无参或者多个参数的话需要定义圆括号
③可选的大括号:如果主体中只有一个语句,就不需要使用大括号
④可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指明return关键字来返回值(当然void就不用了)

代码示例:

        Date date=new Date();
        String string="12321";
        //①可选类型声明:不需要声明参数类型,编译器可以统一识别参数值

        //我们的接口是Consumer<T>,里面只有一个需要处理的void accept(T t);抽象方法。另一个用default声明的可以不用考虑。
        Consumer consumer=(x)-> System.out.println("x为:"+x);
        //我们没有声明T类型,那么默认是Consumer<Object>,不用写成Consumer consumer=(Object x)-> System.out.println("x为:"+x);
        consumer.accept(date);//输出   x为:Mon Aug 26 10:09:43 CST 2019(这个时间是我现在写代码的时间,肯定不是你们的哦)
        consumer.accept(string);//输出   x为:12321

        //②可选的参数圆括号:一个参数(不声明参数类型,声明参数类型的话就需要圆括号了)无需定义圆括号,但无参或者多个参数的话需要定义圆括号
        Consumer c1=(x)-> System.out.println("x为:"+x);
        Consumer c2= x -> System.out.println("x为:"+x);//但是Consumer consumer=Object x-> System.out.println("x为:"+x);就是错的

        //我们这次的接口是BiConsumer<T, U>,里面除了一个default方法之外也是只有一个抽象方法void accept(T t, U u);
        BiConsumer<String,Integer> biConsumer=(x,y)-> System.out.println(x+Integer.toString(y));//必须要圆括号哦
        biConsumer.accept("Dmw",9527);//输出   Dmw9527

        //③可选的大括号:如果主体中只有一个语句,就不需要使用大括号
        //上面都没有使用大括号,我们展示一个使用大括号的

        //我们这次的接口是Function<T, U>,里面除了一个default方法之外也是只有一个抽象方法 R apply(T t);
        Function<Integer,String> function=(x)->{
            x=x+255;
            return Integer.toString(x);
        };

        //④可选的返回关键字:如果主体只有一个表达式返回值则不用return关键字,编译器会自动返回值
        //大括号需要指明return关键字来返回值(当然void就不用了)
        //还是用上面那个例子
        Function<Integer,String> f1=(x)-> Integer.toString(x+255);//这个不能加return哦,加了就也必须要大括号

3.Lambda表达式的底层实现
Java 8 内部Lambda 表达式的实现方式在本质是以匿名内部类的形式的实现的
匿名内部类:大家都知道,当我们对某一类只使用一次的时候(只用到了它的一个对象),就没有必要定义成一个单独的类,那么就用到了匿名内部类;

就像我们在前言中自己写的那个TestBiconsumer类 一样,

forEach的声明:forEach(BiConsumer<? super K, ? super V> action)

使用匿名内部类:
map.forEach(new BiConsumer<String, Integer>() {
@Override
public void accept(String string, Integer integer) {
System.out.println(string+"—"+integer);
}
});

使用Lambda表达式:
map.forEach((k,v)-> System.out.println(k+"—"+v));

通过观察,不难看出
(k,v)-> System.out.println(k+"—"+v)
其实就是
new BiConsumer<String, Integer>() {
@Override
public void accept(String string, Integer integer) {
System.out.println(string+"—"+integer);
}
}
也就是说
(k,v)-> System.out.println(k+"—"+v) 其实返回的就是BiConsumer<T,U>接口的一个匿名实现类的一个对象

我们再来观察一下BiConsumer<T,U>接口的定义:
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
Objects.requireNonNull(after);
return (l, r) -> {
accept(l, r);
after.accept(l, r);
};
}
}
注意接口上方那个@FunctionalInterface注解,
@FunctionalInterface标注了这是一个函数式接口!!!
所以以后见到@FunctionalInterface就要想到Lambda表达式可以很简单地利用这个接口。

好,我现在问你:Lambda表达式是什么?
你思索了一下然后骄傲地对我说:
Lambda表达式允许我直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例,简单来说,Lambda表达式是函数式接口一个具体实现的实例 !!!

我:牛逼!好,那我现在又问你:Lambda表达式能干什么?
你继续思索了一下然后还是很骄傲地对我说:
Lambda将接口(具体来说是函数式接口)的实现方式从匿名内部类的形式简化为了一个函数式表达式方式,总的来说就是简化了接口的实现!!!
我:牛逼!

4.函数式接口
上面一点的最后提到了函数式接口,现在我们来说一说函数式接口是什么

函数式接口(Functional Interface)是Java 8对一类特殊类型的接口的称呼。这类接口只定义了唯一的抽象方法的接口(除了隐含的Object对象的公共方法,因此最开始也就做SAM类型的接口(Single Abstract Method)。定义函数式接口的原因是在Java Lambda的实现中,开发组不想再为Lambda表达式单独定义一种特殊的Structural函数类型,称之为箭头类型(arrow type,依然想采用Java既有的类型(class,interface, method等)。原因是增加一个结构化的函数类型会增加函数类型的复杂性,破坏既有的Java类型,并对成千上万的Java类库造成严重的影响。权衡利弊,因此最终还是利用SAM 接口作为 Lambda表达式的目标类型。另外对于函数式接口来说@FunctionalInterface并不是必须的,只要接口中只定义了唯一的抽象方法的接口那它就是一个实质上的函数式接口,就可以用来实现Lambda表达式。

总之啦:函数式接口就是只定义一个抽象方法的接口(可以有多个默认(default)方法)

举个栗子然后吃掉:
就比如很多人都熟悉的Comparator接口吧
@FunctionalInterface
public interface Comparator {
int compare(T o1, T o2);
boolean equals(Object obj);
以及若干个default方法和若干个static静态方法
}
那有些人可能会问了,啊,这么多方法,我好纳闷啊,不是说只定义一个抽象方法嘛,为什么还会有@FunctionalInterface这个注解啊。
听我说,default方法和static方法不算抽象方法,
然后那你一定会说,那不是还有两个抽象方法吗!
我跟你讲,boolean equals(Object obj);这个抽象方法是隐含的Object对象的公共方法,也不算啦 !

在Java 8中已经为我们定义了很多常用的函数式接口基本上都放在:java.util.function包下面,也有比如上面说的Comparator就在java.util包中,一般有以下常用的四大核心接口:
在这里插入图片描述

5.我们先来写一个People类,下面的例子就以People对象作为例子

public class People {

    private int age;
    private String name;


    public People() {
    }

    public People(int age) {
        this.age = age;
    }

    public People(String name) {
        this.name = name;
    }

    public People(int age, String name) {
        this.age = age;
        this.name = name;
    }



    public Integer getAge() {
        return age;
    }
    public String getName() {
        return name;
    }


    public void show(String x,String y ){
        System.out.println("我叫"+name+",我今年"+age+"啦!"+"我会"+x+",也会"+y);
    }


    @Override
    public int hashCode() {
        final int prime = 31;//质数31
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        People other = (People) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return name + "---" + age;
    }

}

这个People类没什么好说的,除了我重写了equals方法和hashcode方法,还多写了一个show方法,其余的应该很简单的。
然后因为这个类是之前写的,当时在学equals那块的知识,所以就重写了,而equals方法一般和hashcode方法一起重写,所以我就一起重写了,我的hashcode方法模仿的是String类的里面hashcode方法,都是用31做质数。

6.注意同样的Lambda可能会有不同的函数式接口哦

       //同样的Lambda,不同的函数式接口
       Comparator<People> o1=(x, y)->x.getAge().compareTo(y.getAge());
       BiFunction<People,People,Integer> o2=(x, y)->x.getAge().compareTo(y.getAge());
       ToIntBiFunction<People,People> o3=(x, y)->x.getAge().compareTo(y.getAge());

7.Lambda表达式引用实例变量和类变量

我们先在类里(main方法外面)面找两个变量来:

   //一个被static修饰的成员变量——类变量
    static People p1 = new People("张三");
    //一个普通的成员变量——实例变量
    People p2= new People("李四");

然后在main方法里面:

//Lambda表达式引用实例变量和类变量例子
       Consumer<People>  c3=(people)->{
           System.out.println(p1);//在Lambda中可以直接引用静态成员变量也就是类变量
           System.out.println(new TestLambda().p2);//但是引用普通的成员变量时需要使用一个 该类实例对象来调用
       };

         //但是使用局部变量要显示声明为final或者事实上的final
         //什么是事实上的final? 如果我声明了一个变量,且在后面不更改它的值,那么那就是事实上的final。
         int  i=1;
         Runnable r=()->System.out.println(i);
         //i=2;这一行的存在会导致上一行编译出错
         //虽然Java7之后可以不写final只要引用默认修饰,但我们还是建议,要被lambda表达式捕获的局部变量就都声明为final

至于为什么局部变量要显示声明为final或者事实上的final,大家可以自行在网上查一查,网上的解释大都是这样子的:
局部变量是保存在栈上的,是线程不共享的,
并且栈上的内容在线程执行完之后就会被GC掉。
而Lambda表达式最终会被处理为一个额外的线程去执行,因为线程不共享
那么就注定了我们拿到的局部变量只能是本体局部变量的一个拷贝
也就是说,在我们Lambda表达式没有执行之前,如果这个局部变量的值变了的话,或者局部变量被GC掉了,意思是本体已经更改了或者不存在了,那么Lambda表达式用到的变量是拷贝过来的又有什么意义呢?
但是其实我还是更倾向于下面这个回答:
Java 8语言上的lambda表达式只实现了capture-by-value,也就是说它捕获的局部变量都会拷贝一份到lambda表达式的实体里,然后在lambda表达式里要变也只能变自己的那份拷贝而无法影响外部原本的变量;但是Java语言的设计者又要挂牌坊不明说自己是capture-by-value,为了以后语言能进一步扩展成支持capture-by-reference留下后路,所以现在干脆不允许向捕获的变量赋值,而且可以捕获的也只有“效果上不可变”(effectively final)的参数/局部变量。

8.Lambda表达式的引用(方法引用,构造函数引用,数组引用)

        //★★★先来看方法引用
        //① 对象::实例方法,将lambda的参数当做方法的参数使用
        Consumer<String> c4=System.out::println;
        //等效于
        Consumer<String> c5=(x)->System.out.println(x);
        c4.accept("Dmw9527");//打印 Dmw9527


        BiConsumer<String,String> b=new People(19,"孟美岐")::show;//将使用accept方法时传入的参数给了show方法
        b.accept("唱歌","跳舞");//打印 我叫孟美岐,我今年19啦!我会唱歌,也会跳舞

        //②类::静态方法,将lambda的参数当做方法的参数使用
        Function<Integer, String> f2= String::valueOf;
        //等效于
        Function<Integer, String> f3 = (x) -> String.valueOf(x);
        String apply = f2.apply(12345);//apply的值是"12345"


        //③类::实例方法,将lambda的第一个参数当做方法的调用者,其他的参数作为方法的参数,开发中尽量少些此类写法,减少后续维护成本。
        BiPredicate<String, String> bp = String::equals;//String是第一个参数的类型,后面equals是方法,第二个参数就是方法的参数
        //等效
        BiPredicate<String, String> bp2 = (x, y) -> x.equals(y);
        boolean test = bp.test("a", "A");//显然test是false


        BiFunction<String, Integer,String> bifunction = String::substring;
        String it = bifunction.apply("i loveyou GEM!", 10);
        //14-10=4。从10这个下标开始开始,对value截取四个字符(10,11,12,13),也就是GEM!
        System.out.println(it);

        //★★★再来看构造方法引用
        //①无参的构造方法模型就是上面的 类::实例方法  的模型
        Supplier<People> p1 = People::new;
        //等效
        Supplier<People> p2 = () -> new People();
        //获取对象
        People people = p1.get();


        //②当有参数时,反而没什么特别的了
        Function<Integer,People> f4=People::new;
        //等效
        Function<Integer, People> f5 = (age) -> new People(age);
        People p3 = f4.apply(12);//构造了一个people,这个人12岁



        Function<String,People> f6=People::new;
        //等效
        Function<String, People> f7 = (name) -> new People(name);
        People p4 = f6.apply("伊泽瑞尔");//构造了一个people,这个人叫伊泽瑞尔


        BiFunction<Integer,String,People> b2=People::new;
        //等效
        BiFunction<Integer, String, People> b3 = (id, name) -> new People(id, name);
        People p5 =b2.apply(15, "琪亚娜");//构造了一个people,这个人15岁,名叫琪亚娜


        //★★★其实还有一个数组引用  格式:Type[]::new;

        Function<Integer, String[]> fun2 = String[]::new;
        String[] strArray = fun2.apply(10);
        //等效
        Function<Integer, String[]> fun1 = (x) -> new String[x];

总结:
在上面两个案例中使用到了一个全新的形式 类或(对象) :: 静态方法/成员方法
若在Lambda表达式中引用了静态方法或成员方法时 只有一句话 ,可以使用 :: 形式进行简化
:: 之前 是这个方法主要的调用发起者 类/对象
:: 之后 静态方法/成员方法
ps:一定要注意 方法后面不要传入参数
调用方法的参数,会通过接口中方法的参数进行传递

9.断言型接口Predicate的三个default方法

//断言型接口的三个方法
        //对于Predicate<T>这个接口,还有三个默认方法值得我们注意一下:and,negate,or

        Predicate<People> ppp=(x)->x.getAge()<18;

        //ppp现在是一个小于18岁的,你可能想立即改为大于等于18的,也就是非。
        Predicate<People> ppp1 = ppp.negate();//产生Predicate对象 ppp 的非。

        //你可能想要把两个Lambda用and方法组合起来,比如一个人年龄既小于18,又大于9
        Predicate<People> ppp2 = ppp.and((x) -> x.getAge() > 9);//用and链接2个lambda生成另一个lambda


        //你还可能想表达这样一个意思:要么年龄小于18,要么年龄大于36,要么那个人叫伊泽瑞尔
        Predicate<People> ppp3 = ppp.or((x) -> x.getAge() > 36).or(x->x.getName().equals("伊泽瑞尔"));

10.再来看两个关于Lambda表达式的实际应用吧

//再来说两个Lambda表达式的实际应用吧。
        //①遍历集合
        String[] names = {"张三","李四","王五"};
        List<String> dalaos  = Arrays.asList(names);
        //你想这样遍历这个集合
        for(String str : dalaos) {
                    System.out.println(str);
                }
        //其实很简单啊
        dalaos.forEach((x)-> System.out.println(x));
        //当然还能更简单
        dalaos.forEach(System.out::println);


        //②数组排序

        //以前我们要对names这个数组进行排序,大概要这样
        Arrays.sort(names);
        //但是这有个前提就是,names中的元素对象必须实现了Comparator接口
        //当然String本来就已经实现了compareTo()方法了

        //当然我们今天讨论的是用比较器的这一种情况,如果非要用比较器实现,就是下面这种情况:
        Arrays.sort(names, new Comparator<String>() {

            @Override
            public int compare(String o1, String o2) {

                return o1.compareTo(o2);
            }
        });

        //但是我们学了LAmbda
        //Lambda表达式
        Arrays.sort(names, (str1,str2)->str1.compareTo(str2));

Lambda表达式的典范做法

①尽量避免在Lambda的方法体中使用{}代码块
保持Lambda表达式的简短和一目了然,因为长长的Lambda表达式通常是危险的,因为代码越长越难以读懂,意图看起来也不明,并且代码也难以复用,测试难度也大。

优先使用
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
String result = "Something " + parameter;
//many lines of code
return result;
}
而不是
Foo foo = parameter -> { String result = "Something " + parameter;
//many lines of code
return result;
};

②使用@FunctionalInterface 注解
如果你确定了某个interface是用于Lambda表达式,请加@FunctionalInterface,表明你的意图,并且养成这样的习惯。不然将来说不定哪一天你抽风了,在这个interface上面加了另外一个抽像方法时,代码就要表示自己很难过了。

③优先使用java.util.function包下面的函数式接口
java.util.function 这个包下面提供了大量的功能性接口,可以满足大多数开发人员为lambda表达式和方法引用提供目标类型的需求。每个接口都是通用的和抽象的,使它们易于适应几乎任何lambda表达式。另外一点就是,里面的接口不会被别人修改~。

④多使用方法引用
在Lambda表达式中 a -> a.toLowerCase()和String::toLowerCase都能起到相同的作用,但两者相比,后者通常可读性更高并且代码会简短。
(这是我们说的类::实例方法的那一种)
这样也契合了我们第①点的目的:让代码更简单。

⑤不要把Lambda表达式和匿名内部类同等对待
虽然我们可以用匿名内部类来实现Lambda表达式,也可以用Lambda表达式来替换内部类,但并不代表这两者是等价的。
这两者在某一个重要概念是不同的:this指代的上下文是不一样的。当您使用内部类时,它将创建一个新的范围。通过实例化具有相同名称的新局部变量,可以用this从封闭范围覆盖原本的类中的实例变量。你还可以在内部类中使用这个关键字作为它实例的引用。但是,这里用this的话,也不能在lambda的主体内覆盖原本的实例变量

看代码:

public class TestEncloseScope {

    private String value= "dmw";


    public String scopeExperiment() {


        Supplier<String> supplier=()->{
            String value="DMW";
            return  this.value;
        };
        return supplier.get();
    }


    public String scopeExperiment1(){
        Supplier<String> supplier=new Supplier<String>() {

            String value="大魔王";
            @Override
            public String get() {
                return this.value;
            }
        };

        return supplier.get();
    }

    public static void main(String[] args) {

        System.out.println("Lambda用this不会覆盖:"+new TestEncloseScope().scopeExperiment());
        System.out.println("匿名内部类用this会覆盖:"+new TestEncloseScope().scopeExperiment1());
    }


}

运行结果如下:

F:\JDK\Java\jdk1.8.0_161\bin\java...
Lambda用this不会覆盖:dmw
匿名内部类用this会覆盖:大魔王

Process finished with exit code 0

⑥不要在Lambda表达式中执行有副作用的操作
"副作用"是严重违背函数式编程的设计原则,比如在在forEach操作里面操作外面的某个List或者设置某个Map这其实是很不严谨的。

函数式编程是完全独立,不依赖外部变量,可复用性高,任何的输入都得到一致的输出,不会污染程序的外部变量或状态,在开发中遵循这样的开发习惯会让你非常受益。
函数式编程的核心原则之一是不改变任何东西。变化会导致错误。如果一个函数不改变传入的参数、全局变量等数据,那么它造成问题的可能性就会小很多。(因此我们前面说的Lambda表达式引用实例变量啊,类变量啊,局部变量啊,其实最好都少用别用,这些都算是依赖外部变量了,并且涉及到这些变量就可能会发生一些改变,卡兹克说过:改变就是好事。不过在我们函数式编程里面,改变可是大忌。)

注意下面这几点就可以了:
1.不要更改变量或对象,通过创建新变量和对象,并在需要时从函数返回它们。
2.声明函数参数,函数内的任何计算仅取决于参数,而不取决于任何全局对象或变量。
3.独立于程序状态或全局变量,只依赖于传递给它们的参数进行计算。
4.限制更改程序状态,避免更改保存数据的全局对象,对程序的副作用尽量小
再来说一下继承及实现具有相同默认方法的父类或接口问题

比如我们有
接口A:
public interface A {
String hi();
String greet();
default void hello() {
System.out.println(“A.hello”);
}
}

接口B:
public interface B {
String hi();
String hh();
default void hello() {
System.out.println(“B.hello”);
}
}

类C实现A,B:

public class C implements A, B{
@Override
public String hi() {
return “C.hi”;
}
@Override
public String greet() {
return “C.greet”;
}
@Override
public String hh() {
return “C.hh”;
}
/**
* 子类优先继承父类的方法,但是显然,这个类中我们并没有如雷, 如果父类没有相同签名的方法,才继承接口的默认方法。
* 编译报错解决1:覆盖法
/
@Override
public void hello() {
System.out.println(“C.hello”);
}
/
*
* 编译报错解决2:指定实现的父接口
*/
// @Override
// public void hello() {
// A.super.hello();
//或者 B.super.hello();
// }
}

这种情况,
此时若不处理hello方法时,类C将编译出错,解决方式要么覆盖,要么指定实现父接口的该方法。

进一步测试继承具有相同方法的父类:

类D:
public class D {
public void hello() {
System.out.println(“D.hello”);
}
}

类C继承类D:

public class C extends D implements A, B{
@Override
public String hi() {
return “C.hi”;
}
@Override
public String greet() {
return “C.greet”;
}
@Override
public String hh() {
return “C.hh”;
}
}

这种情况,
此时若不处理hello方法时,类C不会编译出错,
因为 优先继承父类的机制,所以不会编译出错。
当然也可以自己覆盖啊

总结一下

java8引入lambda表达式是接收了函数式编程语言的思想,例如scala之类的,它将函数视为一等公民,可以使用高阶函数。
和指令式编程相比,函数式编程强调函数的计算比指令的执行重要。
和过程化编程相比,函数式编程里函数的计算可随时调用。
写在最后,lambda表达式可以使代码看起来简洁,但一定程度上增加了代码的可读性以及调试的复杂性,而且也更有风险。
但是还是要多用,但是一定要把我们的函数式编程的原则放在心上。

最后,感谢你们的观看鸭。❥(^_ -)
最后,感谢你们的观看鸭。❥(^_ -)
最后,感谢你们的观看鸭。❥(^_ -)

借鉴文章:https://www.cnblogs.com/linlinismine/p/9283532.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值