思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
前言
《Effective Java》
是Java
开发领域的经典之作,值得每一位Java
开发者研读。书中提供了九十
多条极具价值的编程原则,非常适合那些已经有一定Java
开发经验,且希望更深入了解Java
的程序员来阅读。
本文对原书中九十
多条编程原则进行凝练,从中筛选出二十七条
条具有指导意义的编程原则,希望这些经验对你编程有所帮助!
对象构建
考虑静态工厂方法代替构造函数
静态工厂方法相对构造函数有以下优点:
-
有确定的名字。 如:
BigInteger.probablePrime
要比BigInteger(int, int, Random)
意义更加明确,从前者命名可以知道它返回的是一个可能的素数。 -
不需要每次调用都创建新的对象。 如:
Boolean.valueOf(boolean)
实际返回是静态缓存对象。 -
无需提前考虑返回对象的类是否存在。 典型的如
JDBC
,通过DriverManager.getConnection
来获取连接。由不同的数据库驱动来提供连接的具体实现。
当构造函数有许多参数时,优先考虑构建者模式
当构造函数有很多可选参数时,我们往往需要提供很多种参数组合,这样的代码编写起来非常麻烦,可读性也不好。对于这样的问题我们可以通过使用设计模式的构建者
模式来解决。
通过枚举来实现单例模式
在Java
中,使用枚举来实现单例模式是因为枚举提供了一种更加简单、安全和可靠的方式来创建单例对象。其具有如下优点:
- 线程安全:枚举类型的实例在Java中是线程安全的。因此,无需担心多线程环境下的竞态条件,它可以确保只有一个实例存在。
- 防止反射攻击:枚举类型在Java中不支持反射,因此无法通过反射机制来创建新的实例。这可以有效地防止反射攻击,确保单例不会被绕过。
- 防止序列化问题 :枚举类型默认情况下不可序列化,但可以通过编写
readResolve
方法来防止反序列化时创建新的对象,从而保持单例性质。
避免创建不必要的的对象并及时清除过时的对象引用
不要创建不必要的对象。如果对象不可变,那么它可以被复用。
例如,下面语句会创建两个String
对象:
String s = new String("bikini");
而换一种写法就只会创建一个String s = "bikini";
同时,Java
中还可能存在内存泄漏的问题,因此我们需要将不需要的对象引用及时清除,以避免这种泄漏。
使用 try-with-resources 优于 try-finally
在Java
中,使用try-with-resources
(自动资源管理)通常优于传统的try-finally
块,有以下几个关键原因:
- 简洁性和可读性 :
try-with-resources
提供了一种更简洁的语法,可以更清晰地表达资源管理的代码。在try-with-resources
中,你只需要在try
块中声明和初始化资源,而不需要显式地编写finally
块来释放资源。这使得代码更易读、更易理解。 - 自动关闭资源 :
try-with-resources
确保在离开try
块后自动关闭已打开的资源。无需担心忘记在finally块中关闭资源,这减少了潜在的资源泄漏风险。 - 异常处理 :
try-with-resources
可以更好地处理异常。如果在try
块中发生异常,try-with-resources
会自动关闭已打开的资源,而不会干扰异常传播。这有助于避免异常被掩盖或丢失。
总之,try-with-resources
提供了更简单、更可读、更安全和更强大的资源管理机制,使代码更容易编写和维护,避免了常见的资源管理问题。因此,它通常被视为优于传统的try-finally
块。
对象通用方法
覆盖 equals
的同时重写hashcode
方法
在Java
中,equals()
方法和 hashCode()
方法之间的关系非常重要,因为它们直接影响对象在集合(如HashSet
、HashMap
等)中的行为。因此重写 equals()
的时候别忘了重写 hashCode()
方法。
接口和类
控制访问范围
区分一个组件设计得好不好,唯一重要的因素在于:它对于外部的其他组件而言,是否隐藏了其 部数据和其他实现细节 。 通常, 设计良好的组件会隐藏所有的实现细节,清晰地隔离开来。这是软件设计的基本原之一。
复合优于继承
聚合可以降低类之间的耦合度,使代码更加灵活和独立维护。这样的设计有利于灵活性、可维护性和可扩展性。
接口优于抽象类,且只用于定义类型
接口优于抽象类在于它们提供了更大的灵活性和松散耦合,允许在不同类之间定义共享的行为规范。同时,接口
的作用更多的在于制定规范,因此其更适合用于定义类型。
类层级结构优于带标签的类
类层级结构相对于带标签的类总的来说有以下优点:
- 清晰性和可读性:类层级结构更易理解和阅读,因为对象之间的关系清晰可见。
- 类型安全性:它提供了更强的类型检查,有助于减少运行时错误。
- 扩展性:可以轻松地通过添加新的子类来引入新功能,从而提高代码的可扩展性。
- 代码重用:类层级结构鼓励代码重用,因为子类可以继承父类的属性和方法。
- 可维护性:每个类都有清晰的职责,因此维护代码时更容易定位和修改相关部分。
- 可测试性:清晰定义的接口使得单元测试更加容易,因为可以针对每个类的行为进行测试。
简而言之,类层级结构有助于提高代码的可读性、可维护性和可扩展性,同时减少类型错误,使代码更加稳定。
泛型
优先使用泛型
在Java
中优先使用泛型的主要原因是提供了类型安全性、代码重用、可读性和性能优化。泛型还减少了编程错误、提供更好的工具支持和更严格的文档和自注释。因此,泛型有助于提高代码的质量、可维护性和可读性。
使用有界通配符增加API
灵活性
使用有界通配符(bounded wildcard)
其允许编写更通用、适用于多种数据类型的代码,而不仅仅是特定类型。有界通配符允许限制通配符所能表示的类型范围,从而提供更多的安全性和灵活性。
在Java
中有界通配符有两种类型:上界通配符和下界通配符。
-
上界通配符 :使用
<? extends T>
形式,其中T
是上界类型。这意味着通配符可以匹配T
类型及其子类。使用上界通配符的主要优点是可以在不确定具体子类型的情况下对多种类型的对象进行操作。例如:javapublic double sum(List<? extends Number> numbers) { double total = 0; for (Number number : numbers) { total += number.doubleValue(); } return total; }
这允许
sum
方法接受包括Integer
、Double
等在内的各种Number
子类型的列表。 -
下界通配符 :使用
<? super T>
形式,其中T
是下界类型。这允许通配符匹配T
类型及其父类型。下界通配符通常用于写入数据,以确保将对象存储到特定类型的集合中。例如:javapublic void addIntegers(List<? super Integer> list) { list.add(1); list.add(2); }
这使得
addIntegers
方法可以接受Integer
或其超类(如Number
或Object
)的列表。
总之,有界通配符使代码更加通用,允许处理多种类型,同时保持类型安全性。通过使用上界和下界通配符,可以更好地适应各种数据类型,提高代码的灵活性和可复用性。
方法
检查参数有效性
你应该方法入参信息持有怀疑态度, 为此你必须要对方法入参进行校验,以避免出现对象引用为null
等常见错误。而通过对参数的数据校验,一定程度上可以让你避免因参数传递格式不合法而导致的各种错误。
返回零长度的数组和集合,而不是null
若果方法返回 null
而不是零长度数组或者集合的方法,那么每次用到该方法时都需要 这种曲折的处理方式这样做很容易出错,因为你后期可能会忘记对null
的单独处理,进而导致各种错误。因此,永远不要返回null
,而是返回一个零长度的数组或者集合
,因为返回 null
会使 API
更难以使用,也更容易出错 而且没有任何性能优势。这一点可以参考Mybatis
中执行器查询时的写法,当其无法查询到数据时,其会返回一个长度为零的list
。
为所有导出的方法提供文档注释
及时书写注释!及时书写注释!及时书写注释!重要事情说三遍!
谨慎的设计方法签名
程序开发的本质可以理解为方法的相关调用,即通过调用各种方法
来完成各种复杂的业务逻辑,而一个恰当
的方法签名可以让你快速理解方法的具体含义,而Effective java
对于方法设计有如下建议:
- 谨慎的选择方法名称
- 避免长参数列表
- 对于入参类型的确定,优先使用接口或父类而不是具体类型
(注:更详细内容可参考笔者之前的代码重构之路:编写方法时最易忽视的"问题")
异常处理
对可恢复的情况使用受检异常,有编程错误使用运行时异常
Java
程序设计语言提供了 种可抛出结构( throwable )
:受检异常( checked exception )
、 运行时异常( run-time exception )
和错误( eπor)
如果期望调用者能够适当地恢复 对于这种情况就应该使用受检异常。反之,则抛出一个运行时异常。
优先使用标准的异常
Java
平台类库提供了一组基本的未受检异常,它们满足了绝大多数 API
的异常抛出需求重用标准的异常有多个好处 其中最主要的好处是,对于出现的异常可以更加快速的处理。此外,异常类越少,意味着内存占用越小,装载这些类的时间开销也越少。Jdk
常用的内部类常见使用场合如下:
IllegalArgumentException
:非nul1
的参数值不正确IlleqalStateException
:不适合方法调用的对象状态NullPointerException
:在禁止使用null
的情况下参数值为null
IndexOutOfBoundsException
:下标参数值越界ConcurrentModificationException
:在禁止并发修改的情况下,检测到对象的并发修改UnsupportedOperationException
:对象不支持用户请求的方法
抛出与抽象对应的异常
如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。 当方法传递由低层抽象抛出的异常时,往往会发生这种情况 除了使人感到困惑之外,这也 "污染"了具有实现细节的更高层的 API
例如:
java
public void demo() {
try {
io.read();
} catch (IOException ex) {
throw new RuntimeException();
}
}
这样相当于将IOException
封装为一个RuntimeException
进而使得当出现问题时更加难以定位问题发生地。
在细节消息中包含失败 捕获信息
当程序由于未被捕获的异常而失败的时候,应该将异常堆栈信息及请求参数等内容进行记录,以便更加快捷的定位问题所在。
通用编程
将局部变量的作用域最小化
对于一个变量,其最佳的声明地方应该是在第一次要使用它的地方进行声明。如果变量在使用之前进行声明,这只会造成混乱。对于其他试图理解程序功能的读者来说,因为这种行为只会增加一个分散他们注意力的因素 从而降低代码的可读性。
for-each
循环优先 传统的 for
循环
for each
循环(官方称之为"增强的 for 语句")解决了所有问题 通过完全隐藏迭代器或者索引变 ,避免了混乱和出错的可能。
如果需要精确的答案,请避免使用 float double
float double
类型主要是为了科学计算和工程计算而设计的 它们执行二进制浮,或 运算( binary floating-point arithmetic )
,这是为了在广泛的数值范围上提供较为精确的快速 近似计算而精心设计的 然而,它们并没有提供完全精确的结果,所以不应该被用于需要精 确结果的场合 float double
类型尤其不适合用于货币计算 ,因为要让一个 float
double
精确地表示 0.1
显然是不可能的。
如果其他类型更合适,避免使用String
字符串不适合代其他值类型 当-段数据从文件、网络,或者键盘设备,进入程 序之后,它通常以字符串的形式存在 有一种自然的倾向是让它继续保留这种形式,但是, 只有当这段数据本质上确实是文本信息时,这种想法才是合理的 如果它是数值,就应该被 转换为适当的数值类型,比如 int float
或者 Biginteger
类型 如果它是个"是/ 否
这种问题的答案,就应该被转换为 boolean
类型 。
如果存在适当的值类型,不管是基本类型,还是对象引用,大多应该使用这种类型;如果不存在这样的类型,就应该编写一个类型。
通过接口引用对象
应该优先使用接口而不是类来引用对象。这样会增强程序的灵活性。此外,如果有合适的接口类型存在,那么对于参数、返回值、变量来说就都应该使用接口类型进行声明。
遵守常见命名规范
-
包和模块的名称应该是层次状的,用句号分隔每个部分 每个部分都包括小写字母,极少数情况下还有数字 任何将在你的组织之外使用的包,其名称都应该以你的组织的域名开头
-
类和接口的名称,包括枚举和注解类型的名称,都应该包括一个或者多个单词,每个单词的首字母大写
-
方法和域的名称与类和接口的名称一样 ,都遵守相同的字面惯例,只不过方法或者域的名称的第 个字母应该小写
总结
当然,书中还有其他很多极具建设的意见,在此仅列举出了笔者认为比较重要的二十七条建议,希望对你有多帮助。
如果觉得有帮助,不妨点个收藏,众所周知,收藏等于学会~~