函数式编程思想

函数式编程(Functional Programming, FP)是一种编程范式,它将计算视为数学函数的求值,并避免了改变状态和可变数据。函数式编程强调表达式的评估,而不是执行语句。这与命令式编程形成了对比,后者更关注于如何改变程序的状态。

核心要素

  1. 纯函数: 在 Java 中实现纯函数意味着编写没有副作用的方法。也就是说,给定相同的输入,这些方法总是返回相同的结果,并且不会改变外部状态或产生其他可观察的变化。
  2. 不可变性: Java 鼓励使用不可变类(如 StringInteger),并且开发者可以通过创建自己的不可变类来遵循这一原则。不可变对象一旦创建就不能修改,这有助于避免并发问题并简化调试。
  3. 高阶函数: Java8 引入了函数式接口的概念,它是一种只有一个抽象方法的接口。通过 Lambda 表达式,你可以将函数作为参数传递给另一个函数,或者从另一个函数返回。例如,Function<T, R> 接口可以用来表示接受一个参数T并返回结果R的函数。
  4. 递归: 尽管 Java 不特别强调递归,但你仍然可以在必要时使用递归来解决问题。然而,在循环可能更有效的情况下,通常会优先考虑循环结构。
  5. 惰性求值: Java 的流API 支持中间操作的惰性求值,这意味着只有当终结操作被执行时,流上的所有中间操作才会被处理。这可以提高性能,尤其是在处理大量数据时。
  6. 声明式编程: 流API 使得你可以以一种声明式的方式来描述对集合数据的操作,而不是显式地编写 for 循环和其他控制结构。例如,使用filtermapreduce等操作来表达复杂的查询逻辑。
  7. 组合性: 使用函数式接口和 Lambda 表达式,你可以很容易地组合多个简单的函数来构建复杂的行为。比如,你可以将几个 Function 实例链接在一起,形成一个更大的转换流程。
  8. 类型系统: Java 拥有静态类型系统,这在函数式编程中尤为重要,因为编译器可以在编译时检查函数签名的一致性和类型安全性。

Lambda

在Java中,Lambda 表达式是 Java8 引入的一个重要特性,它允许你以更简洁的方式编写匿名函数。通过使用Lambda 表达式,你可以写出更清晰、更易读的代码,特别是在处理集合类和并发编程时。

Lambda语法

(parameters) -> expression
或
(parameters) -> { statements; }
  • 参数列表:与常规方法一样,可以有零个或多个参数。如果只有一个参数且类型可以推断出来,则可以省略括号。
  • 箭头符号:-> 将参数列表与主体分隔开来。
  • 表达式或语句块:如果主体是一个简单的表达式,则可以直接返回结果;如果是一系列语句,则需要将它们包含在大括号 {} 中,并显式使用 return 语句(如果有返回值)。

代码示例

// 没有参数,返回值为42
Runnable noArg = () -> 42;

// 单个参数,类型可推断
Consumer<String> greet = name -> System.out.println("Hello, " + name);

// 多个参数,带有显式类型
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;

// 带有多个语句的Lambda
Comparator<String> comparator = (x, y) -> {
    int result = x.length() - y.length();
    return result == 0 ? x.compareTo(y) : result;
};

使用前提

  • 必须是函数式接口做方法参数传递。
  • 函数式接口:有且仅有一个抽象方法的接口,通常使用 @FunctionalInterface注解去标注

Lambda表达式省略规则

在Java中,Lambda表达式的语法允许一定的灵活性和简洁性,这意味着某些情况下可以省略一些元素。以下是Lambda表达式可以进行省略的规则:

  1. 参数类型: 如果编译器可以通过上下文推断出参数的类型,则可以省略参数类型。例如:

    // 明确指定类型
    (String s) -> System.out.println(s)
    
    // 省略类型,编译器可以推断
    s -> System.out.println(s)
    
  2. 括号(对于单个参数): 如果只有一个参数,并且该参数的类型可以被推断出来,那么你可以省略包围参数的圆括号。但是,如果没有任何参数或有多个参数,则必须使用圆括号。

    // 一个参数,省略圆括号
    s -> System.out.println(s)
    
    // 多个参数,不能省略圆括号
    (int x, int y) -> x + y
    
  3. 大括号(对于单个表达式): 如果Lambda体只包含一个表达式,你可以省略包围Lambda体的大括号{}。在这种情况下,表达式的值会自动返回。

    // 单个表达式,省略大括号
    x -> x * x
    
    // 多个语句,需要大括号
    x -> {
        System.out.println("Calculating square");
        return x * x;
    }
    
  4. return关键字(对于单个表达式): 如果Lambda体是一个单一表达式并且你已经省略了大括号,那么也可以省略return关键字。当Lambda体由多条语句组成时,或者当你显式地想要从一个非void方法返回时,return关键字是必需的。

    // 单个表达式,省略return
    x -> x * x
    
    // 多个语句或void返回类型,不能省略return
    x -> {
        System.out.println("Calculating square");
        return x * x; // 必须有return
    }
    
  5. 空参数列表: 如果Lambda表达式不接受任何参数,那么你需要提供一对空的圆括号来表示没有参数。

    () -> System.out.println("Hello World")
    

