Java8核心实战之三:函数式接口--乐字节教育

本文深入探讨Java8中Lambda表达式的引入与应用,包括其如何替代匿名内部类,简化代码结构,提升应用程序性能。详细讲解Lambda的语法特性,如参数、返回值、变量使用及类型推断,并通过具体案例展示其在热销商品排序与日志输出优化中的实战效果。

Java8 引入 Lambda 表达式,允许开发者将函数当成参数传递给某个方法,或者把代码本身当作数据进行处理。使用 Lambda 表达式,使得应用变得简洁而紧凑。 很多语言(Groovy、Scala 等)从设计之初就支持 Lambda 表达式。但是 java 中使用的是匿名内部类代替。最后借助强大的社区力量,找了一个折中的 Lambda 实现方案,可以实现简洁而紧凑的语言结构。

一、匿名内部类到Lambda的演化

匿名内部类,即一个没有名字的,存在于一个类或方法内部的类。当我们需要用某个类且只需要用一次,创建和使用合二为一时,我们可以选择匿名内部类,省掉我们定义类的步骤。

匿名内部类会隐士的继承一个类或实现一个接口,或者说匿名内部类是一个继承了该类或者实现了该接口的子类匿名对象。下面看一个匿名内部类的例子

package com.java8;
/*
   定义和使用匿名内部类
*/
public class NoNameClass {
   public static void main(String[] args) {

       Model m = new Model(){
           @Override
           public void func() {
               System.out.println("方法的实现");
          }
      };
       m.func();
  }
}
// 需要被实现的接口
interface Model{
   void func();
}

等价的 Lambda 代码

package com.java8;
/*
   定义和使用Lambda 简化代码
*/
public class NoNameClass {
   public static void main(String[] args) {

       Model m = new Model(){()->{
           System.out.println("方法的实现");
      }};
       m.func();
  }
}

可以看出使用 Lambda 表达式替代了匿名内部类代码,使得代码更加简化、紧凑。

获取学习资源技术干货,lezijie007(暗号33)

二、语法

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

  • 可选类型声明

    不需要声明参数类型,编译器可以统一识别参数值。

  • 可选的参数圆括号

    一个参数无需定义圆括号,但多个参数需要定义圆括号。

  • 可选的大括号

    如果主体包含了一个语句,就不需要使用大括号。

  • 可选的返回关键字

    如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指明表达式返回了一个数值。

Lambda 表达式示例:

三、Lambda使用形式

使用 Lambda 时,实现方法可以有参数,也可以有返回值,如果没指定参数类型,则由编译器自行推断得出。

1、无参带返回值

生成[1,10]之间的任意整数

interface Model2{
   int func();
}
Model2 md2 = () -> {return (int)(Math.random()*10+1)};

Lambda 的改写需要有对应的抽象方法,当没有参数时需要使用()占位,当表达式只有一行代码时,可以省略return和{}

以上的 Lambda 等价于:

Model2 md2 = () -> (int)(Math.random()*10+1);

2、带参带返回值

返回一个对数字描述的字符串。

interface Model3{
   String func(int a);
}
Model3 md3 = (int a) -> {
   return "This is a number " + a;
};

形参写在()内即可,参数的类型可以省略,此时将由编译器自行推断得出,同时还可以省略()

以上的 Lambda 等价于:

md3 =  a -> "This is a number " + a;

省略了参数类型,小括号,同时连带实现体的括号和 return 一并省去。

3、带多个参数

根据输入的运算符计算两个数的运算,并返回结果

interface Model4{
   String func(int a, int b, String oper);
}
Model4 md4 = (a, b, s) -> {
     String res = "";
     if("+".equals(s)){
           res = ( a+b ) + "";
    }else if("-".equals(s)){
           res = ( a-b ) + "";
    }else if("*".equals(s)){
           res = ( a*b ) + "";
    }else if("/".equals(s)){
           res = ( a/b ) + ""; // 暂不考虑除0的情况
    }else{
           res =  "操作有失误";
    }
     return res;
};
System.out.println(md4.func(1,1,"+"));

以上例子为多个参数的 Lambda 表达式,其中省略掉了每一个参数的类型,编译器自动推断。多条语句时实现体的{}不能省。

四、Lambda作为参数

在 Java8 之前,接口可以作为方法参数传入,执行时必须提供接口实现类的实例。从 java8 开始,Lambda 可以作为接口方法实现,当作参数传入,无论从形式上还是实际上都省去了对象的创建。使代码更加的紧凑简单高效。

