EFFECTIVE JAVA, 3E阅读笔记二
创建和销毁对象
1.第1条:用静态工厂方法代替构造器
(1)获取类自身实例方法:
- 类构造器
- 静态工厂方法(与设计模式中的工厂方法模式不同)
(2)优势:
- 静态工厂方法有名称,可以确切描述用途,而构造器的名称无法进行自定义
- 不必每次调用都创建新对象,返回的是相同对象
- 可以返回原本返回类型的任何子类型,而构造方法只能返回所属类的实例对象
- 所返回的对象的类可以随着每次调用而发生变化,取决于方法的参数值
- 方法返回对象所属的类,在编写包含该静态工厂方法的类时可以不存在(比如后面新增的子类,原先不存在的话可以抛异常或给默认值)
(3)缺点:
1)类如果不含公有的或者受保护的构造器,就不能被子类化
2)难以被使用者发现,由于静态工厂方法没有在API文档中明确标识,比较自由,使得它不容易被使用者所发现,所以在没有规范出台之前,通过注释说明,并遵循标准的命名习惯,可以先弥补这一劣势。
下面是静态工厂方法的一些惯用名称:
- from——类型转换方法,它只有单个参数,返回该类型的一个相对应的实例
- of——聚合方法,带有多个参数,返回该类型的一个实例,把他们合并起来
- valueOf——比from和of更繁琐的一种替代方法,该方法返回的实例与它的参数具有相同的值,实际上是类型转换方法
- instance/getInstance——返回的实例是通过方法的参数来描述的,对于单例模式(Singleton)来说,该方法无参数,并返回唯一的实例
- create/newInstance——功能同getInstance,但它能够确保每次调用都返回一个新的实例
- getType——像getInstance一样,但是在工厂方法处于不同类中的时候使用。Type表示工厂方法所返回的对象类型
- newType——像newInstance一样,但是在工厂方法处于不同的类中的时候使用
- type——getType和newType的简版
(4)总结
写程序时,优先考虑静态工厂方法。
2.第2条:遇到多个构造器参数时要考虑使用建造者模式(Builder)
重叠构造器模式可行,但是当有许多参数的时候,代码很难编写(每个参数代表什么)以及难以阅读
JavaBeans模式,不足有:
- 构造过程中JavaBean可能处于不一致的状态,例如:多次调用set方法,重复调用的结果不一样
- 因为要set,会把类做成可变的,需要确保线程安全
建造者模式,它不直接生成需要的对象,而是调用一个包含所有必需参数的构造方法 (或静态工厂) 得到获得一个 builder 对象。然后客户端在builder 对象上调用类似于setter的方法,来设置其它可选参数。最后,客户端调用无参的builder方法生成对象,该对象通常是不可变的。这个builder通常是它所构建的类的一个静态成员类。下面是它的示例:
// Builder Pattern 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; } public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build(); } }
总结
Builder模式十分灵活,可以利用单个builder构建多个对象。
为了创建对象,必须先创建它的构造器,在注重性能的情况下不太适用。
一开始使用构造器或者静态工厂,等需要多个参数时才添加构造器,可能无法控制了,因此,最好一开始就是用构建器。
如果有很多参数,Builder模式是一种不错的选择。
3.第3条:用私有构造器或枚举型强化Singleton属性
Singleton指仅仅被实例化一次的类。Singleton通常会用来代表一个无状态的对象,如函数,或者那些本质上唯一的系统组件。使类成为Singleton会使它的客户端测试变得十分困难,因为无法给Singleton替换模拟实现,除非它实现一个充当其类型的接口。
实现Singleton的两种常见的方法
公有域方法和静态工厂方法,这两种方法都要把构造器保持为私有的,私有构造器仅被调用一次,用来实例化公有的静态final域,提供公有的静态成员,以便允许客户端能够访问该类的唯一实例。
- 公有域方法:公有静态成员是个final域,所以该域总是包含相同的对象引用;相比静态工厂方法更简单
//Singleton with public final field public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis(){...} }
- 静态工厂方法:公有静态成员是个静态工厂方法;灵活(在不改变其API前提下,可以改变该类是否应该成为Singleton);支持泛型
//Singleton with public final field public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis(){...} public static Elvis getINSTANCE() {return INSTANCE;} }
最佳方法:包含单元素的枚举类型
public enum Elvis{ INSTANCE; }
4.第4条:通过私有构造器强化不可实例化的能力
有时可能需要编写只包含静态方法和静态域的类作为工具类,这样的工具类不希望被实例化,实例化对它没有任何意义。
企图通过将类做成抽象类来强制该类不可被实例化是行不通的。解决:由于只有当类不包含显示的构造器时,编译器才会生成缺省的构造器,因此只要让类包含一个私有构造器,它就不能被实例化。
副作用:它使得一个类不能被子类化。所有的构造器都必须是显示或隐式地调用超类的构造器,在这种情况下,子类就没有可以访问的超类构造器可以调用了。
5.第5条:优先考虑依赖注入来引用资源
静态工具类和Singleton类不适合于需要引用底层资源的类
当需要能够支持多个实例,每个实例都使用客户端指定的资源。最简单的模式是,当创建新的实例时,将资源传到构造器中,这是依赖注入的一种形式。
// Dependency injection provides flexibility and testability public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } }
依赖注入的对象资源具有不可变性,因此多个客户端可以共享依赖对象(假设客户需要相同的底层资源)。依赖注入同样适用于构造器、静态工厂和 构建器(Builder)。
总而言之,不要用单例和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注人,它极大地提升了类的灵活性、可重用性和可测试性。
6.第6条:避免创建不必要的对象
对象尽可能利用起来,别重复创建
尽量使用String str = "XXX";而不是String str = new String("XXX")
优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱
private static long sun(){ Long sum = 0L; //错把long写成Long,导致程序构造了大约2^31个多余的Long实例 for(long i = 0; i <= Integer.MAX_VALUE; i++){ sum += i; } return sum; }
然而,小对象的构造器只做很少量的显示工作,所以小对象的创建和回收动作并不会很影响性能,甚至能提升程序的清晰性、简洁性
实在需要新建对象,就不要重用现有对象,毕竟BUG和安全漏洞代价更大
7.第7条:消除过期的对象引用
如果一个栈先增后减,那么,从栈中弹出来的对象不会被当作垃圾回收,因为栈内部维护着对这些对象的过期引用,弹出的已经是过期引用。
解决:如果对象引用已经过期,可以设为null值去清空对象引用。
清空对象引用应该是一种例外,而不是一种规范行为,最好的办法是让包含该引用的变量结束其生命周期
只要类是自己管理内存,就应该警惕内存泄露问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空
内存泄露的另一个常见来源是缓存。一旦你把对象引用放入到缓存中,他就很容易遗忘,对于这个问题,可以使用WeakHashMap代表缓存,WeakHashMap的特点是,当只有自身有对key的引用,没有外部引用,他们就会自动被删除。
内存泄露的第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示地取消注册,那么就会不断堆积。需要确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用,例如将他们保存成为WeakHashMap中的键。
8.第8条:避免使用终结方法和消除方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的,使用了可能会导致行为不稳定、性能降低,以及可移植性问题。
Java9使用清除方法代替了终结方法,降低了危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。
缺点
在于不能保证会被及时执行。
终结方法存在问题
- 未被捕获的异常将会使线程终止
- 性能损失:它阻止了有效的垃圾回收
- 安全问题:从构造器跑出异常时,恶意子类的终结方法可以在构造一部分的应该半途夭折的对象上运行,可以通过编写一个空的final的finalize方法去解决。
如果确实需要终止,可以利用try-with-resources确保终止。
两种用途:
- 当资源的所有者忘记调用它的close方法时,终结方法或者清楚方法可以充当“安全网”
- 与对象的本地对等体(本地非Java对象)相关,当它的Java对等体被回收,它不会被回收,此时可以使用清除方法
总结
除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用清楚方法。使用时,注意它的不确定性和性能后果。
9.第9条:try-with-resources优先于try-finally
要想使用try-with-resources,自定义的类必须实现AutoCloseable接口,包含了单个返回void的close方法。
在处理关闭资源时,优先考虑用try-with-resources,而不是用try-finally。代码将更加简洁、清晰,产生的异常也更有价值(try-finally多个异常时,可能会出现异常覆盖)