为什么写这系列博客?

在阅读《Effective Java》这本书时,我发现有许多地方需要仔细认真地慢慢阅读并且在必要时查阅相关资料才能彻底搞懂,相信有些读者在阅读此书时也有类似感受;同时,在解决疑惑的过程中,还存在着有些内容不容易查找、查找到的解答质量不高等问题,于是我决定把我阅读此书收获到的东西写成博客,期望能够解答某些读者之困惑。

为了方便大家阅读时按章节查找,我会按照原书籍写作顺序来划分博客章节。博客中主要包含以下内容:

  • 我对原文内容的理解(再加工)
  • 一些补充知识(需要理解这些知识才能真正理解该章节内容)

何时考虑用构建器?

类中有几个必选参数,且存在大量可选参数时。

  • 大量指至少有4个
  • 可选指大部分实例只在某几个可选域存在非零值,其他都是零。

如:

public class NutritionFacts{
private final int servingSize;//每份含量,必选
private final int servings;//每罐含量,必选
private final int calories;//卡路里,可选
private final int fat;//总脂肪含量,可选
private final int saturatedFat;//饱和脂肪含量,可选
private final int sodium;//钠含量,可选
private final int cholesterol;//胆固醇,可选
}

有以下几种解决方案:

重叠构造器

设置多个构造方法,并依次增加入参数量,构造方法内部自动调用参数多一个的构造方法,直到调用到最后一个全参数的构造方法。

代码如下(我又额外增加了 饱和脂肪含量 和 胆固醇含量 这两个域):

public class NutritionFacts{
private final int servingSize;//每份含量,必选
private final int servings;//每罐含量,必选
private final int calories;//卡路里,可选
private final int fat;//总脂肪含量,可选
private final int saturatedFat;//饱和脂肪含量,可选
private final int sodium;//钠含量,可选
private final int cholesterol;//胆固醇含量,可选 public NutritionFacts(int servingService,int servings){
this(servingService,servings,0);
} public NutritionFacts(int servingService,int servings,int calories){
this(servingService,servings,calories,0);
} public NutritionFacts(int servingService,int servings,int calories,int fat){
this(servingService,servings,calories,fat,0);
} public NutritionFacts(int servingService,int servings,int calories,int fat,int saturatedFat){
this(servingService,servings,calories,fat,saturatedFat,0);
} public NutritionFacts(int servingService,int servings,int calories,int fat,int saturatedFat,int sodium){
this(servingService,servings,calories,fat,saturatedFat,sodium,0);
} public NutritionFacts(int servingService,int servings,int calories,int fat,int saturatedFat,int sodium,int cholesterol){
this.servingService = servingService;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.saturatedFat = saturatedFat;
this.sodium = sodium;
this.cholesterol = cholesterol;
}
}

使用时,选择包含想传递参数的最短的那个构造器就可以了。如:我想传递calories和fat字段,那么下面的构造函数即可,其他可选参数会自动被设置为0。

NutritionFacts test = new NutritionFacts(240,8,5,4);

缺点如下:

  • 冗余传参

假如,客户端只需要设置最后两个可选参数sodium和cholesterol,但是却需要调用最后一个构造方法,并将所有其他可选参数传入0。

new NutritionFacts(240,8,0,0,0,240,25);

这样的方式需要客户端传入并不需要设置的参数,代码冗余不优雅。

  • 类型相同的相邻参数易传错

如果搞混了两个有相同数据类型又紧挨着的可选域的值,编译时不会出错,但运行时会出现错误行为。

new NutritionFacts(240,8,0,240,50,0,0);//本来想传入这种
new NutritionFacts(240,8,0,50,240,0,0);//实际却传入这种
  • 编写代码和阅读代码均须数数(未使用IDEA时)

编写代码时需要通过数数来确定传入的参数在第一个,同理,阅读代码时也需要数数来确定到底传入了哪些可选参数。

new NutritionFacts(240,8,0,1,0,240,0);

如果使用了IDEA,则数数问题则可以解决:IDEA会在值前提示我们是哪个域:

//"host:"和"port:"是idea添加的提示
Socket client = new Socket(host:"127.0.0.1", port:6666);

这里补充一个基础知识:

有些人会疑惑,给可选域设置一个初始值0不就可以了吗,这样就不会出现冗余传参的问题。但实际上,这种写法是无法通过编译的,因为final修饰的实例域的初始化器和初始化代码块是优先于构造函数执行的(初始化器指 用 = 直接赋值,初始化代码块指用大括号括起来的 各实例域 = 赋值的代码),final修饰的实例域在初始化器初始化后,就不能再通过构造函数进行修改了,所以设置的初始化值无效,而且也达不到后续改变需要的可选参数为非0的目的。