注意事项:

  • Lambda表达式的参数数量和类型必须与目标函数式接口的抽象方法相匹配。
  • 编译器根据上下文(通常是赋值的目标或方法调用的参数)来推断Lambda表达式的类型。
  • 当存在歧义时,可能需要明确指定参数类型或使用其他方式来帮助编译器正确解析代码。

通过合理利用这些省略规则,可以使代码更加简洁易读,同时保持其功能性和意图清晰。

函数式接口

Java 8 引入了函数式接口的概念,它是一种有且仅有一个抽象方法的接口(忽略那些来自 Object 类的方法)。函数式接口可以与 lambda 表达式一起使用,从而实现更简洁、更具表达力的代码。函数式接口在 Java 中通常用于回调机制和事件处理。

为了确保一个接口是函数式接口,你可以使用 @FunctionalInterface 注解。这个注解不是必须的,但是它可以让你的意图更加明确,并且编译器会检查你是否违反了函数式接口的规则(即确保接口中只有一个抽象方法)。

下面是一些常见的函数式接口的例子:

  1. Predicate<T>:接受一个输入参数,返回一个布尔值结果。
  2. Consumer<T>:接受一个输入参数并且无返回。
  3. Function<T, R>:接受一个输入参数,返回一个结果。
  4. Supplier<T>:不接受参数,返回一个结果。
  5. UnaryOperator<T>:接受一个输入参数,返回一个相同类型的结果。
  6. BinaryOperator<T>:接受两个相同类型的输入参数,返回一个相同类型的结果。

这些接口都位于 java.util.function 包中。

这里是一个自定义函数式接口和使用它的例子:

// 定义一个函数式接口
@FunctionalInterface
interface MyFunctionalInterface {
    void doSomething(String message);
}

public class Main {
    public static void main(String[] args) {
        // 使用lambda表达式实现该接口
        MyFunctionalInterface myFunc = (message) -> System.out.println("Doing something: " + message);

        // 调用接口的方法
        myFunc.doSomething("Hello World");
    }
}

在这个例子中,MyFunctionalInterface 是一个函数式接口,因为它只包含一个名为 doSomething 的抽象方法。我们使用 lambda 表达式 (message) -> System.out.println("Doing something: " + message) 来实现这个接口,并通过变量 myFunc 调用它的方法。

函数式接口使 Java 支持了一些函数式编程的概念,如高阶函数(接受其他函数作为参数或返回它们),这为编写更加模块化和可重用的代码提供了便利。

四大函数式接口

Java 8 引入了四大核心的函数式接口,它们位于 java.util.function 包中。这些接口为常见的操作提供了一种类型安全的方法引用和 lambda 表达式的使用。以下是这四个接口的详细介绍:

Predicate 断言型接口

  • 接口定义:boolean test(T t)

  • 描述:这个接口代表了一个接受单个输入参数并返回布尔值结果的谓词(条件)。可以用于过滤操作。

  • 示例:

    Predicate<String> isEmpty = str -> str.isEmpty();
    boolean result = isEmpty.test("Hello"); // false
    

Consumer 消费型接口

  • 接口定义:void accept(T t)

  • 描述:这个接口表示一个接受单个输入参数并且没有返回结果的操作。常用于执行副作用操作,如打印或更新数据。

  • 示例:

    Consumer<String> printIt = str -> System.out.println(str);
    printIt.accept("Hello World!"); // 输出 "Hello World!"
    

Function<T, R> 函数型接口

  • 接口定义:R apply(T t)

  • 描述:这个接口表示一个接受一个参数并产生结果的函数。它通常用于转换操作。

  • 示例:

    Function<String, Integer> stringToInt = num -> Integer.valueOf(num);
    int number = stringToInt.apply("123"); // 返回整数 123
    

