目录

一  创建和销毁对象篇

1  若有多个构造器参数时,优先考虑构造器

当类构造包含多个参数时,同学们会选择 javabeans 模式。在这种模式下,可以调用一个无参构造器来创建对象,然后调用  setter 方法来设置必要和可选的参数。目前较受欢迎的方法之一如在类上加入 lombok 提供的@data注解,来自动生成getter/setterequals 等方法。但是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 类(immutablelistimmutablesetimmuablemap)和上文提到的函数模式集合,实现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),即subsuper的子类型,那么数组类型sub[] 就是super[] 的子类型;数组是具体化的,在运行时才知道并检查它们的元素类型约束。而泛型是不可变的和可擦除的(即编译时强化它们的类型信息,并在运行时丢弃)。

需要警惕 public static final 数组的出现。很有可能是个安全漏洞!

四  方法篇

1  校验参数的有效性

若传递无效的参数值给方法,这个方法在执行复杂、耗时逻辑之前先对参数进行了校验(validation),便很快就会失败,并且可清楚地抛出适当的异常。若没有校验它的参数,就可能会在后续发生各种奇怪的异常,有时难以排查定位原因。

笔者认为,微服务提供的api request 也应沿用这一思想。即在api 请求被服务处理之前,先进行参数校验。每个request应与对应的request validator 绑定。若参数值无效,则抛出特定的clientexception(e.g. illegalargumentexception)。

2  谨慎设计方法签名

谨慎地选择方法的名称:

  • 执行某个动作的方法通常用动词或者动词短语命名:createxxxupdatexxxremovexxxconvertxxxgeneratexxx
  • 对于返回boolean值的方法,一般以 is 开头:isvalidisliveisenabled

避免过长的参数列表:目标是四个参数,或者更少。

  • 当参数过多时,笔者会使用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)例如 intdoublelong boolean。每个基本类型都有一个对应的引用类型,称作装箱基本类型(boxed primitive),对应为integerdoublelong 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

npm镜像站全新上线:

阿里云开源镜像站是由阿里云提供的开源组件、开源操作系统等工具镜像站。npm镜像站全新上线,提高开发效率,让您的构建更加迅速。

到此这篇关于effective java 在工作中的应用总结的文章就介绍到这了,更多相关effective java 的应用内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!