继续思考,那么不设置为final域,这样就可以设置默认的初始化值了,这样就引出了下一种方式,JavaBean方式。

public class NutritionFacts{
private final int calories = 0;//卡路里,可选,初始化为0,省略其他实例域 //省略其他构造函数 public NutritionFacts(int servingService,int servings,int calories,int fat,int saturatedFat,int sodium,int cholesterol){
this.servingSize = servingService;
this.servings = servings;
this.calories = calories;//这行编译器会报错
this.fat = fat;
this.saturatedFat = saturatedFat;
this.sodium = sodium;
this.cholesterol = cholesterol;
}
}

JavaBeans方式

先创建对象,再调用set方法赋值。

代码如下:

public class NutritionFacts{
private int servingSize;//每份含量,必选,通过构造函数设置
private int servings;//每罐含量,必选,通过构造函数设置
private int calories;//卡路里,可选
private int fat;//总脂肪含量,可选
private int saturatedFat;//饱和脂肪含量,可选
private int sodium;//钠含量,可选,
private int cholesterol;//胆固醇含量,可选 public NutritionFacts(int servingSize,int servings){
this.servingSize = servingSize;
this.servings = servings;
} public void setcalories(int calories){
this.calories = calories;
} //以下set函数省略,同上
}

使用时,先创建对象,再依次调用set方法设置需要设置的值即可。

这种方式因为可以按需设置了,所以不仅解决了代码阅读和编写时数参数的问题,还解决了冗余参数的问题;但是却使类从不可变类变成了可变类(因为提供了set函数),可能会带来线程安全问题。

原书中还提到了一个缺点:

遗憾的是,JavaBeans模式自身有着很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中JavaBeans可能处于不一致状态。类无法仅仅通过检验构造器参数的有效性来保证一致性。

“JavaBeans可能处于不一致状态”是什么意思呢?

我认为,理解这句话需要先理解它后面这句话,即“类无法仅仅通过检验构造器参数的有效性来保证一致性”:

这句话其实给出了作者认为的一致性的含义,即,所有参数都校验通过所创建的对象就是符合一致性的。那么怎样做参数校验?作者也给出了答案,即“仅仅通过检验构造器参数”,意思是,通过构造器方式设置可选参数时,通过构造器这一个方法做参数校验即可,但是JavaBeans模式需要调用多个set方法,如果在set函数中的某些方法遗漏了参数校验代码,那么创建出的对象会出现某个或某几个字段的值不符合规则,但是其他值却符合规则的情况,此种情况即是不一致。

所以解决不一致的方式就是需要在set方法中加入参数校验代码,保证当某个传递进来的参数不符合规则时可以立即报错。

虽然不一致问题可以解决,但是从不可变类变成可变类这个问题却无法解决。

构建器模式

通过一个构建器类,先设置参数值,最后再创建对象。

public class NutritionFacts{
private final int servingSize;//每份含量,必选
private final int servings;//每罐含量,必选
private final int calories;//卡路里,可选
private final int fat;//总脂肪含量,可选
private final int saturatedFat;//饱和脂肪含量,可选
private final int sodium;//钠含量,可选
private final int cholesterol;//胆固醇含量,可选 public static class NutritionFactsBuilder{
//注意:这里的final并不是一定需要的
//写上final,我认为可以在编程时让编译器帮助我们检查是否初始化
private final int servingSize;//每份含量,必选
private final int servings;//每罐含量,必选 //注意:这里的设置初始值为0也不是必要的,但是可以增加代码可读性。
//了解Java的人会清楚这里会设置为默认值0
//但是这样写可以让不了解Java的人也清楚的知道默认值被设置为了0
private int calories = 0;//卡路里,可选
private int fat = 0;//总脂肪含量,可选
private int saturatedFat = 0;//饱和脂肪含量,可选
private int sodium = 0;//钠含量,可选
private int cholesterol = 0;//胆固醇含量,可选 public NutritionFactsBuilder(int servingSize,int servings){
this.servingSize = servingSize;
this.servings = servings;
} public NutritionFactsBuilder calories(int calories){
this.calories = calories;
} public NutritionFactsBuilder fat(int fat){
this.fat= fat;
} //以下省略其他可选字段方法 public NutritionFacts build(){
return new NutritionFacts(this);
}
} public NutritionFacts(NutritionFactsBuilder builder){
this.servingService = builder.servingService;
this.servings = builder.servings;
this.calories = builder.calories;
this.fat = builder.fat;
this.saturatedFat = builder.saturatedFat;
this.sodium = builder.sodium;
this.cholesterol = builder.cholesterol;
}
}

缺点:编写冗长,为了创建对象需要先创建一个构建器,某些注重性能的情况下有问题

