Java学习笔记04

Java 反射 Reflection

一、什么是反射

反射机制:程序运行期间,可以获取类的完整信息、操作对象(构造方法、成员变量、成员方法)。

正常正向调用:new 对象() → 编译期确定类型

反射逆向操作:运行期才解析类信息,动态创建对象、调用方法。

包:java.lang.reflect.*;入口:Class

二、反射核心作用

  1. 运行时获取类名、父类、接口、属性、方法、构造器
  2. 运行时动态创建类实例
  3. 运行时调用任意对象的方法、读写成员变量(包括私有 private)
  4. 主流框架底层原理:Spring IoC、MyBatis、注解、JUnit 都大量使用反射

三、获取 Class 对象的 4 种方式

class Student{}

public class GetClassDemo {
public static void main(String[] args) throws ClassNotFoundException {
//方式1:类名.class 【推荐,不会创建对象】
Class<Student> clazz1 = Student.class;

//方式2:对象.getClass()
Student s = new Student();
Class<? extends Student> clazz2 = s.getClass();

//方式3:Class.forName("全限定类名")【框架最常用!】
Class<?> clazz3 = Class.forName("Student");

//方式4:类加载器
Class<?> clazz4 = ClassLoader.getSystemClassLoader().loadClass("Student");
}
}

一个类在 JVM 中只会加载一次,4 种方式拿到的是同一个 Class 对象

四、Class 五大核心组件

通过 Class 可以获取:

  1. Constructor:构造方法
  2. Method:成员方法
  3. Field:成员变量
  4. Annotation:注解
  5. Superclass/Interface:父类、实现接口

五、全套代码 Demo

准备实体类

public class Student {
//私有成员变量
private String name;
public int age;

//无参构造
public Student(){}
//有参构造
public Student(String name, int age){
this.name = name;
this.age = age;
}

//私有方法
private void privateTest(){
System.out.println("私有方法执行");
}

//普通成员方法
public void show(String msg){
System.out.println("show方法:" + msg);
}

@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}

Demo1:反射通过构造器创建对象

import java.lang.reflect.Constructor;

public class ReflectConstructorDemo {
public static void main(String[] args) throws Exception {
Class<Student> clazz = Student.class;

//1. 使用无参构造创建对象
Constructor<Student> con1 = clazz.getConstructor();
Student s1 = con1.newInstance();
System.out.println(s1);

//2. 获取有参构造,创建对象
Constructor<Student> con2 = clazz.getConstructor(String.class, int.class);
Student s2 = con2.newInstance("张三",18);
System.out.println(s2);
}
}

getConstructor() 只能获取public 构造器

getDeclaredConstructor() 可以获取所有权限(private/public)构造器

Demo2:反射操作成员变量 Field(突破私有)

import java.lang.reflect.Field;

public class ReflectFieldDemo {
public static void main(String[] args) throws Exception {
Class<Student> clazz = Student.class;
Student student = clazz.getConstructor().newInstance();

//getField():只能获取public字段
Field ageField = clazz.getField("age");
ageField.set(student, 20);
System.out.println(ageField.get(student));

//getDeclaredField() 获取任意权限字段(private name)
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); //暴力反射:关闭权限检查!
nameField.set(student, "李四");

System.out.println(student);
}
}

Demo3:反射调用方法 Method

import java.lang.reflect.Method;

public class ReflectMethodDemo {
public static void main(String[] args) throws Exception {
Class<Student> clazz = Student.class;
Student stu = clazz.getConstructor().newInstance();

//1. 获取public方法
Method showMethod = clazz.getMethod("show", String.class);
//调用方法:对象,方法参数
showMethod.invoke(stu, "反射调用show");

//2. 获取私有方法
Method privateMethod = clazz.getDeclaredMethod("privateTest");
privateMethod.setAccessible(true); //开启暴力反射
privateMethod.invoke(stu);
}
}

六、关键 API 区分

方法 作用 范围
getConstructor / getMethod / getField 获取public 只能访问公共成员
getDeclaredConstructor / getDeclaredMethod / getDeclaredField 获取所有权限(public/private/protected) 包含私有,但默认不能访问

setAccessible(true)

俗称:暴力反射;作用:取消 Java 访问权限检查,可以读写私有属性、调用私有方法。

安全管理器可以禁止暴力反射。

