Explicación de la programación funcional de Java y las expresiones lambda

Autor: DemonsI

my.oschina.net/demons99/blog/2223079

Por que utilizar la programación funcional

La programación funcional es más a menudo una forma de pensar y una metodología de programación. La principal diferencia entre la programación funcional e imperativa es que la programación funcional le dice al código lo que quieres hacer, mientras que la programación imperativa le dice al código cómo hacerlo. Para decirlo sin rodeos, la programación funcional se basa en una determinada sintaxis o API de llamada al programa.

Por ejemplo, ahora necesitamos encontrar el número más pequeño de un conjunto de números. Si usamos programación imperativa para lograr este requisito, el código escrito es el siguiente:

public static void main(String[] args) {
    int[] nums = new int[]{1, 2, 3, 4, 5, 6, 7, 8};

    int min = Integer.MAX_VALUE;
    for (int num : nums) {
        if (num < min) {
            min = num;
        }
    }
    System.out.println(min);
}

Si usa programación funcional para implementarlo, el código que escribe es el siguiente:

public static void main(String[] args) {
    int[] nums = new int[]{1, 2, 3, 4, 5, 6, 7, 8};

    int min = IntStream.of(nums).min().getAsInt();
    System.out.println(min);
}

De los dos ejemplos anteriores, se puede ver que la programación imperativa necesita implementar detalles lógicos específicos por sí misma. La programación funcional es llamar a la API para completar la realización de los requisitos. El código imperativo original se escribe en una serie de llamadas de función anidadas. En la programación funcional, el código es más conciso y fácil de entender. Por eso se utiliza la programación funcional. de las razones. Por eso se dice que la programación funcional es decirle al código lo que quieres hacer, mientras que la programación imperativa es decirle al código qué hacer, lo cual es un cambio de pensamiento.

Hablando de programación funcional, tenemos que mencionar las expresiones lambda, que son la base de la programación funcional. Cuando Java no admite expresiones lambda, si necesitamos crear un hilo, debemos escribir el siguiente código:

public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("running");
        }
    }).start();
}

El uso de expresiones lambda puede completar la creación de un hilo con una frase de código. Lambda enfatiza la entrada y salida de la función, oculta los detalles del proceso y puede aceptar funciones como entrada (parámetros) y salida (valores de retorno) :

public static void main(String[] args) {
    new Thread(() -> System.out.println("running")).start();
}

Nota: El lado izquierdo de la flecha es la entrada y el derecho es la salida

La función de la expresión lambda es en realidad devolver el objeto de implementación de la interfaz Runnable, que es similar a llamar a un método para obtener el objeto de instancia, excepto que el código de implementación se escribe directamente en la expresión lambda. Podemos hacer una comparación simple:

public static void main(String[] args) {
    Runnable runnable1 = () -> System.out.println("running");
    Runnable runnable2 = RunnableFactory.getInstance();
}

Nuevas funciones de la interfaz JDK8

1. Interfaz de función, la interfaz solo puede tener un método que debe implementarse, que se @FunctionalInterface puede declarar mediante anotaciones. como sigue:

@FunctionalInterface
interface Interface1 {
    int doubleNum(int i);
}

Varias formas de usar expresiones lambda para obtener instancias de implementación de esta interfaz:

public static void main(String[] args) {
    // 最常见的写法
    Interface1 i1 = (i) -> i * 2;
    Interface1 i2 = i -> i * 2;

    // 可以指定参数类型
    Interface1 i3 = (int i) -> i * 2;

    // 若有多行代码可以这么写
    Interface1 i4 = (int i) -> {
        System.out.println(i);
        return i * 2;
    };
}

2. Una característica de la interfaz más importante es el método predeterminado de la interfaz, que se utiliza para proporcionar una implementación predeterminada. El método predeterminado es el mismo que el método de la clase de implementación ordinaria, puede usar palabras clave como esta:

@FunctionalInterface
interface Interface1 {
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }
}

La razón por la que la función del método predeterminado es más importante es que podemos usar esta función para proporcionar una implementación predeterminada en algunas interfaces escritas anteriormente, y no afectará a ninguna clase de implementación ni al código existente. Por ejemplo, la interfaz de Lista con la que estamos más familiarizados, la interfaz de Lista no ha cambiado ningún código desde JDK1.2, y solo después de 1.8 esta nueva característica agregó algunas implementaciones predeterminadas. Esto se debe a que si no hay una característica del método predeterminado, el impacto de modificar el código de la interfaz es enorme y, con el método predeterminado, agregar la implementación predeterminada no puede afectar ningún código.

3. Cuando la interfaz es de herencia múltiple, puede ocurrir el problema de la cobertura del método predeterminado. En este momento, puede especificar qué implementación del método predeterminado de la interfaz usar, como se muestra en el siguiente ejemplo:

@FunctionalInterface
interface Interface1 {
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }
}

@FunctionalInterface
interface Interface2 {
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }
}

@FunctionalInterface
interface Interface3 extends Interface1, Interface2 {

    @Override
    default int add(int x, int y) {
        // 指定使用哪一个接口的默认方法实现
        return Interface1.super.add(x, y);
    }
}

Interfaz de función

En esta sección, echemos un vistazo a las interfaces de funciones importantes que vienen con JDK8:

Puede ver que hay varias interfaces en la tabla anterior, y la más utilizada es la interfaz de función, que nos ahorra definir algunas interfaces de función innecesarias y reduce la cantidad de interfaces. Usamos un ejemplo simple para demostrar el uso de la interfaz de función:

import java.text.DecimalFormat;
import java.util.function.Function;

class MyMoney {
    private final int money;

    public MyMoney(int money) {
        this.money = money;
    }

    public void printMoney(Function<Integer, String> moneyFormat) {
        System.out.println("我的存款: " + moneyFormat.apply(this.money));
    }
}

public class MoneyDemo {
    public static void main(String[] args) {
        MyMoney me = new MyMoney(99999999);

        Function<Integer, String> moneyFormat = i -> new DecimalFormat("#,###").format(i);
        // 函数接口支持链式操作,例如增加一个字符串
        me.printMoney(moneyFormat.andThen(s -> "人民币 " + s));
    }
}

Ejecute el ejemplo anterior, la salida de la consola es la siguiente:

我的存款: 人民币 99,999,999

Si la interfaz de función no se utiliza en este ejemplo, debe definir una interfaz de función usted mismo y no se admite la operación en cadena, como se muestra en el siguiente ejemplo:

import java.text.DecimalFormat;

// 自定义一个函数接口
@FunctionalInterface
interface IMoneyFormat {
    String format(int i);
}

class MyMoney {
    private final int money;

    public MyMoney(int money) {
        this.money = money;
    }

    public void printMoney(IMoneyFormat moneyFormat) {
        System.out.println("我的存款: " + moneyFormat.format(this.money));
    }
}

public class MoneyDemo {
    public static void main(String[] args) {
        MyMoney me = new MyMoney(99999999);

        IMoneyFormat moneyFormat = i -> new DecimalFormat("#,###").format(i);
        me.printMoney(moneyFormat);
    }
}

Luego, echemos un vistazo al uso de la interfaz Predicate y la interfaz Consumer, como se muestra en el siguiente ejemplo:

public static void main(String[] args) {
    // 断言函数接口
    Predicate<Integer> predicate = i -> i > 0;
    System.out.println(predicate.test(-9));

    // 消费函数接口
    Consumer<String> consumer = System.out::println;
    consumer.accept("这是输入的数据");
}

Ejecute el ejemplo anterior, la salida de la consola es la siguiente:

false
这是输入的数据

Estas interfaces generalmente tienen tipos básicos de encapsulación, y no es necesario especificar genéricos cuando se utilizan tipos específicos de interfaces. El siguiente ejemplo:

public static void main(String[] args) {
    // 断言函数接口
    IntPredicate intPredicate = i -> i > 0;
    System.out.println(intPredicate.test(-9));

    // 消费函数接口
    IntConsumer intConsumer = (value) -> System.out.println("输入的数据是:" + value);
    intConsumer.accept(123);
}

Ejecute el código anterior, la salida de la consola es la siguiente:

false
输入的数据是:123

Con el presagio del ejemplo de interfaz anterior, deberíamos tener una comprensión preliminar del uso de la interfaz de función, y luego demostramos el uso de la interfaz de función restante:

public static void main(String[] args) {
    // 提供数据接口
    Supplier<Integer> supplier = () -> 10 + 1;
    System.out.println("提供的数据是:" + supplier.get());

    // 一元函数接口
    UnaryOperator<Integer> unaryOperator = i -> i * 2;
    System.out.println("计算结果为:" + unaryOperator.apply(10));

    // 二元函数接口
    BinaryOperator<Integer> binaryOperator = (a, b) -> a * b;
    System.out.println("计算结果为:" + binaryOperator.apply(10, 10));
}

Ejecute el código anterior, la salida de la consola es la siguiente:

提供的数据是:11
计算结果为:20
计算结果为:100

La interfaz BiFunction tiene una entrada más que la interfaz Function. El siguiente ejemplo:

class MyMoney {
    private final int money;
    private final String name;

    public MyMoney(int money, String name) {
        this.money = money;
        this.name = name;
    }

    public void printMoney(BiFunction<Integer, String, String> moneyFormat) {
        System.out.println(moneyFormat.apply(this.money, this.name));
    }
}