定义接口

在接口中,必须有且仅有一个抽象方法,以确定 Lambda 模板

// 无参无返回值的方法
interface LambdaInterface1{
   void printString();
}
// 带参无返回值的方法
interface  LambdaInterface2{
   void printString(String str);
}

定义方法接收参数

在某方法中需要使用接口作为参数

// 无参
public static void testLambda(LambdaInterface1 lam1){
 lam1.printString();
}
// 带参
public static void testLambda2(String s,LambdaInterface2 lam2){
 lam2.printString(s);
}

Lambda表达式作为参数传入

// 无参Lambda作为参数
testLambda(()->{
    System.out.println("可以简单,可以复杂");
});
// 带参Lambda作为参数
testLambdaParam("hello",(a)->{
    System.out.println(a);
});

五、Lambda中使用变量

在 Lambda 中可以定义自己的局部变量,也可以使用外层方法的局部变量,还可以使用属性。这一点也不难理解,既然是一个方法的实现,只写了一个代码块,那么使用本身所属方法的局部变量和类的属性也并不过分。

public static void main(String[] args) {
    List<String> strs = new ArrayList<String>(){
        {
            add("aaa");
            add("bbb");
            add("ccc");
        }
    };
    int j = 1;
    strs.forEach((str)->{
        int i = 0;
        System.out.println(str + "  " + i + "  " + j);
    });
}

六、Lambda类型推断

类型检查

Lambda 的类型是从使用 Lambda 的上下文推断出来的。 Lambda 表达式的参数与函数式接口内方法的参数,返回值类型相互对应。Lambda 表达式需要的类型,或者说 Lambda 实现的那个函数式接口称之为目标类型。

类型推断

利用目标类型来检查一个 Lambda 是否可以用于某个特定的上下文,推断 Lambda 参数的类型。

七、Lambda表达式实战

1、热销商品排序

排序对于久经开发的你来说可能并不陌生,假如原来你做过电商项目,相信对于电商场景下的商品记录排序操作很有感情,下面我们使用Lambda 来看看热销商品排序的操作。

测试数据这里以手机测试数据为例

/**
* 实际开发数据通常从数据库获取
* 这里使用测试数据
*/
Goods g01=new Goods(1,"小米9",1789,200, BigDecimal.valueOf(2500));
Goods g02=new Goods(2,"华为Mate20",5000,3000, BigDecimal.valueOf(7000));
Goods g03=new Goods(3,"OPPO R17",2000,2827, BigDecimal.valueOf(1500));
Goods g04=new Goods(4,"魅族 Note9",2000,1600, BigDecimal.valueOf(1600));
Goods g05=new Goods(5,"一加6T",8000,5000, BigDecimal.valueOf(3500));
List<Goods> goods= Arrays.asList(g01,g02,g03,g04,g05);

Collections.sort 静态方法实现排序

Collections.sort(goods,(g1,g2)->g1.getSale()-g2.getSale());

List.sort 默认方法实现集合排序

// 使用 Lambda 对商品记录按销量进行排序
goods.sort((g1,g2)->g1.getSale()-g2.getSale());

Stream.sorted 方法实现元素排序

// 多个条件排序情况 Lambda 配置Stream 销量+价格排序 销量相等时按照价格排序
goods =goods.stream().sorted((g1,g2)->g1.getSale()-g2.getSale())
.sorted((g1,g2)->g1.getPrice().compareTo(g2.getPrice()))
.collect(Collectors.toList());

2、日志输出优化

对于项目开发日志打印是一项不可获取的模块,无论是在开发阶段还是项目部署上线后,日志信息的输出对于开发人员来以及运维人员来说都是一项重要的参考指标。

日志输出场景这里以用户模块 UserService 为例,以下为优化前的日志输出代码:

public String login(String userName, String userPwd) {
    logger.info("UserService 接收到参数-->" + userName + "," + userPwd);
    /**
     * 登录逻辑省略
     */
    return "login";
}

日志级别设置到 debug,在开发阶段方便查看后端接收到的参数信息。仔细分析这里的日志代码,可以看到当日志级别设置为 info 时 debug 日志不应该执行输出操作,同时这里调用 debug 方法时,对于传入的字符串参数需要作对应的拼接操作,才会传入过来。当访问的情况在商城项目做活动情况下 这里的情况有可能会变得很糟糕:所有的 debug 信息全部输出 同时会有大量字符串拼接操作,会影响整个应用程序的执行性能。