七、反射优缺点

优点:

  1. 动态性极高,框架实现核心;运行期灵活创建对象、调用方法
  2. 解耦,无需编译期硬编码 new 对象

缺点:

  1. 性能较差:绕过编译优化,需要解析 Class 元数据
  2. 破坏封装:暴力反射可以操作私有成员,违背面向对象封装
  3. 代码可读性差、复杂;编译期无法校验,异常只能运行时抛出

八、常见问题

Q1:Class.forName () 作用?

  1. 加载类,执行静态代码块;
  2. JDBC 经典用法:Class.forName("com.mysql.cj.jdbc.Driver"),驱动类自动注册。

Q2:newInstance () 过时了怎么办?

JDK9 之后 clazz.newInstance() 被废弃;推荐获取无参 Constructor 再调用 newInstance ()

Q3:反射和注解怎么配合?

反射可以读取类 / 方法上注解信息,很多框架(Spring)依靠【反射 + 注解】实现功能。

Q4:什么是暴力反射?

setAccessible(true),跳过权限校验访问私有成员。

九、反射典型应用场景

  1. JDBC 加载数据库驱动
  2. Spring:IOC 容器反射创建 Bean 对象、依赖注入
  3. MyBatis:反射实例化实体类,封装数据库结果集
  4. JUnit:反射执行 @Test 标记的测试方法
  5. 通用工具:对象属性拷贝

JDBC 全套知识点

一、JDBC 概述

定义

JDBC(Java Database Connectivity):Java 访问数据库的统一规范接口,定义了一套访问数据库的标准 API。

  • 接口:java.sql.*javax.sql.*,由 JDK 提供
  • 实现:各大数据库厂商提供数据库驱动(MySQL、Oracle、SQLServer 等),实现 JDBC 接口

核心优势

  1. 一套 Java 代码,更换数据库仅需更换驱动与连接参数,无需大量改业务代码
  2. 统一屏蔽不同数据库底层差异

核心组件

  1. Driver 驱动:数据库厂商实现,桥接 Java 与数据库
  2. DriverManager:驱动管理器,注册驱动、获取数据库连接
  3. Connection:数据库连接对象(会话)
  4. Statement / PreparedStatement / CallableStatement:执行 SQL 语句
  5. ResultSet:查询结果集,封装查询返回的表格数据

二、JDBC 完整执行 6 大固定步骤

  1. 导入数据库驱动依赖
  2. 注册数据库驱动
  3. 获取数据库连接 Connection
  4. 获取 SQL 执行对象 Statement/PreparedStatement
  5. 执行 SQL,处理结果集 ResultSet
  6. 关闭所有资源(ResultSet、Statement、Connection)

三、环境准备(MySQL 为例)

Maven 依赖(MySQL8.0+)

<!-- MySQL驱动 8.x -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>

MySQL5.x 驱动类:com.mysql.jdbc.Driver

MySQL8.x 驱动类:com.mysql.cj.jdbc.Driver

连接 URL 模板

jdbc:mysql://ip:端口/数据库名?参数
常用参数:
useSSL=false 关闭SSL
serverTimezone=Asia/Shanghai 时区(8.0必须加,否则时区报错)
useUnicode=true&characterEncoding=utf8 指定编码

示例:

jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=Asia/Shanghai&useSSL=false&useUnicode=true&characterEncoding=utf8

四、原生 JDBC 代码 Demo

Demo1:Statement(不推荐,存在 SQL 注入)

import java.sql.*;

public class JdbcStatementDemo {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
//1.注册驱动(MySQL8.0驱动内部自动注册,可省略Class.forName)
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
String url = "jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=Asia/Shanghai";
String user = "root";
String pwd = "123456";
conn = DriverManager.getConnection(url, user, pwd);
//3.创建执行对象
stmt = conn.createStatement();
//4.执行查询SQL
String sql = "select id,name,age from user";
rs = stmt.executeQuery(sql);
//5.遍历结果集
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
System.out.println(id + " " + name + " " + age);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//6.关闭资源,逆序关闭
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}

Statement 致命缺点:SQL 注入

拼接字符串 SQL,恶意输入可篡改 SQL 逻辑,项目禁止使用 Statement

Demo2:PreparedStatement(标准推荐,预编译防注入)

