欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  Java

Java避免创建不必要的对象

程序员文章站 2022-05-07 12:16:25
...
小Alan最近看到了《Effective Java》这本书,这本书包含的内容非常丰富,这本书我就不多介绍了,只能默默的说一句,作为一名java开发错过了这本书难免会成为一个小遗憾,所以还是建议有时间的小伙伴能够去看看这本书,时间挤挤总还是有的。这本书介绍的很多东西我现在也还看不太明白,很多东西我们在平时的开发中也不见得会用上,所以我不会每个东西都拿来详细解释一遍,只会从中抽取我们平时开发中比较实用的,以及小Alan这个小菜鸟能够看懂的部分,至于一些不实用的以及比较高深的部分那就只能随着小Alan的工作经历和深入理解再慢慢的整理出来给自己也给部分觉得有用的朋友理清思路。

《Effective Java 》第5条:避免创建不必要的对象

我们把原文拆分成几部分来理解,实现一个一个的小目标,最后来完全理解这一块的内容。

第一部分:一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快速,又流行。如果对象是不可变的,它就始终可以被重用。

反面例子:

String s = new String("啪啪啪");  //Don't do this!

该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数("啪啪啪")本身就是一个String实例,功能方面等同于构造器创建的所有对象。如果这种用法是在一个循环中,或是在一个被频繁调用的方法中,就会创建成千上万不必要的String实例。

改进版本:

String s = "啪啪啪";

这个版本只用了一个String实例,而不是每次执行的时候都创建一个新的String实例。而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。

扩展思路:①在Java1.7中运行,Java会在方法区运行时常量池中记录首次出现的实例,也就是说会在常量池中保存"啪啪啪",那么当你下次调用String s = "啪啪啪";的时候,Java会直接返回这个对象的引用,而不会去重新创建一个新的对象,这样就节省了内存的开销,也可以放心的在循环中去使用,也不怕在方法中被频繁的调用。String s = new String("啪啪啪");实际上创建了两个对象,一个存放在堆中,一个就是保存在常量池中的"啪啪啪",s只是对象的引用保存在栈中,而String s = "啪啪啪";只会创建一个对象保存在常量池中,然后保存一个对象的引用在栈中就ok了(对Java虚拟机理解不是很深入,理解有误请指出,万分感谢)。

第二部分:对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。

扩展思路:

package com.czgo.effective;

/**
 * 用valueOf()静态工厂方法代替构造器
 * @author AlanLee
 * @version 2016/12/01
 *
 */
public class Test {

    public static void main(String[] args) {
        // 使用带参构造器
        Integer a1 = new Integer("1");
        Integer a2 = new Integer("1");
        
        //使用valueOf()静态工厂方法
        Integer a3 = Integer.valueOf("1");
        Integer a4 = Integer.valueOf("1");
        
        //结果为false,因为创建了不同的对象
        System.out.println(a1 == a2);
        
        //结果为true,因为不会新建对象
        System.out.println(a3 == a4);
    }

}

可见,使用静态工厂方法valueOf不会新建一个对象,避免大量不必要的对象被创建,实际上很多类默认的valueOf方法都不会返回一个新的实例,比如原文提到的Boolean类型,不仅仅是Java提供的这些类型,我们在平时的开发中如果也有类似的需求不妨模仿Java给我们提供的静态工厂方法,给我们自己的类也定义这样的静态工厂方法来实现对象的获取,避免对象的重复创建,但是也不要过度迷信使用静态工厂方法的方式,这种方式也有它的弊端(有关静态工厂方法的知识可以看看《Effective Java》第一条),个人很少使用这种方式,平时的类多创建个对象也不会有太大的影响,只要稍微注意下用法就ok了。

第三部分:除了重用不可变的对象之外,也可以重用那些已知不会修改的可变对象。书上写的例子让人非常难以理解,我也没花时间去看了,我给大家想出来一个类似的例子,也不知道是否是这个意思,多多指教!

反面例子:

package com.czgo.effective;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBUtilBad {
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/imooc";
    private static final String UNAME = "root";
    private static final String PWD = "root";

    public static Connection getConnection() {
        Connection conn = null;
        try {
            // 1.加载驱动程序
            Class.forName("com.mysql.jdbc.Driver");
            // 2.获得数据库的连接
            conn = DriverManager.getConnection(URL, UNAME, PWD);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
}

该类提供的getConnection方法获取JDBC数据库连接对象,每次调用该方法都会新建一个conn实例,而我们知道在平时的开发中数据库连接对象往往只需要一个,也不会总是去修改它,没必要每次都去新创建一个连接对象,每次都去创建一个实例不知道程序会不会出现什么意外情况,这个我不知道,但有一点是肯定的,这种方式影响程序的运行性能,增加了Java虚拟机垃圾回收器的负担。我们可以对它进行改进。

改进版本:

package com.czgo.effective;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBUtil {
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/imooc";
    private static final String UNAME = "root";
    private static final String PWD = "root";

    private static Connection conn = null;

    static {
        try {
            // 1.加载驱动程序
            Class.forName("com.mysql.jdbc.Driver");
            // 2.获得数据库的连接
            conn = DriverManager.getConnection(URL, UNAME, PWD);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() {
        return conn;
    }
}

我们使用了静态代码块来创建conn实例,改进后只有在类加载初始化的时候创建了conn实例一次,而不是在每次调用getConnection方法的时候都去创建conn实例。如果getConnection方法被频繁的调用和使用,这种方式将会显著的提高我们程序的性能。除了提高性能之外,代码的含义也更加的清晰了,使得代码更易于理解。

第四部分:Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的键(key)。粗看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生变化的时候,所有其他返回对象也要发生变化,因为它们是由同一个Map实例支撑的。虽然创建keySet视图对象的多个实例并无害处,却也是没有必要的。

package com.czgo.effective;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class TestKeySet {

    public static void main(String[] args) {
        
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("A", "A");
        map.put("B", "B");
        map.put("C", "C");
        
        Set<String> set = map.keySet();
        Iterator<String> it = set.iterator();
        while(it.hasNext()){
            System.out.println(it.next()+"①");
        }
        
        System.out.println("---------------");
        
        map.put("D", "D");
        set = map.keySet();
        it = set.iterator();
        while(it.hasNext()){
            System.out.println(it.next()+"②");
        }
        
    }

}

第五部分:有一种创建多余对象的新方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type<引用类型>)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和引用类型之间的差别变得模糊起来,但是并没有完全消除。它们在语义上还有着微妙的差别,在性能上也有着比较明显的差别。考虑下面的程序,它计算所有int正值的总和。为此,程序必须使用long变量,因为int不够大,无法容纳所有int正值的总和:

package com.czgo.effective;

public class TestLonglong {

    public static void main(String[] args) {
        Long sum = 0L;
        for(long i = 0; i < Integer.MAX_VALUE; i++){
            sum += i;
        }
        System.out.println(sum);
    }
    
}

段程序算出的结果是正确的,但是比实际情况要慢的多,只因为打错了一个字符。变量sum被声明成Long而不是long,意味着程序构造了大约2的31次方个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。将sum的声明从Long改成long,速度快了不是一点半点。结论很明显:要优先使用基本类型而不是引用类型,要当心无意识的自动装箱。

最后,不要错误地认为"创建对象的代价非常昂贵,我们应该尽可能地避免创建对象"。相反,由于小对象的构造器只做很少量的显示工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。

反之,通过维护自己的对象池(Object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而如今的JVM(Java虚拟机)具有高度优化的垃圾回收器,如果是轻量的对象池可能还不如垃圾回收器的性能。

这里我们说到“当你应该重用现有对象的时候,请不要创建新的对象”,反之我们也应该考虑一个问题“当你应该创建新对象的时候,请不要重用现有的对象”。有时候重用对象要付出的代价要远远大于因创建重复对象而付出的代价。必要时,如果没能创建新的对象实例将会导致潜在的错误和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。

相关标签: Java