Supplier 供给型接口

  • 接口定义:T get()

  • 描述:这个接口表示一个不接受任何参数并返回一个结果的供应商。它可以用来代替构造函数或工厂方法来创建对象。

  • 示例:

    Supplier<Date> now = () -> new Date();
    Date currentDate = now.get(); // 创建一个新的Date实例
    

方法引用

Java 8 引入了方法引用,作为一种简化代码的特性,它允许直接引用已有的方法或构造器,而不需要使用 lambda 表达式来描述。方法引用可以被视为 lambda 表达式的简写形式,当需要调用的方法已经存在时,它们可以帮助减少冗余代码,并使代码更加简洁和易于理解。

使用规则

  1. 被引用的方法要写在重写方法里面。
  2. 被引用的方法的参数,返回值要和需要重写的方法的参数返回值保持一致,而且引用方法最好就是重写方法的参数和返回值。
  3. 满足上面两个要求可以简化为方法引用,在 Lambda 表达式的基础上再进行省略,进行的步骤有下:
    • 省略重写方法的参数。
    • 省略 ->。
    • 省略引用方法的参数。
    • 将引用方法的 . 改成 ::

代码示例

import java.util.ArrayList;
import java.util.function.Consumer;

public class Demo01 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("张三");
        list.add("李四");
        list.add("王五");
        list.add("赵六");
        list.add("田七");

        /*
         * accept() 是重写方法:参数类型为String,无返回值
         * accept() 方法里面使用了println()方法,参数类型为String,无返回值
         * 满足要求,可以使用方法引用进行简化
         */
        list.forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        });
        
        // 先简化为Lambda表达式
        list.forEach(s -> System.out.println(s));
        
        // 使用方法引用进行简化
        list.forEach(System.out::println);
    }
}

常用的方法引用类型

方法引用使用双冒号 :: 操作符,它连接类或对象与方法名称(或构造器)。以下是四种主要的方法引用类型:

  1. 静态方法引用

    • 语法:ClassName::staticMethodName

    • 示例:

      // 定义一个函数式接口
      interface Converter<F, T> { T convert(F from); }
      
      // 使用静态方法引用来实现字符串到整数的转换器
      Converter<String, Integer> converter = Integer::parseInt;
      int result = converter.convert("123"); // 输出123
      
  2. 特定对象的实例方法引用

    • 语法:object::instanceMethodName

    • 示例:

      // 创建一个实例
      String delimiter = ", ";
      // 使用实例方法引用来创建一个连接器
      BiFunction<String, String, String> joiner = delimiter::join;
      String joined = joiner.apply("Java", "is"); // 输出 "Java, is"
      
  3. 任意对象的实例方法引用

    • 语法:ClassName::methodName

    • 这种引用用于表示任何给定类型的对象上的某个方法。

    • 示例:

      // 使用任意对象的实例方法引用来比较两个字符串
      Comparator<String> comparator = String::compareToIgnoreCase;
      int comparison = comparator.compare("Java", "java"); // 输出0
      
  4. 构造方法引用

    • 语法:ClassName::new

    • 可以用来引用类的构造器,创建新对象。

    • 示例:

      // 定义一个函数式接口
      interface Factory<T> { T create(); }
      
      // 使用构造方法引用来创建一个工厂
      Factory<Person> personFactory = Person::new;
      Person person = personFactory.create(); // 调用无参构造器创建Person对象
      

除了以上四种常用的方法引用类型,还有一种不常使用的数组类型

  • 语法:TypeName[]::new

  • 示例:

    // 定义一个函数式接口
    interface ArrayFactory<T> { T[] create(int size); }
    
    // 使用数组引用来创建一个String数组的工厂
    ArrayFactory<String> stringArrayFactory = String[]::new;
    String[] array = stringArrayFactory.create(10); // 创建一个大小为10的String数组
    

方法引用的主要优点在于它们可以使代码更简洁、易读且易于维护。通过使用方法引用,我们可以避免重复书写相同的逻辑,同时提高代码的可重用性和清晰度。此外,它们还可以增强代码的函数式编程风格,使得代码结构更加灵活和模块化。

Steam流

Java 8 引入了 Stream API,这是一个用于处理集合数据的强大工具。Stream 不是数据结构,它实际上并不存储元素,而是充当一个管道,允许你以声明式的方式对数据源(如数组、列表或集合)进行一系列的转换操作。流可以并行执行这些操作,从而利用多核处理器的能力来提高性能。