原理:SQL 模板预编译,使用?占位符,参数单独赋值,SQL 结构固定,杜绝注入。

import java.sql.*;

public class JdbcPreparedDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=Asia/Shanghai";
conn = DriverManager.getConnection(url, "root", "123456");
//SQL模板,?为占位符
String sql = "select * from user where name = ? and age = ?";
pstmt = conn.prepareStatement(sql);
//给占位符赋值:下标从1开始
pstmt.setString(1, "张三");
pstmt.setInt(2, 18);
//执行查询,无需传入SQL
rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getInt("id") + " " + rs.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}

Demo3:增删改(executeUpdate)

//新增、修改、删除统一使用 executeUpdate(),返回受影响行数
String insertSql = "insert into user(name,age) values(?,?)";
pstmt = conn.prepareStatement(insertSql);
pstmt.setString(1, "李四");
pstmt.setInt(2, 22);
int rows = pstmt.executeUpdate();
System.out.println("影响行数:"+rows);

五、三大执行对象对比

对象 特点 安全性 适用场景
Statement 字符串拼接 SQL,每次重新编译 极易 SQL 注入 几乎淘汰
PreparedStatement 预编译,? 占位符,可复用 SQL 防注入,安全 日常 CRUD 首选
CallableStatement 专门调用存储过程 安全 数据库存储过程调用

六、核心 API 详解

  1. Connection
  • createStatement():创建 Statement
  • prepareStatement(sql):创建预编译对象
  • setAutoCommit(false):关闭自动提交,开启事务
  • commit():事务提交
  • rollback():事务回滚
  1. PreparedStatement 常用方法
  • setXxx(下标,值):给?赋值,下标从1开始
  • executeQuery():执行查询,返回 ResultSet
  • executeUpdate():执行增删改,返回 int(影响行数)
  1. ResultSet 结果集
  • next():指针下移,有数据返回 true,末尾 false
  • getXxx(列名/列下标):获取值,列下标从 1 开始
  • beforeFirst():指针回到最开头

七、JDBC 事务管理(核心考点)

JDBC 默认自动提交:单条 SQL 执行完成立刻提交。

多 SQL 原子操作必须手动事务:

try{
conn.setAutoCommit(false); //关闭自动提交,开启事务
//多条DML操作
pstmt1.executeUpdate();
pstmt2.executeUpdate();
conn.commit(); //全部正常,提交事务
}catch(Exception e){
conn.rollback(); //异常,回滚
}

八、工具类封装(开发常用,简化重复代码)

把连接、关闭资源抽取为工具类,配合 properties 配置文件

db.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=Asia/Shanghai
user=root
password=123456

JdbcUtil.java

import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

public class JdbcUtil {
private static Properties prop = new Properties();
//静态加载配置文件
static {
try(InputStream is = JdbcUtil.class.getResourceAsStream("/db.properties")){
prop.load(is);
Class.forName(prop.getProperty("driver"));
}catch (Exception e){
e.printStackTrace();
}
}
//获取连接
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(
prop.getProperty("url"),
prop.getProperty("user"),
prop.getProperty("password")
);
}
//关闭资源
public static void close(Connection conn, PreparedStatement pstmt, ResultSet rs){
try{
if(rs != null) rs.close();
if(pstmt != null) pstmt.close();
if(conn != null) conn.close();
}catch (SQLException e){
e.printStackTrace();
}
}
//增删改没有ResultSet,重载方法
public static void close(Connection conn, PreparedStatement pstmt){
close(conn,pstmt,null);
}
}

九、常见问题与优化

  1. SQL 注入原因与解决方案

原因:Statement 字符串拼接;解决:强制使用 PreparedStatement 占位符预编译。

  1. 原生 JDBC 痛点

  2. 频繁创建销毁连接,性能极差

  3. 代码冗余,大量重复模板代码

    解决方案:

    数据库连接池(Druid、HikariCP)、MyBatis/MyBatis-Plus、JPA

  4. 驱动自动注册原理

JDBC4.0+ 无需手动Class.forName()

驱动包META-INF/services/java.sql.Driver文件记录驱动类,JVM 自动加载。

