首页 > 编程语言 >Effective Java理解笔记系列-第2条-何时考虑用构建器?

Effective Java理解笔记系列-第2条-何时考虑用构建器?

时间:2024-08-29 15:48:26浏览次数:12  
标签:Java Effective int calories private 笔记 servings fat final

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

  • 大量指至少有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
    {

标签:Java,Effective,int,calories,private,笔记,servings,fat,final
From: https://www.cnblogs.com/ging/p/18386842

相关文章

  • 笔记:第二节快速入门 流程图的使用(下)
     视频第九分钟左右,outArray的size设置为5,初始值定为zeros(1,5),仿真失败。作者将初始值改为为zeros(5,1),之后成功。我尝试将初始值仍定为zeros(1,5),只需将outArray的size设置为-1(代表继承之前格式的意思)或者改为(1,5),仿真也可以运行.按照我的理解对constant的输入[12345]或......
  • 循环(Java篇)
    循环可以干嘛?我们在学习循环的时候可能会有点懵,什么是循环?它可以干嘛?我这里为什么要用循环来写这段代码?等问题。首先我们来讲一下循环可以干嘛循环是什么?在Java中,循环是一种控制结构,可以用于重复执行某段代码,知道满足特定的条件。循环结构可以简化代码,减少重复。这样讲是不......
  • 拉格朗日插值优化 DP 做题笔记
    本来想在洛谷题单里找斜率优化DP的,然后发现了一个拉格朗日插值优化DP的题单,就点进去尝试了一下。题单。于是先看了雨兔的题解,学了CF995F的做法,然后A了这个题。雨兔题解的链接和我的代码见CF上的提交记录。现在正在做后面的题。P3643[APIO2016]划艇\(a_i,b_i......
  • java毕业设计:基于SSM的超市进销存管理系统【代码+论文+PPT】
    全文内容包括:1、采用技术;2、系统功能;3、系统截图;4、配套内容。索取方式见文末微信号,欢迎关注收藏!一、采用技术语言:Java1.8框架:SSM数据库:MySQL5.7、8.0开发工具:IntelliJIDEA旗舰版其他:Maven3.8以上二、系统功能用户管理:负责系统用户的注册、登录、权限分配及个人信息管理......
  • 学习笔记3——栈(C++版)
     接下来教大家如何实现一个栈,我们采用单链表的形式。其实说白了就是给单链表写个入栈和出栈的函数就好了。和上一篇文章说的一样,这里说的栈和stack容器是不一样的。如何实现栈1.定义链表节点structStackNode{DataTypeval;ListNode*next;ListNode(DataTy......
  • 学习笔记2——队列(C++版)
    注意了,这里说的队列并不是STL容器库里面的queue。像链表、队列、栈、二叉树其实是一种数据结构,而vector、queue、set等是容器,是不同的概念。队列的实现可以有很多方式,可以用结构体内储存数组来实现,也可以用结构体内储存结构体来实现,我们这里选择后者。如何实现一个队列1.......
  • PCIe学习笔记(一)-------1.2 PCIe总线简介
    1,PCIe概览PCIe是第三代外围设备总线,英文缩写为PCIe或者PCIExpress。PCIe是点对点,全双工的差分传输信号总线。点对点互连表示链路上的电气负载有限,从而使发送和接收频率可扩展到更高。PCIe目前成熟的版本有GEN1,GEN2,GEN3,GEN4和GEN5,每一代相较上一代传输速率和传输带宽都有了很大幅......
  • 【Markdown笔记】设置字体颜色——转载https://blog.csdn.net/u012028275/article/det
     【Markdown笔记】设置字体颜色dadalaohua于2021-04-0517:53:19发布阅读量5.7w 收藏 293点赞数103分类专栏: Markdown笔记 文章标签: markdown latex html版权GitCode开源社区文章已被社区收录加入社区Markdown笔记专......
  • Java子类继承父类,静态代码块,代码块,构造方法执行顺序
    最近在做笔试时碰到这样一道题目publicclassTest{ publicstaticvoidmain(String[]args){ Sonson=newSon(); }}classFather{ static{ System.out.println("A"); } { System.out.println("B"); } Father(){ ......
  • JavaScript高阶 day-04
    目录一.严格模式1.1什么是严格模式1.2开启严格模式1.3严格模式中的变化二.高阶函数三.闭包3.1变量的作用域3.2什么是闭包3.3闭包的作用3.4闭包的案例四.递归4.1什么是递归4.2利用递归求1~n的阶乘五.赋值(地址传递)-浅拷贝-深拷贝5.1地址传递5.2Object.......