首页 > 编程语言 >Java函数式编程基础之【Lambda表达式】疑难问题析解

Java函数式编程基础之【Lambda表达式】疑难问题析解

时间:2024-11-06 12:19:05浏览次数:5  
标签:Java 接口 析解 匿名 new public 表达式 Lambda

一、Lambda表达式概述
Lambda表达式是Java 8 引入的一个重要特性,它是函数式编程的基础。Lambda表达式本质上是一种匿名函数(Anonymous Function),是代码块。
Lambda表达式允许将函数作为方法的参数或者将代码块作为数据进行传递。

匿名内部类和Lambda表达式
匿名内部类和Lambda表达式都是在Java中可把代码块作为数据进行传递的容器,
它们可以用来传递行为(函数),可作为输入参数或返回值。

  • 匿名内部类
    匿名内部类最常见的用法是图形用户界面(GUI)编程中组件的事件处理中。例如:为了响应用户的操作,需要注册一个事件监听器。用户单击按钮时程序的响应动作。
    匿名内部类的版本:
	/***GUI事件处理匿名内部类的版本***/
	button.addActionListener(new ActionListener()){
		public void actionPerformed(ActionEvent event){
			System.out.println("你点击了按键!");
		}
	};

在这个例子中,我们创建了一个匿名内部类,它实现了ActionListener()接口。当用户点击按钮时,按钮会调用方法actionPerformed(ActionEvent event)来响应,会在屏幕上显示“你点击了按键!”提示信息。

另一个示例:

	/***线程实例的匿名内部类的版本***/
	Thread thread = new Thread(new Runnable() {
    	public void run() {
        	System.out.println("我是匿名内部类的线程!");
    	}
	});
	thread.start();

匿名内部类线程版本,用匿名类实现了Runnable()接口的一个实例,并以此为参数来构建一个线程。

  • Lambda表达式:
    Lambda表达式是Java 8中引入的一种更简洁、更直观的方式来表示函数式接口的实例,使得代码更紧凑。
    下面这行是GUI事件处理Lambda表达式的版本,这是一个带参数的Lambda表达式:
	/***GUI事件处理Lambda表达式的版本***/
	button.addActionListener(event->System.out.println("你点击了按键!"));

再看一个用无参数的Lambda表达式创建线程的版本:

	/***线程实例的Lambda表达式的版本***/
	Thread thread = new Thread(() -> {
    	System.out.println("我是Lambda表达式实现的线程!");
	});
	thread.start();

Lambda表达式甚至不需要标识名,这一点与匿名内部类相似。
Lambda表达式的引入让Java代码更加简洁明了,提高了代码的可读性和可维护性。

Lambda表达式的输入参数应当是值,不能是变量
Lambda表达式的输入参数不能是变量,尤其是在Lambda表达式内是不可变的。
在Lambda表达式中,如果引用了外部的变量,则这些变量必须是final修饰的或 事实上的final(effectively final)变量。final变量表示它的值不可变,而 事实上的final(effectively final)变量在Lambda表达式中被隐式视为final,即它的值在Lambda表达式中不可再被修改。这个限制是为了避免在Lambda表达式中修改外部变量引发线程安全问题。Lambda表达式是一种闭包,它可以访问外部作用域的变量,如果允许在Lambda表达式内部修改这些变量,可能导致多个线程同时修改同一个变量,引发线程安全问题。

在匿名内部类中,需要引用方法中变量作为参数时,需要把变量用final修饰,这意味着这参数不能在匿名内部类中随意改变。
还是以按钮的监听器为例来说明:
匿名内部类的版本2:

final String name = getUserName();
button.addActionListener(new ActionListener()){
	public void actionPerformed(ActionEvent event){
		System.out.println(name +  ":你好!");
	}
};

Java 8 虽然放松了这一限制,可以引用 非final变量,但是作为Lambda表达式输入参数,
在Lambda表达式中引用的外部变量必须是final修饰的或 事实上的final(effectively final)变量。
Lambda表达式的版本2:

	String name = getUserName();
	button.addActionListener(event->System.out.println(name +  ":你好!"));

如果你试图给该变量多次赋值,然后在Lambda表达式中引用它,编译器就会报错,无法通过编译。

Lambda表达式是什么类型?
Lambda表达式是函数接口的实例,因此它的类型是函数接口。
但是Lambda表达式本身不包含实现的函数接口的信息;该信息可从其上下文环境中推导出来的。例如,下面这个Lambda表达式:

	  x -> 2 * x 

这是二个操作符的函数接口:

	 interface IntOperation { int operate(int i); } //IntOperation函数接口
	 interface DoubleOperation { double operate(double i); } //DoubleOperation函数接口

下面这二行代码的写法都是合法的

	IntOperation iOp = x -> x * 2; //把Lambda表达式赋值给IntOperation函数接口
	DoubleOperation dOp = x -> x * 2; //把Lambda表达式赋值给DoubleOperation函数接口

Lambda表达式是对象吗?

