【Java新特性学习 二】JDK8: 语言新特性之Lambda表达式、函数式接口、接口的默认方法和静态方法

简介: 【Java新特性学习 二】JDK8: 语言新特性之Lambda表达式、函数式接口、接口的默认方法和静态方法

本篇Blog开始学习和实践Java8中的新特性,主要分为两大部分:语言新特性库函数新特性,重点落在工作中经常会用到的几个重大特性:

  • 语言新特性:Lambda表达式和函数式接口,接口的默认方法和静态方法,方法引用
  • 库函数新特性:Optional,Streams,Date/Time API(JSR 310),Base64,并行数组

接下来按照如下几个结构分别介绍和学习以上知识点:基本概念,解决问题,语法范式,实践操作,我发现虽然经常听到函数式编程这样的名词,但是好像从来不知道具体是什么,所以这次一并了解下函数式编程的概念。

函数式编程

什么是函数式编程?函数式编程是一种编程范式,除了函数式编程之外还有 命令式编程声明式编程 等编程范式

  • 命令式编程:命令式编程 是面向计算机硬件的抽象,有变量、赋值语句、表达式、控制语句等,可以理解为 命令式编程就是冯诺伊曼的指令序列。 它的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么,其实就是代码脚本段
  • 声明式编程:声明式编程 是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。SQL 语句就是最明显的一种声明式编程的例子,SELECT * FROM collection WHERE num > 5,其实就是表达式结果。它不需要创建变量用来存储数据,它也不包含循环控制的代码如 for, while
  • 函数式编程:函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。

函数式编程的本质就是:函数式编程中的函数不是指计算机中的函数,而是指数学中的函数,即自变量的映射。函数的值取决于函数的参数的值,不依赖于其他状态,比如abs(x)函数计算x的绝对值,只要x不变,无论何时调用、调用次数,最终的值都是一样

函数式编程特点

函数式编程有两个特点:函数是第一等公民,函数是纯函数

  • 函数是第一等公民:是指函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值
// 赋值
var func1 = function func1() {  }
// 函数作为参数
function func2(fn) {
    fn()
}   
// 函数作为返回值
function func3() {
    return function() {}
}
  • 函数是纯函数:纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数。纯函数的两个特点:相同的输入必有同输出,函数无副作用

这两个特点的示例如下:

// 是纯函数
function sum(x,y){
    return x + y
}
// 输出不确定,不是纯函数
function random(x){
    return Math.random() * x
}
// 有副作用,不是纯函数
function setFontSize(el,fontsize){
    el.style.fontsize = fontsize ;
}
// 输出不确定、有副作用,不是纯函数
let count = 0;
function addCount(x){
    count+=x;
    return count;
}

函数式编程优劣

综合来理解就是函数式编程,不变、不变、不变,固定的输入产生固定的输出且对外部没有任何影响,那么好处显而易见,所有的操作都是幂等的:

  • 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
  • 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
  • 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
  • 隐性好处。减少代码量,提高维护性

缺点也就是命令式编程可以发挥作用的:

  • 性能差:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
  • 资源占用:为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
  • 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作

大致了解了函数式编程后再来看看Java是如何使用Lambda表达式来应用函数式编程这一理念的。

函数式接口

Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式java.lang.Runnablejava.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了),举个简单的函数式接口的定义:

@FunctionalInterface
public interface Functional {
    void method();
}

不过有一点需要注意,默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的。

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();
    default void defaultMethod() {            
    }        
}

常见的函数式接口如下:

java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口有:

方法释义如下:

接口的默认方法和静态方法

我们上文说到,函数式接口只能有一个功能函数,但是默认方法和静态方法除外,那么什么是默认方法和静态方法呢?Java 8使用两个新概念扩展了接口的含义:默认方法和静态方法

接口的默认方法

默认方法使得开发者可以在不破坏二进制兼容性的前提下,往现存接口中添加新的方法**,即不强制那些实现了该接口的类也同时实现这个新加的方法**。

默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写,例子代码如下

private interface Defaulable {
    // Interfaces now allow default methods, the implementer may or 
    // may not implement (override) them.
    default String notRequired() { 
        return "Default implementation"; 
    }        
}
private static class DefaultableImpl implements Defaulable {
}
private static class OverridableImpl implements Defaulable {
    @Override
    public String notRequired() {
        return "Overridden implementation";
    }
}

Defaulable接口使用关键字default定义了一个默认方法notRequired()。DefaultableImpl类实现了这个接口,同时默认继承了这个接口中的默认方法;OverridableImpl类也实现了这个接口,但覆写了该接口的默认方法,并提供了一个不同的实现

接口的静态方法

Java 8也允许在接口中定义静态方法

private interface DefaulableFactory {
    // Interfaces now allow static methods
    static Defaulable create( Supplier< Defaulable > supplier ) {
        return supplier.get();
    }
}

默认方法和静态方法应用

下面的代码片段整合了默认方法和静态方法的使用场景:

