语法糖:java的持续活力
本文为作者阅读java实战(第二版)第一部分的理解和笔记,大佬推荐的很好的一本书
1. Java8,9,10以及11的变化
- Stream API。
流是一系列数据项,一次只生成一项,程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。
尽管流水线实际上是一个序列,但不同加工站的运行一般是并行的。
你可以从一个更高层次的抽象来写java8程序了:思路变成了把这样的流变成那样的流。
- 向方法传递代码的技巧。
编程语言的整个目的就在于操作值,这些值被称为一等公民,编程语言的其他结构也有助于表示值得结构,但这些结构在执行期间不能被传递,因而是二等公民。
java8的设计者允许将方法做为值进行传递,提升编程语言的其他结构为一等公民。
- 接口的默认方法。
java9提供了模块系统,允许你通过语法定义由一系列包组成的模块。
java8增加了默认方法,方便接口设计者扩充接口而不影响其他三方实现,使用default关键字。
java可以实现多个接口,接口有了默认实现,就会产生某种形式的多重继承,java8采用了一些限制来避免出现类似于C++中臭名昭著的菱形继承问题。
2. 通过行为参数化传递代码
2.1. 初步理解,使用策略设计模式
简单理解策略设计模式即定义一族算法,把他们封装起来(称为”策略“),然后再运行时选择一个算法。
下面我们使用策略模式来简单理解一下行为参数化。
测试:
编写灵活的prettyPrintApple方法编写一个prettyPrintApple方法,它接受一个Apple的List,并可以对它参数化,以多种方式根据苹果生成一个String输出(有点儿像多个可定制的toString方法)。例如,你可以告诉prettyPrintApple方法,只打印每个苹果的重量。此外,你可以让prettyPrintApple方法分别打印每个苹果,然后说明它是重的还是轻的。
2.2. 可以使用匿名内部类的形式简化代码
prettyPrintApple(apples, new AppleFormatter() {
@Override
public String accept(Apple apple) {
return apple.color+"色";
}
});
看上面代码,会发现依然很冗余。
如果你用的是java8且装了阿里的P3C插件,你会发现代码明显有多余不需要写的地方。
2.3. 在java8中使用lambda简化写法
prettyPrintApple(apples, apple -> apple.color+"色");
接下来写几个典型使用
- 用Comparator来排序
java8之前
apples.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
java8之后
apples.sort((a,b)->a.getWeight().compareTo(b.getWeight()));
标准写法,有专门的方法
apples.sort(Comparator.comparing(Apple::getWeight));
- 用Runnable执行代码块
java8之前
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我执行了");
}
});
java8之后
Thread thread1 = new Thread(()-> System.out.println("我执行了"));
- 通过Callable返回结果
java8之前
ExecutorService threadPool = Executors.newCachedThreadPool();
Future<String> future = threadPool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
});
java8
Future<String> submit = threadPool.submit(() -> Thread.currentThread().getName());
小结
- 行为参数化就是一个方法接收多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
- 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
- 传递代码就是将新行为作为参数传递给方法。但在java8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在java8之前就可以用匿名类来减少。
- javaAPI包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。
🤔 为方法定义一个接口作为参数,具体策略由调用方传递其行为,将行为运用lambda简单化
3. lambda表达式
lambda表达式可以很简单地表示一个行为或传递代码。
3.1. lambda是什么
lambda表达式理解为简单的表示可传递的匿名函数的一种方式,
它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
基本写法:Lambda表达式有三个部分
- 参数列表:方法参数
- 箭头:把参数列表和方法主体隔离开来
- 方法主体:方法的执行主体
例子
代码 | 解释 |
---|---|
(String s)->s.length() | 具有一个String类型的参数并返回一个int。 |
(Apple a)->a.getWeight()>150 | 一个Apple类型的参数并返回一个boolean |
(int x,int y)->{System.out.println("Result");System.out.println(x+y);} | 两个int类型的参数,没有返回值,多行需要大括号包起来。 |
()->42 | 没有参数,返回一个int |
3.2. 在哪里以及如何使用Lambda
根据前面的例子,我们可以知道,你可以在函数式接口上使用lambda表达式。
即lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例
3.2.1 函数式接口
函数式接口就是只定义一个抽象方法的接口。有且仅有一个抽象方法,默认方法可以有多个。
3.2.2 函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法的签名叫作函数描述符。
函数描述符---->函数的参数列表和返回值类型构成函数描述符,Runnable接口的抽象方法run方法不接收参数也没有返回值,此即代表该抽象方法的函数描述符,传递lambda实现的时候也应该为没有参数没有返回值的函数。
举例:
抽象方法 | 函数描述符 |
---|---|
public void run(); | ()->{} |
public String hello(String name) | String->String |
Public String hello(String name,Integer age) | (String,Integer)->String |
@FunctionalInterface注解是怎么回事?
在最新的javaAPI上,会发现函数式接口带有@FunctionalInterface注解,这个注解用于表示该接口会被设计成一个函数式接口。
@FunctionalInterface注解不是必须的,他的作用就像@Override,用于表示该接口被设计成一个函数式接口。
3.2.3 示例
public static String processFile() throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("src/main/resources/hello.txt"))){
return reader.readLine();
}
}
此时是固定代码,只能读取文件中的第一行,如果需求更改我们将要修改代码,下面我们修改一下
首先想到的是将return逻辑封装为一个接口,其函数描述符显而易见BufferedReader->String,于是我们定义函数式接口如下
接口
@FunctionalInterface
public interface BufferedReaderProcessor {
public String readLine(BufferedReader reader) throws IOException;
}
逻辑
- 重写processFile,调用函数式接口返回结果
- 主逻辑传入接口的具体实现逻辑。
public class Main {
public static void main(String[] args) throws IOException {
new FileReader("src/main/resources/hello.txt");
String processFile = processFile(BufferedReader::readLine);
System.out.println(processFile);
String processFile1 = processFile((BufferedReader br) -> {
return br.readLine() + br.readLine();
});
System.out.println(processFile1);
}
public static String processFile(BufferedReaderProcessor processor) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("src/main/resources/hello.txt"))){
return processor.readLine(reader);
}
}
}
3.3. 使用函数式接口
函数式接口只定义了一个抽象方法,函数式接口抽象方法的签名称为函数描述符,javaAPI中已有几个函数式接口,如Comparator、Runnable和Callable。
下面简要介绍几个新的函数式接口,Predicate、Consumer和Function。
3.3.1 Predicate
该接口定义了一个名为test的抽象方法,它接受泛型T对象,并返回一个boolean。当你的业务中存在一个不确定的判断逻辑时。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
3.3.2 Consumer
该接口定义了一个名为accept的抽象方法,它接受泛型T对象,无返回值。当你需要执行一段代码而不需要返回值时使用。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
3.3.3 Function
该接口定义了一个名为apply的抽象方法,它接受泛型T参数对象,返回泛型R对象。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
3.3.4 基本类型函数式接口泛型限制问题
回顾基本类型引用类型:解决前面的三个函数式接口泛型限制只能传基本类型的问题
java类型要么是引用类型,要么是基本类型,但是java中泛型只能绑定引用类型。这是由泛型内部的实现方式造成的。因此java中存在一种自动装箱和自动拆箱的机制。但是这在性能方面是要付出代价的,装箱后的本质上就是把基本类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的基本值。
解决方案:
Java8为前面所说的函数式接口带来了一个专门的版本,以便在输入输出都是基本类型时避免自动装箱操作。
比如IntPredicate、DoublePredicate、LongPredicate、IntConsumer、DoubleConsumer、LongConsumer、ToIntBiFunction、ToDoubleFunction、IntToDoubleFunction等
3.3.5 java8中的常用函数式接口
函数式接口 | 函数描述符 | 基本类型特化 |
---|---|---|
Predicate | T->boolean | IntPredicate; LongPredicate; DoublePredicate; |
Consumer | T->void | IntConsumer; LongConsumer; DoubleConsumer; |
Function<T,R> | T->R | IntFunction; IntToDoubleFunction; IntToLongFunction; LongFunction; LongToDoubleFunction; LongToIntFunction; DoubleFunction; DoubleToIntFunction; DoubleToLongFunction; ToIntFunction; ToLongFunction; ToDoubleFunction; |
Supplier | ()->T | BooleanSupplier; IntSupplier; LongSupplier; DoubleSupplier; |
UnaryOperator | T-T | IntUnaryOperator; LongUnaryOperator; DoubleUnaryOperator; |
BinaryOperator | (T,T)->T | IntBinaryOperator; LongBinaryOperator; DoubleBinaryOperator; |
BiPredicate<T,U> | (T,U)->boolean | |
BiConsumer<T,U> | (T,U)->void | ObjIntConsumer; ObjLongConsumer; ObjDoubleConsumer; |
BiFunction<T,U,R> | (T,U)->R | ToIntBiFunction<T,U>; ToLongBiFunction<T,U>; ToDoubleBiFunction<T,U>; |
3.3.5.1 lambda异常机制
已有的函数式接口中的任何一个都不允许抛出受检异常。如果你需要lambda表达式来抛出异常,有两种方式:定义一个自己的函数式接口,并声明受检异常,或者把lambda包在一个try/catch块中。
3.3.6 类型检查、类型推断以及限制
在我们使用lambda表达式时,它为函数式接口生成一个实例。然而,lambda表达式本身并不包含它在实现哪个函数式接口的信息
3.3.6.1类型检查
Lambda的类型时从使用Lambda的上下文推断出来的
下面我们从一个Lambda中看到该表达式背后发生了什么
List<Apple> heavierThen150g = filter(inventory,(Apple apple)->apple.getWeight()>150)
filter(List<Apple>inventory, Predicate<Apple> p);
- 首先找到filter方法的声明。
- 根据filter方法的声明我们可以知道该Lambda对应的参数是接口Predicate
- Predicate是一个函数式接口,定义了一个叫做test的抽象方法。
- test方法去描述了一个函数描述符,它可以接受一个Apple返回一个boolean.
3.3.6.2 同样的Lambda,不同的函数式接
了解了上面类型检查的概念,同一个lambda表达式就可以与不同的函数式接口联系起来,只要他们的抽象方法签名能够兼容。
特殊的void兼容规则
如果一个lambda的主体是一个语句表达式,他就可一个返回void的函数描述符兼容(当然参数列表页需要兼容)。
例如,以下两行都是合法的,尽管 List 的 add 方法返回了一个 boolean,而不是 Consumer 上下文(T -> void)所要求的 void:
// Predicate 返回了一个 boolean Predicate<String> p = (String s) -> list.add(s); // Consumer 返回了一个 void Consumer<String> b = (String s) -> list.add(s);
3.3.6.3 类型推断
编译器会从上下文中推断出用什么函数式接口来配合Lambda表达式,这意味着参数列表的类型可以从函数描述符中获得。
我们可以在Lambda语法中省去标注参数类型。
显示写出类型和隐藏他们,没有优劣之分,如何让代码更易读,
3.3.7 使用局部变量
上面我们看到的lambda表达式的例子都是使用的参数列表中的变量。Lambda表达式也允许使用自有变量,即上下文中的局部变量和成员变量。他们被称作捕获Lambda。
Lambda可以没有限制的捕获实例变量和静态变量,但局部变量必须显示声明为final或者事实上是final。
对局部变量的限制
- 实例变量和局部变量背后的实现有一个关键不同。实例变量存储在堆中,局部变量存在于栈上。如果 Lambda 可以直接访问局部变量,而且 Lambda 是在一个线程中使用的,则使用 Lambda 的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java 在访问自由局部变量时,实际上是在访问它的副本,而不是访问基本变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
- 这一限制不鼓励你使用改变外部变量的典型命令式编程模式
闭包定义:闭包就是一个函数的实例,且它可以无限制的访问那个函数的非本地变量。
例如:闭包可以作为参数传递给另一个参数,它可以访问和修改作用域之外的变量。
现在java8的Lambda和匿名类可以做类似于闭包的事情:他们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。
3.4. 方法引用
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。
3.4.1 管中窥豹
方法引用可以被看做仅仅调用方法的Lambda的一种快捷写法,方法引用就是让你根据已有的方法实现来创建Lambda表达式,但是显示的指明方法的名称,代码的可读性会更好。
如何使用:目标引用放在分隔符::前,方法的名称放在后面
例如:Apple::getWeight就是引用了Apple类中定义的方法getWeight。就是Lambda表达式(Apple apple)->apple.getWeight()的快捷写法。
Lambda 及其等效方法引用的例子
Lambda | 等效方法引用 |
---|---|
(Apple apple) -> apple.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
(String s) -> this.isValidName(s) | this::isValidName |
如何构建方法引用
(1) 指向静态方法的方法引用(例如 Integer 的 parseInt 方法,写作 Integer::parseInt)。例如()->Integer.parseInt()可以写成Integer::parseInt。
(2) 指向任意类型实例方法的方法引用(例如 String 的 length 方法,写作 String::length)。例如(String s)->s.length()可以写成String::length。
(3) 指向现存对象或表达式实例方法的方法引用(假设你有一个局部变量expensiveTransaction保存了 Transaction 类型的对象,它提供了实例方法 getValue,那你就可以这么写 expensiveTransaction::getValue)。简单理解为本类中的方法。例如(String string) -> this .startsWithNumber(string); 可以写成this::startsWithNumber。
3.4.2 构造函数引用
对于一个现有构造函数,你可以利用它的名称和关键字 new 来创建它的一个引用:ClassName::new。
构造函数引用的对照
无参构造解析,无参构造的方法签名是()-Apple,所以适合使用Supplier
含义 | Lambda | 等效方法引用 |
---|---|---|
没有参数的构造函数 | Supplier c1 = () -> new Apple(); Apple a1 = c1.get(); | Supplier c1 = Apple::new; Apple a1 = c1.get(); |
一个参数的构造函数 | Function<Integer, Apple> c2 = (weight) -> new Apple(weight); | Function<Integer, Apple> c2 = Apple::new; |
两个参数的构造函数 | BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight); | BiFunction<Color, Integer, Apple> c3 = Apple::new; |
思考三个参数的构造函数?
答案:自定义一个满足要求的函数式接口
4. 小结
- Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回值类型,可能还有一个可以抛出的异常列表。
- Lambda表达式让你可以讲解地传递代码。
- 函数式接口就是仅仅声明了一个抽象方法的接口。
- 只有在接受函数式接口的地方才可以使用Lambda表达式。
- Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并将整个表达式作为函数式接口的一个实例。
- java8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function<T,R>、Supplier、Consumer和BinaryOperator。详见上表3.3.5
- 为了避免装箱操作,对Predicate和Function<T,R>等函数式接口的基本类型特化:IntPredicate、IntToLongFunction等。
- 环绕执行模式(即在方法所必须的代码中间,你需要执行点什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
- Lambda表达式所代表的类型称为目标类型。
- 方法引用让你重复使用现有的方法并直接传递它们。
- Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。