业务逻辑中如何更好的处理空值?

本文探讨了Java集合处理的最佳实践,特别是在返回空集合而非null,以及如何利用Optional提高代码可读性和安全性方面。深入分析了listUser和get方法的实现,提出了使用Optional的场景化建议,包括作为单个实体返回值时的判空约定。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


1 场景

存在一个UserSearchService用来提供用户查询的功能:

public interface UserSearchService{
  List<User> listUser();

  User get(Integer id);
}
2 问题现场

对于面向对象语言来讲,抽象层级特别的重要。尤其是对接口的抽象,它在设计和开发中占很大的比重,我们在开发时希望尽量面向接口编程。
对于以上描述的接口方法来看,大概可以推断出可能它包含了以下两个含义:

  • listUser(): 查询用户列表
  • get(Integer id): 查询单个用户

当我们进行代码逻辑简单的推断分析之后:

  • listUser() 如果没有数据,那它是返回空集合还是null呢?
  • get(Integer id) 如果没有这个对象,是抛异常还是返回null呢?
2.1 深入listUser研究

我们先来讨论:

listUser()

这个接口,我们经常看到如下实现:

public List<User> listUser(){
    List<User> userList = userListRepostity.selectByExample(new UserExample());
    if(CollectionUtils.isEmpty(userList)){//spring util工具类
      return null;
    }
    return userList;
}

这段代码返回时null,从我多年的开发经验来讲,对于集合这样返回值,最好不要返回null,因为如果返回了null,会给调用者带来很多麻烦。你将会把这种调用风险交给调用者来控制。

如果调用者是一个谨慎的人,他会进行是否为null的条件判断。如果他并非谨慎,或者他是一个面向接口编程的狂热分子(当然,面向接口编程是正确的方向),他会按照自己的理解去调用接口,而不进行是否为null的条件判断,如果这样的话,是非常危险的,它很有可能出现空指针异常!

根据墨菲定律来判断: “很有可能出现的问题,在将来一定会出现!

基于此,我们将它进行优化:

public List<User> listUser(){
    List<User> userList = userListRepostity.selectByExample(new UserExample());
    if(CollectionUtils.isEmpty(userList)){
      return Lists.newArrayList();//guava类库提供的方式
    }
    return userList;
}

对于接口(List listUser()) 它一定会返回List,即使没有数据,它仍然会返回List(集合中没有任何元素);
通过以上的修改,我们成功的避免了有可能发生的空指针异常,这样的写法更安全!

2.2 深入研究get方法

对于接口

User get(Integer id)

你能看到的现象是,我给出id,它一定会给我返回User.但事实真的很有可能不是这样的。

我看到过的实现:

public User get(Integer id){
  return userRepository.selectByPrimaryKey(id);//从数据库中通过id直接获取实体对象
}

相信很多人也都会这样写。

通过代码的时候得知它的返回值很有可能是null! 但我们通过的接口是分辨不出来的!

这个是个非常危险的事情。尤其对于调用者来说!

我给出的建议是,需要在接口明显时补充文档,比如对于异常的说明,使用注解@exception:

public interface UserSearchService{

  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体
   * @exception UserNotFoundException
   */
  User get(Integer id);

}

我们把接口定义加上了说明之后,调用者会看到,如果调用此接口,很有可能抛出“UserNotFoundException(找不到用户)”这样的异常。

这种方式可以在调用者调用接口的时候看到接口的定义,但是,这种方式是“弱提示”,如果调用者忽略了注释,有可能就对业务系统产生了风险,这个风险有可能导致一个亿!

除了以上这种”弱提示”的方式,还有一种方式是,返回值是有可能为空的。那要怎么办呢?

我认为我们需要增加一个接口,用来描述这种场景.
引入jdk8的Optional,或者使用guava的Optional.看如下定义:

public interface UserSearchService{

  /**
 * 根据用户id获取用户信息
 * @param id 用户id
 * @return 用户实体,此实体有可能是缺省值
   */
  Optional<User> getOptional(Integer id);
}

Optional有两个含义: 存在 or 缺省。
那么通过阅读接口getOptional(),我们可以很快的了解返回值的意图,这个其实是我们想看到的,它去除了二义性。
它的实现可以写成:

public Optional<User> getOptional(Integer id){
  return Optional.ofNullable(userRepository.selectByPrimaryKey(id));
}
2.3 深入入参

通过上述的所有接口的描述,你能确定入参id一定是必传的吗?我觉得答案应该是:不能确定。除非接口的文档注释上加以说明。
那如何约束入参呢?
我给大家推荐两种方式:

  • 强制约束
  • 文档性约束(弱提示)

1.强制约束,我们可以通过jsr 303进行严格的约束声明:

public interface UserSearchService{
  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体
   * @exception UserNotFoundException
   */
  User get(@NotNull Integer id);

  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体,此实体有可能是缺省值
   */
  Optional<User> getOptional(@NotNull Integer id);
}

