Java进阶-Lambda
前言
最近都没什么时间给自己充电,不过想了想还是要继续学习的,所以想着开始写一期博客关于:以前用过但是又没学懂的知识
,可以称之为薛定谔的知识
,就是我看得懂,也会用,但是为什么要这么用,什么时候这么用?我不知道
这一节的第一讲就从最近用的比较多的Lambda表达式开始吧
,本文仅当为入门学习,如有遗漏还请指出。
Lambda表达式
什么是Lambda表达式
Lambda表达式
是自Java 8 版本引入的一个重要特性
,也可以说是最重要的一个特性,它提供了一种更简洁、更直接的方式来表示功能接口(即只包含一个抽象方法的接口)的匿名实现
。Lambda表达式允许你以一种更接近数学中函数概念的方式编写代码
,使得Java编程更加灵活和表达力更强。
注意上面的:它提供了一种更简洁、更直接的方式来表示功能接口(即只包含一个抽象方法的接口)的匿名实现(省略了方法名的实现)
。
这一句可以简单缩短为:提供了只有一个方法接口的匿名实现方式
,首先它是接口的实现方式,什么实现方式呢?匿名实现。
还是没看太懂?没事,我们接着往下看
初识Lambda表达式
我们先看一个Lambda表达式的简单使用,先认识认识它们,然后再慢慢摸索。
下面我们来看一个常见的Lambda表达式的使用,以及它们的格式。
Lambda表达式的简单使用
以下是一个使用Lambda表达式实现自定义接口的示例
首先创建一个只有一个抽象方法的的接口
。
@FunctionalInterface//用于表示这是一个函数式接口的注解 即该接口中只有一个方法 关于函数后续会提到
public interface DemoLambdaInterface {
int operate(int a , int b);//两个数的简单操作
}
那我们再来看看如何使用Lambda表达式实现该接口
public class Test {
public static void main(String[] args) {
//接口中方法的匿名实现
DemoLambdaInterface demoLambdaInterface = (a , b) -> a + b;
//调用跟常规接口方法调用没有区别
System.out.println("使用Lambda表达式匿名实现的接口调用结果为:" + demoLambdaInterface.operate(1, 2));
}
}
Lambda表达式格式分析
相信大家对于上述的代码,还是看不懂,你确定这一段是Java代码?(a , b) -> a + b;
其实将这段代码:DemoLambdaInterface demoLambdaInterface = (a , b) -> a + b;
补全你就知道了
补全之后就是以下这样的
//接口中方法的匿名实现
DemoLambdaInterface demoLambdaInterface = (a , b) -> {
return a + b;
};
结合我们接口中的定义
@FunctionalInterface//用于表示这是一个函数式接口的注解 即该接口中只有一个方法 关于函数后续会提到
public interface DemoLambdaInterface {
int operate(int a , int b);//两个数的简单操作
}
看出来什么没有,没错就是那个! 接口的实现嘛,不过我们一般写实现都是带了方法名的
int operate(int a , int b) {
return a + b;
}
现在我们来分析上述代码,
(a , b)表示接口方法的入参,但是没有类型,比如int ,String
->
是一个特殊的语法符号,被称为箭头操作符
或者Lambda操作符
,起到参数与方法体分割的作用
其后的{}
中的内容就是方法体的具体实现内容
了。
而在Lambda表达式中单行代码是可以省略{}和return
的,也就成了上面的:
DemoLambdaInterface demoLambdaInterface = (a , b) -> a + b;
如果方法的参数为空,那括号里可以什么都不写了
与传统接口方法实现的比较
如果按照常规的方法
,来实现接口方法,我们一般怎么写?
首先肯定是创建一个实现类
DemoLambdaInterfaceImpl:
public class DemoLambdaInterfaceImpl implements DemoLambdaInterface {
//实现接口方法
int operate(int a , int b) {
return a + b;
}
}
然后使用的时候再通过创建实现类对象去调用:
DemoLambdaInterface dli = new DemoLambdaInterfaceImpl();
dli.operate(1 , 2);
与上面的Lambda表达式相比,谁简洁一目了然。
理解Lambda表达式
我们先来做点课前准备工作,将以下几部分学习完再理解Lambda表达式
就很简单了。以下几部分同样是Lambda表达式的重要特性。
函数式编程
Lambda表达式
可以说是实现函数式编程的一个重要工具
。那么什么是函数式编程呢?
函数式编程是一种编程范式
,强调程序构建在不可变数据和纯函数之上
。
不可变数据好理解,关键是后面的纯函数什么?
纯函数
是指对于相同的输入总是产生相同的输出
,并且没有‘副作用’
,这里的副作用是:不改变外部状态或依赖外部状态
。
注意这两点:相同的输入总有相同的输出
,不改变外部状态或依赖外部状态
。
要快速理解这两句话,我们要从相反的角度
看:相同的输入得到不同的输出,改变外部状态或者依赖外部状态的就不是函数式编程
。
非纯函数实例
来看以下非纯函数的代码:
public class Test {
static int gamePeopleCount = 0;
public static void main(String[] args) {
String demoStr = "小游戏玩家: ";
int a = addGamePeopleCount("小明");
System.out.println("当前游戏人数:" + a);
addGame(demoStr , "小明");
}
public static int addGamePeopleCount(String name) {
System.out.println(name + " 加入了游戏。");
//每次返回的结果不一样
gamePeopleCount+=1;
return gamePeopleCount;
}
public static void addGame(String demoStr , String name) {
//修改了demoStr的值 也就改变了外部状态
demoStr = demoStr +" "+ name;
System.out.println(demoStr);
}
}
纯函数示例
再看一个纯函数的示例
:
public int add(int a , int b) {
return a + b;
}
在纯函数中,很明显可以看出上述两个特征:相同的输入总有相同的输出
,不改变外部状态或依赖外部状态
。
函数式编程在Lambda表达式中的体现
上面的代码就是
//接口中方法的匿名实现
DemoLambdaInterface demoLambdaInterface = (a , b) -> a + b;
闭包
很多人说:Lambda表达式也可称为闭包
。那什么是闭包(仅仅从Lambda表达式出发)?
闭包是一个函数,它能够访问并记住其自身定义时所在作用域中的变量
,甚至延长变量的生命周期。
也就是说闭包是一个能记住其自身定义时所在作用域中变量的函数
。
说人话就是,一个闭包可以调用到上下文中的局部变量
。再通俗一点就是:。。。。还是代码说事吧
闭包与Lambda表达式的示例
@FunctionalInterface
public interface DemoLambdaInterface {
void sayHello();
}
这里的@FunctionalInterface
注解并不是强制要求的,它是一个标记,用于指示接口是设计为一个函数式接口
(只有一个抽象方法
)。
public class Test {
public static void main(String[] args) {
//Lambda表达式的简单使用
String localVariable = "这是局部变量,并没有传递给方法 ";//如果一个局部变量没有通过参数传递给方法,一般是无法使用的
//Lambda表达式
DemoLambdaInterface demoLambdaInterface = () -> {
System.out.println(localVariable + "hello");
};
//在这里调用
demoLambdaInterface.sayHello();
}
}
在上述方法中,可以看到localVariable
并没有作为方法参数传递给sayHello方法
,但是demoLambdaInterface对象在这里还是可以使用到localVariable变量,这就是我们上述讲的:一个闭包可以调用到上下文中的局部变量
,也就是你的函数可以使用到当前上下文的局部变量
。
类型推导-匿名内部类
类型推导
作为Lambda表达式的一大特性之一,怎么能没有呢?我们以匿名内部类为例子说明。
匿名内部类(Anonymous Inner Class)
是Java中一种特殊的内部类(如果这个都不知道就真的需要好好补课了),它没有明确的类名
。匿名内部类通常用于实现接口或继承其他类
,并且只在创建它的代码块中直接使用,而不需要
显式地为其定义类名。
这个应该或多或少都接触过,比如我们最常见的Thread
的使用:
Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是匿名内部类");
}
});
myThread.start();
这其中的构造方法:
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
这里的方法参数为Runnable
的对象,但其实这里的Runnable是一个接口
:
@FunctionalInterface
public interface Runnable {
/**
一些注释
*/
public abstract void run();
}
对于一个接口对象,如果你要按照正常的写法的话,要么再写一个实现类
,要么使用其子类
,然后使用实现类去传参给Thread的构造方法
。
是不是听着就很麻烦,但现在使用匿名内部类就可以在构造时直接自己实现这个类
,而不用创建新的实现类。就是上面的
Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是匿名内部类");
}
});
myThread.start();
但是使用Lambda表达式更为直接也更为简洁
匿名内部类与Lambda表达式的示例
在以下Lambda表达式中,Lambda表达式通过类型推导
,返回的是一个接口的实现
Thread t = new Thread(() -> {
System.out.println("我是Lambda表达式");
});
t.start();
当你将Lambda表达式赋值给一个函数式接口类型的变量
时,Java编译器会自动进行类型推导
。在这个例子中,编译器知道t是一个Thread
类型的变量,它的构造函数接受一个Runnable类型的参数
。因此,编译器将Lambda表达式推断为实现了Runnable接口的匿名类的一个实例
。
优点嘛,简洁!!!简洁!!!还是T~~~M的简洁!!!
常规使用Lambda表达式的场景
好了,说了那么多,我们再来看看,日常使用Lambda表达式的场景吧
函数式接口的实现
就是我们第一个例子中的,带有@FunctionalInterface
注解的接口叫做函数式接口
,都是可以使用Lambda表达式实现的。这样的接口中只有一个抽象方法
,如上面提到的Runnable
接口,这就是一个很标准的
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
但有时候,一个拥有很多的方法的接口,依然有@FunctionalInterface
注解,也可以使用Lambda表达式实现,比如上面也说过的比较器Comparator
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
//其他方法
}
这是因为它只有一个自己的抽象方法
。其他的方法要么不是抽象方法
,要么就是继承来的
,都不被计算在内。比如以下就只有一个compare(T , T)的抽象方法
如何实现,我们后续会以这个接口举例
并发编程
这里主要是使用Lambda表达式来进行线程池的创建
,Lambda表达式允许以一种简洁、声明式的方式编写功能逻辑
,减少了传统的匿名类或显式方法定义的冗长。
如以下部分
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LambdaThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 提交任务到线程池,使用Lambda表达式定义Runnable任务
for (int i = 0; i < 10; i++) {
int taskId = i;
executorService.submit(() -> {//Lambda表达式替代以前的匿名内部类
System.out.println("Task ID " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Thread interrupted.");
}
});
}
// 关闭线程池,不再接受新任务,等待所有已提交的任务完成
executorService.shutdown();
while (!executorService.isTerminated()) {
// 等待直到所有任务完成
}
System.out.println("All tasks completed.");
}
}
排序和比较
这个比较器(Comparator
)接口大家应该很常用,尤其是我需要按照对象某个属性进行排序的时候
@FunctionalInterface//可以看出来是一个函数式接口
public interface Comparator<T> {
int compare(T o1, T o2);//大于返回正数 小于返回负数 等于返回0
//其他接口
}
以及String
的compareTo
方法
现在我们来看看如何使用Lambda表达式来快速对集合中的元素排序
(常规的就不讲了),我们这里创建一个简单的实体类Student
为例子
@Data//get-set方法的生成
@AllArgsConstructor//全参构造函数
public class Student {
int id;
String name;
int age;
}
具体实现
public class SortLambdaTest {
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student(2 , "ZhangSan" , 15));
list.add(new Student(1 , "LiSi" , 13));
list.add(new Student(4 , "WangWu" , 26));
list.add(new Student(3 , "ZhaoLiu" , 18));
//使用Lambda表达式按照不同的对象属性排序
System.out.println("================按照ID排序========================");
//按照id排序
Collections.sort(list , (stu1 , stu2) -> stu1.getId() - stu2.getId());
//使用Lambda表达式遍历输出输出
list.forEach(stu -> System.out.println(stu.toString()));
System.out.println("=================================================");
System.out.println("==================按照NAME排序=====================");
//按照名字排序
Collections.sort(list , (stu1 , stu2) -> stu1.getName().compareTo(stu2.getName()));//使用compareTo方法
list.forEach(stu -> System.out.println(stu.toString()));
System.out.println("=================================================");
//课后作业 按照年龄排序
}
}
从输出结果中可以确定自己写的有无正确排序
这个其实很常用,比如我们在调用外部接口时,对方返回的数据默认是以主键排序,但其实我们本地的逻辑是要使用Name排序或者手机号排序,这样的话是需要我们再进行一次排序。
与其给每一个实体类加一个实现比较器接口,再实现比较方法,Lambda表达式的实现更为简洁。
集合与流
在这一章之前,我们需要了解以下什么是什么是流,流有哪些特点
方面
流的前置了解
在 Java 中,流 (Stream)
是 Java 8
引入的一种新的抽象,用于对集合(如列表、集合等)进行复杂的数据处理操作
。流提供了一种声明式的方式来处理数据
,使得代码更加简洁和易读。
流操作主要分为两种类型:中间操作 (intermediate operations) 和终端操作 (terminal operations)
。
中间操作指的是,该方法会返回一个新的流
,如以下图中的方法。返回值都是流或者其子类
终端操作是指流的执行,并且返回结果
。(中间操作并不会执行以及返回结果,可以理解为把要做的操作记录下来)
其实可以简单理解为:返回值不是Stream的都是终端方法
。
这里列出常用中间操作与终端操作的方法
中间操作 (Intermediate Operations)
filter(Predicate<? super T> predicate)
:筛选符合条件的元素,生成一个包含满足条件元素的新流。(比如获取流中所有带“三文鱼”字符的元素)map(Function<? super T, ? extends R> mapper)
:将流中的每个元素转换为另一种形式,生成一个包含转换后元素的新流。(就是来做映射)flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
:将流中的每个元素转换为一个流,然后将这些流合并成一个新的流。(同上)sorted(Comparator<? super T> comparator)
:根据提供的比较器对流中的元素进行排序,生成一个排序后的新流。(排序)distinct()
:去重
终端操作 (Terminal Operations)
collect(Collector<? super T, A, R> collector)
:转换成集合forEach(Consumer<? super T> action)
:遍历流对所有元素进行操作,比如+1count()
:计算总数量并返回anyMatch(Predicate<? super T> predicate)
:检查流中的任意一个元素是否匹配给定的条件findFirst()
:返回流中的第一个元素的Optional
对象,如果流为空则返回空的Optional
其大致特点如下
声明式编程
:流 API 允许你使用声明式的方法
来处理数据,而不是使用传统的命令式方法。(声明式编程
指的是关注于描述数据的操作
,而非具体的执行过程。比如你只需要告诉流去执行过滤(filter)、映射(映射)、返回集合(collect)
等操作,而不需要关心怎么实现。)链式操作
:流操作可以链式调用
,每个中间操作都会返回一个新的流
,这里其实很好理解,因为每个操作返回的是新的当前对象,那么自然可以继续调用当前对象所拥有的方法。惰性求值
:流的中间操作是惰性的
,只有在终端操作
调用时才会执行,也就是你挂了一串鞭炮,没有点燃引线之前,你的鞭炮不会响。不可变性
:流操作不会修改原始数据
,而是返回一个新的流。
集合、流以及Lambda的简单结合
说了那么多,来点代码理解下
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamWithLambda {
public static void main(String[] args) {
List<String> list = Arrays.asList("coco", "alice", "bob" , "joker","worker" ,"bob");
//使用Lambda表达式过滤出包含o的所有元素
List<String> oList = list.stream()
.filter(s -> s.contains("o"))//使用Lambda表达式来定义过滤操作 中间操作
.collect(Collectors.toList());//中间操作
System.out.println("所有包含o的数据");
//同样使用流遍历输出元素
oList.stream()
.forEach(str -> System.out.println(str));//终端操作
System.out.println("=======================================");
System.out.println("链式操作后的数据");
//链式编程
list.stream()
.filter(s -> s.contains("o"))//所有包含o的数据 中间操作
.sorted()//默认排序 中间操作
.distinct()//去重 中间操作
.forEach(str -> System.out.println(str));//终端操作 遍历
System.out.println("=======================================");
//原来的数据是不会变的
System.out.println("上述操作后,原来数据为:");
list.forEach(s -> System.out.println(s));
System.out.println("=======================================");
//判断时是否包含coco 使用anyMatch去判断是否包含 allMatch是指所有元素满足的条件
if (list.stream().anyMatch(s -> s.equals("coco"))) {
System.out.println("字符中包含coco");
} else {
System.out.println("没有coco");
}
}
输出如下
所有包含o的数据
coco
bob
joker
worker
bob
=======================================
链式操作后的数据
bob
coco
joker
worker
=======================================
上述操作后,原来数据为:
coco
alice
bob
joker
worker
bob
=======================================
字符中包含coco
可以看到如果你对于一个你拿到的数据不满意
,随时可以使用Stream以及Lambda表达式
去得到你需要的数据。无论是排序,映射,过滤,去重以及别的操作
,都非常简便快捷。
并行流
当然了,Stream
同样可以多线程
操作,这也是比较重要的一部分:并行流(通过parallelStream()方法得到)
。
值得注意的并行流是指多个线程去处理你的中间操作,以提高效率,但是并不保证线程安全
。
对于敏感数据可以考虑以下操作避免并行流带来的线程不安全
:
确保并行流处理时的线程安全,可以通过以下几种方式来实现:
-
使用线程安全的数据结构
:使用线程安全
的数据结构,比如ConcurrentHashMap
、CopyOnWriteArrayList
等,因为它们内部已经处理好了线程同步问题。 -
无副作用的函数
:确保你在流操作中使用的Lambda表达式或函数是无副作用的
,即它们不改变外部状态,只依赖于输入值产生输出值。 -
同步访问共享资源
:如果确实需要在Lambda表达式中修改共享资源
,需要适当同步。可以考虑使用synchronized块或方法
,或者使用其他同步工具类比如锁
,如ReentrantLock(可重入锁)
-
原子变量与CAS操作
:对于计数、累加
等操作,可以使用AtomicInteger、AtomicLong
等原子类 -
局部变量与ThreadLocal
:尽可能使用局部变量
,或者在必要时使用ThreadLocal
存储线程特有的数据
上述方法可以有效确保并行流处理时的数据一致性与线程安全。
以下是一个简单使用并行流的例子:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ParallelStreamTest {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().mapToInt(n -> n * n).sum();
System.out.println("所有平方和: " + sum);
}
}
其他
Lambda表达式中的双冒号( :: )
在Java中,Lambda表达式的::
操作符被称为方法引用(Method Reference)
,它是Lambda表达式的另一种更简洁的写法,用于引用已有方法
。方法引用允许你直接引用现有类或对象的方法
,而不需要
显式定义Lambda表达式体。这种方法引用的语法看起来像是类名或对象实例后面跟着两个冒号,然后是方法名。
方法引用的几种形式:
-
静态方法引用:
ClassName::staticMethodName
- 当你需要引用一个静态方法时使用。例如,
Integer::parseInt
引用了Integer.parseInt(String)
静态方法。
- 当你需要引用一个静态方法时使用。例如,
-
特定对象的实例方法引用:
instance::methodName
- 如果你有一个具体的对象,并想引用它的实例方法,可以这样做。例如,对于一个特定的
String
对象str
,str::length
引用了str.length()
方法。
- 如果你有一个具体的对象,并想引用它的实例方法,可以这样做。例如,对于一个特定的
-
任意对象的实例方法引用:
ClassName::methodName
- 当引用一个所有实例都有的方法(非静态方法)时,可以使用这种方式。例如,
String::length
可以用来代替Lambda表达式(s) -> s.length()
。
- 当引用一个所有实例都有的方法(非静态方法)时,可以使用这种方式。例如,
-
构造器引用:
ClassName::new
- 构造器引用用于创建对象的新实例,例如,
ArrayList::new
可以用来创建一个新的ArrayList
实例。
- 构造器引用用于创建对象的新实例,例如,
使用场景:
-
简化Lambda表达式:当你发现Lambda表达式做的事情
仅仅是调用一个已存在的方法时
,可以考虑使用方法引用来替代,使代码更简洁。 -
集合操作:在处理集合时,如使用
sort
、map
、filter
等操作时,方法引用可以简化对元素的操作逻辑。 -
函数式接口:作为函数式接口(如
Function
、Consumer
、Predicate
等)的实现时,如果符合方法引用的条件,优先考虑使用方法引用。 -
并行流:在并行流中,方法引用同样可以提高代码的可读性和性能,尤其是在不需要维护额外状态的并行处理场景下。
示例:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 使用Lambda表达式
names.sort((a, b) -> a.compareTo(b));
// 使用方法引用简化
names.sort(String::compareTo);
在这个例子中,String::compareTo
就是方法引用,它直接引用了String
类的compareTo
方法,代替了Lambda表达式(a, b) -> a.compareTo(b)
,使得代码更加简洁明了。
写在最后
拖来拖去,写了大半个月,才写完,有的部分也还没讲清楚,Lambda表达式作为Java8最核心的特性,涉及的方方面面实在是太多了,有的部分我也没有深入的接触到,具体使用的场景也不多,此文也仅仅当给各位作为一个入门的学习。