欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

荐 【Java】lambda表达式与函数式接口的完美配合

程序员文章站 2022-03-26 20:33:58
▊ lambda表达式的引入▌为什么需要lambda表示式?举几个栗子:sort方法需要传入一个实现Comparator接口的类的实例;Timer需要传入一个实现ActionListener接口的类的实例;Thread需要传入一个实现Runnable接口的类的实例…这是为什么?它们需要的真的是这个实例对象吗?透过现象看本质:它们真正需要的,是一个"函数",是一个告诉它们,根据什么去排序、被触发后执行什么、线程去执行什么任务的"函数"(comp...

 

▊ lambda表达式的引入

 
为什么需要lambda表示式?

举几个栗子:
sort方法需要传入一个实现Comparator接口的类的实例;Timer需要传入一个实现ActionListener接口的类的实例;Thread需要传入一个实现Runnable接口的类的实例…
 
这是为什么?它们需要的真的是这个实例对象吗?
 
透过现象看本质:它们真正需要的,是一个"函数",是一个告诉它们,根据什么去排序、被触发后执行什么、线程去执行什么任务的"函数"(compare、actionPerformed、run)。可是,Java不允许"函数"作为参数

所以,才将这个"函数"(或者说是代码块),做成一个接口的一个抽象方法;我们需要"函数"作为参数时,就做出一个该接口的实现类的一个实例对象,作为参数———这个使命,被委托给了一个对象
 
Java8之后,这个作为参数的对象,可以被一个lambda表达式所取代了。

这无疑大大简化了代码,在某些情况下提升了效率——更重要的是,这是大势所趋的"函数式编程"思想的又一次胜利。

在没有计算机的数学时代,逻辑学家Church意识到他需要将一个函数符号化,他使用了希腊字母λ——λ的发音即为lambda
从那之后,带有参数的表达式就被称为lambda表达式

 
lambda表示式的语法

"箭头函数"是lambda表达式的其中一种。即()->{}

>>> lambda表达式是对对象的替代。下面我们演示一下这两种方式,并介绍lambda表达式的简单语法

// 使用匿名内部类,实现多线程
new Thread(new Runnable() {
	 @Override
	 public void run() {
	      System.out.println(Thread.currentThread().getName() + "线程已被创建");		
	 }
}).start();
	
// 使用Lambda表达式(代替Runable接口的具体实现类)
new Thread(() -> System.out.println(Thread.currentThread().getName() + "线程已被创建")).start();



// 使用匿名内部类,实现山的高度的排序
Arrays.sort(mountains, new Comparator<Mountain>() {
	@Override
	public int compare(Mountain m1, Mountain m2) {
		return m1.height - m2.height;
	}
]);

// 使用Lambda表达式(代替Comparator的具体实现类)
Arrays.sort(mountains, (Mountain m1, Mountain m2) -> {
	return m1.height - m2.height;
});

// Lambda省略规则:
// 		1.参数类型可省
//		2.一个参数时,()可省
//		3.函数体只有一句时,"{}" ";" "return"可以一起省
// 		4.参数类型可省————可以【推导】出来时
//		5.其实,这个接口的名字也省了————因为可以【推导】出来

Arrays.sort(mountains, (m1, m2) -> m1.height - m2.height);
其实"作为参数"这样的说法并不完整,lambda表达式同样可以作为"返回值"

public class Demo {

    public static Comparator<String> getComparator() {
        return new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                return s1.length() - s2.length();
            }
        };
    }

    public static Comparator2<String> getComparator2(){
        return (s1, s2)-> s1.length() - s2.length();
    }

    public static void main(String[] args) {
        String[] arr = {"loli", "hahaha", "x"};
        Arrays.sort(arr, getComparator());
        Arrays.sort(arr, getComparator2());
        System.out.println(Arrays.toString(arr));
    }
}


很容易发现,传入lambda表达式的形参都是接口,准确的说,是一个函数式接口
只要是函数式接口作为形参,就需要传入一个实现该接口的对象————lmabda表达式本身不是对象,但可以推导并生成出一个对象

 

 
 

▊ 函数式接口

 
函数式接口

有且只有一个抽象方法的接口。

@FunctionalInterface						// 注解:检测接口是否是一个函数式接口
修饰符 interface 接口名称 {
	public abstract void method();			// public abstract 还是推荐加上
}

函数式接口作为形参时,也就是需要传入该接口的对象时,就可以传入一个lambda表达式。
 
下面的思想一定要理解后,牢牢掌握 :

最好把lambda表达式看作一个函数——用这个函数覆盖接口的抽象方法,并生成一个该接口的实例对象。

这种做法比传入一个对象省略了不少代码(甚至省去了接口名称,因为可以推导出来)。

 

懒惰计算

懒惰计算也称为延迟求值计算。顾名思义。

>>> 举个例子:
public class Demo1 {
    public static void printer(boolean isOk, String s){			// 打印机(只有在isOk允许时才打印)
        if(isOk){
            System.out.println(s);
        }
    }

    public static void main(String[] args) {
        String s1 = "loli";
        String s2 = "suki";
        String s3 = "saikou";
        printer(false, s1 + s2 + s3);
    }
}