public class MoneyDemo {
    public static void main(String[] args) {
        MyMoney me = new MyMoney(99999999, "小明");

        BiFunction<Integer, String, String> moneyFormat = (i, name) -> name + "的存款: " + new DecimalFormat("#,###").format(i);
        me.printMoney(moneyFormat);
    }
}

Ejecute el código anterior, la salida de la consola es la siguiente:

小明的存款: 99,999,999

Referencia de método

Después de aprender sobre las expresiones lambda, usualmente usamos expresiones lambda para crear métodos anónimos. Pero a veces solo necesitamos llamar a un método existente. El siguiente ejemplo:

Arrays.sort(stringsArray, (s1, s2) -> s1.compareToIgnoreCase(s2));

En jdk8, podemos usar una nueva característica para acortar esta expresión lambda. El siguiente ejemplo:

Arrays.sort(stringsArray, String::compareToIgnoreCase);

Esta función se llama Referencia de método. El método se hace referencia a la forma estándar: 类名::方法名. (Nota: solo necesita escribir el nombre del método, no los paréntesis).

Actualmente hay cuatro formas de referencia de método:

A continuación, usamos un ejemplo simple para demostrar varias formas de escribir referencias a métodos. Primero defina una clase de entidad:

public class Dog {
    private String name = "二哈";
    private int food = 10;

    public Dog() {
    }

    public Dog(String name) {
        this.name = name;
    }

    public static void bark(Dog dog) {
        System.out.println(dog + "叫了");
    }

    public int eat(int num) {
        System.out.println("吃了" + num + "斤");
        this.food -= num;
        return this.food;
    }

    @Override
    public String toString() {
        return this.name;
    }
}

Llame al método en la clase de entidad por referencia de método, el código es el siguiente:

package org.zero01.example.demo;

import java.util.function.*;

/**
 * @ProjectName demo
 * @Author: zeroJun
 * @Date: 2018/9/21 13:09
 * @Description: 方法引用demo
 */
public class MethodRefrenceDemo {

    public static void main(String[] args) {
        // 方法引用,调用打印方法
        Consumer<String> consumer = System.out::println;
        consumer.accept("接收的数据");

        // 静态方法引用,通过类名即可调用
        Consumer<Dog> consumer2 = Dog::bark;
        consumer2.accept(new Dog());

        // 实例方法引用,通过对象实例进行引用
        Dog dog = new Dog();
        IntUnaryOperator function = dog::eat;
        System.out.println("还剩下" + function.applyAsInt(2) + "斤");

        // 另一种通过实例方法引用的方式,之所以可以这么干是因为JDK默认会把当前实例传入到非静态方法,参数名为this,参数位置为第一个,所以我们在非静态方法中才能访问this,那么就可以通过BiFunction传入实例对象进行实例方法的引用
        Dog dog2 = new Dog();
        BiFunction<Dog, Integer, Integer> biFunction = Dog::eat;
        System.out.println("还剩下" + biFunction.apply(dog2, 2) + "斤");

        // 无参构造函数的方法引用,类似于静态方法引用,只需要分析好输入输出即可
        Supplier<Dog> supplier = Dog::new;
        System.out.println("创建了新对象:" + supplier.get());

        // 有参构造函数的方法引用
        Function<String, Dog> function2 = Dog::new;
        System.out.println("创建了新对象:" + function2.apply("旺财"));
    }
}

Inferencia de tipo

A través de los ejemplos anteriores, sabemos que la razón por la que se pueden usar expresiones Lambda es que debe haber una interfaz funcional correspondiente. Esto es consistente con el lenguaje fuertemente tipado de Java, lo que significa que no puede escribir expresiones Lambda de forma arbitraria en cualquier parte del código. De hecho, el tipo de Lambda es el tipo de interfaz funcional correspondiente. Otra base para las expresiones lambda es el mecanismo de inferencia de tipos. Cuando la información de contexto es suficiente, el compilador puede inferir el tipo de la tabla de parámetros sin la necesidad de nombrar explícitamente.

Por tanto, el tipo de expresión Lambda se infiere del contexto de Lambda, el tipo requerido por la expresión Lambda en el contexto se denomina tipo de destino, como se muestra en la siguiente figura:

A continuación, usamos un ejemplo simple para demostrar varios tipos de inferencia de expresión Lambda, primero defina una interfaz funcional simple:

@FunctionalInterface
interface IMath {
    int add(int x, int y);
}

El código de muestra es el siguiente:

public class TypeDemo {

    public static void main(String[] args) {
        // 1.通过变量类型定义
        IMath iMath = (x, y) -> x + y;

        // 2.数组构建的方式
        IMath[] iMaths = {(x, y) -> x + y};

        // 3.强转类型的方式
        Object object = (IMath) (x, y) -> x + y;

        // 4.通过方法返回值确定类型
        IMath result = createIMathObj();

        // 5.通过方法参数确定类型
        test((x, y) -> x + y);

    }

