Effective Java 中文第二版,读书笔记

记录自己狭隘的理解,想要深入去看书
环境jdk8
文中缓存就是静态变量,或者方法中的集合Map等
有疑问的地方标记※

第二章:创建和销毁对象

1.静态工厂方法代替构造器

即静态方法返回本类的实例
优点:
1.静态方法有方法名称更加直观
2.不必每次都创建新的对象
2.1单例模式
2.2线程池、连接池。这种可复用且较消耗内存资源的对象
2.3固定范围内的对象
Integer的valueOf方法,在创建-128~127之间时不创建新的对象,直接从缓存中取

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

又如Boolean类的valueOf方法:

 public static final Boolean TRUE = new Boolean(true);
 public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

3.静态方法可以返回原返回类型的任何子类对象,返回对象具有灵活性

缺点:这俩缺点可以直接忽略,这也叫缺点么2233

  1. 查阅文档不方便,当你想创建对象时,如果有构造可以直接使用,但如果没有构造方法就要去一个一个看静态方法
  2. 类如果没有public/protected的构造(如单例模式需要将构造函数私有化),就不能被子类继承。但你可以使用装饰者模式,而不使用继承

2.构造函数有多个参数时,且参数个数不确定,且需要线程安全,推荐使用构建者

如:lombok的builder注解,就是构建着模式

比如:一个类有10个属性,11个构造方法,构造方法参数个数分别为0-10,就会发现构造方法特别多,而且参数顺序也是严格的
方案一:setter方法,先调用空参构造生成对象,之后设置属性,缺点设置属性的动作并不具有原子性,中间可以开启多线程!那么就出现线程安全问题
方案二:builder注解,就是构建着模式,创建对象和设置属性就是一个原子操作

其实一般情况下,不用这么搞!工作中方案一完全够用
1.你会发现工作中builder并不是全能的,(比如创建对象之后赋值操作)然后你就加上了setter,这样的话builder完全就是没什么用了
2.就是在防止别人创建对象时开启了线程,才会使用builder
3.builder注解和setter注解同时使用。我认为是比较愚蠢的
4.用idea查看builder注解的实体的.class文件,你就会发现多了很多代码,还有一个内部类,构建类!也就是创建对象时,先创建构建类实例,这TM的不恶心人么,浪费资源!也就是为了安全时才开启builder

3.private构造器或者枚举,实现Singleton单例

私有构造

/**
 * 单例的Demo
 * 但是AccessibleObject的 setAccessible方法,通过反射机制可以调用私有构造
 */
public class Singleton {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton(){}
}

但可以使用反射调用私有构造

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //通过静态属性获取实例
        Singleton singleton = Singleton.INSTANCE;
        Singleton singleton1 = Singleton.INSTANCE;
        //静态实例在类加载时就被创建,所以是单例,true
        System.out.println(singleton == singleton1);
        //通过反射调用私有构造方法
        Class clazz = Singleton.class;
        //获取构造方法
        Constructor cons = clazz.getDeclaredConstructor();
        //设置私有构造方法为可访问
        cons.setAccessible(true);
        //使用私有构造方法获取实例
        Object obj = cons.newInstance();
        //实例属于类Singleton,true
        System.out.println(obj instanceof Singleton);
        //破坏了单例,false
        System.out.println(singleton == obj);
    }
}

抵御反射攻击

public class Singleton implements Serializable {
    public static final transient Singleton INSTANCE = new Singleton();
    private Singleton(){
        //当创建第二个实例就会抛异常
        if(null!=INSTANCE){
            throw new RuntimeException();
        }
    }
}

当然也可以使用静态方法实现
好处:灵活性,在不改变API的情况下,可以对方法进行修改

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton(){
        if(null!=INSTANCE){
            throw new RuntimeException();
        }
    }
    //方法是灵活的,方法的实现可以修改
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

当单例的类需要可序列化,就需要实现Serializable接口
但序列化和反序列化都会创建一个新的实例
所以实例属性必须是瞬时的transient(不可序列化的属性)
static的属性也是不能被序列化的

public class Singleton implements Serializable{
    //实现Serializable,之后加上transient修饰
    private static final transient Singleton INSTANCE = new Singleton();

    private Singleton() {
        if (null != INSTANCE) {
            throw new RuntimeException();
        }
    }
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

单例最佳实现。枚举,提供了序列化机制,还可以防止多次实例化

public enum EnumSingleton {
    INSTANCE;
}

4.私有构造器,强化不可实例化的能力

工具类的实例化是没有意义的,但默认有无参构造

/**
 * 工具类,内方法都是静态,无需实例
 */
public class UtilClass {
    //私有构造,并在其调用时抛出异常
    private UtilClass(){
        throw new AssertionError();
    }
}

这种代码怎么说。。。谁又会去创建工具类的实例呢?防止小白么?

5.避免创建不必要的对象