十、高频知识点

  1. PreparedStatement 为什么防注入?

    SQL 结构提前预编译固化,? 仅做参数,不会解析为 SQL 关键字,恶意输入只会当成普通字符串。

  2. 占位符下标为什么从 1 开始?

    JDBC 规范定义,和数据库列下标保持一致。

  3. 事务四大特性 ACID?

    原子性、一致性、隔离性、持久性。

  4. 连接池作用?

    复用数据库连接,减少创建销毁开销,控制最大连接数防止数据库崩溃。

Java 正则表达式

一、基础介绍

  1. 定义

正则表达式(Regex):一套字符串匹配规则,用来检索、替换、截取、校验文本。

Java 正则核心包:java.util.regex

核心两个类:

  • Pattern:正则表达式编译对象,封装匹配规则
  • Matcher:匹配器,执行匹配、查找、分组提取
  1. 使用场景

手机号 / 邮箱 / 身份证校验、日志提取、敏感词过滤、文本截取、字符串替换。


二、正则语法速查表

  1. 普通元字符(单字符匹配)
符号 含义
. 匹配任意单个字符(换行\n除外)
\d 数字 [0-9]
\D 非数字
\s 空白字符(空格、制表符、换行)
\S 非空白字符
\w 单词字符 [a-z,A-Z,0-9,_]
\W 非单词字符
[] 字符集,匹配括号内任意一个字符,[abc]匹配 a/b/c
[^ ] 取反,[^0-9]匹配非数字
  1. 量词(限定前面字符出现次数)
符号 含义
? 0 次 或 1 次
+ 至少 1 次(1~ 无数)
* 0 次及以上
{n} 固定 n 次
{n,} 最少 n 次
{n,m} n~m 次
  1. 边界匹配
符号 含义
^ 字符串开头
$ 字符串结尾
\b 单词边界(空格、标点、首尾)
  1. 分组与或
  • ():分组,可单独捕获内容、反向引用
  • |:或,a|b匹配 a 或者 b
  • (?:):非捕获分组,只分组不提取
  1. 转义说明(Java 独有重点)

正则里\是转义符,Java 字符串中\需要写成\\

例:正则\d → Java 代码必须写成"\\d"

  1. 贪婪 / 非贪婪
  • 默认:贪婪匹配,尽可能多匹配;
  • ?放在量词后开启非贪婪(懒惰匹配):.*?尽可能少匹配。

三、Java 三种常用使用方式