    public static IMath createIMathObj() {
        return (x, y) -> x + y;
    }

    public static void test(IMath iMath){
        return;
    }
}

Referencia variable

Las expresiones Lambda son similares a las clases internas o las clases anónimas que implementan una interfaz específica, por lo que las reglas para hacer referencia a variables en expresiones Lambda son las mismas que cuando nos referimos a variables en clases anónimas. El siguiente ejemplo:

public static void main(String[] args) {
    String str = "当前的系统时间戳是: ";
    Consumer<Long> consumer = s -> System.out.println(str + s);
    consumer.accept(System.currentTimeMillis());
}

Vale la pena mencionar que antes de JDK1.8 generalmente configuramos las variables externas a las que se accede en la clase anónima en final, y en JDK1.8 las variables externas a las que se accede en la clase anónima se establecen en final de forma predeterminada. Por ejemplo, si cambio el valor de la variable str ahora, ide mostrará un error:

En cuanto a por qué las variables se establecen en final, esto se debe a que no se pasa por referencia en Java, y las variables se pasan por valor. Si la variable no se establece en final, si se cambia la referencia de la variable externa, el resultado final será incorrecto.

Usemos un conjunto de imágenes para demostrar brevemente la diferencia entre pasar valores y pasar referencias. Tome la lista como ejemplo. Cuando se trata solo de pasar un valor, la referencia a la variable externa en la clase anónima es un objeto de valor:

Si la variable de lista apunta a otro objeto en este momento, entonces el objeto de valor en la clase anónima sigue siendo el objeto de valor anterior, por lo que debemos establecerlo en final para evitar que cambien las referencias de variables externas:

Si se pasa por referencia, la referencia a la variable externa en la clase anónima no es un objeto de valor, sino un puntero a esta variable externa:

Entonces, incluso si la variable de lista apunta a otro objeto, la referencia en la clase anónima cambiará a medida que cambie la referencia de la variable externa:

Expresiones en cascada y curry

En la programación funcional, las funciones pueden recibir o devolver otras funciones. Una función ya no es como en la programación tradicional orientada a objetos, es solo una fábrica o generador de objetos, y también puede crear y devolver otra función. Las funciones que devuelven funciones se pueden convertir en expresiones lambda en cascada Cabe destacar que el código es muy corto. Aunque esta sintaxis puede parecer muy desconocida al principio, tiene su propio propósito.

Una expresión en cascada es una combinación de varias expresiones lambda. Esto implica el concepto de una función de orden superior. La llamada función de orden superior es una función que puede devolver una función. El siguiente ejemplo:

// 实现了 x + y 的级联表达式
Function<Integer, Function<Integer, Integer>> function1 = x -> y -> x + y;
System.out.println("计算结果为: " + function1.apply(2).apply(3));  // 计算结果为: 5

Aquí  y -&gt; x + y se devuelve como una función a la expresión de nivel superior, por lo que la salida de y -&gt; x + yla expresión de primer nivel es  esta función. Puede ser más fácil de entender si usa paréntesis:

x -> (y -> x + y)

Las expresiones en cascada pueden realizar el curado de funciones. En pocas palabras, el curado es convertir una función con múltiples parámetros en una función con un solo parámetro, como se muestra en el siguiente ejemplo:

Function<Integer, Function<Integer, Function<Integer, Integer>>> function2 = x -> y -> z -> x + y + z;
System.out.println("计算结果为: " + function2.apply(1).apply(2).apply(3));  // 计算结果为: 6

El propósito del procesamiento de funciones es estandarizar funciones, que se pueden combinar de manera flexible para facilitar el procesamiento unificado, etc. Por ejemplo, solo puedo llamar al mismo método en el ciclo sin llamar a otro método para lograr el elemento en una matriz. Y cálculo, El código es el siguiente:

public static void main(String[] args) {
    Function<Integer, Function<Integer, Function<Integer, Integer>>> f3 = x -> y -> z -> x + y + z;
    int[] nums = {1, 2, 3};
    for (int num : nums) {
        if (f3 instanceof Function) {
            Object obj = f3.apply(num);
            if (obj instanceof Function) {
                f3 = (Function) obj;
            } else {
                System.out.println("调用结束, 结果为: " + obj);  // 调用结束, 结果为: 6
            }
        }
    }
}

Las expresiones en cascada y el currying generalmente no son muy comunes en el desarrollo real, por lo que un poco de comprensión de sus conceptos es suficiente, aquí hay solo una breve introducción, si está interesado en ellos, puede consultar materiales relacionados.

FIN

Amigos a los que les gusta este artículo, hagan clic en la imagen para seguir la cuenta de suscripción y ver más contenido emocionante.

Lectura recomendada:

我知道你 “在看”

Supongo que te gusta

Origin blog.csdn.net/qq_39507327/article/details/111026994
Recomendado
Clasificación