Lambda表达式是函数接口的实例,因此它是Object子类的实例。Object也是Lambda表达式超类。下面这些都是合法的赋值:

    Runnable r = () -> {};   // 创建一个Runnable类型的Lambda表达式,并赋值给句柄r
    Object obj = r;            // 向上转型为Object

Lambda表达式的作用域规则

虽然有的情景,Lambda表达式可以代替匿名内部类,但是Lambda表达式和匿名内部类的作用域规则是不同的。下面我们从this关键字和super关键字来进行说明。

在Java中,lambda表达式中的this关键字和super关键字具有特定的含义和使用场景。

Lambda表达式中的this关键字指向的是当前类的实例(包含Lambda表达式的类实例),Lambda表达式则只是当前类的一个属性或行为(方法、函数)。
这与匿名内部类中的this关键字不同,匿名内部类中的this指向的是匿名类本身。
我们来看一个测试验证例程LambdaScopeRules,例如:


public class LambdaScopeRules {
    private String name = "LambdaScopeRules主类";
    public void test() {
        // 匿名类实现
        new Thread(new Runnable() {
            String name = "匿名类A";
            @Override
            public void run() {
                System.out.println("匿名类的this指向的类:" + this.name);
            }
        }).start();
        
        // lambda表达式实现
        new Thread(() -> {
        	String name = "lambda表达式";
            System.out.println("Lambda的this指向的类:" + this.name);
        }).start();
    }
    
	public static void main(String[] args) {
		LambdaScopeRules rulesTest = new LambdaScopeRules();
		rulesTest.test();
	}
}

测试结果如下所示:
在这里插入图片描述
从测试结果可得出结论

  • 匿名类的this指针表示的就是匿名类自身,因此匿名类(与其他类一样)有自己的命名空间,它有类级别的作用域。
  • Lambda 表达式的this指针表示的则是其主类(宿主类)。Lambda表达式没有引入新的命名空间,Lambda表达式只有代码块级别的作用域。

同样道理,对于super关键字也有与this关键字类似的不同。有兴趣的读者可以自行增加测试代码进行验证。

Lambda 表达式应用场景
Lambda 表达式主要用于实现只有一个抽象方法的接口,即函数式接口。例如,Java中的ActionListener、Runnable、Callable、Comparator和Consumer等都是函数式接口。
下面这些都是Java中库中定义的一部分函数接口:

	public interface Consumer<T> { void accept(T t); } 
    public interface Runnable { void run(); } 
    public interface Callable<V> { V call() throws Exception; } 
    public interface ActionListener { void actionPerformed(ActionEvent e); } 
    public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); } 

类似的,如果一个方法接受声明于java.util.function包内的接口,例如Predicate、Function、Comparable、Consumer或 Supplier,那么就可以向其传递Lambda表达式。

函数接口是只有一个抽象方法的接口。

例如,集合上的forEach方法可以具有以下签名:

    public void forEach(Consumer<? super T> consumer); 

方法forEach()需要一个实现Consumer接口的实例作为参数。这里我们就可以使用一个Lambda表达式作为方法forEach()的入口参数。
我们来看一个实例,假设pointList是一个列表List
我们使用forEach来处理java.awt.Point列表中每个元素的x和y坐标,实现坐标的移动功能。如果使用Consumer的匿名内部类来实现,如下是匿名内部类实现版本:

		 pointList.forEach(new Consumer<Point>() { 
		        public void accept(Point p) { 
		            p.move(p.y, p.x);
		        } 
		  });

Lambda表达式实现版本,则如下所示:

	pointList.forEach(p -> p.move(p.y, p.x)); 

这是一个代码演示版本,无法真正在屏幕上演示。其完整的代码如下:

import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class MovePoint {
	public static void main(String[] args) {
		List<Point> pointList = new ArrayList<>();
		pointList.add(new Point(12, 68));
		pointList.add(new Point(65, 26));
		pointList.add(new Point(252, 18));
		
		//版本一,匿名内部类实现版本
		 pointList.forEach(new Consumer<Point>() { 
		        public void accept(Point p) { 
		            p.move(p.y, p.x);
		        } 
		  });
		//版本二,Lambda表达式实现版本
		 pointList.forEach(p -> p.move(p.y, p.x));  
	}
}

Lambda 表达式能否完全替代匿名内部类
既然Lambda表达式这么好,那么它是否可以完全替代匿名内部类呢?
当然不行,因为Lambda表达式只能用于实现只有一个抽象方法的接口(即函数式接口)。如果一个接口或抽象类有多个抽象方法,那么它就不是一个函数式接口,就不能使用Lambda表达式来实现,这种情形还是需要使用传统的匿名类或具体的类来实现。

二、Lambda表达式的语法

Lambda表达式使用箭头->作为标志,将参数和函数体分隔开,参数可以有零个或多个,函数体可以是一个表达式、一段代码块或一个自定义的函数(方法)。如果主体是一个表达式,它将直接返回该表达式的结果;如果主体是一个代码块或自定义函数,它将按照常规的Java语法执行,并且还可以使用return语句来返回值。

Lambda表达式的基本语法 如下:

	(parameters) -> expression  	/***格式一***/
	(parameters) -> { statements; } /***格式二***/