上述这种构建器模式,其实就是简单工厂模式,即客户端依赖具体类NutritionFactsBuilder来创建NutritionFacts,不符合针对接口编程这个设计原则,所以书中提到了这种模式的优化方式---抽象工厂模式,即通过创建一个接口Builder,提供build()方法,让NutritionFactsBuilder实现这个接口,这样客户端就可以面向接口Builder编程,如果修改了具体实现,则除了创建新的具体实现Builder以外,客户端其他的代码都不需要修改。

public interface Builder<T>{
public T build();
}

这里补充下工厂模式的最后一种:工厂方法模式。我在网络上查阅资料时发现许多人会把这个模式和抽象工厂模式搞混,其实这两者并不相同。

简而言之,工厂方法模式更适合用来控制一个方法的整体业务流程。整体业务流程由具体代码以及各个业务方法调用组成,而其中某些业务方法是需要由不同的子类来实现的,所以工厂方法模式编写时并不是定义一个抽象的接口(抽象工厂模式),而是利用抽象父类来限定一个方法的整体业务流程,然后提供一个或多个抽象的protected业务方法由子类继承父类来重写,以此实现上述目的。

补充

最后,书中还提到了Class的newInstance这个方法,这个方法在Java9中被标记为过时,而且在第三版书籍中已经被去除,虽然已被删除及标记过时,但了解它的原理也是有必要的,因为如果能够理解站在当时的视角为什么会写这段文字,又理解它为什么会被删除,对巩固Java基础大有裨益。

缺点逐句解读:

该方法总是试图调用无参构造函数:然而可能类中并不存在无参构造函数

如果用new的方式创建,不存在无参构造函数却想要调用无参构造函数时,编译器会检测出来,但是使用newInstance,需要等到运行期才能发现此事。

运行时处理:Instantiation Exception 和 IllegalAccessException(这两个都是受检异常)

我查阅了JDK1.6版本的源码,关于这两个异常的注释如下:

IllegalAccessException – if the class or its nullary constructor is not accessible.

InstantiationException – if this Class represents an abstract class, an interface, an array class, a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.

第一点所说的不存在无参构造函数的情况,是属于会抛出InstantiationException的情况之一。该方法的签名用throws 关键字明确抛出异常,需要调用者处理。

public T newInstance()
throws InstantiationException, IllegalAccessException

客户端代码必须在运行时处理IllegalAccessException和InstantiationException,这样既不雅观也不方便

关键字还是运行时!虽然客户端会编写try-catch代码来处理这两个异常,但是很明显,这两个异常仍然是在运行期间才会发生并被处理的,如果不用newInstance,编译器就会发现你想访问的类是noAccess的或者 你调用的无参构造函数并不存在 或者 你试图创建了一个抽象类 一个接口 等等之类的问题。

上面这几句其实说的是一件事,就是运用newInstance会把问题推后到运行期而非在编译期解决,作者认为是不好的。关于书中作者对编译期提前发现问题如此执着的原因,我猜可能在于有些软件并不能非常轻松的在本地运行起来(虽然我还没有接触过),在本地运行如此不易的情况下,编译期能及时发现问题就显得难能可贵了。

newInstance方法还会传播由无参构造器抛出的任何异常,newInstance缺乏相应的throws语句

这个问题比较重要,也是后来这个方法过时的原因。

调用构造器时,如果发生非受检异常,newInstance方法直接向上传播并且不写throws语句没有任何问题,但是如果发生受检异常,这就代表着需要调用者处理该异常,newInstance方法直接抛出却不写throws语句破坏了原来的目的:失去了编译器强制异常检测的功能。

可以看到,后来的替代者(如下)的newInstance方法可以不仅仅调用无参构造函数,并且还提供了一个由构造函数抛出的异常的包装异常InvocationTargetException来解决受检异常被抛出却无throws语句的问题。

这样一来,原来的newInstance就可以安心退休了。

InvocationTargetException – if the underlying constructor throws an exception.

