1.考虑使用静态方法代替构造方法
对于类而言,最常用的获取其实例的方法就是通过构造方法,还用一种方法,就是提供公有的静态工厂方法返回此类的实例。
静态工厂方法的优势:
1.静态工厂方法有名称,可以更确切的描述被返回的对象。并且一个类只有一个给定签名的构造方法,虽然程序员们可以通过重载构造方法的方式来实现,但是这种方式并不优雅。
2.不必在每次调用都返回一个新对象,工厂方法可以重复利用实例进行实例控制,提升性能
3.可以返回原类型的子类型对象,适用于基于接口的框架,比如Collection接口和Collections实现类,因为接口中不能出现静态工厂方法,所以静态工厂方法都放在了不可实例化的Collections中
如:
List list = Collections.synchronizedList(new ArrayList())
4.返回对象的类可以根据输入的变化而变化
//这样很麻烦
Map<String, List<String>> m = new HashMap<String, List<String>>();
//这样很简洁
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
5.返回对象不需要再写这个方法的时候就存在,服务提供者框架的基础,参考JDBC的实现,使服务提供者和服务使用者解耦
缺点:
1.类如果不含有公有的或者受保护的构造方法,就不能被继承。如果使用静态工厂方法获得实例,而不提供公有的构造方法,把构造方法写为私有private,那么该类就不能被继承扩展,这就是使用复合而不是继承来扩展类了。
2.静态工厂方法的第二个缺点是,程序员很难找到它们
总之,静态工厂方法和公共构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,
静态工厂更可取,因此避免在没有考虑静态工厂的情况下直接选择使用公共构造方法
2.当构造方法参数过多时使用 builder 模式
比如要创建这么一个对象:
创建对象的方式:
1.可伸缩构造方法,但是参数过多会变得难以阅读和维护,读者不知道这些值是什么意思,并且必须仔细地去数参数才能找到答案。一长串相同类型的参数可能会导致一些细微的 bug。如果客户端不小心写反了两个这样的参数,编译器并不会报错,但是程序在运行时会出现错误行为。
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
2.通过java bean模式,在构造过程中 JavaBean 可能处于不一致的状态
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings = -1; // Required; no default value
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
// Setters
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
}
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
3.通过建造者模式,即能保证像重叠构造器的安全性,也能保证像 JavaBeans 模式那么好的可读性
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
//使用
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
3.私有构造方法或者枚举类实现单例
Singleton(单例)指仅仅被实例化一次的类. 通常用来代表那些本质上唯一的系统组件
单例的实现: 私有构造方法, 类中保留一个字段实例(static, final), 用public直接公开字段或者用一个public static的getInstance()方法返回该字段(懒汉,饿汉)
java1.5之后可以用枚举来实现单例,这样做的好处是可以防止反射来多次实例化
4.使用私有构造器执行非实例化
只包含静态方法和静态域的类名声不太好, 因为有些人会滥用它们来编写过程化的程序. 尽管如此, 它们确实也有特有的用处,比如: java.lang.Math, java.util.Arrays把基本类型的值或数组类型上的相关方法组织起来; java.util.Collections把实现特定接口的对象上的静态方法组织起来; 还可以利用这种类把final类上的方法组织起来, 以取代扩展该类的做法。
这种工具类(utility class)不希望被实例化, 然而在缺少显式构造器的情况下, 系统会提供默认构造器, 可能会造成这些类被无意识地实例化。通过做成抽象类来强制该类不可被实例化, 这是行不通的, 因为可能会造成”这个类是用来被继承的”的误解, 而继承它的子类又可以被实例化。所以只要让这个类包含一个私有的构造器, 它就不能被实例化了. 进一步地, 可以在这个私有构造器中抛出异常。
这种做法还会导致这个类不能被子类化, 因为子类构造器必须显式或隐式地调用super构造器. 在这种情况下, 子类就没有可访问的超类构造器可调用了。
总结一下就是,工具类可以考虑私有化构造器,不让其实例化
5.依赖注入优于硬连接资源(hardwiring resources)
就是让类所依赖的对象作为参数让使用者调用的时候注入
让常变化的东西由使用者决定,可以结合builder和工厂模式
什么是硬连接资源,我们可以理解为,一个类,依赖其它基础类,但是过于理想化,指定了它只连接一个基础类。例如我们有一个工具类:拼音检查器,它可以检查拼音是否正确,那么我们的字典里是有所有拼音的,可以给我们提供一个基础的检查数据,当我们用硬连接资源的方式去思考时,这个拼音检查器的基础数据只由《新华字典》提供。是不是感觉这是不合理的,因为每种语言都有自己的字典,数据只由《新华字典》来提供的话,感觉是不灵活,并且单一的。
哪里用
当一个类依赖一个或其他多个底部资源类的时候,我们可以考虑使用依赖注入优先于硬连接资源。
怎么实现
使用静态方法来替代硬连接资源,这种方式是不灵活,并且不能够测试的。代码实现如下:
/**
* 通过静态方法来替代硬资源连接
*
* @author gongguowei01@gmail.com
* @since 2020-01-12
*/
public class SpellChecker {
//仅提供了一个资源
private static final Lexicon dictionary = "依赖的资源";
//私有构造方法,非实例化
private SpellChecker() {
}
//拼音检查器提供给外部类调用的方法
public static boolean isValid(String word) {
return true;
}
}
使用单例来替代硬连接资源,同样也是不灵活的 ```java /**
- 使用单例来替代硬连接资源 *
- @author gongguowei01@gmail.com
@since 2020-01-12 */ public class SpellChecker02 { private final Lexicon dictionary = “依赖的资源”;
public static NSTANCE = new SpellChecker02();
private SpellChecker02() {
}
//拼音检查器提供给外部类调用的方法 public static boolean isValid(String word) {
return true;
}
}
- 使用依赖注入替代硬连接资源,依赖注入提供了灵活性和可测试性。虽然下方的实例仍是一个基础资源,但是它由外部类提供,这样保证了我们的SepllChecker提供的功能不变,但是数据却是灵活多变的。
```java
/**
* 使用依赖注入替代硬资源连接
*
* @author gongguowei01@gmail.com
* @since 2020-01-12
*/
public class SpellChecker03 {
private final Lexicon dictionary;
//需要检查什么资源,由外部类提供基础的数据
public SpellChecker03(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
}
不要用单例和静态方法类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为,需要使用一个或多个底部资源类的一个类,静态方法、单例来实现是不适合的,因为它们不够灵活,也不能够测试。使用静态方法,我们如果希望它能够使用一个以上的底部资源类,我们可以通过使 dictionary 属性设置为非 final,并添加一个方法来更改现有拼写检查器中的字典,从而让SpellChecker 支持多个字典,但这感觉是笨拙的。使用依赖注入来实现硬资源连接,也有一定的弊端,一般大型的项目,可能有数千个依赖项,这让我们的项目变得混乱,不过我们可以使用Spring框架来消除这些混乱。依赖注入,它极大地提升了类的灵活性、可重用性和可测试性。
6.避免创建不必要的对象
比如:String s = “bikini”; 不要 String s = new String(“bikini”);
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
//可以考虑提出常亮字符串
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile( "^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
7.消除过期的对象引用
过期对象的引用:指的是永远不会再被解除的引用。为什么会出现这样的情况,这是因为我们的栈在显示增长,然后再收缩,那么从栈中弹出来的对象将不会被当做垃圾回收,即使使用的栈的程序不再引用这些对象,它们也不会被回收。因为,栈内部维护着对这些对象的过期引用。
没有消除过期对象实例,代码如下:
/**
* @author gongguowei01@gmail.com
* @since 2020-01-19
*/
public class Stack {
public Object[] elements;
public int size = 0;
private static final int DEFAULT_VALUE = 16;
public Stack() {
elements = new Object[DEFAULT_VALUE];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (0 == size) {
throw new EmptyStackException();
}
return elements[--size];
}
public void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
上方代码测试实例如下:
/**
* @author gongguowei01@gmail.com
* @since 2020-01-19
*/
public class Test {
public static void main(String[] args) {
Stack stack = new Stack();
stack.push("1");
stack.push("2");
stack.pop();
stack.push("3");
}
}
运行结果如下:
当我们调用pop()方法时,size变为1,但是elements数组中还存在过期对象“1”的引用。
消除过期对象的引用,代码实例如下:
/**
* @author gongguowei01@gmail.com
* @since 2020-01-19
*/
public class Stack02 {
public Object[] elements;
public int size = 0;
private static final int DEFAULT_VALUE = 16;
public Stack02() {
elements = new Object[DEFAULT_VALUE];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (0 == size) {
throw new EmptyStackException();
}
Object result = elements[--size];
//消除过期对象引用
elements[size] = null;
return result;
}
public void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
哪里用
在有可能出现内存泄露的场景:
1.缓存
2.自己管理内存的场景
3.监听器和其他回调
总结
不消除过期对象的引用,将会引起内存泄露,内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器(heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的
8.避免使用 Finalizer 和 Cleaner 机制
终结方法(finalizer)和清除方法(cleaner)都是不可预测的,两者都有非常严重的性能损失
终结方法还有个严重的安全问题:终结方法攻击
java9中清除方法代替了终结方法
合理用途:
当资源所有者忘记调用它的close方法时,终结方法和清洁方法可以充当安全网
与对象的本地对等体一起使用
9.使用 try-with-resources 语句替代 try-finally语句
单个资源需要关闭时
// try-finally - No longer the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
//可改为:
// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader( new FileReader(path))) {
return br.readLine();
}
}
多个资源时显得混乱:
// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {OutputStream out = new FileOutputStream(dst);
try {byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n); }
finally {
out.close();
} }
finally {
in.close();
}
}
//第二个异常完全冲掉了第一个异常,导致在异常堆栈跟踪中没有第一个异常的记录,
//可改为:
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
//更清晰
可以在 try-with-resources 语句中添加 catch 子句,就像在常规的 try-finally 语句中一样。这允许你
处理异常,而不会在另一层嵌套中污染代码。作为一个稍微有些做作的例子,这里有一个版本的
firstLineOfFile 方法,它不会抛出异常,但是如果它不能打开或读取文件,则返回默认值:
// try-with-resources with a catch clause
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))){
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
结论很明确:在处理必须关闭的资源时,使用 try-with-resources 语句替代 try-finally 语句。 生成的
代码更简洁,更清晰,并且生成的异常更有用。 try-with-resources 语句在编写必须关闭资源的代码时会
更容易,也不会出错,而使用 try-finally 语句实际上是不可能的。
10. 重写 equals 方法时遵守通用约定
不需要重写equals的情况:
1.每个类的实力都是固定唯一的,如Thread这样代表活动实体而不是值的类
2.不需要判断逻辑相等
3.父类已经重写过,子类可以继承
4.类是私有的或者包级私有的,equals方法永远不会被调用,不需要重写
重写准则:
1.自反性: 对于任何非空引用 x, x.equals(x) 必须返回 true。
2.对称性: 对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true。
3.传递性: 对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true, y.equals(z) 返回 true,则 x.equals(z)必须返回 true。
4.一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则x.equals(y) 的多次调用必须始终返回 true 或始终返回 false。
5.对于任何非空引用 x, x.equals(null) 必须返回 false。
一些提醒:
1.重写equals同时也要重写hashcode方法
2.不要让equals方法试图太聪明。如果只是简单地测试用于相等的属性,那么要遵守equals约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
3.在equals方法声明时,不要讲参数Object替换成其他类型
如:
// Still broken, but won’t compile
@Override
public boolean equals(MyClass o) { … }
//这样编译过不了,去掉@Overrid,就不是重写,而是重载了
11. 重写 equals 方法时同时也要重写 hashcode方法
重写equals一定要重写hashcode方法,如果不这样做,在HashMap和HashSet中会不正确
Object规范:
1.对一个对象多次调用hashcode方法时,返回值必须一致
2.两个对象equals相等,hashcode必须相等
3.两个对象equals不相等,hashcode不做要求(可以相等也可以不相等)
如果HashMap put一个对象作为key,key不重写hashcode方法,get这个key可能得不到,因为hashcode变了
不要试图冲哈希码计算中排除重要的属性来提升性能
不要为hashCode返回的值提供详细的规范,因此客户端不能合理的依赖它;你可以改变它的灵活性
12.始终重写toString方法
toString的通用约定,返回的字符串应该是一个简洁但内容丰富的表示
不要仅仅反回内存地址
13.谨慎的使用clone方法
Cloneable接口的目的是作为一个mixin接口,它用来做什么的?它决定了Object的受保护的clone方法的实现行为:如果一个类实现了Cloneable接口,那么Object的clone方法将返回改对象的逐个属性的拷贝,否则会抛出CloneNotSupportedException 异常。这是一个非常反常的接口 使用,而不应该被效仿。 通常情况下,实现一个接口用来表示可以为客户做什么。但对于 Cloneable 接 口,它会修改父类上受保护方法的行为
说在前面
有些专家级程序员干脆从来不去覆盖clone方法,也从来不去调用它,除非拷贝数组。
其他方式
可以提供一个构造函数或者工厂去实现clone功能。
相比于clone,它们有如下优势:
- 不依赖于某一种很有风险的、语言之外的对象创建机制;
- 不要求遵守尚未定制好的文档规范;
- 不会与final域发生冲突;
- 不会抛出不必要的受检异常;
- 不需要进行类型转换;
例如,通用集合的实现都提供了一个拷贝构造函数,它的参数类型为Collection或Map。
假如要把一个HashSet拷贝成一个TreeSet:
HashSet s = ...
new TreeSet(s)
如果一定要覆盖clone方法,那么则需要了解以下它的注意事项了。
Clone规范
x.clone() != x //true
x.clone().getClass() == x.getClass() //true
x.clone.equals(x) // true
行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。
Clone做法
- 所有实现了Cloneable接口的类都应该用一个公有方法覆盖clone;
此公有方法首先要调用super.clone,然后在修正需要修正的域; ```java // 伪代码 class User implements Cloneable {
@Override public User clone() {
User user = (User)super.clone(); // 1.先调用super.clone
user.set ... // 2.在修正
}
}
**Clone要点**<br />如果覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象,如果类的所有父类都遵守这条规则,那么调用super.clone最终会调用Object的clone方法,从而创建出正确类的实例。这种机制大体上类似于自动的构造器调用链。<br />**简单Clone**<br />如果类中包含的每个域是一个基本类型的值,或者包含的是一个指向不可变对象的引用,那么调用clone被返回的对象则可能正是所需要的对象,在这种情况下不需要在做进一步的处理。<br />**复杂Clone**<br />如果类中包含的域是指向一个可变对象的引用,那么就要小心的对其进行clone。<br />例如,若类中存在一个Object[]数组,则可以参考一下做法:
```java
// 伪代码
class Stack {
private Object[] elements;
private int size = 0;
@Override
public Stack clone() {
Stack result = (Stack) super.clone();
result.elements = this.elements.clone();
}
}
还有一种情况,若类中存在一个对象或者集合(自定义对象、List、Map等),那么光调用这些对象的clone还不够,例如编写一个散列表的clone方法,它的内部数据包含一个散列桶数组:
// 伪代码
class HashTable implements Cloneable {
private Entry[] buckets = ...
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(key, value, next) ...
}
}
如果只调用了buckets.clone,其实克隆出来的buckets和被克隆的buckets内的entry是引用着同一对象的。
这种情况下,必须单独拷贝并组成每个桶的链表,例如:
// 伪代码
class HashTable implements Cloneable {
private Entry[] buckets = ...
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(key, value, next) ...
}
// 提供一个深拷贝函数
Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
@Override
public HashTable clone() {
try ...
HashTable result = (HashTable) super.clone();
result.buckets = new Enrty[buckets.length];
for(int i=0;i<buckets.length;i++) {
if(buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
}
return result;
catch CloneNotSupportedException e ...
}
}
提供一个深拷贝方法,遍历源对象的buckets,将它拷贝到新对象中。
这种做法有一个确定,如果散列桶很长,很容易导致栈溢出,因为递归的层级太多!
解决这种问题,可以采用迭代(iteration)来代替递归(recursion),修改一下deepCopy方法:
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next) {
p.next = new Entry(p.next.key, p.next.value, p.next.next);
}
return result;
}
最好还做到
Object的clone方法被声明为可跑出CloneNotSupportedException异常,但是,覆盖版本的clone方法可能会忽略这个声明。公有的clone方法应该省略这个声明,因为不会跑出受检异常的方法用起来更轻松。
如果专门为了继承而设计的类覆盖类clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:
- 声明为protected;
- 抛出CloneNotSupportedException;
- 不实现Cloneable接口;