日志输出场景这里以用户模块 UserService 为例,日志输出代码优化

  • 输出日志前判断日志输出级别
  • 借助 Lambda 延迟日志内容输出
/**
* 添加info方法
* 判断日志打印级别
* 当条件成立时 输出日志信息
* @param logger
* @param message
*/
public void info(Log logger, Supplier<String> message){
   if(logger.isInfoEnabled()){
       logger.info(message.get());
  }
}

public String login(String userName, String userPwd) {
   //logger.info("UserService 接收到参数-->" + userName + "," + userPwd);
   // 延迟Lambda 表达式执行 只有确定
   info(logger,()->"UserService 接收到参数-->" + userName + "," + userPwd);
   return "login";
}

八、Lambda优势与使用场景

Lambda 表达式的引入取代了匿名内部类,使得代码变得简洁、紧凑,同时 Lambda 的惰性特点,在开发时能够提高应用程序的执行性能。

对于 Lambda 的应用场景,从代码结构来说通常是结合函数式接口来使用,使得开发是面向函数来进行编程,也是 Java8 引入的一种新的思想-函数式编程(后续会介绍)。同时还会结合前面讲到的接口默认方法提现到应用开发中。

基础篇-0-Java虚拟机导学课程 11:33 基础篇-1-初识JVM 22:27 基础篇-2-Java虚拟机的组成 04:47 基础篇-3-字节码文件的组成-以正确的姿势打开字节码文件 10:41 基础篇(补)-3.5-字节码文件的组成-基础信息 15:54 基础篇-4-字节码文件的组成-常量池和方法 25:51 基础篇-5-字节码文件常见工具的使用1 11:43 基础篇-6-字节码文件常见工具的使用2 22:20 基础篇-7-类的生命周期加载阶段 22:09 基础篇-8-类的生命周期2连接阶段 19:58 基础篇-9-类的生命周期3初始化阶段 26:27 基础篇-10-类加载器的分类 13:56 基础篇-11-启动类加载器 13:36 基础篇-12-扩展和应用程序类加载器 16:26 基础篇-13-双亲委派机制 18:43 基础篇-14-打破类的双亲委派机制-自定义类加载器 25:16 基础篇-15-打破双亲委派机制2-线程上下文类加载器 20:17 基础篇-16-打破双亲委派机制3-osgi和类的热部署 11:53 基础篇-17-JDK9之后的类加载器 09:05 基础篇-18-运行时数据区-程序计数器 15:42 基础篇-19--局部变量表 19:20 基础篇-20--操作数栈和帧数据 12:08 基础篇-21--内存溢出 15:28 基础篇-22-堆内存 25:56 基础篇-23-方法区的实现 16:25 基础篇-24-方法区-字符串常量池 20:40 基础篇-25-直接内存 12:39 基础篇-26-自动垃圾回收 11:32 基础篇-27-方法区的回收 11:32 基础篇-28-引用计数法 15:41 基础篇-29-可达性分析法 20:25 基础篇-30-软引用 24:40 基础篇-31-弱虚终结器引用 12:08 基础篇-32-垃圾回收算法的评价标准 13:31 基础篇-33-垃圾回收算法1 10:05 基础篇-34-垃圾回收算法-分代GC 20:19 基础篇-35-垃圾回收器1 15:54 基础篇-36-垃圾回收器2 11:44 基础篇-37-垃圾回收器3 15:51 基础篇-38-g1垃圾回收器 26:23 实战-1-内存泄漏和内存溢出 21:25 实战-2-解决内存泄漏-监控-top命令 12:16 实战-3-解决内存泄漏-监控-visualvm 12:50 实战-4-解决内存泄漏-监控-arthas tunnel 15:18 实战-5-解决内存泄漏-监控-prometheus-grafana 17:53 实战-6-解决内存泄漏-堆内存状况对比 08:39 实战-7-解决内存泄漏-内存泄漏产生的几大原因 16:01 实战-8-内存泄漏产生的原因2 13:30 实战-9-内存泄漏产生的原因3 10:43 实战-10-内存泄漏产生的原因4 10:04 实战-11-内存泄漏产生原因2-并发请求问题 17:30 实战-12-导出堆内存快照并使用MAT分析 08:38 实战-13-MAT内存泄漏检测原理 17:23 实战-14-服务器导出内存快照和MAT使用小技巧 13:31 实战-15-实战1-查询大数据量导致的内存溢出 26:24 实战-16-实战2-mybatis导致的内存溢出 10:34 实战-17-实战3-k8s容器环境导出大文件内存溢出 26:13 实战-18-系统不处理业务时也占用大量的内存 14:13 实战-19-文章审核接口的内存问题 18:28 实战-20-btrace和arthas在线定位问题 20:15 实战-21-GC调优的核心目标 11:23 实战-22-GC调优的常用工具 12:05 实战-23-GC调优的常见工具2 14:25 实战-24-常见的GC模式 13:38 实战-25-基础JVM参数的设置 28:31 实战-26-垃圾回收器的选择 18:04 实战-27-垃圾回收参数调优 07:56 实战-28-实战-GC调优和内存调优 30:43 实战-29-性能问题的现象和解决思路 10:49 实战-30-定位进程CPU占用率高的问题 18:52 实战-31-接口响应时间很长问题的定位 14:44 实战-32-火焰图定位接口响应时间长的问题 12:03 实战-33-死锁问题的检测 14:37 实战-34-基准测试框架JMH的使用 28:24 实战-35-实战-性能调优 26:36 高级篇-01-GraalVM介绍 12:13 高级篇-02-GraalVM的两种运行模式 15:43 高级篇-03-使用SpringBoot3构建GraalVM应用 15:08 高级篇-04-将GraalVM应用部署到函数计算 25:13 高级篇-05-将GraalVM应用部署到Serverless 09:14 高级篇-06-参数优化和故障诊断 22:31 高级篇-07-垃圾回收器的技术演进 13:09 高级篇-08-ShenandoahGC 22:50 高级篇-09-ZGC 14:35 高级篇-10-实战案例-内存不足时的垃圾回收测试 09:47 高级篇-11-JavaAgent技术 12:16 高级篇-12-JavaAgent环境搭建 15:24 高级篇-13-查看内存的使用情况 18:48 高级篇-14-生成内存快照 13:47 高级篇-15-获取类加载器的信息 16:26 高级篇-16-打印类的源码 18:00 高级篇-17-使用ASM增强方法 29:45 高级篇-18-使用ByteBuddy打印方法执行的参数和耗时 21:55 高级篇-19-APM系统和数据采集 24:30 原理篇-01-栈上的数据存储 15:05 原理篇-02-boolean在栈上的存储方式 22:48 原理篇-03-对象在堆上的存储1 17:27 原理篇-04-对象在堆上的存储2 25:14 原理篇-05-方法调用的原理1-静态绑定 19:26 原理篇-06-方法调用的原理2-动态绑定 15:25 原理篇-07-异常捕获的原理 12:00 原理篇-08-JIT即时编译器 14:49 原理篇-09-JIT即时编译器优化手段1-方法内联 16:49 原理篇-10-JIT即时编译器优化手段2-逃逸分析 09:03 原理篇-11-g1垃圾回收器原理-年轻代回收 27:57 原理篇-12-g1垃圾回收器原理-混合回收 17:24 原理篇-13-ZGC原理 26:27 原理篇-14-ShenandoahGC原理 09:39 面试篇-01-什么是JVM 16:38 面试篇-02-字节码文件的组成 15:02 面试篇-03-什么是运行时数据区 20:09 面试篇-04-哪些区域会出现内存溢出 11:56 面试篇-05-JDK6-8内存区域上的不同 14:36 面试篇-06-类的生命周期 17:17 面试篇-07-什么是类加载器 17:05 面试篇-08-什么是双亲委派机制 12:15 面试篇-09-如何打破双亲委派机制 18:10 面试篇-10-tomcat的自定义类加载器 31:18 面试篇-11-如何判断堆上的对象有没有被引用 10:05 面试篇-12-JVM中都有哪些引用类型 16:58 面试篇-13-theadlocal中为什么要使用弱引用 12:16 面试篇-14-有哪些垃圾回收算法 24:54 面试篇-15-有哪些常用的垃圾回收器 18:55 面试篇-16-如何解决内存泄漏问题 23:52 面试篇-17-常见的JVM参数 11:11 这是目前我学习的视频集合,要不要全看,或者少了什么,有哪些重要内容需要进行学习汇总或刷题或通过小实例验证
10-02
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值