Effective Java 在工作中的应用总结
简介: 《Effective Java》是一本经典的 Java 学习宝典,值得每位 Java 开发者阅读。笔者将书中和平日工作较密切的知识点做了部分总结。
作者 | 宜秋
来源 | 阿里技术公众号
《Effective Java》是一本经典的 Java 学习宝典,值得每位 Java 开发者阅读。笔者将书中和平日工作较密切的知识点做了部分总结。
一 创建和销毁对象篇
1 若有多个构造器参数时,优先考虑构造器
当类构造包含多个参数时,同学们会选择 JavaBeans 模式。在这种模式下,可以调用一个无参构造器来创建对象,然后调用 setter 方法来设置必要和可选的参数。目前较受欢迎的方法之一如在类上加入 Lombok 提供的@Data注解,来自动生成getter/setter、equals 等方法。但是JavaBeans模式无法将类做成不可变(immutable,详见“使可变形最小化”章节)。这就需要开发者自己掌控值的更新情况,确保线程安全等。
推荐:Builder模式
Builder 模式通过 builder 对象上,调用类似 setter 的方法,设置相关的参数(类似 Proto Buffers)。最后,通过调用 build 方法来生成不可变的对象(immutable object)。使用 Builder 模式的方法之一包括在类上加入 Lombok 提供的 @Builder 注解。
应用:API Request & Response
在微服务架构中,服务的请求(request)和响应(response)往往包含较多参数。在处理请求的过程中,笔者也常常会担心误操作修改了请求的内容。所以,笔者倾向使用Builder模式。
我们可使用Builder模式来构建该类型对象。在构建过程中,若需要引入额外逻辑(e.g. if-else),可先返回Builder对象,最后再调用build方法。
import lombok.Builder;
/** 请求类 */
@Builder
public class SampleRequest {
private String paramOne;
private int paramTwo;
private boolean paramThree;
}
/** 响应类 */
@Builder
public class SampleResponse {
private boolean success;
}
/** 服务接口 */
public interface SampleFacade {
Result< SampleResponse> rpcOne(RequestParam< SampleRequest>);
}
/** 调用 */
public void testRpcOne() {
SampleRequest request =
SampleRequest.builder().paramOne("one").paramTwo(2).paramThree(true).build();
Result< SampleResponse> response = sampleFacade.rpcOne(request);
}
2 通过私有构造器强化不可实例化的能力
有些类,例如工具类(utility class),只包含静态字段和静态方法。这些类应尽量确保不被实例化,防止用户误用。
推荐:私有化类构造器
为了防止误导用户,认为该类是专门为了继承而设计的,我们可以将构造器私有化。
public class SampleUtility {
public static String getXXX() {
return "test";
}
/** 私有化构造器 */
private SampleUtility() {}
}
/** 直接调用方法 */
public static void main(String[] args) {
System.out.println(SampleUtility.getXXX());
}
二 类和接口篇
1 最小化类和成员的可访问性
尽可能地使每个类或者成员不被外界访问。
推荐:有的时候,为了测试,我们不得不将某些私有的(private)类、接口或者成员变成包级私有的(package-private)。这里,笔者推荐大家使用 Guava 提供的 @VisiableForTesting 注解,来提示这是为了测试而使可访问级别变为包级私有,放宽了限制。
import com.google.common.annotations.VisibleForTesting;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
String getXXX() {
return "test";
}
此外,也有小伙伴推荐 PowerMock 单元测试框架。PowerMock 是 Mockito 的加强版,可以实现完成对private/static/final方法的Mock(模拟)。通过加入 @PrepareForTest 注解来实现。
public class Utility {
private static boolean isGreaterThan(int a, int b) {
return a > b;
}
private Utility() {}
}
/** 测试类 */
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
@RunWith(PowerMockRunner.class)
@PrepareForTest({Utility.class})
public class UtilityTest {
@Test
public void test_privateIsGreaterThan_success() throws Exception {
/** 测试私有的 isGreaterThan 方法 */
boolean result = Whitebox.invokeMethod(Utility.class, "isGreaterThan", 3, 2);
Assertions.assertTrue(result);
}
}
2 使可变形最小化
不可变类(immutable class)是指类对应的实例被创建后,就无法改变其成员变量值。即实例中包含的所有信息都必须在创建该实例的时候提供,并在对象的生命周期内固定不变。
不可变类一般采用函数(functional)模式,即对应的方法返回一个函数的结果,函数对操作数进行运算但并不修改它。与之相对应的更常见的是过程的(procedure)或者命令式的(imperative)做法。使用这些方法时,将一个过程作用在它们的操作数上,会导致它的状态发生改变。
如在“若有多个构造器参数时,优先考虑构造器”一节中提到,不可变对象比较简单,线程安全,只有一种状态。使用该类的开发者无需再做额外的工作来维护约束关系。另外,可变的对象可以有任意复杂的状态。若 mutator 方法(e.g. update)无详细的描述,开发者需要自行阅读方法内容。笔者经常会花费较多时间弄清楚在某方法内,可变对象的哪些字段被更改,方法结束后会不会影响后续的对象操作。笔者推荐传入不可变对象,基于此用更新的参数创建新的不可变对象返回。虽然会创建更多的对象,但是保证了不可变形,以及更可读性。
推荐:Guava Collection之Immutable类
笔者在日常开发中倾向将 Immutable 类(ImmutableList,ImmutableSet,ImmuableMap)和上文提到的函数模式集合,实现mutator 类方法。
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
/** 推荐 */
private static final ImmutableMap< String, Integer> SAMPLE_MAP =
ImmutableMap.of("One", 1, "Two", 2);
/** 推荐:确保原input列表不会变化 */
public ImmutableList< TestObj> updateXXX(ImmutableList< TestObj> input) {
return input.stream()
.map(obj -> obj.setXXX(true))
.collect(toImmutableList());
}
/** 不推荐:改变input的信息 */
public void filterXXX(List< TestObj> input) {
input.forEach(obj -> obj.setXXX(true));
}
三 泛型篇
1 列表优先于数组
数组是协变的(covariant),即Sub为Super的子类型,那么数组类型Sub[] 就是Super[] 的子类型;数组是具体化的,在运行时才知道并检查它们的元素类型约束。而泛型是不可变的和可擦除的(即编译时强化它们的类型信息,并在运行时丢弃)。
需要警惕 public static final 数组的出现。很有可能是个安全漏洞!
四 方法篇
1 校验参数的有效性
若传递无效的参数值给方法,这个方法在执行复杂、耗时逻辑之前先对参数进行了校验(validation),便很快就会失败,并且可清楚地抛出适当的异常。若没有校验它的参数,就可能会在后续发生各种奇怪的异常,有时难以排查定位原因。
笔者认为,微服务提供的API request 也应沿用这一思想。即在API 请求被服务处理之前,先进行参数校验。每个request应与对应的request validator 绑定。若参数值无效,则抛出特定的ClientException(e.g. IllegalArgumentException)。
2 谨慎设计方法签名
谨慎地选择方法的名称:
- 执行某个动作的方法通常用动词或者动词短语命名:createXXX,updateXXX,removeXXX,convertXXX,generateXXX
- 对于返回boolean值的方法,一般以 is 开头:isValid,isLive,isEnabled
避免过长的参数列表:目标是四个参数,或者更少。
- 当参数过多时,笔者会使用Pair,Triple或辅助类(e.g. 静态成员类)
public class SampleListener {
public ConsumeConcurrentlyStatus consumeMessage(String input) {
SampleResult result = generateResult(input);
...
}
private static SampleResult generateResult(String input) {
...
}
/** 辅助类 */
private static class SampleResult {
private boolean success;
private List< String> xxxList;
private int count;
}
}
3 返回零长度的数组或者集合,而不是null
若一个方法返回 null 而不是零长度的数组或者集合,开发者需要加入 != null 的检查,有时容易忘记出错,报NullpointerException。
说到此,笔者想额外提一下 Optional。网络上有很多关于 Optional 和 null 的使用讨论。Optional 允许调用者继续一系列流畅的方法调用(e.g. stream.getFirst().orElseThrow(() -> new MyFancyException()))。以下为笔者整理的观点。
/** 推荐:提示返回值可能为空。*/
public Optional< Foo> findFoo(String id);
/**
* 中立:稍显笨重
* 可考虑 doSomething("bar", null);
* 或者重载 doSomething("bar"); 和 doSomething("bar", "baz");
**/
public Foo doSomething(String id, Optional< Bar> barOptional);
/**
* 不推荐:违背 Optional 设计的目的。
* 当 Optional 值缺省时,一般有3种处理方法:1)提供代替的值;2)调用方法提供代替的值;3)抛出异常
* 这些处理方法可以在字段初始或赋值的时候处理。
**/
public class Book {
private List< Pages> pages;
private Optional< Index> index;
}
/**
* 不推荐:违背 Optional 设计的目的。
* 若为缺省值,可直接不放入列表中。
**/
List< Optional< Foo>>
五 通用程序设计篇
1 如果需要精确的答案,请避免使用float和double
float 和 double 类型主要用于科学工程计算。它们执行二进制浮点运算,为了在数值范围上提供较为精准的快速近似计算。但是,它们并不能提供完全精确的结果,尤其不适合用于货币计算。float 或者 double 精确地表示0.1 是不可行的。
若需系统来记录十进制小数点,可使用BigDecimal。
2 基本类型优先于装箱基本类型
基本类型(primitive)例如 int、double、long 和 boolean。每个基本类型都有一个对应的引用类型,称作装箱基本类型(boxed primitive),对应为Integer、Double、Long 和 Boolean。如书中提到,它们的区别如下:
/** 推荐 */
public int sum(int a, int b) {
return a + b;
}
/** 不推荐:不必要的装箱 */
public Integer sum(Integer a, Integer b) {
return a + b;
}
若无特殊的使用场景,推荐总是使用基本类型。若不得不使用装箱基本类型,注意 == 操作和 NullPointerException 异常。装箱基本类型的使用场景:
- 作为集合中的元素(e.g. Set< Long>)
- 参数化类型(e.g. ThreadLocal< Long>)
- 反射的方法调用
六 异常
1 每个方法抛出的异常都要有文档
始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确地记录下抛出每个异常的条件。
在日常工作中,笔者调用其他组的 API 时,有时会发现一些意料之外的异常。良好的文档记录,可以帮助 API 调用者更好得处理相关的异常。文档记录可包括:异常的类型,异常的 error code,和描述。
2 其他
一些公司将 API 产生的异常分成 ClientException 和 ServerException。一般 ClientException (e.g. 无效的服务 request ) 是由调用方非常规调用 API 导致的异常处理,可不在服务端主要的异常监测范围中。而 ServerException(e.g. 数据库查询超时)是由服务端自身原因导致的问题,平时需要着重监测。
七 引用
Bloch, Joshua. 2018. Effective Java, 3rd Edition
原文链接
本文为阿里云原创内容,未经允许不得转载。
Effective Java 在工作中的应用总结的更多相关文章
- 2016-12-15 java开发工作中,尚未做的工作
目前未涉及的工作内容: 1.将静态页导入工程,建成jsp文件.配置css.js.img文件.跳转路径 2.未用工具生成pojo dao service等源码文件 3.未签入自己写的代码 4.未搭建子系 ...
- Builder模式的思考(Effective Java)
<Effective Java>(第2版)中第二条中提到:遇到多个构造器参数时要考虑用构建器.在复习static关键字和内部类时回头看了一下,这才明白了为什么要用静态内部类来做处理,这里记 ...
- 《Effective Java》-——用私有构造器或者枚举类型强化Singleton属性
Singleton指仅仅被实例化一次的类.Singleton通常被用来代表那些本质上唯一的系统组件,比如窗口管理器或者文件系统.使类成为Singleton会使它的客户端测试变得十分困难,因为无法给Si ...
- Effective Java 第三版——46. 优先考虑流中无副作用的函数
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——16.在公共类中使用访问方法而不是公共属性
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Java异常(二) 《Effective Java》中关于异常处理的几条建议
概要 本章是从<Effective Java>摘录整理出来的关于异常处理的几条建议.内容包括:第1条: 只针对不正常的情况才使用异常第2条: 对于可恢复的条件使用被检查的异常,对于程序错误 ...
- Java工作中常用到的工具
刚看但网上一篇Java最流行工具的文章,结合自己日常应用总结一下开发工作中常用到的工具. 一,Java版本,jdk8,也是Java普及最快的版本,除了本身性能提升外,新增的函数式编程特性也让开发效率更 ...
- 关于工作中.net转java遇到的一个远程调用传递重复参的问题。
工作中遇到一个很奇怪的传参问题.之前.net使用的是一个List列表,列表中有几个重复的参数.列表中使用的model类是KeyValue. 我使用java模仿其写法,传递List和KeyValue.对 ...
- 工作中常用到的Java集合类有哪些?
前言 只有光头才能变强. 文本已收录至我的GitHub精选文章,欢迎Star:https://github.com/ZhongFuCheng3y/3y Java集合是我认为在Java基础中最最重要的知 ...
- Java工作中的并发问题处理方法总结
Java工作中常见的并发问题处理方法总结 好像挺久没有写博客了,趁着这段时间比较闲,特来总结一下在业务系统开发过程中遇到的并发问题及解决办法,希望能帮到大家 问题复现 1. "设备Aの奇怪分 ...
随机推荐
- 安装centos系统必做操作
关闭SELinux sed -i '/^SELINUX=/c SELINUX=disabled' /etc/selinux/config 在Linux系统中临时关闭SELinux(Security-E ...
- drf(视图组件)
一. 前言 Django REST framwork 提供的视图的主要作用 1. 控制序列化器的执行(检验.保存.转换数据) 2. 控制数据库查询的执行 二. 两个视图基类 两个视图基类: APIVi ...
- 并发CPU伪共享及优化
伪共享 缓存系统中是以缓存行(cache line)为单位存储的.缓存行是2的整数幂个连续字节,一般为32-256个字节.最常见的缓存行大小是64个字节.当多线程修改互相独立的变量时,如果这些变量共享 ...
- 【OpenCV】OpenCV (C++) 与 OpenCvSharp (C#) 之间数据通信
OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在Linux.Windows.Android和Mac OS操作系统上. 它轻量级而且高效--由一 ...
- cyc_to_led
Entity: cyc_to_led File: cyc_to_led.v Diagram Generics Generic name Type Value Description MD_SIM_AB ...
- FPGA的PCB设计
FPGA的PCB设计 一.FPGA的高速电路板设计 PCB板的设计规模增大,IO传输问题也就出现.为了兼容其他高速模块,必须对PCB的设计进行优化. 1️⃣电源滤波,降低系统噪声2️⃣匹配信号线3️⃣ ...
- Circle Loss:从统一的相似性对的优化角度进行深度特征学习 | CVPR 2020 Oral
论文提出了Circle loss,不仅能够对类内优化和类间优化进行单独地处理,还能根据不同的相似度值调整对应的梯度.总体而言,Circle loss更灵活,而且优化目标更明确,在多个实验上都有较好的表 ...
- 【Java】try {}里有一个 return 语句,那么紧跟在这个 try 后的 finally {}里的 code 会不会被执行,什么时候被执行,在 return 前还是后?
try {}里有一个 return 语句,那么紧跟在这个 try 后的 finally {}里的 code 会不会被执行,什么时候被执行,在 return 前还是后? package com.test ...
- 数组栈(ArrayStack)
栈 栈是一种线性结构,相比与数组,栈对应的操作时数组的子集,只能从一端添加元素,也只能从一端取出元素,是一种 后进先出(Last In First Ou,LIFO) 的数据结构. push pop ...
- #并查集,树状数组#洛谷 5610 [Ynoi2013] 大学
题目 分析 设最大值为 \(mx\),考虑每个数最多被除以 \(\log{mx}\) 次,那么加上树状数组的维护为 \(O(n\log{n}\log{mx})\) 问题就是如何快速找到这些位置,可以对 ...