2.文档性约束
在很多时候,我们会遇到遗留代码,对于遗留代码,整体性改造的可能性很小。
我们更希望通过阅读接口的实现,来进行接口的说明。
jsr 305规范,给了我们一个描述接口入参的一个方式(需要引入库 com.google.code.findbugs:jsr305):可以使用注解: @Nullable @Nonnull @CheckForNull 进行接口说明。

比如:

public interface UserSearchService{
  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体
   * @exception UserNotFoundException
   */
  @CheckForNull
  User get(@NonNull Integer id);

  /**
   * 根据用户id获取用户信息
   * @param id 用户id
   * @return 用户实体,此实体有可能是缺省值
   */
  Optional<User> getOptional(@NonNull Integer id);
}

通过空集合返回值,Optional,jsr303,jsr305这几种方式,可以让我们的代码可读性更强,出错率更低

  • 空集合返回值 :如果有集合这样返回值时,除非真的有说服自己的理由,否则,一定要返回空集合,而不是null
  • Optional: 如果你的代码是jdk8,就引入它!如果不是,则使用Guava的Optional,或者升级jdk版本!它很大程度的能增加了接口的可读性!
  • jsr 303: 如果新的项目正在开发,不防加上这个试试!一定有一种特别爽的感觉!
  • jsr 305: 如果老的项目在你的手上,你可以尝试的加上这种文档型注解,有助于你后期的重构,或者新功能增加了,对于老接口的理解!
2.4 Optional的场景化的使用建议
2.4.1 Optional不要作为参数

如果你写了一个public 方法,这个方法规定了一些输入参数,这些参数中有一些是可以传入null的,那这个时候是否可以使用Optional呢?

我给的建议是: 一定不要这样使用!

举个例子:

public interface UserService{
  List<User> listUser(Optional<String> username);
}

这个例子的方法 listUser,可能在告诉我们需要根据username查询所有数据集合,如果username是空,也要返回所有的用户集合.

当我们看到这个方法的时候,会觉得有一些歧义:

“如果username是absent,是返回空集合吗?还是返回全部的用户数据集合?”

Optioanl是一种分支的判断,那我们究竟是关注 Optional还是Optional.get()呢?
我给大家的建议是,如果不想要这样的歧义,就不要使用它!

2.4.2 Optional作为单个实体的返回值,可以进行判空约定

单个实体的返回:
那Optioanl可以做为返回值吗?
其实它是非常满足是否存在这个语义的。
你如说,你要根据id获取用户信息,这个用户有可能存在或者不存在。
你可以这样使用:

public interface UserService{
  Optional<User> get(Integer id);
}

当调用这个方法的时候,调用者很清楚get方法返回的数据,有可能不存在,这样可以做一些更合理的判断,更好的防止空指针的错误!

当然,如果业务方真的需要根据id必须查询出User的话,就不要这样使用了,请说明,你要抛出的异常.

只有当考虑它返回null是合理的情况下,才进行Optional的返回

2.4.3 集合实体的返回,不建议使用Optional

集合实体的返回如果是Optional的化,会让调用者产生困惑,是否我判断了Optional之后,还用进行isEmpty的判断嘛?

2.4.4 单纯的使用Optional变量

对于

Optional<User> userOptional = .....

如果有这样的变量userOptional ,请记住:

  • 一定不能直接使用get ,如果这样用,就丧失了Optional本身的含义 ( 比如userOptional .get() )
  • 不要直接使用getOrThrow ,如果你有这样的需求:获取不到就抛异常。 那就要考虑,是否是调用的接口设计的是否合理
2.4.5 getter场景下的Optional不建议使用

对于一个java bean,所有的属性都有可能返回null,那是否需要改写所有的getter成为Optional类型呢?

我给大家的建议是,不要这样滥用Optional.

即便 我java bean中的getter是符合Optional的,但是因为java bean 太多了,这样会导致你的代码有50%以上进行Optinal的判断,这样便污染了代码。(我想说,其实你的实体中的字段应该都是由业务含义的,会认真的思考过它存在的价值的,不能因为Optional的存在而滥用)

我们应该更关注于业务,而不只是空值的判断。

请不要在getter中滥用Optional.

