自学内容网 自学内容网

关于函数式接口和编程的解析和案例实战

匿名内部类

匿名内部类的学习和使用是实现lambda表达式和函数式编程的基础。是想一下,我们在使用接口中的方法的时候,正常流程都是定义这个接口的实现类,然后使用实现类的对象调用接口中的方法。下面展示一个接口的方法使用的常规方法

public interface Test {
    void forTest();
}

如果想要使用这个接口,我们需要定义Test的实现类

// 定义一个学生类实现 Swim 游泳接口
public class TestImpl implements Test{
    // 实现方法
    @Override
    public void test() {
        //for test
        System.out.println("测试test方法");
    }
    //测试
    public static void main(String[] args) {
        // 创建 Student 类的对象 s1
        TestImpl t = new TestImpl();
        //打印调用实现的方法
        t.test();
    }
}

“匿名”在哪里

1. 匿名了实现的接口所在的父类
java中可以根据传入的对象类型,区分这个对象的类信息,所以匿名的第一层就是省略了这个类,省去了 implements ClassName中的ClassName。之所以被称为 “匿名”,主要是因为它没有显式地定义类名,并且在创建对象的同时就直接实现了某个接口或者继承了某个类,以下是关于它为何可以实现匿名以及具体匿名了哪些信息的详细解释:

2. 匿名了外部独立的类定义结构
匿名内部类将类的定义和使用紧密结合在了一起,它直接嵌套在创建对象的代码语句中,没有像常规类那样在外部单独的代码块里呈现完整的类结构,比如类的修饰符(public、private 等)、类的继承关系(除了在匿名内部类中体现的继承自某个类或者实现某个接口)等这些在普通类定义中可能出现的结构信息都被隐去了,整个类的定义仿佛是 “匿名” 地融入到了使用它的那一处代码当中,使代码结构更加简洁,不过也相对牺牲了一些代码的清晰性和可维护性。换而言之,匿名内部类中我们的TestImpl也不再需要显示给出,这里我们以实现自定义比较器作为示例

List<Integer> nums = Arrays.asList(1,5,3,7,11,6,2);
nums.sort(new Comparator<Integer>(){
     @Override
     public int compare(Integer i1, Integer i2){
            return i2- i1;
     }
});

3. 其他信息补充说明
对于内部的方法入参和重写的注解,是实现一个接口方法中必须的信息,有无返回值需要根据接口方法定义是void还是其他区分,以上就是匿名内部类的使用。在日常开发中,基于参数是一个接口方法返回值的这种写法较为常见,例如:Runnable或者Comarator等

函数式编程

当前java 8中提供了很多基于函数式的新特性。其中函数式接口有代表性的非 lambda表达式莫属。相较于匿名内部类,lambda表达式更加精简,仅保留传入的实参和返回值以及计算逻辑。上文中的自定义比较器在lambda表达式下可以优化为:“

List<Integer> nums = Arrays.asList(1,5,3,7,11,6,2);
nums.sort((i1, i2) -> i2- i1);

lambda表达式的条件

1. 必须实现的是函数式接口

函数式接口是指只包含一个抽象方法的接口(除了从 Object 类继承的公共方法,如 equals、hashCode 等,这些不算额外的抽象方法)。这是最关键的前提条件,因为 Lambda 表达式本质上就是为了简洁地实现函数式接口而设计的语法糖。

代码示例:“下面的Runnable仅包含一个接口,因此可以改写为lambda表达式

new Runnable() {
    @Override
    public void run() {
        System.out.println("执行任务");
    }
};

如果是含有多个抽象方法的接口,无法使用lambda表达式改写,因为编译器无法区分你需要覆盖的具体方法,所以下面的接口无法适配lambda表达式的改造

MyTestInterface myObj = new MyTestInterface() {
    @Override
    public void method1() {
        // 具体实现逻辑
    }

    @Override
    public int method2(int num) {
        return num * 2;
    }
};

2. 接口抽象方法的参数和返回值类型需明确可推断
虽然 Java 有类型推断机制,但在使用 Lambda 表达式改写匿名内部类时,接口抽象方法的参数类型和返回值类型要能够相对清晰地确定,以便编译器能正确解析 Lambda 表达式所代表的逻辑。例如比较器的重写,编译器可以根据传入的实参判断类型是String

interface StringJoinerFunction {
    String join(String s1, String s2);
}
public class StringJoinerExampleWithError {
    public static void main(String[] args) {
        StringJoinerFunction joiner = (s1, s2) -> {
            System.out.println(s1 + s2);
            return s1+s2; // 返回值类型与接口定义的String不符,无法正确改写
        };
        System.out.println(joiner.join("Hello", "World"));
    }
}

3. 实现逻辑相对简单,无复杂的语句块或逻辑分支
这个要求仅仅基于代码本身的可读写,例如一些if else逻辑也可以用于lamvbda表达式,但是却失去了代码简洁原本的意义。不如直接使用匿名内部类或者实现接口的形式完成抽象方法的调用。

