注:本文是《Effective Java》学习的笔记。
刚看完《java编程思想》这本书。看到网上又推荐了一个叫《EffectiveJava》第三版的书就入手了。
这本书意在更有效的java编程。提供了多条不同使用时的建议。
一: 创建和销毁对象(与创建型设计模式有关)
1.用静态工厂方法代替构造器。
此处的静态工厂方法与设计模式中的工厂方法不是一个。此处所指的是除了公有public的构造方法来创建对象的另一种创建对象的形式。
下面是静态工厂方法与构造器的一些不同点、
(1)名称清晰易懂。构造方法都是类名加参数这种。静态工厂方法就是可以有方法名来起到理解的作用。这个作用也不是特别大。
(2)不必每次调用时都创建一个新的对象。像传统的构造器是每次都创建一个新的实例。而这个静态方法可以像单例模式那样工作。即多次调用返回相同对象。
(3)静态工厂方法可以返回原返回类型的任何子类型。
(4)静态工厂方法返回的对象可以随每次调用而发生变化。取决于静态工厂方法的参数值。
(5)方法返回的对象锁属的类,在编写包含该静态工厂方法的类时可以不存在。就像上面那个demo似的。
Demo: 如果你想创建一个fruit类,这个fruit类有三个子类apple,orange,banana。这时你可以通过入参来创建对象而返回类型是fruit来保证你的程序不会出错。这个入参可以是类名然后通过反射来返回实例。
tips"个人总结:这个静态工厂方法在使用单例模式和继承关系时常常被使用。比如要写一套根据不同类型进行不同计算的服务,这就很好的能使用到工厂模式。
2.遇到多个构造器参数时要考虑使用构造器。
当有多个成员变量时,一个多参的构造器会很麻烦,这时通常使用JavaBean的方式setter创建对象时设置成员变量的值。但是使用setter时,就肯定不是一次的操作,此时可能会出现线程不安全。如果在方法里面new对象会避免这种情况。因为方法中的栈引用是不被多线程共享的。
还有一种创建方式是建造者模式Builder。这个在之前的Okhttp工具类中使用过。原理是通过静态内部类不断返回当前对象,最后创建一个类对象来实现随意多参构造。下面是一个简单的例子。
package com.zy.girl.effective;
public class ConBuilderFact {
private final int a;
private final int b;
private final int c;
public static class Builder {
private final int a;
private int b;
private int c;
public Builder(int a) {
this.a = a;
}
public Builder bCon(int b) {
this.b = b;
return this;
}
public Builder cCon(int c) {
this.c = c;
return this;
}
public ConBuilderFact build() {
return new ConBuilderFact(this);
}
}
public ConBuilderFact(Builder builder) {
this.a = builder.a;
this.b = builder.b;
this.c = builder.c;
}
public String toString(){
return "a="+a+"b="+b+"c="+c;
}
public static void main(String[] args){
ConBuilderFact con = new ConBuilderFact.Builder(1).bCon(2).build();
System.out.println(con);
}
}
tips"个人总结: 对于参数多的类可以使用Builder生成器模式来创建对象。像OkHttp这个Util,需要设置多个参数。这样能更安全更好的赋参。因为setter要注意线程安全的问题。当然setter是在方法中的new出来的对象处使用时没问题的。方法中的对象是局部变量。而多个构造函数比较麻烦但是也是线程安全的。像po,vo这种类使用setter就好了。
3.用私有构造器或者枚举类型来强化Singleton属性。
singleton通常被用来作为一个无状态的对象。或者那些本质上唯一的系统组件。
对于单例模式有多种实现方式,要权衡线程安全与懒加载而衍生出不同的形式。需要私有化构造方法,以防止外部调用。
在使用单例模式时除了线程安全和懒加载需要考虑之外,还需要考虑反射和序列化会不会打破单例的平衡。
防止setAccessible()方法反射调用私有构造。需要在构造函数中创建第二个对象时抛异常。
public class Singleton {
private final static Singleton SINGLETON = new Singleton();
private Singleton(){}
public static Singleton getSingleton(){
return SINGLETON;
}
}
上述就在反射时打破了平衡,可以这么改。当然下面只是一个简单的写法。
对于反序列化是不调用构造函数的。需要创建 readResolve()方法。在方法中返回这个SINGLETON来保证反序列化是也是那个实例。 最好的一种方法实现单例模式是单元素的枚举。
public enum SingletonEnum {
INSTANCE;
public static SingletonEnum getInstance(){
return INSTANCE;
}
}
枚举是完全不让多次实例化的方法。
tips"个人总结:单例模式创建对象最好使用枚举的方法。除了需要非Enum继承的情况下使用其他方法。
4.通过私有构造器强化不可实例化的能力。
这一条是在编写像Util,Collections这些工具类时保证不再外部被实例化。私有构造方法来实现。下面是Objects的源码。
在设计时可以在构造方法中抛出一个异常,这样可以保证外部不能通过反射调用,当然这不是必须的。因为这并不像单例模式那样敏感。他只是为了内部不访问。访问没有意义。访问也没啥影响。
tips"个人总结:以后编写工具类时构造设成私有的就好了。
5.优先考虑依赖注入来引用资源。
通过构造器将需要的对象注入进去。
public class Inject {
private final Apple apple;
public Inject(Apple apple){
this.apple = Objects.requireNonNull(apple);
}
}
tips"个人总结:不要用单例或静态工具类来实现依赖多个底层资源的类。而应该将这些类通过构造器传入。像常使用的spring框架。这样更加灵活可变。
6.避免创建不必要的对象
String s = new String("aaaa"); ------修改成 String s = "aaaa";
多次使用也使用的是方法区中的字符串常量不会每次都创建对象。
对于同时提供了静态工厂方法和构造器的类要优先考虑使用工厂方法。因为构造器每次调用只要不抛异常就会创建对象。而工厂方法在一定程度上编码后会做到对象的复用。
对于昂贵的对象最后将他重用。在String的matches正则匹配的时候会创建一个Pattern对象。而这个对象只用了一次就垃圾回收了。这时候需要改成可复用的。
private static final Pattern pat = Pattern.compile("^sad$"); 类加载时候创建这个实例
pat.matcher(s).matches(); 返回bool判断是否匹配。这样每次调用时使用的是一个Pattern实例。所以这个String 实例的matches方法是很耗性能的。
对于自动装箱也会很耗性能。对于sum定义成包装类会很慢。定义成long基本类型会快好多倍。
对于像数据库连接池这样的对象可以使用数据库连接池来重用,其他的小对象还是直接创建就行,因为小对象开销也不是很大。
7.消除过期的对象引用
对于不会再使用的对象要置为null 。以栈为例。
public class StackTest { private Object[] elements; private int size = 0; public StackTest() { elements = new Object[16]; } public Object pop() { if (size == 0) { throw new EmptyStackException(); } return elements[size--]; //此时只是返回但是位置处还是有元素存在,而这个元素是逻辑上被干掉的。此时内存泄漏 } public Object popChange() { //将pop的元素置null。修复了内存泄漏。 if (size == 0) { throw new EmptyStackException(); } Object result = elements[size--]; elements[size--] = null; return result; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } }
清空对象引用并不是一个规范的行为。最好的方式是让包含该引用的变量结束其生命周期。比如在一个方法中声明的局部变量,方法返回时生命周期便结束了。
对于缓存可能会存在内存泄漏的问题。所以最好在缓存中设置引用存放的时间。常使用的是外部缓存像redis此时就把对象放到redis里了,不需要考虑这个内存泄漏的问题。
内存泄漏的第三个常见来源是监听器和其他回调。确保回调立即被当作垃圾回收的最佳方法就是只保存他们的弱引用。例如使用WeakHashMap
tips:最好的方法就是借助heap剖析工具。
8.避免使用终结方法和清除方法。
不要主动使用清除方法来垃圾回收清除对象。对于需要关闭的资源使用try-with-resources来关闭实现AutoCloseable的类。
9.使用try-with-resources 代替try-finally
这个try-with-resources是JDK1.7加入的。作用在资源的关闭。这个解决了try-finally块异常问题。
如果try和finally都发生异常,肯定是抛finally中的异常。那try中的异常就会被覆盖掉。
而使用try-with-resources出现了第一个异常,在发生异常就会被限制住,以做到只有一个异常,这样为改错提供了方便。同时这个也使代码更加简洁。这个try包围的()既是需要关闭资源的类,这里面的类实现了AutoCloseable。此时不需要写finally语句。
try(InputStream is = new FileInputStream(""); OutputStream os = new FileOutputStream(""); ){ is.read(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
如下是这个try-with-resources 编译后的代码。
try { InputStream is = new FileInputStream(""); Throwable var4 = null; try { OutputStream os = new FileOutputStream(""); Throwable var6 = null; try { is.read(); } catch (Throwable var33) { var6 = var33; throw var33; } finally { if (os != null) { if (var6 != null) { try { os.close(); } catch (Throwable var32) { var6.addSuppressed(var32); } } else { os.close(); } } } } catch (Throwable var35) { var4 = var35; throw var35; } finally { if (is != null) { if (var4 != null) { try { is.close(); } catch (Throwable var31) { var4.addSuppressed(var31); } } else { is.close(); } } } } catch (FileNotFoundException var37) { var37.printStackTrace(); } catch (IOException var38) { var38.printStackTrace(); }