写点什么

重读 Effective JAVA(一)- 精进自己的 JAVA 技术

作者:xfgg
  • 2023-10-24
    福建
  • 本文字数:4390 字

    阅读完需:约 14 分钟

重读Effective JAVA(一)- 精进自己的JAVA技术

添加了个人的理解,不喜勿喷

考虑使用静态工厂方法代替构造方法

  1. 静态工厂方法的好处

  • 与构造函数不同,静态工厂方法具有名字。

个人理解:当构造函数用来生成对象时,如果生成对象的请求参数不同,只能用重载的方式,方法名字相同而参数不同,但是如果使用静态工厂的方式,可以用名字来很好的阐述该方法是用来生成什么样的对象。

  • 与构造函数不同,每次被调用的时候,不要求非得创建一个新的对象

个人理解:可以很好的实现单例模式,保证了不会频繁的创建对象,保证不会有两个相等的实例出现,a == b 的时候才有 a.equals(b)为 true

  • 与构造函数不同,可以返回一个原返回类型的子类型对象

个人理解:选择返回对象的类型有了更大的灵活性

  1. 静态工厂方法的缺点

  • 类如果不含公有的或者受保护的构造函数,就不能被子类化

  • 它们与其他静态方法没有任何区别

使用私有构造函数强化 singleton 属性

实现 singleton 有两种方法,两种方法都要把构造函数保持为私有的,并且提供一个静态成员,以便允许客户能够访问该类唯一的实例。

  1. 公有静态成员是一个 final 域

public class Elvis {  	public static final Elvis INSTANCE = new Elvis();  	private Elvis() {      ....    }}
复制代码

好处在于组成类的成员的声明很清楚地表明了这个类是一个 singleton,公有的静态域是 final 的,所以该域将总是包含相同的对象引用。

个人理解:单例模式的实现方法,可以用在数据库连接和日志记录器。

  1. 提供一个公有的静态工厂方法

public class Elvis {  	private static final Elvis INSTANCE = new Elvis();  	private Elvis()	{      	....    }  	public static Elvis getInstance()	{      	....    }}
复制代码

好处在于提供灵活性,在不改变 API 的前提下,可以做 singleton,也可以不做。

个人理解:懒初始化,只在需要的时候创建单例对象,可以用于配置管理器,缓存管理器

ps:如果需要序列化的话,必须提供一个 readResolve 方法

private Object readResolve() throws ObjectStreamException{  	return INSTANCE;}
复制代码

通过私有构造函数强化不可实例化的能力

只要让这个类包含单个显式的私有构造函数,则它不可被实例化

个人理解:防止不想被实例化或子类化的类被实例化占用内存,比如单例模式,工具类,常量类等

避免创建重复的对象

通常使用静态工厂方法避免创建重复的对象

个人理解:用的机会很少,没有深入了解,强行追求导致代码风格变化,得不偿失。

消除过期的对象引用

感觉不用过于追求,除了一些特殊的场景,一般是不用考虑的,不过内存泄漏真的是一个很毒的点

消除对象引用的三种方法

  1. 清空对象引用:引用过后置 null

MyClass object = new MyClass();// 使用object// ....object = null; // 清空对象引用,使得之前创建的MyClass实例可以被垃圾回收
复制代码
  1. 重用一个本来已经包含对象引用的变量

MyClass object = new MyClass();// 使用object// ....object = new MyClass(); // 重用变量,使得之前创建的MyClass实例可以被垃圾回收
复制代码
  1. 让变量结束其生命周期:

public void foo() {    MyClass object = new MyClass();    // 使用object    // ....} // 变量object在这个point失去作用,使得它引用的MyClass实例可以被垃圾回收
复制代码

避免使用终结函数

不要用 finalizer(会延迟执行,不能保证及时的执行)!要用的话 和 try catch finally 一起用!

两个合理的用途

  1. 当一个对象的所有者忘记了前面段落中建议的显式终止方法情况下,可以充当安全网

  2. 与对象的本地对等体有关,本地对等体是一个本地对象,普通对象通过本地方法委托给一个本地对象,因为本地对象不是一个普通对象,所以不会被回收。

在改写 equals 的时候遵守通用约定

不需要改写 equals 的情况:

  1. 一个类的每个实例本质上都是唯一的

  2. 不关心一个类是否提供了逻辑相等的测试功能

  3. 超类已经改写了 equals,从超类继承过来的行为对于子类也是合适的

  4. 一个类是私有的,或者是包级私有的,并且可以确定它的 equals 方法永远也不会被调用

需要改写 equals 的情况:

当一个类有自己特有的逻辑相等概念(不同于对象身份的概念),而且超类也没有改写 equals 以实现期望的行为。

个人理解:想要实现和对象原先 equals 不同的相等判断,比如只判断某一个字段相同

通用约定

  • 自反性:对于任意的引用值 x,x.equals(x)一定为 true

个人理解:这个基本不会具体考虑,一般都是成立的

  • 对称性:对于任意的引用值 x 和 y,当且仅当 y.equals(x)为 true 时,x.equals(y)也一定返回为 true

个人理解:重写 equals 判断关于字符串大小写不敏感,但是 String 类型自带的 equals 并不会判断,所以写的时候两边都要考虑

  • 传递性:x = y,y = z,x=z

个人理解:y 的基础上创建一个子类 z,z 新增了一些字段影响 equals 的比较结果,要想在扩展一个可实例化的类的同时,既要增加新的特征,同时还要保留 equals 约定,没有一个简单的办法可以做到这一点,需要用类层次来代替联合,具体如何操作,后面会提到。

  • 一致性:无论请求多少次都返回 true

个人理解:基本不用考虑

  • 对于任意的非空引用值 x:x = null 返回 false

个人理解:每次重写添加一行代码判断即可

个人理解:很难一直记住,但是一定要遵守

实现高质量 equals 方法:

  1. 使用==操作符检查”实参是否为指向对象的一个引用“

  2. 使用 instanceof 操作符检查”实参是否为正确的类型“

  3. 把实参转换到正确的类型

  4. 对于该类中每一个关键域,检查实参中的域与当前对象中对应的域值是否匹配

  5. 编写完成 equals 后,问自己三个问题,是否对称,传递,一致

  6. 总要改写 hashcode

  7. 不要企图让 equals 过于聪明

  8. 不要依赖不可靠的资源

  9. 不要将声明中的 object 对象替换为其他类型

改写 equals 时总要改写 hashCode

hashcode 约定内容

  1. 无论调用多少次返回地都是同一个整数

  2. 如果两个对象 equals 判断相同,hashcode 必须返回相同的整数

  3. equals 不相等,hashcode 不是一定要产生不同的结果,但是不同的结果可以提供散列的性能

如果 hashcode 是固定的,那就会从散列表退化成链表,因为每个对象都被映射到同一个散列桶

如何简单写出一个好的散列函数

  1. 把某个非零常数值,比如说 17,保存在一个叫 result 的 int 类型变量中

  2. 对于对象中每一个关键域完成一下步骤:

  • 为该域计算 int 类型的散列码 c:

  • 如果该域是 boolean 类型,则计算 f ? 0: 1,

  • 如果该域是 byte,char,short 或者 int 类型,则计算(int)f

  • 如果该域是 long 类型,则计算(int)(f ^(f >>> 32))

  • 如果该域是 float 类型,则计算 Float.floatToIntBits(f)

  • 如果该域是 double 类型,则计算 Double.DoubleToLongBits(f),得到一个 long 类型,然后在根据 long 类型的方法计算散列值

  • 如果该域是一个对象引用,并且该类的 equals 方法通过递归调用 equals 的方式来比较这个域,则同样对这个域递归调用 hashcode

  • 如果该域是一个数组,则把每个元素当作单独的域来处理,递归调用上述方法

  1. 按照下面的公式,把计算的散列值组合到 result 中

result = 37*result+c
复制代码
  1. 返回 result

  2. 重新验证一遍

总是要改写 toString

  1. 提供一个好的 toString 实现,可以使一个类用起来更加愉快

  2. 为 toString 返回值中包含的所有信息,提供一种编程访问途径

谨慎地改写 clone

  1. 如果改写一个非 final 类的 clone 方法,则应该返回一个通过调用 super.clone 而得到的对象

  2. clone 方法是另一个构造函数,你必须确保它不会伤害到原始的对象,并且正确地建立起被克隆对象中的约束关系

  3. 最好提供其他的途径来代替对象拷贝(提供一个拷贝构造函数),或者干脆不提供克隆

使类和成员的可访问能力最小化

使用访问控制机制(决定类、接口、成员的可访问性)尽可能地使没一个类或成员不被外界访问,对于成员有四种可能的访问级别

  • 私有的(private)-只有在声明该成员的顶层类内部才可以访问这个成员

  • 包级私有的(package-private)-声明该成员的包内部的任何类都可以访问这个成员

  • 受保护的(protected)-该成员声明所在类的子类可以访问这个成员

  • 公有的(public)-任何地方都可以访问该成员

非零长度的数组总是可变的,所以具有公有的静态 final 数组域几乎总是错误的

公有数组应该替换成一个私有数组,以及一个共有的非可变列表,或者将公有的数组域替换为一个公有的方法

支持非可变性

为了使一个类成为非可变类,要遵循五条规则

  1. 不要提供任何可以修改 i 对象的方法

  2. 保证没有可被子类改写的方法

  3. 使所有的域都是 final 的

  4. 使所有的域都成为私有的

  5. 保证对于任何可变组件的互斥访问

可以通过提供函数的做法提供对外修改的权限

非可变对象本质上是线程安全的,不要求同步,非可变对象可以被自由的共享,也可以共享它们的内部信息

非可变对象为其他对象(无论是可变的还是非可变的)提供大量的构件,但是对于每一个不同的值都要求一个单独的对象。

个人理解:其实随着 springboot 注解的实现,支持非可变性已经不是那么泛用,至少个人在后台开发的过程中基本没有这部分需求

复合优先于继承

继承是代码重用的有力手段,但是使用不当也会使代码难懂,软件脆弱

同一个包的内部使用继承是安全的,跨越包边界的继承是非常危险的

复合是指在新的类中添加一个私有域,引用了这个已有类的一个实例而不是扩展一个类

原来已有的类变成新类的一个组成部分,新类中的每个实例方法都可以调用被包含的已有类实例中对应的方法,并返回它的结果,这被称为转发

那什么时候采用继承呢

只有当子类真正是超类的子类型的时候,继承才是合适的,对于两个类 A 和 B,只有当两者之间确实存在 is-a 关系的时候,类 B 才能扩展类 A

个人理解:继承违背了封装性原则,只有当两个类基本完全相似只有部分字段不同时我才会考虑使用继承的方法

接口优先于抽象类

接口和抽象类最明显的区别:抽象类允许包含某些方法的实现,但是接口不允许

已有的类可以很容易被更新,以实现新的接口

接口是定义 mixin(混合类型)的选择

接口使得我们可以构造出非层次结构的类型框架

接口使得安全地增强一个类的功能成为可能

可以通过结合接口和抽象类,对于期待导出的每一个重要接口都提供一个抽象的骨架

个人理解:接口运用在常见的 controller,service,dao 三层架构中,抽象和接口结合可以实现根据不同的要求获取不同的类进行实现

优先考虑静态成员类

嵌套类:定义在另一个类的内部的类,为外围类提供服务,有四种方式:静态成员类,非静态成员类,匿名类,局部类,优先使用静态成员类

个人理解:使用静态成员类在一个类中提供一个新的类保证了封装性,同时提供了扩展性,匿名类常见的用法是创建一个过程对象(thread,runnable,timertask),用在静态工厂方法,在复杂的类型安全枚举类型

检查参数的有效性

个人理解:如果一个方法有参数,方法第一行一定要是检查参数的有效性,保证代码正常执行

需要时使用保护性拷贝

对于构造函数的每个可变参数进行保护性拷贝是必要的

保护性拷贝动作是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是原始对象

谨慎设计方法的原型

  1. 谨慎选择方法的名字

  2. 不要过于追求提供便利的方法

  3. 避免长长的参数列表

  4. 对于参数类型,优先使用接口而不是类

  5. 谨慎地使用函数对象

谨慎地使用重载

永远不要导出两个具有相同参数的重载方法!!!

返回零长度的数组而不是 null

返回 Collections.emptyList()即可


发布于: 刚刚阅读数: 6
用户头像

xfgg

关注

THINK TWICE! CODE ONCE! 2022-11-03 加入

目前:全栈工程师(前端+后端+大数据) 目标:架构师

评论

发布
暂无评论
重读Effective JAVA(一)- 精进自己的JAVA技术_Java_xfgg_InfoQ写作社区