方式 1:String 自带简便方法(日常快速校验)

  1. boolean matches(String regex)全串严格匹配(等价于^正则$
  2. String replaceAll(String regex,String replacement):正则全局替换
  3. String[] split(String regex):按正则分割字符串

Demo:字符串自带方法

public class StringRegexDemo {
public static void main(String[] args) {
//1.手机号校验 1开头,第二位3-9,后面9位数字
String phoneReg = "^1[3-9]\\d{9}$";
String phone = "13812345678";
System.out.println(phone.matches(phoneReg)); //true

//2.替换所有数字为空
String str = "a1b2c3d4";
String res = str.replaceAll("\\d", "");
System.out.println(res); //abcd

//3.按非字母分割
String s = "java,python;c++ go";
String[] arr = s.split("[,; ]");
for (String item : arr) {
System.out.print(item + " ");
}
}
}

方式 2:Pattern + Matcher 精准查找、分组提取(最常用)

核心方法:

  • Pattern.compile(reg):编译正则
  • matcher.find():查找下一个匹配片段,找到返回 true
  • matcher.group():获取匹配到的完整内容
  • matcher.group(n):获取第 n 个分组内容(括号分组)

Demo1:提取所有数字

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PatternMatcherDemo {
public static void main(String[] args) {
String content = "订单1001,金额99.9,编号20260802";
String reg = "\\d+";
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(content);

while (matcher.find()) {
System.out.println("匹配:" + matcher.group());
}
}
}

Demo2:分组捕获(提取邮箱用户名、域名)

public class GroupDemo {
public static void main(String[] args) {
String email = "zhangsan@qq.com";
//分组1:用户名 分组2:域名
String reg = "^(\\w+)@(.+)$";
Pattern p = Pattern.compile(reg);
Matcher m = p.matcher(email);
if (m.matches()) {
System.out.println("用户名:" + m.group(1));
System.out.println("域名:" + m.group(2));
}
}
}

方式 3:替换分组内容

//隐藏手机号中间4位
public static void main(String[] args) {
String phone = "13812345678";
String res = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
System.out.println(res); //138****5678
}

$1代表第 1 分组内容,$2代表第 2 分组内容


四、高频实用正则模板(可直接复制使用)

//1.手机号
String phone = "^1[3-9]\\d{9}$";
//2.邮箱
String email = "^\\w+@[a-zA-Z0-9]+(\\.[a-zA-Z]{2,})+$";
//3.身份证18位
String idCard = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$";
//4.纯数字
String number = "^\\d+$";
//5.6-16位密码:字母+数字,至少包含1个数字1个字母
String pwd = "^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z]{6,16}$";

五、贪婪匹配与非贪婪匹配案例

public static void main(String[] args) {
String str = "<div>内容1</div><div>内容2</div>";
//贪婪匹配 .* 一次性匹配到最后
Matcher m1 = Pattern.compile("<div>.*</div>").matcher(str);
m1.find();
System.out.println(m1.group()); //整串全部匹配

//非贪婪 .*? 最短匹配
Matcher m2 = Pattern.compile("<div>.*?</div>").matcher(str);
while (m2.find()){
System.out.println(m2.group());
}
}

六、重要知识点

  1. matches () 和 find () 区别
  • matches()整串完全匹配,默认带^$
  • find():片段查找,只要字符串存在匹配片段就返回 true。
  1. 正则性能问题
  • 复杂正则易出现回溯爆炸,大量文本下卡顿;
  • 能用普通字符串方法就不用复杂正则。
  1. 分组编号规则

从左到右,左括号(顺序依次为 1、2、3;group (0) 永远代表完整匹配内容。

  1. 非捕获分组(?:xxx)

只用来分组限定范围,不会占用分组编号,适合不需要提取、只做规则划分的场景。

Lambda表达式

Lambda表达式的基础语法:Java8中引入了一个新的操作符”->“ 该操作符称为箭头操作符或Lambda操作符

基础介绍

概念

Lambda 表达式是Java 8 新增的语法,本质是简洁的匿名函数,用来简化函数式接口的实现,替代臃肿的匿名内部类,把代码当成参数传递。

核心定位:行为参数化,把一段逻辑当做方法入参。

作用

  1. 简化匿名内部类写法,代码更精简;

  2. 配合流式编程Stream,实现集合便捷遍历、筛选、排序;

  3. 用于线程、回调、Comparator 比较器等场景。

  4. 语法格式

(参数列表) -> {方法体}
  • ->:Lambda 运算符,分为左侧参数、右侧执行体

四种简写规则(重点)

  1. 参数只有 1 个:小括号()可以省略
s -> System.out.println(s);
  1. 无参数:必须保留空括号()
() -> System.out.println("无参");
  1. 参数类型可省略:编译器自动类型推断,无需声明参数类型
// 简写
(a,b)->a+b;
// 完整写法
(Integer a,Integer b)->a+b;
  1. 方法体只有一行代码:大括号{}return、分号可全部省略
// 单行有返回值,省略{}和return
(a,b)->a+b;
// 多行代码必须加{},return不能省略
(a,b)->{
int sum = a+b;
return sum*2;
};

Lambda 使用前提:函数式接口

  1. 定义

函数式接口有且仅有一个抽象方法的接口,可以有默认方法、静态方法、Object 重写方法。 Lambda 只能绑定函数式接口,用来实现那唯一的抽象方法。

  1. 注解标识

@FunctionalInterface:强制校验当前接口是否为函数式接口,编译报错提醒,推荐所有函数式接口都加。

@FunctionalInterface
interface MyFunc{
void test(); // 唯一抽象方法
default void hello(){} // 默认方法允许存在
static void staticFunc(){} // 静态方法允许存在
@Override
String toString(); // Object方法不计入抽象方法
}
  1. Java 内置四大核心函数式接口(高频使用)
接口 抽象方法 作用 适用场景
Consumer<T>消费型 void accept(T t) 接收参数,无返回值 遍历、打印、修改对象
Supplier<T>供给型 T get() 无参数,返回对象 生成对象、随机数
Predicate<T>断定型 boolean test(T t) 入参,返回布尔值 集合筛选、条件判断
Function<T,R>函数型 R apply(T t) T 入参,R 返回值 类型转换、数据处理

示例

// Consumer 消费
Consumer<String> con = s->System.out.println(s);
con.accept("测试");

// Supplier 供给
Supplier<Integer> sup = ()->new Random().nextInt(10);

// Predicate 判断
Predicate<Integer> pre = n->n>5;

// Function转换
Function<String,Integer> fun = str->Integer.parseInt(str);

Lambda 三大引用(方法引用、构造器引用、数组引用)

属于 Lambda 进一步简写,当 Lambda 只是调用已有方法时使用,格式更简洁。

方法引用 3 种形式

  1. 对象::实例方法:Lambda 调用某个对象的普通成员方法
Consumer<String> c = str->System.out.println(str);
// 简写
Consumer<String> c = System.out::println;
  1. 类::静态方法:调用类静态方法
Comparator<Integer> com = (a,b)->Integer.compare(a,b);
Comparator<Integer> com = Integer::compare;
  1. 类::实例方法:参数 1 作为调用者,参数 2 作为方法入参
BiPredicate<String,String> bp = (s1,s2)->s1.equals(s2);
BiPredicate<String,String> bp = String::equals;

构造器引用

格式:类名::new,适配 Supplier/Function

Supplier<User> sup = ()->new User();
Supplier<User> sup = User::new;

Function<String,User> fun = name->new User(name);
Function<String,User> fun = User::new;

数组引用

格式:类型[]::new

Function<Integer,String[]> fun = len->new String[len];
Function<Integer,String[]> fun = String[]::new;

Lambda 变量作用域

可访问范围

  1. 静态变量、成员变量:可读可修改;
  2. 局部变量隐式 final,Java8 之后局部变量即使不加 final,也不允许二次修改;
int num = 10;
// 合法,num未修改
Runnable run = ()->System.out.println(num);
// 报错,Lambda引用局部变量不能重新赋值
num = 20;

禁止使用

Lambda 内部不能定义与外部重名的局部变量;不允许使用this指向外部类以外对象,Lambda 没有独立 this。

Lambda 与匿名内部类区别

对比项 Lambda 匿名内部类
结构 只能绑定函数式接口 普通接口、抽象类都能用
this 指向 外层类对象 匿名内部类自身
变量 局部变量隐式 final 局部变量必须显式 final (JDK7)
编译 生成 invokedynamic 动态调用 生成独立 class 文件
可读性 极简,逻辑复杂可读性差 结构完整,复杂逻辑清晰

Lambda+Stream 流式编程(最常用场景)

Lambda 最大价值就是配合 Stream 处理集合,核心中间操作 + 终止操作

常用中间操作(返回 Stream,可链式调用)

  • filter (Predicate):过滤元素
  • map (Function):元素转换
  • sorted (Comparator):排序
  • distinct ():去重
  • limit (n):截取前 n 个
  • skip (n):跳过前 n 个

常用终止操作(关闭流,不可继续调用)

  • collect ():转为 List、Set、Map
  • forEach (Consumer):遍历
  • count ():统计数量
  • max/min:最值
  • anyMatch/allMatch/noneMatch:匹配判断

示例

List<Integer> list = Arrays.asList(1,2,3,4,5,6);
// 筛选偶数、平方、升序、转为集合
List<Integer> res = list.stream()
.filter(n->n%2==0)
.map(n->n*n)
.sorted()
.collect(Collectors.toList());

Lambda 优缺点

优点

  1. 代码极度精简,消除模板代码;
  2. 便于流式编程,集合处理优雅;
  3. 延迟执行,性能在大批量集合更优。

缺点

  1. 调试困难,无单独类文件,堆栈不直观;
  2. 复杂逻辑可读性差,多层嵌套难以理解;
  3. 无法重载,仅支持单抽象方法接口;
  4. 递归实现繁琐。

高频易错点总结

  1. 无抽象方法、多个抽象方法的接口不能用 Lambda
  2. 多行方法体必须加大括号,有返回值必须写 return;
  3. 局部变量一旦被 Lambda 引用,就不可二次赋值;
  4. Lambda 不能抛出受检异常,需要手动 try-catch;
  5. 不要滥用 Lambda,超长复杂逻辑优先普通方法。

Java Stream API

Stream 是 Java8 引入的流式编程,用于集合、数组等数据源的遍历、过滤、映射、聚合、排序等操作,核心思想:把集合转为流式流水线,链式调用,延迟执行,大幅简化循环代码,配合 Lambda、方法引用使用。

一、Stream 核心定位

  1. 用途:对集合数据做复杂计算,替代传统 for/foreach 循环;
  2. 特点
    • 单向一次性:流只能遍历一次,消费完毕就关闭,不可重复使用;
    • 惰性求值:中间操作只记录逻辑,不执行,只有终止操作触发才真正计算
    • 不修改原数据源:所有操作产生新流 / 新结果,原集合不变;
    • 支持串行、并行流stream()串行,parallelStream()多线程并行。
  3. 和集合区别
    • 集合:侧重存储数据
    • Stream:侧重处理数据

二、Stream 三要素

  1. 数据源:集合、数组、IO 流、生成器;
  2. 中间操作:链式调用,返回新 Stream,惰性执行;
  3. 终止操作:关闭流,执行计算,返回结果 /void。

1.如何创建 Stream(数据源)

1)集合创建(最常用)

List<String> list = new ArrayList<>();
Stream<String> stream = list.stream(); // 串行流
Stream<String> parallelStream = list.parallelStream(); // 并行流

2)数组创建

