effectivejava在工作中的应用总结(代码片段)

阿里云云栖号 阿里云云栖号     2023-01-05     342

关键词:

简介: 《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

原文链接
本文为阿里云原创内容,未经允许不得转载。

effectivejava中的方法部分

1.检查参数的有效性非公有方法应使用断言(Assertion)来检查它们的参数,公有方法需要在Javadoc中标明一旦参数违反限制时,会抛出什么异常。但并不是说对参数的任何限制都是好事,应当在通用的原则,遵循上面的指导原则。... 查看详情

浮点数在金额计算中的使用总结

...法做个总结,以规范后续使用。1.double和float的不准确《effectiveJava》中指出,float和double类型主要是为科学计算和工程计算而设计的,不应该被用于需要精确结果的场合,尤其不适合于货币计算,见如下代码:publicclassDoubleHandlesta... 查看详情

java:effectivejava学习笔记之避免使用终结方法(代码片段)

Java避免使用终结方法避免使用终结方法1、finalize()基本概念2、finalize()的执行过程3、为什么要避免覆盖并使用finalize方法?4、如果类中的资源确实需要被释放,我们应该怎么做?5、终结方法的利弊5.1、终结方法的好... 查看详情

《effectivejava》——读后总结

这本书在Java开发的行业里,颇有名气。今天总算是粗略的看完了…后面线程部分和序列化部分由于心浮气躁看的不仔细。这个月还剩下一周,慢慢总结消化。 1、静态工厂方法代替构造器静态工厂方法有名称,能确切地... 查看详情

这两年在大数据行业中的工作总结

这两年在大数据行业中的工作总结今天呢,主要回顾这两年来,在大数据行业公司从事大数据类的前端开发的工作。最近刚刚换了一份工作,把我的参考技术A这两年在大数据行业中的工作总结今天呢,主要回顾这两年来,在大... 查看详情

mongodb中的索引操作总结(代码片段)

基础篇(能解决工作中80%的问题):MongoDB的概述、应用场景、下载方式、连接方式和发展历史等MongoDB数据类型、重要概念以及shell常用指令MongoDB文档的各种增加、更新、删除操作总结MongoDB各种查询操作总结MongoDB对列的各... 查看详情

hive在工作中的调优总结(代码片段)

总结了一下在以往工作中,对于HiveSQL调优的一些实际应用,是日常积累的一些优化技巧,如有出入,欢迎在评论区留言探讨~EXPLAIN查看执行计划建表优化分区分区表基本操作,partitioned二级分区动态分区分桶分... 查看详情

git在工作中的用法总结-使用篇(代码片段)

上一篇介绍了git的环境安装配置,本篇对git在工作中常用的用法进行总结,已满足大部分的日常工作需求,对于其他的一些git命令用法在今后使用到时我也会更新上来,文中如有错误,欢迎大家指出来,谢谢~一、git本地基础用... 查看详情

effectivejava高效编程(代码片段)

前言最近读了这本书,发现平时在写代码的过程中,也无意有意的使用到了书中的观点,特此记录下来,后用。创建和销毁对象建议我们什么时候创建,怎么创建,什么时候销毁,怎么销毁,以及... 查看详情

effectivejava总结的78条

1、考虑用静态工厂方法代替构造器2、遇到多个构造器参数时要考虑用构造器3、用私有构造器或者枚举类型强化Singleton属性4、通过私有构造器强化不可实例化的能力5、避免创建不必要的对象6、消除过期的对象引用7、避免使用fi... 查看详情

effectivejava第三版——79.避免过度同步(代码片段)

Tips书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code注意,书中的有些代码里方法是基于Java9API中的,所以JDK最好下载JDK9以上的版本。79.避免过度同步条目78警告我们缺乏同步的危险性。这一条目则涉及相反的问题... 查看详情

关于java反射基础知识/编码经验的一些总结(代码片段)

...整理》面试笔记为原型,结合工作中学习的知识。《EffectiveJava》、《编写高质量代码(改善Java程序的151个建议)》这两本书为方向进行整理。笔记立足DevOps。开发+运维+测试三个方向 查看详情

关于java异常基础知识/编码经验的一些总结(代码片段)

...整理》面试笔记为原型,结合工作中学习的知识。《EffectiveJava》、《编写高质量代码(改善Java程序的151个建议)》这两本书为方向进行整理。笔记立足DevOps。开发+运维+测试三个方向 查看详情

javascript中数组的应用总结

...用,今天对js中的数组部分进行归纳总结,以便在以后的工作中有所参考。1.在js中数组的定义方式有两种:  vara=[1,2,3,4];  varb=newArray(1,2,3,4);这两种创建数组的方式并无区别,可根据习惯自行选用。2.通过命令对数组的长度... 查看详情

effectivejava第十章并发

 EFFECTIVE JAVA 第十章 并发 66.同步访问共享的可变数据  *java语言规范保证读或写一个变量是原子的(可以保证返回的值是某个线程保存在该变量中的),除非这个变量的类型为long或double。(但并不保证一个... 查看详情

java:effectivejava学习笔记之消除过期对象引用(代码片段)

Java消除过期对象引用消除过期对象引用1、Java的垃圾回收机制2、Java中的内存泄露3、常见的内存泄露参考消除过期对象引用很多人可能在想这么一个问题:Java有垃圾回收机制,那么还存在内存泄露吗?答案是肯定的&#... 查看详情

effectivejava中文版第2版

...反映了Java5中重要的变化,并删去了过时的内容。 《EffectiveJava中文版(第2版)》中的每条规则都以简短、独立的小文章形式出现,并通过示例代码加以进一步 查看详情

近期工作中的一些复盘和总结

近来由于工作太忙,期间几度想撂挑子不干了,后来一想,大部分的让自己难受的事情,其实在一开始就埋下了隐患。比如延时优化问题,当时那个代码写的那叫一个惨不忍睹,后来自己看着都累的不行,真心不想再维护了,当... 查看详情