Supplier

Supplier(供给者) Supplier是一个不接受任何输入参数但返回一个结果的操作。它主要用于生成数据或对象。Supplier接口定义了一个get方法,该方法不接受任何输入参数并返回一个结果。表示从函数式接口返回的对象中获取Supplier内的数据

 Supplier<String> supplier =  () -> {
       return "Hello World";
 };
 System.out.println(supplier.get());

Supplier在企业级开发中的应用场景大约有以下几个方面

使用示例

1. 动态加载配置
一些数据库配置信息和连接池的加载如果消耗较大的资源,并且希望使用时候动态加载的情况下,可以使用Supplier预先定义这个连接配置项。避免在应用启动阶段就占用大量内存和初始化时间。
例如,创建一个数据库连接池对象,代码示例如下:

    public static Supplier<DataSource> supplier = () -> {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("xxxx:3306/pdb_19c");
        dataSource.setUsername("Hikari");
        dataSource.setPassword("123456");
        return dataSource;
    };

    public static DataSource getDataSource () {
        return (DataSource) Optional.ofNullable(supplier).get();
    }

    public static Connection getConnectionDyn() throws Exception {
        return getDataSource().getConnection();
    }

2. 生成默认数据

继续上面的思路,可以使用static final修饰默认值,使用Supplier处理指向默认值的配置,用作某属性为空的兜底配置

   private static final Supplier<Integer> portSupplier = () -> {
        Optional<Integer> configPort = readPortFromConfig();
        return configPort.orElse(DEFAULT_PORT);
    };

    private static Optional<Integer> readPortFromConfig() {
        // 模拟从配置文件读取端口号,这里假设返回Optional.empty()表示读取失败
        Integer i1 = null;
        return Optional.ofNullable(i1);
    }

    public static void main(String[] args) {
        System.out.println(portSupplier.get());
    }

3. 实现灵活的策略模式
策略模式在企业级开发中常用于根据不同情况选择不同的业务逻辑执行方式。Supplier 结合 Lambda 表达式可以让策略模式的实现更加简洁和灵活,下面是用Supplier实现的策略模式

    public static Supplier<BigDecimal> normalMemberDiscount = () -> {
        return BigDecimal.valueOf(0.9); // 9折
    };

    // 高级会员折扣策略
    public static Supplier<BigDecimal> premiumMemberDiscount = () -> {
        return BigDecimal.valueOf(0.8); // 8折
    };

    public static BigDecimal calculateDiscount(Order order, Supplier<BigDecimal> discountSupplier) {
        return order.getAmount().multiply(discountSupplier.get());
    }

4. 生成运行时的测试数据
Supplier 结合 Lambda 表达式可以方便地实现这一点,让测试数据的生成更加灵活和动态。
例如,在测试一个用户注册模块时,需要生成不同的用户信息作为测试数据

class UserTestDataGenerator {
    private static Supplier<String> usernameSupplier = () -> {
        Random random = new Random();
        return "user_" + random.nextInt(1000);
    };

    private static Supplier<String> passwordSupplier = () -> {
        Random random = new Random();
        return "pass_" + random.nextInt(1000);
    };

    private static Supplier<String> emailSupplier = () -> {
        Random random = new Random();
        return "user_" + random.nextInt(1000) + "@example.com";
    };

    public static Supplier<User> userSupplier = () -> new User(usernameSupplier.get(), passwordSupplier.get(), emailSupplier.get());
}

Consumer

序言:顾名思义,是消费者的意思。这个函数本身不接收返回值类型,一般实现打印、输出、入参的转换处理等操作
1. 代码示例
下面给出Consumer定义的示例,可以看到这个对象的定义类似于一个Comparator比较器,其作用是接收一个字符串,然后执行accept方法中对于字符串的操作。

 Consumer<String> con = new Consumer<String>() {
      @Override
      public void accept(String string) {
         System.out.println("string values :" + string);
            }
        };
 con.accept("Hello World"); 

如果使用新版idea编译器的亲们,可以发现编译器提示这个方法的优化写法为使用lambda表达式的形式,即下面的格式:

  Consumer<String> con = string -> System.out.println("string values :" + string);
  con.accept("Hello World");

accept

accept在笔者看来可以视作一个开关,当主线程调用这个方法的时候,执行开关内部的逻辑。可以类比线程的submit方法,执行内部的代码块,:

 public static void main(String[] args) {
        // 定义一个Consumer<String>类型的变量,使用Lambda表达式实现其accept方法
        Consumer<String> stringConsumer = (str) -> System.out.println(str);

        // 调用accept方法,传入一个字符串参数
        stringConsumer.accept("Hello, World!");
    }
}

由上面的代码可以了解到,我们设置Consumer的定义,并且在想要让其执行的地方应用accept()触发Consumer的函数部分,输出了Hello World