>>> 看出这个"打印机"的弊端了吗 ?
>>> 当isOk为false时,字符串并不需要被打印,而"字符串的拼接"这个行为在传参时就实实在在的发生了————这无疑是一种浪费
>>> 下面看看我们如何使用 函数式接口+lambda表达式 优化这个问题

------- ↓ Builder.java ------------
@FunctionalInterface
public interface Builder {
    public abstract String stringBilder();
}

-------- ↓ Demo2.java -------------
public class Demo2 {
    public static void printer(boolean isOk, Builder bl){
        if (isOk){
            System.out.println(bl.stringBilder());
        }
    }

    public static void main(String[] args) {
        String s1 = "loli";
        String s2 = "suki";
        String s3 = "saikou";
        printer(true, ()-> s1 + s2 + s3);
    }
}

这的确需要理解 >_< :

优化后的方法传入的参数不是拼接的字符串,而是字符串拼接的函数

这样,字符串拼接这个行为就委托给了这个接口对象——在真正运行时,才会触发字符串拼接这个行为;而当isOk为false时,显然这段代码不会被运行到,拼接字符串的行为自然也就被避免了。

所以说:此时的"拼接字符串"是个懒惰计算——被延迟到了if块的内部
 
懒惰计算的特点是原先的参数是个在调用时就会实实在在执行的动作,被替换成了一个描述该动作的函数

>>> 这个特点其实是比较明显的:

func(m, n, new Date(2077, 6, 21));
↓ 优化
func(m, n, ()->new Date(2077, 6, 21));		// (形参)函数式接口是个生产型接口:Supplier

 
 
常见的函数式接口

Supplier—get——生产工厂

Consumer—accept——消费者

Predicate—test——判断

Function—apply——类型转换
 
将函数式接口作为形参的语义是:

我要(生产、使用、判断、类型转换)一个数据;但是具体的行为(生产一个怎样的数据、怎样去使用数据、根据什么去判断、怎样去转换)就由之后传入的lambda表达式决定吧!

具体含义及其典例代码:戳这里,一定要戳 ! ! ! ()

 

 
 

▊ 方法引用

 
方法引用是lambda表达式的孪生兄弟———都作为函数式接口的实例

方法引用和lambda表达式一样,本身都不是一个对象。但作为参数时【推导并生成】了一个对象
 
方法引用的语法有三种情况(方法引用转化成lambda表达式):

object::instanceMethod(对象::普通实例方法)———直接补参数就行

Class::instanceMethod(类::普通实例方法)——— 类作为一个隐式参数,再补参数

Class::staticMethod(类::静态方法)——— 直接补参数就行

1.
System.out::println	  =>    x->System.out.println(x)  	
separator::equals	  =>    x->separator.equals(x)		

2.
String::trim	 	  =>	x->x.trim()					
String::concat   	  =>   (x, y)->x.concat(y)			

3.
Integer::valueOf	  =>    x->Integer.valueOf(x)		
Integer::sum		  =>   (x, y)->Integer.sum(x, y)

理解

  1. 通俗的说,方法引用就是lambda表达式的进一步简写———从方法引用,可以推导完全等价的lambda表达式
  2. 第一种和第三种情况其实是完全一致的,只是分别是对象层面和类层面
  3. 第二种情况特殊记忆———类名作为一个隐式参数
  4. 不难发现,将lambda表达式进一步简写为方法引用的前提是:只完成一个行为。比如 s->s.length()==3,需要完成"取长度"和"作比较"两个行为,你很难把它缩写为方法引用
     
【补充知识点】
"构造器引用"————可以认为是一个"方法名为new的方法引用"

Integer::new    等价于lambda表达式:    x->new Integer(x)
String[]::new	等价于lambda表达式:	 x->new String[x]

 

 
 

▊ lambda表达式的变量作用域

public class Demo {
    public static String create(Supplier<String> sub){
        return sub.get();
    }

    public static void main(String[] args) {
        String str = "loli";
        String s = create(()->{
            return str;
        });
        System.out.println(s);		// 输出"loli"
    }
}

>>> 作为参数的lambda表达式中,使用到了一个未在lambda表达式中定义的变量————str
>>> 由此观之,lambda表达式的作用域,绝对不局限在它的()-{}的大括号中

实际上,lambda表达式的作用域,不是大括号,而是大括号的外围——和()->{}所在的那一整句话在同一作用域

就本例而言,str不是在lambda表达式中定义的,而是被lambda表达式捕获(captured)的。
 
这种捕获,是一种更为严格的闭包(closure)

之所以是闭包,是因为lambda表达式从外部捕获了一个变量并据为己有,保存在自己的大括号中

之所以说更为严格,是因为Java要求捕获的这个变量,必须是事实最终变量(effectively final)——捕获之前,它在外围作用域不能"乱动";捕获之后,也不能对它进行修改

public class Demo {
    public static int create(Supplier<Integer> sub) {
        return sub.get();
    }

    public static void main(String[] args) {	// 下面的lambda表达式都省略了一个大括号
        for (int i = 0; i < 10; i++) {		
            create(() -> i);				// Error:捕获之前,该变量不能"乱动"
        }

        int n = 3;
        create(() -> n++);					// Error:捕获之后,也不能去修改它
    }
}

 

 

 

 

 

 

 

 

 

 

 
 

End

By a Lolicon

本文地址:https://blog.csdn.net/m0_46202073/article/details/107318329