  1. String不要这么用,除非特殊需求
//每次执行都会创建一个新的对象,不要这么做,除非需求就是要两个不同的实例
String s = new String("str");
//每次执行,重用对象(在常量池中)
String s1 = "str";
  1. 方法中使用的对象都是同一个,可以提取成静态,避免每次调用都创建

  2. 优先使用基本数据类型,而不是包装类,要当心自动装箱

    /**
     * 计算和,
     * sum被声明成了Long,而不是long,就会多创建大约2的31次方个实例
     * 书里是这么写的:改成long,运行时间从43秒变成6.8秒
     * 但我实际上运行时间是:5.8秒变成了0.7秒,但也是数量级别的优化,很强
     */
    public static void sum() {
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
        System.out.println(sum);
    }

6.消除过期的对象引用(防止内存泄漏)

内存泄漏:就是对象不使用了,但对象的引用还在

  1. 当类自己管理内存时(操作原始数组元素时),应该小心内存泄漏问题
    如:自己管理数组
/**
 * 栈伪代码
 * 不考虑数组越界,不考虑扩容,长度固定16
 */
public class Stack {
    //定义一个长度为16的Object的数组
    private Object[] element = new Object[16];
    //已占用数组大小
    private int size = 0;

    //添加一个元素
    public void push(Object e) {
        element[size++] = e;
    }

    //删除一个元素
    public Object pop(Object e) {
        return element[--size];
    }
}

比如现在长度是8,里面的值是1-8,在删除一个元素时,只是将size改为7,数组中元素还在。8还再,这个8就属于内存泄漏。优化后的代码如下

 public Object pop(Object e) {
        Object result=  element[--size];
        //防止内存泄漏,清空size处对象的引用
        element[size] = null;
        return result;
    }
  1. 缓存:就是自己使用缓存时注意是否需要管理其生命周期
    2.1 如果需要:使用线程定时检查,Timer、ScheduledThreadPoolExecutor等
    或者在add时检查。如:LinkedHashMap利用removeEldestEntry方法※
    其他复杂情况:使用Java.long.ref※
    2.2 在缓存之外存在,对某个键的引用,可以用WeakHashMap:就是说WeakHashMap的键只有自己引用时就会被回收,被其他变量引用时才不会被回收
  2. 监听器、回调※

7.避免使用终结方法

就是不要使用finalize方法回收对象,危险,不稳定,降低性能
不合理的使用

  1. 即使你调用了,不保证及时执行甚至不执行(及时执行是GC算法的主要功能,但在不同JVM实现的又不一样。就是换个jvm就有可能无法运行)
  2. finalize方法线程优先级低。方法栈是进栈出栈的过程,可能遇到终结方法并不能及时执行,导致栈下面的方法不能及时出栈,然而其他方法又要进栈。就容易造成OOM,解决方法就是不使用finalize方法
  3. 异常在终结方法中,则异常的堆栈信息,都不会打印出来
  4. 影响程序效率。慢400多倍

合理的使用

  1. 流系列对象,显式终止是使用close方法(Timer/Connection也具有终结方法)
  2. 对象的本地对等体※,jvm不会对其回收,需要有显式的回收方法,终止方法可以是本地方法或者它调用本地方法
  3. 如果类(不是Object)有终结方法,且子类覆盖了终结方法,子类的终结方法必须手动调用超类的终结方法。并且在try中执行子类的终结方法,在finally中调用超类的终结方法,保证超类的终结方法一定会被执行,否则父类的终结方法永远执行不了

对于所有对象都通用的方法

8.覆盖equals时遵守通用约定

首先Object的hashCode方法的注释明确规定
equals相等hashCode必须相等
hashCode相等,equals可以不相等
所以重写equals必须重写hashCode,因为要保证规则一

Object的equals方法,如下,比较的是对象的地址值

 public boolean equals(Object obj) {
        return (this == obj);
    }

不需要重写equals的情况

  1. 保证类的每个实例都是唯一的。
    因为每次new就会在堆内存产生一个唯一的地址值,保证实例唯一,比较地址值即可。Object的equals方法比较的就是地址值,无需重写。如Thread,每次new都是一个新的线程,保证唯一性
  2. 不关心类是否提供了逻辑相等的测试功能。
    如Random类,设计者不认为用户需要这种行为
		//获取一个随机数
        Random random = new Random();
        int i = random.nextInt();
        //下面的代码有什么意义呢
        Random random1 = new Random();
		System.out.println(random.equals(random1));
  1. 父类已经覆盖equals,子类就继承了父类的equals,无特殊需求的话,子类就没必要覆盖equals。如AbstractSet,AbstractList,AbstractMap覆盖了equals,子类无需实现

需要重写的情况

  1. private的类或者包级私有,那么它的equals永远不会被调用,这种情况必须覆盖,防止意外调用

未完持续。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值