Stream 的特点

  • 管道性:多个操作可以链接起来形成一个管道,每个操作都会对其前一个操作的结果进行处理。
  • 懒求值:中间操作(intermediate operations)不会立即执行,只有当终端操作(terminal operation)被调用时才会触发整个管道的执行。
  • 不可变性:流的操作不会修改原始数据源;相反,它们会返回一个新的流或结果。
  • 短路:某些终端操作(如 findFirstanyMatch)可以在找到匹配项后立即终止流的处理。

创建 Stream

可以从各种数据源创建流,包括集合、数组、文件等:

List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");

// 从集合创建流
Stream<String> stream = myList.stream();

// 从数组创建流
Stream<String> arrayStream = Stream.of("a", "b", "c");

Stream流常用方法

Stream API 提供了多种方法来处理数据集合,这些方法可以分为两大类:中间操作和终端操作。中间操作返回一个新的流,允许你链接多个操作;而终端操作则会触发流的执行,并产生一个结果或副作用。下面是 Stream 流的一些常用方法:

中间操作

这些操作返回一个新的流,并且可以链式调用:

  • filter(Predicate<T> p):创建一个由满足给定谓词的所有元素组成的流。
  • map(Function<T, R> f):将每个元素转换为另一个值,通过提供的函数映射到新的流。
  • flatMap(Function<T, Stream<R>> f):对每个元素应用一个函数,该函数的结果是一个流,然后将所有这些流合并成一个流。
  • distinct():返回由不同元素(根据对象的 equals 方法)组成的流。
  • sorted()sorted(Comparator<? super T> comparator):根据自然顺序或指定比较器对流中的元素进行排序。
  • peek(Consumer<? super T> c):主要用于调试目的,在不改变流内容的情况下,对流中的每个元素执行指定的操作。
  • limit(long maxSize):返回一个最多包含 maxSize 个元素的流。
  • skip(long n):返回一个跳过前 n 个元素的流。

终端操作

这些操作会导致流的遍历和关闭,一旦执行完终端操作,就不能再使用该流:

  • forEach(Consumer<? super T> c):对流中的每个元素执行指定的操作。
  • forEachOrdered(Consumer<? super T> c):按照遇到的顺序对流中的每个元素执行指定的操作。
  • toArray()toArray(IntFunction<A[]> if):将流中的元素收集到数组中。
  • reduce(BinaryOperator<T> bo)reduce(T t, BinaryOperator<T> bo):通过对所有元素应用累加函数,将流缩减为单个值。
  • collect(Collector<? super T, A, R> c):使用收集器将流中的元素收集到复杂的数据结构中,如列表、集合、映射等。
  • min(Comparator<? super T> c):返回最小元素的 Optional 描述符,使用给定的比较器确定。
  • max(Comparator<? super T> c):返回最大元素的 Optional 描述符,使用给定的比较器确定。
  • count():返回流中元素的数量。
  • anyMatch(Predicate<? super T> p):如果至少有一个元素匹配给定的谓词,则返回 true。
  • allMatch(Predicate<? super T> p):如果所有元素都匹配给定的谓词,则返回 true。
  • noneMatch(Predicate<? super T> p):如果没有元素匹配给定的谓词,则返回 true。
  • findFirst():返回流中第一个元素的 Optional 描述符。
  • findAny():返回流中任意元素的 Optional 描述符,通常用于并行流。

并行流

可以通过调用 stream.parallel() 或直接使用 collection.parallelStream() 来创建并行流。并行流可以在多线程环境中并行处理数据,但是要注意这可能会带来额外的开销,并不是所有的操作都适合并行化。

示例代码

这里是一个简单的例子,展示了如何使用流来过滤和转换列表中的元素:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<String> myList = Arrays.asList("apple", "banana", "orange", "grape", "melon");

        // 使用流过滤和转换列表元素
        Map<Integer, String> result = myList.stream()
            .filter(s -> s.length() > 4) // 过滤长度大于4的字符串
            .map(String::toUpperCase)   // 将字符串转换为大写
            .sorted()                   // 排序
            .collect(Collectors.toMap(String::length, s -> s, (s1, s2) -> s1)); // 收集到Map

        System.out.println(result);
    }
}

这段代码首先创建了一个字符串列表,然后创建了一个流,通过一系列中间操作过滤掉长度小于等于4的字符串,将剩余字符串转为大写并排序,最后通过 collect 方法将结果收集到一个 Map 中,其中键是字符串长度,值是对应的字符串。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