public static void main( String[] args ) {
    Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
    System.out.println( defaulable.notRequired() );
    defaulable = DefaulableFactory.create( OverridableImpl::new );
    System.out.println( defaulable.notRequired() );
}

输出结果如下:

Default implementation
Overridden implementation

由于JVM上的默认方法的实现在字节码层面提供了支持,因此效率非常高。默认方法允许在不打破现有继承体系的基础上改进接口。该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()removeIf()等等

Lambda表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性,Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),使用 Lambda 表达式可以使代码变的更加简洁紧凑

解决问题

核心解决的问题就是:让程序员使用更少的代码实现同样的功能。虽然看着很先进,其实Lambda表达式的本质只是一个语法糖,由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能

语法范式

lambda表达式允许通过表达式来代替功能接口lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体【body可以是一个表达式或一个代码块】

左侧:Lambda表达式的参数列表 -> 右侧:Lambda表达式中要执行的功能

右侧实现可以是表达式,也可以是代码块

(parameters) -> expression
(parameters) ->{ statements; }

对于左侧的参数列表而言:

  • 可选参数个数:参数可以没有,也可以只有一个或多个。
  • 可选参数类型声明不需要声明参数类型,编译器可以统一识别参数值
  • 可选参数圆括号一个参数无需定义圆括号,但多个参数需要定义圆括号

对于右侧功能实现来说

  • 可选返回主体:如果返回主体是表达式,那么编译器自动返回表达式值,如果主体是单行语句,那么返回类型可以看做void,如果主体是代码块,需要用return关键字指定返回值
  • 可选返回关键字:如上所述,如果主体是代码块,需要用return关键字指定返回值
  • 可选大括号:如果主体只包含单行语句(表达式),就不需要使用大括号,如果主体是多行代码块,需要使用大括号

对于右侧功能,总结而言:单行语句或表达式,可以省略return关键字和{},几个简单的例子:

// 1. 不需要参数,返回值为 5  
() -> 5  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

实践操作

既然一个新的事物出现,一定有其原因,也就是之前一定有痛点,那么新事物才有价值,这里只列举当前我看到的Lambda解决的问题

1 快速处理集合

我们一般在一些集合操作时使用Lambda表达式,没有lambda之前:

public static void main(String[] args) {
        List<String> list = Arrays.asList("t", "m", "l");
        for (String s : list) {
            System.out.println(s);
        }
    }

有了lambda之后,只需要一行代码,将参数和功能组合成一个入参。

Arrays.asList( "t", "m", "l" ).forEach(e -> System.out.println( e ) );

为什么这里可以使用lambda呢,因为这里的forEach的入参是一个功能接口:

default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

再进去一步:

package java.util.function;
import java.util.Objects;
/**
 * Represents an operation that accepts a single input argument and returns no
 * result. Unlike most other functional interfaces, {@code Consumer} is expected
 * to operate via side-effects.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #accept(Object)}.
 *
 * @param <T> the type of the input to the operation
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Consumer<T> {
    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

如同上面说的理论能力,上面这个代码中的参数e的类型是由编译器推理得出的,e也可以自定义类型:

Arrays.asList( "t", "m", "l" ).forEach( ( String e ) -> System.out.println( e ) );

如果Lambda表达式需要更复杂的语句块,则可以使用花括号将该语句块括起来,类似于Java中的函数体:

public static void main(String[] args) {
        Arrays.asList("t", "m", "l").forEach((String e) ->
                {
                    String a = e + "帅";
                    System.out.println(a);
                }
        );
    }

2 取代匿名内部类

关于内部类可以看我的这篇Blog【Java SE基础 九】Java内部类,所谓匿名内部类就是:匿名内部类是直接使用 new 来生成一个对象的引用,匿名内部类没有类的名称,很多场景下可以简化我们代码,但是Lambda出现后告诉我们可以更简化:它只聚焦于核心实现:

public class Demo01Inner {
    public static void main(String[] args) {
        //使用匿名内部类的方式实现多线程。
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行了");
            }
        }).start();
        //使用Lambda表达式实现多线程
        new Thread(() -> System.out.println(Thread.currentThread().getName() + "执行了")).start();
    }
}

同样为什么这里可以使用lambda呢:

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

再进去一步:

package java.lang;
/**
 * The <code>Runnable</code> interface should be implemented by any
 * class whose instances are intended to be executed by a thread. The
 * class must define a method of no arguments called <code>run</code>.
 * <p>
 * This interface is designed to provide a common protocol for objects that
 * wish to execute code while they are active. For example,
 * <code>Runnable</code> is implemented by class <code>Thread</code>.
 * Being active simply means that a thread has been started and has not
 * yet been stopped.
 * <p>
 * In addition, <code>Runnable</code> provides the means for a class to be
 * active while not subclassing <code>Thread</code>. A class that implements
 * <code>Runnable</code> can run without subclassing <code>Thread</code>
 * by instantiating a <code>Thread</code> instance and passing itself in
 * as the target.  In most cases, the <code>Runnable</code> interface should
 * be used if you are only planning to override the <code>run()</code>
 * method and no other <code>Thread</code> methods.
 * This is important because classes should not be subclassed
 * unless the programmer intends on modifying or enhancing the fundamental
 * behavior of the class.
 *
 * @author  Arthur van Hoff
 * @see     java.lang.Thread
 * @see     java.util.concurrent.Callable
 * @since   JDK1.0
 */
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

