语法糖:java的持续活力

简介: 语法糖:java的持续活力

语法糖:java的持续活力

本文为作者阅读java实战(第二版)第一部分的理解和笔记,大佬推荐的很好的一本书

1. Java8,9,10以及11的变化

  1. Stream API。

    流是一系列数据项,一次只生成一项,程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。

    尽管流水线实际上是一个序列,但不同加工站的运行一般是并行的。

    你可以从一个更高层次的抽象来写java8程序了:思路变成了把这样的流变成那样的流。

  2. 向方法传递代码的技巧。

    编程语言的整个目的就在于操作值,这些值被称为一等公民,编程语言的其他结构也有助于表示值得结构,但这些结构在执行期间不能被传递,因而是二等公民。

    java8的设计者允许将方法做为值进行传递,提升编程语言的其他结构为一等公民。

  3. 接口的默认方法。

    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+"色");

接下来写几个典型使用

  1. 用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));
  1. 用Runnable执行代码块

java8之前

Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我执行了");
            }
        });

java8之后

Thread thread1 = new Thread(()-> System.out.println("我执行了"));
  1. 通过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());

小结

  1. 行为参数化就是一个方法接收多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
  2. 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
  3. 传递代码就是将新行为作为参数传递给方法。但在java8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在java8之前就可以用匿名类来减少。
  4. 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;
}

逻辑

  1. 重写processFile,调用函数式接口返回结果
  2. 主逻辑传入接口的具体实现逻辑。
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);
  1. 首先找到filter方法的声明。
  2. 根据filter方法的声明我们可以知道该Lambda对应的参数是接口Predicate
  3. Predicate是一个函数式接口,定义了一个叫做test的抽象方法。
  4. 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

对局部变量的限制

  1. 实例变量和局部变量背后的实现有一个关键不同。实例变量存储在堆中,局部变量存在于栈上。如果 Lambda 可以直接访问局部变量,而且 Lambda 是在一个线程中使用的,则使用 Lambda 的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java 在访问自由局部变量时,实际上是在访问它的副本,而不是访问基本变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
  2. 这一限制不鼓励你使用改变外部变量的典型命令式编程模式
闭包

定义:闭包就是一个函数的实例,且它可以无限制的访问那个函数的非本地变量。

例如:闭包可以作为参数传递给另一个参数,它可以访问和修改作用域之外的变量。

现在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. 小结

  1. Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回值类型,可能还有一个可以抛出的异常列表。
  2. Lambda表达式让你可以讲解地传递代码。
  3. 函数式接口就是仅仅声明了一个抽象方法的接口。
  4. 只有在接受函数式接口的地方才可以使用Lambda表达式。
  5. Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并将整个表达式作为函数式接口的一个实例。
  6. java8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function<T,R>、Supplier、Consumer和BinaryOperator。详见上表3.3.5
  7. 为了避免装箱操作,对Predicate和Function<T,R>等函数式接口的基本类型特化:IntPredicate、IntToLongFunction等。
  8. 环绕执行模式(即在方法所必须的代码中间,你需要执行点什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
  9. Lambda表达式所代表的类型称为目标类型。
  10. 方法引用让你重复使用现有的方法并直接传递它们。
  11. Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。
目录
相关文章
|
3月前
|
自然语言处理 安全 Java
Java 语法糖是什么?
语法糖是一种编程语言的设计概念,旨在通过更简洁、易读的方式表示某些操作,提升代码可读性和减少错误。它不增加语言功能,而是简化代码。Java中的语法糖包括自动装箱与拆箱、增强型for循环、泛型、可变参数、try-with-resources、Lambda表达式、方法引用、字符串连接、Switch表达式和类型推断等,这些特性使Java代码更为简洁易读。
100 23
|
Oracle 安全 小程序
Java:一段坎坷但充满活力的发展历程
Java:一段坎坷但充满活力的发展历程
139 0
|
7月前
|
IDE Java 开发工具
你知道 Java 中关键字 enum 是一个语法糖吗?反编译枚举类
你知道 Java 中关键字 enum 是一个语法糖吗?反编译枚举类
97 0
【面试题精讲】Java 中有哪些常见的语法糖?
【面试题精讲】Java 中有哪些常见的语法糖?
|
Java 编译器 索引
Java语法糖:甜化你的编程体验
Java语法糖:甜化你的编程体验
Java语法糖:甜化你的编程体验
|
安全 Java
Java语法糖 : 使用 try-with-resources 语句安全地释放资源
使用 try-with-resources 语句自动关闭资源的类都实现了AutoCloseable 接口。
286 0
Java语法糖 : 使用 try-with-resources 语句安全地释放资源
|
自然语言处理 前端开发 安全
JVM系列之:初识Javac编译器和Java语法糖
JVM系列之:初识Javac编译器和Java语法糖
187 0
JVM系列之:初识Javac编译器和Java语法糖
|
Java 编译器 程序员
Java 中的语法糖,真甜。(二)
我们在日常开发中经常会使用到诸如泛型、自动拆箱和装箱、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等,我们只觉得用的很爽,因为这些特性能够帮助我们减轻开发工作量;但我们未曾认真研究过这些特性的本质是什么,那么这篇文章,cxuan 就来为你揭开这些特性背后的真相。
126 0
Java 中的语法糖,真甜。(二)
|
Java 编译器 程序员
Java 中的语法糖,真甜。(一)
我们在日常开发中经常会使用到诸如泛型、自动拆箱和装箱、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等,我们只觉得用的很爽,因为这些特性能够帮助我们减轻开发工作量;但我们未曾认真研究过这些特性的本质是什么,那么这篇文章,cxuan 就来为你揭开这些特性背后的真相。
105 0
Java 中的语法糖,真甜。(一)