int[] arr = {1,2,3};
IntStream stream = Arrays.stream(arr);
Stream<Integer> stream2 = Stream.of(arr);

3)静态方法 Stream.of ()

Stream<Integer> stream = Stream.of(1,2,3,4,5);

4)无限流(生成无限序列)

// 迭代生成:从0开始,每次+2
Stream.iterate(0, n -> n+2).limit(10).forEach(System.out::println);
// 随机数生成
Stream.generate(Math::random).limit(5).forEach(System.out::println);

5)其他数据源

文件:Files.lines(Paths.get("xxx.txt"))

枚举:Enum.stream()

2. 中间操作(Intermediate)

调用后返回 Stream,不执行计算,可链式拼接,常用分类:过滤、映射、排序、去重、截断、合并。

一.筛选与切片

方法 作用
filter(Predicate) 过滤,保留满足条件元素
limit(n) 截取前 n 个元素
skip(n) 跳过前 n 个元素
distinct() 去重(依赖 equals/hashCode)

示例:

List<Integer> nums = Arrays.asList(1,2,2,3,4,5,6);
nums.stream()
.filter(x -> x>2)
.distinct()
.skip(1)
.limit(2)
.forEach(System.out::println); // 输出4、5

二.映射 map 系列(元素类型转换)

  1. map(Function):一对一转换,每个元素映射成一个新元素
