目录
2022年圣诞节到来啦,很高兴这次我们又能一起度过~
1、lambda表达式的语法
java中lambda表达式通常写成下边这个样子:
- 参数列表——这里采用了Comparator接口中compare方法的参数,两个Apple对象。
- 箭头——箭头 -> 把参数列表与Lambda主体分隔开。
- Lambda主体——比较两个Apple的重量。
抽象下,lambda表达式的语法就是下边的模样,看起来还是比较简单的
(parameters) -> {expression}
不能省略大括号情况:
- 方法体有返参时不能省略大括号
- 方法体有多行逻辑代码时不能省略大括号
省略小括号情况:
- 方法只有一个参数,而且这个参数的类型可以推导出,那么可以省略小括号
(1)为什么需要使用lambda表达式?
通过下边这断代码,可以清晰的领略到使用lambda表达式的好处:
public class Sorting {
public static void main(String... args) {
//排序数据:根据重量进行排序
List<Apple> inventory = new ArrayList<>();
inventory.addAll(Arrays.asList(new Apple(80, "green"), new Apple(155, "green"), new Apple(120, "red")));
//1、单独定义一个比较器类
inventory.sort(new AppleComparator());
System.out.println(inventory);
//2、使用内部类
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
System.out.println(inventory);
//3、使用lambda表达式
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
System.out.println(inventory);
//4、使用方法引用
inventory.sort(comparing(Apple::getWeight));
System.out.println(inventory);
}
public static class Apple {
private Integer weight = 0;
private String color = "";
public Apple(Integer weight, String color) {
this.weight = weight;
this.color = color;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String toString() {
return "Apple{" +
"color='" + color + '\'' +
", weight=" + weight +
'}';
}
}
static class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
}
同样是排序操作,你有没有发觉使用lambda表达式后,整个代码都清晰简单多了。
2、使用lambda表达式的条件:函数式接口
用一句话来说,函数式接口就是只定义一个抽象方法的接口。接口中现在还可以拥有默认方法(后续再探讨这个)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
用函数式接口可以干什么呢? Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。具体说来,就是函数式接口一个具体实现的实例。// lambda表达式是函数式接口一个具体实现的实例
public static void main(String[] args) {
// 传统方式方式:这是一个Runnable接口具体实现的实例
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
//lambda方式:这也是一个Runnable接口具体实现的实例
Runnable runnable1 = () -> System.out.println("Hello");
}
所以,Lambda 表达式本质上就是函数式接口实例化过程的简化版。
你可能会问,那为什么只有在需要函数式接口的时候才可以传递Lambda呢?在需要其他非函数式接口的时候就不能用吗?这其实就是语言设计者的一个设计方式的问题,特定的语法只能在特定的条件下使用,一方面带来便利,一方面也避免语言变得更加复杂。//锦上添花之作
Java 8的 java.util.function 包中引入了一些新的函数式接口。比如Predicate、Consumer和Function等。
(1)Predicate 接口
java.util.function.Predicate<T> 接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。所以,在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。//接收参数,返回一个boolean值
比如,Stream中的 filter 方法,接收一个 Predicate 类型的入参
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
//Stream中的filter方法
Stream<T> filter(Predicate<? super T> predicate);
使用示例://大多数情况下,lambda都会结合Stream来使用
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("A","B","C"));
list.stream().filter(r -> !r.equals("A")).collect(Collectors.toList());
}
(2)Consumer 接口
java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回值(void)。如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。//接收参数,无返回值
比如,Stream中的 forEach 方法,接收一个 Consumer 类型的入参
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
//Stream中的forEach方法
void forEach(Consumer<? super T> action);
在下面的代码中,使用forEach方法,接受一个String的列表,并配合Lambda来打印列表中的所有元素(对其中每个元素执行操作)。
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.stream().forEach(System.out::println);
}
(3)Function 接口
java.util.function. Function<T,R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口。//接收一个类型,返回另一个类型
比如,Stream中的 map 方法,接收一个 Function 类型的入参
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
//Stream中的map方法
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
在下面的代码中,利用map方法,以将一个String列表映射到一个新的String列表。
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> collect = list.stream().map(String::toLowerCase).collect(Collectors.toList());
}
最后,汇总一些Lambda及函数式接口的例子:
//在介绍函数式接口时,使用了Stream中的相关方法进行举例。实际上,我们大多数的应用场景都可以借助Stream来进行解决,可以理解为Stream是在函数式接口上做了进一步的封装,在它的基础上使用lambda也非常的方便。
请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个 try/catch 块中。
3、方法引用
方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。//方法引用是一个lambda表达式的快捷写法
它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。
它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,String::toLowerCase 就是引用了String类中定义的方法 toLowerCase。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(String s) -> ss.toLowerCase() 的快捷写法。
你可以把方法引用看作仅仅针对涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了。
(1)为什么说使用方法引用时,没有实际调用这个方法?
方法引用是一种简化 Lambda 表达式的机制,在方法引用中,你指定了一个现有方法的名称,但并没有在这个引用中执行该方法。相反,该引用仅代表了对方法的引用,以便在需要时按需调用。什么意思?通过下边的一段代码便清晰了
// Lambda 表达式
Consumer<String> consumerLambda = s -> System.out.println(s);
// 方法引用
Consumer<String> consumerMethodRef = System.out::println;
在上面的例子中,System.out::println 是一个方法引用,指向了 println 方法。但在创建 consumerMethodRef 时,并没有立即执行 println 方法,而是创建了一个 Consumer 的实例,这个实例能够在需要时调用 println 方法。//所以,方法引用只是一段待执行的逻辑,与Lambda的本质是一样的
(2)方法引用分类
1)指向静态方法的方法引用,例如 Integer 的 parseInt 方法,写作 Integer::parseInt。
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3"));
list.stream().map(Integer::parseInt).forEach(System.out::println);
}
2)指向任意类型的实例方法的方法引用,例如 String 的 length方法,写作 String: : length。
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("Hello", "Hi", "World"));
list.stream().map(String::length).forEach(System.out::println);
}
3)指向现有对象的实例方法的方法引用,假设你有一个局部变量 Employee e,它支持实例方法 getName,那么你就可以写成 e::getName。
public static void main(String[] args) {
Employee e = new Employee("jack", 20);
Runnable runnable = e::getName; //Runnable接口的实现
}
4)构造函数的引用。对于一个现有构造函数,可以利用它的名称和关键字new来创建它的一个引用:ClassName :: new。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数,它适合 Supplier 的签名 () -> HashMap。你可以这样做:
public static void main(String[] args) {
Supplier<Map<String, Object>> supplier = HashMap::new;
}
以下是四种分类方式的汇总:
类型 | 语法 | 例子 |
引用静态方法 | ContainingClass::staticMethodName | Person::compareByAge |
引用特定对象的实例方法 | ContainingObject::instanceMethodName | myComparisonProvider::compareByName myApp::appendStrings2 |
对特定类型任意对象的实例方法的引用 | ContainingType::methodName | String::compareToIgnoreCase String::concat |
对构造函数的引用 | ClassName::new | HashSet::new |
4、复合lambda表达式
复合lambda表达式就是把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输人。你可能会想,函数式接口中怎么可能有更多的方法呢?毕竟,这违背了函数式接口的定义啊!窍门在于,这些方法都是默认方法,也就是说它们不是抽象方法。
(1)比较器复合
逆序:Comparator 接口有一个默认方法 reverse 可以使给定的比较器逆序。
public static void main(String[] args) {
Employee e = new Employee("jack", 20);
List<Employee> list = new ArrayList<>(Arrays.asList(e));
//逆序
list.sort(Comparator.comparing(Employee::getSalary).reversed());
}
比较器链:如果想对一个Comparator比较的结果来做进一步的比较,那么使用thenComparing方法就可以提供第二个Comparator。
Comparator.comparing(Employee::getSalary).thenComparing(Employee::getName);
(2)谓词复合/Predicate接口
谓词接口包括三个方法:negate、and 和 or,让你可以重用已有的 Predicate 来创建更复杂的谓词。比如,你可以使用 negate 方法来返回一个 predicate 的非:
public static void main(String[] args) {
Employee e0 = new Employee("jack", 20);
Employee e1 = new Employee("Mark", 50);
List<Employee> list = new ArrayList<>(Arrays.asList(e0, e1));
Predicate<Employee> predicate = s -> s.getName().equals("jack");
Predicate<Employee> negate = predicate.negate(); //取非
Predicate<Employee> and = predicate.and(employee -> employee.getSalary() > 30) //连接词
.or(employee -> employee.getSalary() < 20);
list.stream().filter(predicate); //实际使用
}
(3)函数复合/Function接口
Function接口拥有 andThen 和 compose 两个默认方法,它们都会返回Function的一个实例。
public static void main(String[] args) {
Function<Integer, Integer> f0 = x -> x + 1;
Function<Integer, Integer> f1 = x -> x * 2;
Function<Integer, Integer> andThen = f0.andThen(f1);
Integer apply0 = andThen.apply(2); // (2+1)*2 -> 6
Function<Integer, Integer> compose = f0.compose(f1);
Integer apply1 = compose.apply(2); // (2*2)+1 -> 5
}
如上例所示,根据执行结果可知,andThen是在一个步骤之后进行连接操作,compose是在一个步骤之前进行连接操作。
关于Stream流的相关操作,可以参考我的这篇文章《Java中的Stream流和流操作》
想详细了解Java函数式接口的相关内容,可以参考这篇文章《Java函数式接口》
至此,本文结束。