《Effective Java》学习笔记 - (6) 避免创建不必要的对象

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

最近在看《Effective Java》这本书,也顺便记录下书里的一些内容,有些看不懂的暂时先放着。


避免创建不必要的对象

1. 引入

一般来说,程序里面最好能用单个对象,而不是需要一个就创建一个,其实这种方式也很常见:

  • springboot的@Autowired注解
  • 单例模式
  • 池化技术(这里归为这一类其实是想表达用了类似的思想,预先创建好直接用)

2. 解决的一些建议

1. 关于常量String的创建

String s = new String("hello world");

这个错误用法就在于是直接new出来的String字符串,这时候的字符串是分配在堆空间,并且每次new的时候对于同一个字符串都是不同的对象,如果是放在while循环中,会创建出成千上万种不必要的实例,改进的方法:

String s = "hello world"

这样创建出来的常量首先会在常量池中找,找不到就在堆内存中new一个放进常量池中,下次再赋值的时候直接到常量池引用就行了,这样的好处就是:

  • 减少了大量的不必要对象的创建
  • 减少内存损耗,减低内存泄漏的风险



2. 对于静态工厂方法和构造器

对于提供了静态工厂方法和构造器的不可变类,应该优先使用静态工厂方法而不是构造器,因为使用构造器会导致有可能创建出一堆不必要的对象。比如Boolean.valuef(String)方法和Boolean(String)构造器。

//内部返回了两个静态的Boolean对象,全局唯一
public static Boolean valueOf(String s) {
        return parseBoolean(s) ? TRUE : FALSE;
}

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

可以看出valueOf方法内部实际上是返回了全局唯一的Boolean对象,而如果要自己进行new,就会产生不必要的对象导致浪费了。
写程序里面常常使用的一些工具类方法,里面的方法都定义成静态的,其实也是这个道理。



3. “昂贵的对象”

书中给了一个匹配的例子,确定一个字符串是否为一个有效的罗马数字

static boolean isRomanNumeral(String s){
	return s.matchs("^(?=.)M*(C[MD] | D?C{0, 3})" + 
		"(X[CL] | L?X{0,3})(I[XV]|V?I{0,3})$");
	)
}

这个方法的实现在于它依赖的String.matches方法。虽然这个方法最容易去查看字符串是否和正则表达式匹配,但是不适合在注重性能的情形中使用。我们查看内部的源码方法:

public boolean matches(String regex) {
    return Pattern.matches(regex, this);
}

//Pattern.matches
public static boolean matches(String regex, CharSequence input) {
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(input);
    return m.matches();
}

//Pattern.compile
public static Pattern compile(String regex) {
    return new Pattern(regex, 0);
}

查看源码之后不难发现,我们传入了一个regex正则表达式,而源码在底层的Pattern.compile(regex)方法中为表达式创建了一个Pattern对象,如果是while循环中,那么这样的对象有可能创建成千上万个,造成的性能损耗是很大的,所以要对这种写法进行改造,改造之后我们只创建一个Pattern对象:

Public class RomanNumerals{
	private static final Pattern ROMAN = Pattern.comple(
		"^(?=.)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();
	}
}

我们直接采用源码的写法,自己创建一个全局唯一的Pattern 正则表达式对象,然后对s进行匹配,跳过了原来的Pattern.compile(regex) 这行代码,所以不会说调用一次就产生一次新的Pattern对象。

优势对比(书上的结果):一个8位字符的输入

  • 原来的写法花了1.1μs
  • 改进后的写法花了0.17μs,快了6.5倍

当然了,如果这个静态方法一直没有被调用,其实可以尝试进行懒加载,但是不建议这么做,因为这样会使得方法的实现更加复杂,有可能改完的代码性能也不一定超过原来的代码,因为实现懒加载的过程中难免也会降低时间效率。



4. 适配器

适配器是指这样一个对象:它把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口。由于适配器出了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。

比如Map接口中的KeySet方法返回了所有的key的集合(Set),我们查看源码的时候也可以看到,同一个HashMap返回的KeySet始终都是一个来的,而不是每次都要重新new一个

public Set<K> keySet() {
    Set<K> ks = this.keySet;
    if (ks == null) {
        ks = new HashMap.KeySet();
        this.keySet = (Set)ks;
    }

    return (Set)ks;
}

虽然返回的Set实例是可以改变的,但是所有的返回的对象在功能上是相同的,所以只需要设置一个KeySet就好了。创建多个KeySet视图对象没有害处,但是没必要,也没有什么好处。



5. 自动装箱拆箱

自动装箱这种方法如果使用不恰当也会造成创建出来很多不必要的对象。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有消除。 有时在写代码的时候会觉得类似Integer i = 10等等这些没什么问题,因为有自动装箱和自动拆箱在,不需要我们去转化,但是看接下来这一段代码:

//求int类型数据相加的总和
public class TestStatic {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        sum();
        long end = System.currentTimeMillis();
        //时间消耗:5715 约等于 5.715秒
        System.out.println("时间消耗:" + (end - start));

    }

    private static long sum(){
        Long sum = 0L;
        for(long i = 0; i <= Integer.MAX_VALUE; i++){
            sum += i;
        }
        return sum;
    }
}

这段代码得出来的结果是正确的,但是有一个很大的问题就是使用了Long 类型而不是long,这就导致了在2的31次方 + 1次循环中,创建出来了大约2的31次方个Long实例,每次进行相加的时候都会创建一个。那么这时候的运行时间是大约5.7秒,这个时间是很久的了,我们再看看把Long改成long之后的时间提升了多少:

public class TestStatic {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        sum();
        long end = System.currentTimeMillis();
        //时间消耗:588 约等于0.5秒
        System.out.println("时间消耗:" + (end - start));

    }

    private static long sum(){
        long sum = 0L;
        for(long i = 0; i <= Integer.MAX_VALUE; i++){
            sum += i;
        }
        return sum;
    }
}

可以看出,换成普通long类型之后,减少了不必要的自动装箱过程和创建实例过程,时间效率大概是原来的10倍。所以结论很明显:要优先使用基本数据类型而不是装箱基本类型,要当心无意识的自动装箱。



3. 总结

  • 避免创建而不是不创建

当然,不要错误地认为创建对象的代价非常昂贵,相反,由于小对象的构造器只是做很少量的显示工作,所以小对象的创建和回收动作是非常廉价的,特别是现代的JVM优其实已经是高度优化了。通过创建附加的对象,让程序变得清晰、简洁,其实也算是一件好事。

  • 池化技术

池化技术也不是一定就要用的,通过池化技术维护自己的一个对象池,避免创建对象也不一定就有用,除非这个对象是很重要的,正确使用的典型例子就是数据库连接池。不要轻易使用池化技术,维护一个对象池必定会把代码弄得很乱,同时增加内存占用,还会消耗性能,效率也不一定就比JVM自动回收的要快。现代JVM具有高度优化的垃圾回收器,性能很容易超过轻量级对象池的性能。

  • 保护性拷贝

这里没有谈到这方面的内容,值得注意的是,在提倡使用保护性拷贝的时候,因重用对象的代价会远远大于创建重复对象付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的Bug和安全漏洞,而创建重复对象只是影响程序的风格和性能。






如有错误,欢迎指出

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值