andThen

如果一个操作之后还有其他操作,可以将其Consumer对象放到andThen的参数位置上,这个是因为andThen相当于执行accept的accept,从源码分析上可以得到这样的结论

default Consumer<T> andThen(Consumer<? super T> after) {
     Objects.requireNonNull(after);
     return (T t) -> { accept(t); after.accept(t); };
 }

consumer之间可以使用andThen进行串联式的编排,例如对姓名进行输出之后转换字符串为小写,通过consumer的定义和组装可以轻松实现。同时这两consumer对象也可以放在函数方法的形参位置作为回调方法使用

        List<String> arr = Arrays.asList("Wang", "Zi", "Meng");

        Consumer<String> con2 = out -> System.out.print("会员姓名:"+ out +";");
        Consumer<String> con3 = str2 -> System.out.println("小写版本为: " +str2.toLowerCase());
        con2.andThen(con3);

        arr.stream().forEach(con2.andThen(con3)::accept);

输出结果:
在这里插入图片描述

使用场景

使用函数式的consumer和普通的for循环有什么区别,将通过下面的示例进行展示

case 1 代码简洁性与可读性

如果在for循环中需要多重处理,并且这段代码整体写在循环体内容易造成多层嵌套或者本身具有一定程度的复用性,应该考虑将其抽象出来作为一个Consumer对象,例如循环处理某一个属性,需要将属性进行字符的转换或者精度的保留
  Consumer<String> con = string -> System.out.println(string.toUppercase());
  con.accept("Hello World");

2. 更好地支持函数式编程范式

Java 8 引入了函数式编程的一些特性,Consumer 作为函数式接口(只包含一个抽象方法 accept 的接口),符合函数式编程中对行为(操作)的抽象概念。它可以方便地与其他函数式接口(如 Predicate、Function 等)以及 Stream API 等配合使用,实现更高级、更灵活的编程模式,比如对集合进行过滤(使用 Predicate)后再对满足条件的元素进行消费(使用 Consumer)等操作,能够在代码中更好地体现数据的转换、处理流程,让代码更具逻辑性和条理性,同时也便于进行代码的单元测试等维护工作。

3. 增强代码的可复用性和灵活性

将操作抽象为 Consumer 接口,可以方便地在不同的地方复用这些操作逻辑。例如,前面提到的将字符串转换为大写的 Consumer 操作,可以在多个需要对字符串进行此处理的地方重复使用,只需要传递这个 Consumer 实例即可。而且通过将 Consumer 作为方法参数,能让方法的功能更加灵活多样,根据传入的不同 Consumer 实现不同的业务逻辑,提高了代码应对不同需求变化的能力,降低了代码的耦合度,使得整个代码库更加易于扩展和维护。虽然在很多情况下使用普通逻辑确实也能实现相同的功能,但 Consumer 函数式接口凭借其在代码简洁性、函数式编程支持以及复用性和灵活性等方面的优势,在 Java 编程中有着广泛且合适的应用场景,能够帮助开发人员更高效、优雅地编写代码,应对各种复杂的业务需求

Functional

通过lambda表达式可以看出匿名内部类的优化写法,func定义如下:

Function<Integer,String> func = new Function<Integer, String>() {
            @Override
            public String apply(Integer integer) {
                return String.valueOf(integer);
            }
        };
        list.stream().forEach(li -> {
            System.out.println(func.apply(li));
        });

        Function<Integer,String> funcLLambda = integer -> String.valueOf(integer);

BiFunctional

这个接口有四个类型参数,T、U、V 分别对应三个输入参数的类型,而 R 对应返回结果的类型,其唯一的抽象方法 apply 接受三个参数(分别为 T、U、V 类型),并返回一个 R 类型的结果,符合接受三个元素作为入参并返回结果的需求,并且由于标注了 @FunctionalInterface,可以很好地使用 Lambda 表达式来实现它。

代码示例

   private BiFunction<Double,Double,Double> biFunction = (Double a, Double b) -> Math.sqrt(a*a + b*b);

TriFunctional

这个接口有四个类型参数,T、U、V 分别对应三个输入参数的类型,而 R 对应返回结果的类型,其唯一的抽象方法 apply 接受三个参数(分别为 T、U、V 类型),并返回一个 R 类型的结果,符合接受三个元素作为入参并返回结果的需求,并且由于标注了 @FunctionalInterface,可以很好地使用 Lambda 表达式来实现它

代码示例

 public static void main(String[] args) {
        TriFunction<String, String, String, String> formatFunction = (str1, str2, str3) -> {
            return String.format("姓名: %s, 年龄: %s, 城市: %s", str1, str2, str3);
        };
        String result = formatFunction.apply("张三", "25", "北京");
        System.out.println(result);
    }

原文地址:https://blog.csdn.net/Wang_Zimeng/article/details/143926612

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!