<think>嗯,用户之前问过关于客户数据库迁移到公司数据库的挑战,现在接着问如何处理ETL会更好。这说明他们可能正在计划或正在进行数据迁移项目,需要具体的ETL优化方法。首先,我需要回顾ETL的基本流程,然后结合之前的挑战,比如数据结构差异、数据质量问题等,给出具体的处理策略。用户可能希望得到实用的步骤或工具推荐,而不仅仅是理论上的建议。 接下来,我应该考虑用户可能的实际需求。他们可能遇到性能问题,比如处理大数据量时的速度,或者数据清洗的复杂性。这时候需要提到分区处理、增量抽取等方法。另外,数据质量方面,之前提到过脏数据和格式问题,所以需要强调数据清洗的具体步骤,比如去重、标准化等。 用户可能还想知道如何选择工具,比如开源的Apache NiFi还是商业的Informatica,或者云服务如AWS Glue。需要比较它们的优缺点,帮助用户根据实际情况选择。此外,验证和监控也是关键,用户可能担心迁移后的数据准确性,所以需要包括校验方法和监控指标。 还要考虑到用户的安全和合规需求,比如数据脱敏和权限控制,这在之前的回答中也提到过,所以需要再次强调。最后,提供分阶段实施和文档管理的建议,帮助用户系统化地管理整个ETL流程,避免遗漏重要步骤。 总结来说,用户需要的是一个结构清晰、覆盖ETL全流程的优化方案,结合之前的挑战,给出具体的解决方案和工具建议,确保他们能够顺利实施数据迁移项目。</think>以下是优化ETL(Extract-Transform-Load)流程的关键策略与实践方法,结合结构化步骤与场景化示例: --- ### **一、设计阶段优化** #### 1. **精准定义需求** - **目标**:明确业务规则与数据用途,避免过度清洗或遗漏关键字段。 - **示例**: - 若目标为生成财务报表,需明确“收入”字段的计算逻辑(是否含税、是否合并子公司数据)。 - 若目标为用户画像,需保留原始行为日志的细粒度数据。 #### 2. **Schema映射预分析** - **工具**:使用数据对比工具(如Redgate Schema Compare)生成字段映射表。 - **示例**: ```markdown | 客户字段 | 公司字段 | 转换规则 | |---------------|----------------|----------------------------| | `cust_name` | `user_name` | 去除首尾空格,转大写 | | `phone_raw` | `phone` | 提取数字,添加国家代码+86 | ``` --- ### **二、抽取(Extract)优化** #### 1. **增量抽取替代全量抽取** - **方法**:通过时间戳、版本号或变更日志(如MySQL Binlog)捕获增量数据。 - **优势**:减少带宽与处理时间,尤其适用于TB级数据。 #### 2. **分区与并行化** - **实践**: - 按日期或ID范围切分数据块(如`WHERE date BETWEEN '2023-01-01' AND '2023-01-31'`); - 使用多线程/分布式框架(如Apache Spark)并行拉取。 --- ### **三、转换(Transform)优化** #### 1. **分层清洗策略** - **步骤**: - **基础清洗**:去重、补全空值(如用默认值填充缺失的`gender`字段); - **标准化**:统一格式(如将`2023/1/1`转为`DATE`类型); - **业务规则**:应用计算逻辑(如根据`订单金额`和`汇率`计算本地货币值)。 #### 2. **使用内存计算减少I/O** - **工具**:在内存中完成复杂计算(如Pandas处理DataFrame),避免反复读写临时表。 #### 3. **预计算高频指标** - **场景**: - 若目标库需频繁查询“用户月度消费总额”,可在ETL阶段直接生成聚合结果表,避免实时计算开销。 --- ### **四、加载(Load)优化** #### 1. **批量写入与事务控制** - **方法**: - 使用`INSERT BATCH`(如PostgreSQL的`COPY`命令)替代逐条插入; - 按数据块划分事务,避免单一大事务锁表。 #### 2. **目标库适配写入** - **优化点**: - 对关系型数据库(如MySQL)按索引顺序写入; - 对NoSQL数据库(如MongoDB)使用批量`BulkWrite`接口。 --- ### **五、容错与监控** #### 1. **异常处理机制** - **设计**: - 记录脏数据到隔离区(Quarantine Zone),并触发告警; - 设置重试策略(如网络中断时自动重试3次)。 #### 2. **全链路监控** - **指标**: ```markdown - 抽取耗时占比 | 转换错误率 | 加载记录数/秒 - 内存峰值使用量 | 脏数据比例 | 数据一致性校验结果 ``` - **工具**:Grafana可视化看板 + Prometheus指标采集。 --- ### **六、工具与技术选型建议** | **场景** | **推荐工具** | **优势** | |------------------------|--------------------------|------------------------------------------| | 简单结构化数据迁移 | Apache NiFi | 低代码拖拽式配置,支持数据流监控 | | 复杂业务逻辑转换 | Python + PySpark | 灵活编写自定义清洗逻辑,适合分布式处理 | | 云原生环境 | AWS Glue + Lambda | 无服务器架构,按用量计费 | | 企业级任务调度 | Airflow | 支持依赖管理、定时触发与任务重试 | --- ### **七、典型错误与规避方法** 1. **错误**:在转换阶段过度依赖SQL导致性能瓶颈 **规避**:将计算下推至抽取阶段(如用`SELECT`过滤无效数据)。 2. **错误**:忽略字符集差异(如UTF-8 vs. GBK) **规避**:在抽取后立即统一转码。 3. **错误**:未清理测试数据导致生产环境污染 **规避**:严格隔离测试与生产流水线,使用不同的凭证和存储路径。 --- ### **总结** 高效ETL需遵循**“轻量抽取、分层转换、可控加载”**原则,结合业务需求选择工具,并通过监控-反馈闭环持续优化。关键成功因素包括: - 增量处理降低资源消耗 - 内存计算加速转换 - 自动化校验保障数据一致性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值