// 字符串转长度
Stream.of("java","stream").map(String::length).forEach(System.out::println);
  1. flatMap(Function):一对多,把流中每个元素拆解为流,扁平化

解决:集合嵌套、拆分字符串、拆分数组

// 拆分字符串 ["a,b","c,d"] → 平铺为 a,b,c,d
List<String> list = Arrays.asList("a,b","c,d");
list.stream()
.flatMap(s -> Arrays.stream(s.split(",")))
.forEach(System.out::println);

基础类型映射:mapToInt/mapToLong/mapToDouble 作用:拆箱,避免频繁包装类,提升性能,对应IntStream/LongStream/DoubleStream

  1. 排序 sorted

sorted():自然排序,元素实现 Comparable

sorted(Comparator):自定义比较器

Stream.of(3,1,2).sorted().forEach(System.out::println);
// 降序
Stream.of(3,1,2).sorted((a,b)->b-a).forEach(System.out::println);
  1. peek 调试

peek(Consumer):遍历查看元素,不改变流,多用于打印日志调试:

Stream.of(1,2,3).peek(System.out::println).map(x->x*2).collect(Collectors.toList());

3.终止操作(Terminal)

一旦调用,流关闭,触发所有中间操作执行,不可再使用流。 分为:遍历、查找匹配、规约、收集、统计。

  1. 遍历 forEach /forEachOrdered
  • forEach:无序遍历,并行流乱序;
  • forEachOrdered:保证顺序遍历。
  1. 查找与匹配(返回布尔 / Optional)
方法 作用
allMatch(Predicate) 全部匹配返回 true
anyMatch(Predicate) 任意一个匹配返回 true
noneMatch(Predicate) 全都不匹配返回 true
findFirst() 获取第一个元素 Optional
findAny() 随机获取一个,并行流效率高
boolean all = Stream.of(1,2,3).allMatch(x->x>0);
Optional<Integer> first = Stream.of(1,2,3).findFirst();
  1. 规约 reduce(聚合计算,核心)