//这样调用
getDeclaredConstructor(Class<?>... parameterTypes).newInstance(Object ... initargs) //关注InvocationTargetException
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{

Effective Java理解笔记系列-第2条-何时考虑用构建器?的更多相关文章

  1. Effective Java 学习笔记之第七条——避免使用终结(finalizer)方法

    避免使用终结方法(finalizer) 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的. 不要把finalizer当成C++中析构函数的对应物.java中,当对象不 ...

  2. java学习笔记系列整理说明

    java学习笔记系列整理说明 ​ 陆陆续续也巩固学习java基础也有一段时间了,这里整理了一些我认为比较的重要的知识点,供自己或者读者以后回顾和学习.这是一个学习笔记系列,有自己的整理重新撰写的部分, ...

  3. 《Effective Java》笔记45-56:通用程序设计

    将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性. 要使用局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方才声明,不要过早的声明. 局部变量的作用域从它被声明的 ...

  4. [Effective Java 读书笔记] 第二章 创建和销毁对象 第二条

    第二条 遇到多个构造器参数时,可以考虑用构建器 当遇到有多个构造器参数时,常见的是用重叠构造器,即: public class TestClass{ public TestClass(int para ...

  5. Effective java读书笔记

    2015年进步很小,看的书也不是很多,感觉自己都要废了,2016是沉淀的一年,在这一年中要不断学习.看书,努力提升自己 计在16年要看12本书,主要涉及java基础.Spring研究.java并发.J ...

  6. Effective Java阅读笔记——引言

    “我很希望10年前就拥有这本书.可能有人认为我不需要任何Java方面的书籍,但是我需要这本书.” ——Java之父 James Gosling 在图书馆找到这本java著作时,首先看到了这句话.   ...

  7. Effective Java 读书笔记(一):使用静态工厂方法代替构造器

    这是Effective Java第2章提出的第一条建议: 考虑用静态工厂方法代替构造器 此处的静态工厂方法并不是设计模式,主要指static修饰的静态方法,关于static的说明可以参考之前的博文&l ...

  8. Effective Java读书笔记完结啦

    Effective Java是一本经典的书, 很实用的Java进阶读物, 提供了各个方面的best practices. 最近终于做完了Effective Java的读书笔记, 发布出来与大家共享. ...

  9. Effective Java 读书笔记之一 创建和销毁对象

    一.考虑用静态工厂方法代替构造器 这里的静态工厂方法是指类中使用public static 修饰的方法,和设计模式的工厂方法模式没有任何关系.相对于使用共有的构造器来创建对象,静态工厂方法有几大优势: ...

  10. 《Effective Java》笔记 :(一)创建和销毁对象

    一 .考虑用静态工厂方法代替构造器 1. 静态工厂方法与设计模式中的工厂方法模式不同,注意不要混淆 例子: public static Boolean valueOf(boolean b){ retu ...

随机推荐

  1. 字符串编码(ASCII, GBK, ANSI, Unicode(‘u‘), UTF-8编码)(转载)

    [版权声明]本篇文章以征得博主同意,再行转载. 出自[hxxjxw] 原文链接:https://blog.csdn.net/hxxjxw/article/details/90140663 目录 字符串 ...

  2. AICA第6期-学习笔记汇总

    AICA第6期-学习笔记汇总 AICA第六期|预科班课程 1.<跨上AI的战车> 2.<产业中NLP任务的技术选型与落地> 3.<计算机视觉产业落地挑战与应对> 4 ...

  3. 从BIOS+MBR迁移到UEFI+GPT 并修复Ubuntu Grub2 UEFI引导

    之前在虚拟机里使用了默认配置安装了Ubuntu16.04,由于需要扩充磁盘空间不得不将磁盘从MBR分区表转换到GPT分区表. 简单介绍一下思路:首先通过Windows下的DiskGenius软件备份U ...

  4. MACOS 降级

    最近升级了macos 15.2,结果导致外接显示器显示不正常,经常断掉或者黑屏,因此macos进行降级处理: 1. 首先在App Store下载Ventura 系统; 2. 准备一个16G的U盘,然后 ...

  5. (五).NET6.0使用Serilog进行配置和实现日志记录

    1.首先安装Serilog六件套神装包 也可以对个别相应的包进行删除等,例如:1是读取配置文件的,如果不需要通过配置文件进行操作,就可以不使用这个包.2是打印到控制台的,如果不需要打印到控制台,也可以 ...

  6. python SQLAlchemy ORM——从零开始学习03 如何针对数据库信息进行排序

    03 如何进行排序 3-1准备工作: 因为要排序,所以需要随机多谢数据,model见后文.也需要random进行随机 from model import User, Engine from sqlal ...

  7. jQuery详解

    目录 jQueryJS中创建对象jQuery选择器jQuery 操作 DOMjQuery 事件jQuery 动画JSON :Python工具 - pipPython工具 - VirtualEnvWEB ...

  8. cmake-4

    cmake-4学习,参考 cmake构建c++项目快速入门2-1 cmake构建c++项目快速入门2-2 了解 cmake的工作原理: Windows下用cmake编译cmake (1)先下载cmak ...

  9. ef 值转换与值比较器

    前言 简单介绍一下,值转换器和值比较器. 正文 为什么有值转换器这东西呢? 那就是这个东西一直必须存在. 比如说,我们的c# enum 对应数据库的什么呢? 是int还是string呢? 一般情况下, ...

  10. Windows中GNURadio的安装

    对于一个常常使用Python的人来讲(此处指我),conda环境是必不可少的,(Anaconda或Miniconda). 在Windows中且已经安装过conda环境的情况下,安装GNURadio特别 ...