原则一:用静态工厂方法代替构造器
静态工厂方法和工厂设计模式、抽象工厂设计模式不是一个东西,但是它们的目的是一样的,都是为了建造对象。
那么既然都是为了建造对象,静态工厂在于服务于它本身的类,去向客户端提供它服务的这个类的对象,不像工厂设计模式,那里的工厂,提供的是它们需要帮忙提供的对象。
类除了向客户端提供构造方法,还可以向客户端提供静态工厂方法。
那么如果用静态工厂方法代替构造器就需要有优点,使其在权衡缺点的情况下能给开发人员带来更大的好处。
优势1:静态工厂方法与构造器不同的第一大优势在于,它们有名称。
这个点很好去去理解,由于每个类的构造器可以进行重载,但是它们构造器的名称都是一成不变的,所以具体要使用哪个构造器,需要开发人员在开发时注释写明用处,需要用户查询源码orAPI,但是如果使用静态工厂方法,此时就可以在方法命名上出文章,也就是不同的命名对应不同的对象。
如,我们可以使用下面这个例子(不一定恰当,只是为了说明),RandomListCreate类的目的时为了创建随机数列表,那么我们将构造器私有化,提供两个静态的创建对象方法,分别是创建10个随机数参数列表,和创建100个随机数参数列表。
public class RandomListCreate {
private List<Integer> list;
private RandomListCreate(List<Integer> list){
this.list = list;
}
public static RandomListCreate create10(){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int num = (int)(Math.random() * 100);
list.add(num);
}
return new RandomListCreate(list);
}
public static RandomListCreate create100(){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
int num = (int)(Math.random() * 100);
list.add(num);
}
return new RandomListCreate(list);
}
public List<Integer> getList(){
return list;
}
}
编译器如何检查两个方法是不是同一个方法呢?采用的是方法的签名,方法的签名和方法的名称与方法的参数有关。那么如果我们想用同样地参数,构造的对象是不同的目的时,这是一个方法就是使得参数类型在顺序上不同,但是这样的做法肯定是不好的,但是如果使用静态工厂方法,当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器。
优势2:静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新的对象。
如果只有构造方法,那么我们每次构建对象的时候,都需要去new一个对象,这时每次的调用都需要去创建一个对象。
但是,如果我们使用静态工厂,我们可以使用该类已经预先构造好的实例,或者将构造好的实例缓存起来,以便下次使用。
public class ObjectSave {
//说明1
private static ObjectSave os;
//说明2
private static HashMap<String,ObjectSave> osm;
private ObjectSave(){
}
static {
//说明1
os = new ObjectSave();
//说明2
osm = new HashMap<>();
ObjectSave o1 = new ObjectSave();
ObjectSave o2 = new ObjectSave();
osm.put("o1",o1);
osm.put("o2",o2);
}
public static ObjectSave getOS(){
return os;
}
public static ObjectSave getOSByKey(String key){
return osm.get(key);
}
}
例如上述的代码,我定义了一个静态对象的引用,一个静态HashMap,并在静态代码块中进行初始化一些对象,那么下面其它人如果使用就可以使用静态工厂方法获取已经创建好的实例或缓存。
优势3:静态工厂方法与构造器不同的第三大优势在于,它们可以返回原类型的任何子类型对象。
这个优势也比较好理解,若存在子类,就可以让子类继承它,从而返回子类对象,这也就是多态性的使用。
但是同时它的缺点也比较明显,因为子类若继承,它的构造方法需要默认调用super(),也就是父类的无参构造器,此时就不能将父类进行私有化。
所以我们可以考虑组合,使用接口(jdk8之后接口中可以有静态方法)。
public interface ReturnChild {
public static ReturnChild getObject(){
return new ReturnChildImpl();
}
}
public class ReturnChildImpl implements ReturnChild {
}
优势4:静态工厂方法的第四大优势在于,所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
这就是有点简单工厂那味道了,也就是说,我在使用静态工厂获取对象时,可以传入一些参数来获取不同的对象,这里引用一个例子,使用EnumSet的例子。
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
EnumSet是一个抽象类,它是借助静态工厂方法来返回对象,若要创建集合的长度小于等于64,就返回其子类RegularEnumSet,若创建集合的长度大于64,就返回其子类JumboEnumSet。
优势5:静态工厂的第五大优势在于,方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
这个优势我在阅读《Effective Java》时,说实话一开始是不能很看得懂的。确实是比较难以理解,因为书上说的例子太晦涩难懂了。
不过仔细想一下,如果大家不适用静态工厂,那么久需要直接去使用类的静态方法去构造对象,而静态工厂本质上只是一个返回该类对象的方法而已,所以说,我们未必要使用静态方法。
想一想,根据类的全类名是不是也可以获取对象,它使用了什么?
没错,是反射,所以我们就可以在静态方法中使用反射来获取该类的对象,由于Class.forName的方式只需要传入一个String参数,那么我们只要知道这个类它即将被定义在哪里,它的类名是什么就可以了。
书中在这里同时到了一个服务提供者框架,并用数据库连接JDBC来进行说明。
服务提供者框架由四个部分组成,分别是服务接口、提供者注册API、服务访问API于服务提供者接口。
服务接口,是服务者提供的一些功能的接口,如JDBC中的Connection,就是为了连接数据库。
提供者注册API,这时提供者用来注册实现的,如JDBC中的DriverManager.registerDriver(Driver driver),是为了加载驱动,当客户端向该方法提供对应的数据库驱动对象时,就加载对应的数据库驱动,如是mysql的driver自然就是加载mysql的驱动。
服务访问API,这是用来获取服务的实例的,如服务接口中提及的Connection,它只是一个接口,此时当我们注册完成之后,就要获取对应数据库的连接了,此时就要使用服务访问API,如JDBC中的DriverManager.getConnection。
服务提供者接口,表示产生服务接口之实例的工厂对象,这部分也很好理解,其实就是Driver,那么参考JDBC中我们获取Diver,就要使用其对应的全类名。
同时,服务提供者框架有很多变种,如JDBC这一套,它的实现是通过桥接模式的,桥接模式也就是让抽象和实现进行分离。
缺点1:静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化。
这个缺点我在上面也有提及,是子类构造器默认会调用父类的空参构造器,所以就导致这个缺点,上面也提及了解决方法就是使用组合(不通过继承的方式,而是通过接口实现的方式)。
缺点2:静态工厂方法的第二个缺点在于,程序员较难发现它们。
这个其实就是需要开发人员写好文档啦~
书中给的建议:优先使用静态工厂方法而不是构造器。
原则二:遇到多个构造器参数时要考虑使用构建器
这个原则本质上在提什么事情呢?
就是说,如果构造一个对象时,需要的参数太多了,那么就会显得非常难受,假设一个构造器
有100个参数,并且这些参数中还有好多连着的int,int,int,这时去辨别到底该输入哪个参数就会变得异常难受,并且可能会出错。
那么为了解决以上问题,我们可以有两个想法,第一个想法就是使用重叠构造器。
public class Constructer {
private int arg1;
private int arg2;
private int arg3;
public Constructer(int arg1) {
this(arg1,0);
}
public Constructer(int arg1, int arg2) {
this(arg1,arg2,0);
}
public Constructer(int arg1, int arg2, int arg3) {
this.arg1 = arg1;
this.arg2 = arg2;
this.arg3 = arg3;
}
}
也就是说,我们需要传入几个参数就传入哪几个参数来构造对象,但是缺点也非常明显,因为此时第一会导致类的设计非常臃肿,第二,如果我是传入arg1与arg3来创建对象呢?是不是没有现成的方法?而且就我个人觉得这样的设计完成没有必要,由于类在进行初始化时会给成员变量进行赋初值。
那么第二个想法,是学JavaBean,我用默认构造器创建一个空对象,然后用set去设置,也就是一组set,但是此时的缺点也是比较明显的,且不谈一组set恶心人,那么一组set是可以被中断的,在多线程的情况下就会存在线程安全问题。
此时一个非常好的设计模式就出现了,那就是建造者模式(一种设计模式),使用建造者模式来构建对象将会变得更加清晰。
Builder,Builder,Builder,yyds!
我们可以通过静态内部类Builder来进行设置参数的值(内部类的参数与外界完全一致),当设置完成之后,调用build()方法来构建外部类对象,此时就是一个完美的选择,那么此时也会和JavaBean有一样的问题,会不会线程不安全?那是不会的,有jvm会保证线程的安全性。
那么我们就可以将上述的Constructer类修改成以下形式:
public class Constructer {
private int arg1;
private int arg2;
private int arg3;
public Constructer(Builder builder) {
this.arg1 = builder.arg1;
this.arg2 = builder.arg2;
this.arg3 = builder.arg3;
}
public static class Builder{
private int arg1;
private int arg2;
private int arg3;
public Builder(){
}
public Builder setArg1(int arg1){
this.arg1 = arg1;
return this;
}
public Builder setArg2(int arg2){
this.arg2 = arg2;
return this;
}
public Builder setArg3(int arg3){
this.arg3 = arg3;
return this;
}
public Constructer build(){
return new Constructer(this);
}
}
public static void main(String[] args) {
Builder builder = new Constructer.Builder();
Constructer build = builder.setArg1(1).setArg2(2).setArg3(3).build();
}
}
简而言之,如果类的构造器或者静态工厂中具有多个参数,在设计这种类时,构建器模式就是一种不错的选择。
原则三:用私有构造器或者枚举类型强化Singleton属性
这个其实也就是讲究一个单例模式,但是呢,还是需要注意几个问题。
依据单例模式,其实一共就是两种,分别是懒汉和饿汉,懒汉是线程安全的,饿汉是线程不安全的,需要两次判断并加锁,而饿汉的getInstance()其实也就很类似于静态工厂方法了,因为构造器是私有的,对象也是私有的。
在这里我们要探讨一个问题,那就是在存在反射之后,私有也没那么安全了,所以我们可以在构造器要求被创建第二个实例时,加入异常,如以下的方式:
public class ReinforceSingleton {
public static final ReinforceSingleton rs = new ReinforceSingleton();
public ReinforceSingleton() {
if (rs != null){
throw new RuntimeException();
}
}
}
这里同时也可以考虑静态工厂的优势,比如用饿汉的getInstance,我们可以通过提供方法引用的方式来提供对象。
public class ReinforceSingleton {
public static ReinforceSingleton rs;
private ReinforceSingleton() {
}
public static ReinforceSingleton getInstance(){
if(rs == null){
synchronized (ReinforceSingleton.class){
if(rs == null)
rs = new ReinforceSingleton();
}
}
return rs;
}
public static void test(Supplier<ReinforceSingleton> supplier){
System.out.println(supplier.get());
}
public static void main(String[] args) {
test(ReinforceSingleton::getInstance);
}
}
个人感觉有点推迟到使用再创建对象的味道了。
那么如果对于一个单例对象想把它进行序列化呢?
此时一定要注意的是仅仅implements Serializable是完全不够的,因为在每次反序列化一个实例时,都会创建一个新的实例。
那么这个问题如何解决呢?
其实对于对象的序列化与反序列化有一个hook(钩子方法),当实现了它之后,就不会出现上述的情况了,也就是readResolve方法,示例如下:
public class Singleton implements Serializable {
public static final Singleton instance = new Singleton();
private Singleton() {
}
private Object readResolve() throws ObjectStreamException {
// instead of the object we're on,
// return the class variable INSTANCE
return instance;
}
public static void main(String[] args){
Singleton s = Singleton.instance;
System.out.println(s);
try (FileOutputStream fos = new FileOutputStream("SerializableTest.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream("SerializableTest.txt");
ObjectInputStream ois = new ObjectInputStream(fis);){
oos.writeObject(s);
Object o = ois.readObject();
System.out.println(o);
}catch (IOException e){
e.printStackTrace();
}catch (ClassNotFoundException e){
e.printStackTrace();
}
}
}
Create.Singleton@2503dbd3
Create.Singleton@2503dbd3
这样子的结果就是一样的,否则就是不一样的,上述IO流的对象都使用了try-with-resource,但是还是jdk9的好用,可以不声明在里面,由其给我自动释放连接。
那么实现单例的一个更好的方式就是枚举了,枚举无偿提供了序列化机制,绝对防止多次实例化,即使面对复杂实例化或者反射攻击。
//推荐方式,很安全!
public enum Elvis {
INSTANCE(1,2);
private Elvis(int a,int b) {
this.a = a;
this.b = b;
}
private int a;
private int b;
}
原则四:通过私有构造器强化不可实例化的能力
首先要想清楚为什么要强化不可实例化的能力,这是因为对于一些工具类而言,就是没有必要去进行实例化的,所以我们要将构造器做成私有的。
但是私有的一定也有问题,通过反射就可以撬动了,所以一样,可以在构造器中加入异常,确保不能使用构造器。
此外,也要强调,企图通过将类做出抽象类来强制该类不可实例化是行不通的,因为该类的子类可以实例化,所以我觉得可以使用接口,哈哈哈哈!不过没那么方面,还是强化私有构造器吧。
在这种情形下,就是没有子类了。
原则五:优先考虑依赖注入来引用资源
考虑依赖注入的话,首先要清楚什么是依赖注入,依赖注入最简单的模式是当创建一个新的实例时,就将该资源传输到构造器中,使该类对其产生依赖。
比如我们有一台拼写检查机,它需要依赖字典资源,所以我们就可以在构建拼写检查机时,进行依赖注入。
public class Dictonary {
}
public class SpellChecker {
private final Dictonary dictonary;
public SpellChecker(Dictonary dictonary) {
this.dictonary = dictonary;
}
}
那么另一个方式,就是我们可以把上述的final去掉,因为一个拼写检查机可能不止使用一台字典,此时还可能会需要多个字典,那么需要哪个对象,就要创建哪个对象,那么我们干脆在依赖注入时传入一个工厂对象,可以通过Suppiler<T>来提供。
原则六:避免创建不必要的对象
这个原则的意思是,有些对象在无意识中可能被创建,所以要避免,就比如说以下例子,对比两者的不同。
for (int i = 0; i < 100; i++) {
String s = new String("s");
}
for (int i = 0; i < 100; i++) {
String s = "s";
}
考虑以下极端的情况,对于第一种,我们每次创建一个s时都在堆中创建一个“s”,执行完都创建100个了,而下面一个自始至终就只有一个,因为时字符串字面量,保存在静态方法区,可以被重用。
另一种要注意的是,自动装箱问题这种东西就很麻烦,因为一不小心,就会创建很多装箱类型,比如如下情况:
Integer num = 10;
for (int i = 0; i < 10000; i++) {
num += i;
}
此时i会被自动装箱成Integer,就会很浪费时间,要注意!我们要优先使用基本数据类型!
原则七:消除过期对象的引用
对于java来说,不被使用的对象会被自动回收,但是在某些情况下,就会出现问题,可能会出现内存泄漏。
这个问题也就是原则七种的消除过期对象的引用,如果一个对象过期了,不再使用了,要把对它的引用消除,也就是将那个引用变量置为null。
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity(){
if (elements.length == size)
elements = Arrays.copyOf(elements,2 * size + 1);
}
}
如我们针对以上代码,我们会发现一件事情,就是elements的存储空间自初始化之后是不会改变的,而我们用的size是记录我们当前用了多少空间。
试想以下,当我们入栈了一些元素,之后又出栈了一些元素,虽然逻辑上这些元素已经出栈了,但是它本质上还是在elements这个列表当中,所以仍然存在引用,此时就存在过期引用的现象,也会出现内存泄露。
那么如何改进呢?
public Object pop(){
if (size == 0)
throw new EmptyStackException();
elements[size] = null;
return elements[--size];
}
在出栈之前把它的引用置为空,时间久了不使用就会被回收。
注意:只要是自己管理内存就容易出现内存泄露的问题。
内存泄露的另一个来源是缓存,比如我们使用HashMap来制作缓存,若出现遗忘,就会导致它在不使用之后的很长一段时间内仍然存在缓存之中。
这时候我们可以考虑若引用,如WeakHashMap来代表缓存:
那么本质上,WeakHashMap也就是一种WeakReference,那么WeakReference是什么呢?
首先它是一种弱引用,我们平常用new出来的对象都是一种强引用,而若引用是这样定义的:
WeakReference<Stack> weakReference = new WeakReference<Stack>(new Stack());
weakReference.get();
我们可以通过get来获取原对象的引用,那么它有什么好处呢?当没有人再去引用这个Stack()对象时,它就会被gc,这样能有效避免遗忘清理缓存的问题。
内存泄露的第三个常见来源是监听器和其它回调,如果你实现了一个API,客户端在这个API种注册回调(其实也就是交了一个对象过去),如果这个时候没有显示的取消注册,此时就会产生内存泄露问题。
public interface Listener {
void callBack();
}
public class API {
private Listener listener;
public void setListener(Listener listener) {
this.listener = listener;
}
public void callBack(){
listener.callBack();
}
}
public class Client {
public static void main(String[] args) {
API api = new API();
api.setListener(() -> System.out.println("回调了"));
api.callBack();//没人回调就在这里模拟了
}
}
在这里我们对于listener的引用就没有被消除!!
原则八:避免使用终结方法和清除方法
这个原则说白了,不要自己去使用java提供的System.gc或者jdk9之后提供的cleaner。
原则九:try-with-resources优先于try-finally
对于这个原则,我也觉得是优先使用try-with-resources,因为真的太方便了,都不用自己去管理连接的释放,因为我们使用IO流等其它一些连接对象时,耗费的资源都是巨大的,此时呢,如果没有及时去关闭,就会造成问题,我们写一个读文件的例子,来进行说明。
public class ReadFile {
public static void read(String fileName) {
File file = new File(fileName);
FileInputStream fis = null;
int a;
try {
fis = new FileInputStream(file);
while ((a = fis.read()) != -1){
System.out.println((char) a);
}
}catch (FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private ReadFile(){
throw new RuntimeException();
}
public static void main(String[] args) {
read("test.txt");
}
}
大家可以看得到使用try-catch-finally写的方式恨得很烦,但是使用try-resources-with就会舒服得多:
public class ReadFile {
public static void read(String fileName) {
File file = new File(fileName);
try(FileInputStream fis = new FileInputStream(file);){
int a;
while ((a = fis.read()) != -1){
System.out.println((char) a);
}
}catch (FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private ReadFile(){
throw new RuntimeException();
}
public static void main(String[] args) {
read("test.txt");
}
}
不用自己去close,真的舒服~