将流中元素反复结合,得到一个值,求和、最值、拼接。

  1. 单参数:reduce(BinaryOperator) 返回 Optional
Optional<Integer> sum = Stream.of(1,2,3).reduce((a,b)->a+b);
  1. 双参数:reduce(初始值, BinaryOperator) 返回具体类型
Integer sum = Stream.of(1,2,3).reduce(0,Integer::sum);
  1. 三参数:并行规约专用。
  1. 收集 collect(最常用,Collectors 工具类)

collect(Collector):把流转为集合、数组、字符串、分组、分区、统计。

(1)转为集合

List<Integer> list = stream.collect(Collectors.toList());
Set<Integer> set = stream.collect(Collectors.toSet());
// 指定ArrayList
ArrayList<Integer> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));

(2)转数组

Integer[] arr = stream.toArray(Integer[]::new);

(3)拼接字符串 joining

String str = Stream.of("a","b","c").collect(Collectors.joining("-","[","]"));
// [a-b-c]

(4)统计类 Collectors

IntSummaryStatistics stat = Stream.of(1,2,3).collect(Collectors.summarizingInt(x->x));
stat.getMax(); stat.getMin(); stat.getSum(); stat.getAverage(); stat.getCount();

快捷方法:count()、max()、min()

(5)分组 groupingBy

List<User> users = ...;
// 按年龄分组
Map<Integer,List<User>> group = users.stream().collect(Collectors.groupingBy(User::getAge));
// 多级分组
Map<Integer,Map<String,List<User>>> group2 = users.stream()
.collect(Collectors.groupingBy(User::getAge, Collectors.groupingBy(User::getGender)));

(6)分区 partitioningBy

按条件分为 true/false 两组

Map<Boolean,List<User>> part = users.stream()
.collect(Collectors.partitioningBy(u->u.getAge()>18));

(7)聚合收集:求和、平均数、最值

Collectors.summingInt()、averagingInt()、maxBy()、minBy()

三、并行流 parallelStream

开启方式

stream.parallel() // 串行流转并行
list.parallelStream() // 直接创建并行流

底层基于 Fork/Join 框架,多线程拆分合并。

注意事项

  1. 并行不一定更快:数据量小、简单计算,线程开销 > 计算耗时;
  2. 并行无序,需要顺序用 forEachOrdered
  3. 禁止在并行流操作非线程安全变量(ArrayList、普通成员变量会并发异常);
  4. 有状态操作(limit、sorted、distinct)并行效率会大幅下降。

四、Optional 配合 Stream(空指针防护)

findFirst、findAny、reduce 都会返回 Optional,避免 NPE:

Integer num = Stream.of(1,2,3).filter(x->x>10).findFirst().orElse(0);

五、常用实操经典案例

案例 1:去重、过滤、求和

List<Integer> list = Arrays.asList(1,1,2,3,4,5);
int sum = list.stream().distinct().filter(x->x%2==0).reduce(0,Integer::sum);

案例 2:对象集合分组统计

class Student{String name;Integer age;}
// 统计各年龄人数
Map<Integer,Long> ageCount = studentList.stream()
.collect(Collectors.groupingBy(Student::getAge, Collectors.counting()));

案例 3:扁平化集合

List<List<Integer>> nested = Arrays.asList(Arrays.asList(1,2),Arrays.asList(3,4));
List<Integer> flat = nested.stream().flatMap(List::stream).collect(Collectors.toList());

六、重要知识点

  1. 流只能用一次:中间操作后流消费完毕,重复调用抛 IllegalStateException
  2. 惰性求值:中间操作写了异常逻辑,不调用终止操作不会报错;
  3. map 与 flatMap 混淆:普通映射用 map,拆分层级用 flatMap;
  4. 并行流线程不安全:不要修改外部变量;
  5. distinct 依赖 equals,自定义对象重写 equals+hashCode;
  6. 基础流(IntStream)无装箱开销,大数据优先使用。

七、Java8~Java11 Stream 新增小特性

  1. Stream.ofNullable():支持 null 数据源,null 会生成空流,防空指针;
  2. takeWhile(predicate):从头截取直到不满足条件;
  3. dropWhile(predicate):从头丢弃直到不满足条件。