其中:parameters是输入参数,参数可以有零个或多个。
expression是表示一个表达式;它可以是一个表达式、一段代码块或一个自定义的函数(方法)。
statements; 表示多条语句。
通常情况函数体的前后要由大括号括起来{ statements; };单行语句可省略大括号和最后的分号。

Lambda表达式的示例:

  • 无参数的Lambda表达式:
	() -> System.out.println("Lambda表达式无参数示例!");
  • 带参数的Lambda表达式:
	/***单行Lambda表达式***/
	x -> 2 * x;  //单个参数可省略括号
	(int x, int y) -> x + y;  //参数列表中的参数可以是显式类型声明
	(x, y) -> x + y;  //如果编译器可以根据上下文推断出参数类型,则可以省略类型声明
  • 带参数的多行Lambda表达式:
	c -> { 
			int s = c.size(); 
			c.clear(); 
			return s; 
		}

示例:
我们最后再来看一个简单的例程,本例用Lambda表达式定义了一个线程:

	Runnable r = ()->new Timer(1000, e->System.out.println("日期时间:"+new Date())).start();

这行代码使用无参数的Lambda表达式定义了一个可运行的实例(实现了Runnable接口)。需要说明的是最后的“.start()”不是启动线程,它启动的是定时器Timer。
这个例子中共有两个Lambda表达式,其中的“e->System.out.println(“日期时间:”+new Date())”,是一个单参数的Lambda表达式,作为定时器的回调方法(函数)参数。这实现
的是ActionListener函数接口。
下面是完整的例程代码:

import java.util.Date;
import javax.swing.Timer;
public class Lambda {
	public static void main(String[] args) {
		Runnable r=()->new Timer(1000, e->System.out.println("日期时间:"+new Date())).start();
		r.run(); //启动线程
		while(true); //主线程无限循环
	}
}

例程的运行时每隔1秒打印一次日期时间,其运行结果如下所示:
在这里插入图片描述

参考文献

标签:Java,接口,析解,匿名,new,public,表达式,Lambda
From: https://blog.csdn.net/weixin_42369079/article/details/143272414

相关文章

  • 基于JavaScript的关键词过滤示例
    业务场景中,合作第三方的的各种AI内容审核模型,完全达不到满意的状态,奇怪这么简单的一个东西,有这么复杂吗,自己动手来一个DEMO,给开发,仅供参考。<!DOCTYPEhtml><htmllang="zh"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-wi......
  • JavaScript的对象事件监听处理,交互式网页的关键!
    目录一、对象的事件二、常用的事件及处理1、鼠标事件(1) mousedown事件(2) mouseup事件(3) click事件(4)dblclick事件(5)mouseenter事件(6)mouseleave事件(7)wheel事件2、键盘事件(1)keydown事件(2)keyup事件3、表单事件(1)概念 (2)先设置一个简单的表单(3)获取表单节点(4)添加表......
  • gRPC说明及使用(java版)
    官方文档:https://grpc.io/docs/what-is-grpc/introduction/一gRPC允许你定义四种服务方法:一元RPC,其中客户端向服务器发送单个请求并得到单个响应,就像普通函数调用一样。rpcSayHello(HelloRequest)returns(HelloResponse);服务器流式RPC中,客户端向服务器发送请......
  • Java中的I/O模型——BIO、NIO、AIO
    1. BIO(BlockingI/O)1. 1BIO(BlockingI/O)模型概述        BIO,即“阻塞I/O”(BlockingI/O),是一种同步阻塞的I/O模式。它的主要特点是,当程序发起I/O请求(比如读取数据或写入数据)时,程序会一直等到这个请求完成,才继续往下执行。在BIO模型下,每个连接都需要一个独立的线程......
  • java设计模式之工厂模式
    简单分享下java中设计模式–工厂模式工厂模式(FactoryPattern)是面向对象编程中常用的设计模式之一,它属于创建型模式。工厂模式的主要目的是使用工厂方法来创建对象,而不是直接使用new关键字实例化对象。这样可以提高程序的扩展性和维护性。以下是Java中简单工厂模式的案......
  • JavaScript用法
    JavaScript 用法HTML中的Javascript脚本代码必须位于 <script> 与 </script> 标签之间。Javascript脚本代码可被放置在HTML页面的 <body> 和 <head> 部分中。<script>标签如需在HTML页面中插入JavaScript,请使用<script>标签。<script>和</script>......
  • 宁德时代Java面试题及参考答案
    MySQL的底层实现机制是怎样的?MySQL主要包括以下几个核心的底层实现部分。存储引擎层是MySQL的关键。InnoDB是最常用的存储引擎,它以页为单位进行存储,默认页大小是16KB。数据存储在表空间中,表空间可以由多个文件组成。InnoDB采用了B+树的数据结构来存储索引和数据......
  • 【java】实战-力扣题库:有序数组的平方
    问题描述给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。问题分析:既然给定的是一个非递减顺序的数组我们可以使用双指针,一个指向左边,一个指向右边,比较两边平方后的大小。哪个大,就把那个数放到当前数......