有了Lambda,不需要再定义接口、不需要new实例、不需要run()的方法声明等,只care核心实现即可。这就是函数式或者声明式编程的魅力,不是万物皆对象的,有些时候实现可以如此简单。

3 匿名方法的使用

只要是接口函数也都可以使用lambda,如果不使用lambda,我们调用参数和比较器来进行排序:

public class Person {
    // ...
    LocalDate birthday;
    public int getAge() {
        // ...
    }
    public LocalDate getBirthday() {
        return birthday;
    }   
    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }
    // ...
}

假设我们有一个Person数组,并且想对它进行排序,这时候,我们可能会这样写

Person[] rosterAsArray = new Person[]{
                new Person("003", LocalDate.of(2021,9,1)),
                new Person("001", LocalDate.of(2021,2,1)),
                new Person("002", LocalDate.of(2021,3,1)),
                new Person("004", LocalDate.of(2021,12,1))};
class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
Arrays.sort(rosterAsArray, new PersonAgeComparator());

其中sort的方法签名为:

static <T> void sort(T[] a, Comparator<? super T> c)

Comparator接口是一个功能接口:

public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);//equals是Object中的方法,这种对Object类的方法的重新声明会让方法不再是抽象的
    ...
    }

因此可以使用 lambda 表达式,而不是定义然后创建一个实现以下内容的类的新实例ComparatorComparator

Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
        return a.getBirthday().compareTo(b.getBirthday());
    }
);

这种比较两个实例的出生日期的方法我们可以在Person中定义,并且已经有了,所以上述写法还可以再简化:

Arrays.sort(rosterAsArray,
    (a, b) -> Person.compareByAge(a, b)
);

总结一下

总而言之,通过写这篇博客刷新了一些关于函数式编程模式的一些认知,最深刻的感受就是我们可能太OOP了,有时候感觉没有对象就什么也干不成了,做一些逻辑实现时总要定义一些冗余的类,接口,其实有些时候只需要关注最核心的实现可以减少大量的代码编写,忘记实例创建、忘记方法声明、忘记new吧,只关注核心实现!当然这也不是说别OOP了,其实无论是命令式编程还是函数式编程都有其具体的应用场景,Java这样的OOP语言也不会把函数式编程模式完全隔绝出去,这才引入了Lambda。所以一切不以应用场景为前提的优劣比较都是耍流氓

相关文章
|
19天前
|
数据采集 监控 Oracle
GraalVM 24 正式发布阿里巴巴贡献重要特性 —— 支持 Java Agent 插桩
阿里巴巴是 GraalVM 全球顾问委员会的唯一中国代表,阿里云程序语言与编译器团队和可观测团队合作实现了 GraalVM 应用的无侵入可观测能力,并在 ARMS 平台上线了该功能。目前在 GraalVM 24 中发布的是支持 Java agent 的第一步,其余能力将在 GraalVM 的后续版本中陆续发布。
116 21
|
6天前
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
42 5
|
13天前
|
Java 开发者
Java 中的 toString() 方法详解:为什么它如此重要?
在Java开发中,`toString()`方法至关重要,用于返回对象的字符串表示。默认实现仅输出类名和哈希码,信息有限且不直观。通过重写`toString()`,可展示对象字段值,提升调试效率与代码可读性。借助Lombok的`@Data`注解,能自动生成标准化的`toString()`方法,简化开发流程,尤其适合字段较多的场景。合理运用`toString()`,可显著提高开发效率与代码质量。
43 0
|
13天前
|
存储 Java 开发者
Java 中的 equals 方法:看似简单,实则深藏玄机
本文深入探讨了Java中`equals`方法的设计与实现。默认情况下,`equals`仅比较对象引用是否相同。以`String`类为例,其重写了`equals`方法,通过引用判断、类型检查、长度对比及字符逐一比对,确保内容相等的逻辑。文章还强调了`equals`方法需遵循的五大原则(自反性、对称性等),以及与`hashCode`的关系,避免集合操作中的潜在问题。最后,对比了`instanceof`和`getClass()`在类型判断中的优劣,并总结了正确重写`equals`方法的重要性,帮助开发者提升代码质量。
49 1
|
13天前
|
Java
java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?
本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。
24 0
|
1月前
|
安全 IDE Java
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
55 1
|
Java
java中用静态方法返回类名
54powerman最初碰到这个问题,首先想到的是getClass()方法,如下尝试:public static String getClassName(){ String className=null; className=this.getClass().getName();//静态方法中不可访问变量 this return className;}结果失败。
879 0
|
2月前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
201 60
【Java并发】【线程池】带你从0-1入门线程池
|
15天前
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
51 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
|
1月前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
89 23