>  기사  >  Java  >  Java는 불필요한 객체 생성을 방지합니다.

Java는 불필요한 객체 생성을 방지합니다.

高洛峰
高洛峰원래의
2016-12-01 16:27:151163검색

리틀 앨런은 최근에 "Effective Java"라는 책을 읽었습니다. 이 책은 매우 풍부한 내용을 담고 있습니다. 저는 이 책을 자세히 소개하지 않겠습니다. Java 개발자로서 이 책은 필연적으로 그리울 것입니다. 작은 후회가 되기 때문에 그래도 시간이 남는 친구들에게는 이 책을 읽어보라고 권한다. 나는 아직 이 책에 소개된 내용 중 많은 부분을 이해하지 못하고 일상적인 개발에서 반드시 많은 부분을 사용하지 않을 수도 있으므로 모든 것을 자세히 설명하지는 않고 이 책에서 최대한 추출할 수 있습니다. 일상적인 개발에서 실용적인 부분과 초보자인 Little Alan이 이해할 수 있는 부분에 대해서는 Little Alan의 작업 경험과 깊은 이해를 바탕으로 천천히 정리할 수 있습니다. 그것이 유용하다고 생각하는 몇몇 친구들을 위한 생각.

"효과적인 자바" 5조: 불필요한 객체 생성을 피하세요

원문을 여러 부분으로 나누어 이해하고, 작은 목표를 하나씩 달성하고, 최종적으로 이 부분의 내용을 완전히 이해하게 됩니다. .

1부: 일반적으로 말해서, 필요할 때마다 동일한 기능을 가진 새 객체를 만드는 것보다 객체를 재사용하는 것이 더 좋습니다. 재사용은 빠르고 대중적입니다. 객체가 변경 불가능한 경우 언제든지 재사용할 수 있습니다.

부정적 예:

String s = new String("Papapa"); //이 작업을 수행하지 마세요!

이 문이 실행될 때마다 둘 다 새 항목을 생성합니다. 문자열 인스턴스이지만 객체를 생성하는 이러한 작업은 모두 불필요합니다. String 생성자에 전달된 매개변수("papapa")는 그 자체가 String 인스턴스이며, 이는 생성자에 의해 생성된 모든 객체와 기능적으로 동일합니다. 이 사용법이 루프에 있거나 자주 호출되는 메서드에 있으면 수천 개의 불필요한 String 인스턴스가 생성됩니다.

개선된 버전:

String s = "Papapa";

이 버전은 String 인스턴스가 실행될 때마다 새 String 인스턴스를 생성하는 대신 하나의 String 인스턴스만 사용합니다. 또한 동일한 문자열 리터럴을 포함하는 한 동일한 가상 머신에서 실행되는 모든 코드에 대해 객체가 재사용된다는 것을 보장합니다.

확장된 아이디어: ① Java 1.7에서 실행될 때 Java는 메소드 영역에서 실행될 때 상수 풀에 첫 번째 인스턴스를 기록합니다. 즉, "pah pah pah"가 상수 풀에 저장된다는 의미입니다. 다음에 String s = "Papapa";를 호출하면 Java는 새 객체를 다시 생성하는 대신 이 객체에 대한 참조를 직접 반환합니다. 이렇게 하면 메모리 오버헤드가 절약되고 메서드에서 안전하게 루프를 사용할 수 있습니다. 메소드에서 자주 호출되는 것을 두려워하지 않습니다. String s = new String("PaPaPaPa"); 실제로 두 개의 객체를 생성합니다. 하나는 힙에 저장되고 다른 하나는 상수 풀에 저장됩니다. s는 스택에 저장된 객체에 대한 참조일 뿐입니다. 및 String s = "Papapa"; 객체를 생성하여 상수 풀에 저장한 다음 스택에 객체에 대한 참조를 저장합니다. (저는 Java 가상 머신에 대해 깊이 이해하지 못합니다. 제가 잘못 이해했다면 지적해 주세요.)

2부: 정적 팩토리 메서드와 생성자를 모두 제공하는 불변 클래스의 경우 일반적으로 생성자 대신 정적 팩터리 메서드를 사용하여 불필요한 객체 생성을 방지할 수 있습니다. 예를 들어 정적 팩터리 메서드 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 메소드는 Java에서 제공하는 유형뿐만 아니라 원본 기사에서 언급한 부울 유형과 같은 새 인스턴스를 반환하지 않습니다. 일상적인 개발에서 비슷한 요구 사항이 있는 경우 정적 인스턴스를 모방하는 것이 좋습니다. 우리 자신의 클래스를 제공하기 위해 Java에서 제공하는 팩토리 메소드는 객체 획득을 달성하고 객체의 반복 생성을 피하기 위해 정적 팩토리 메소드를 정의하지만 정적 팩토리 메소드 사용에 대해 지나치게 미신적이지는 않습니다. 정적 팩토리 메소드에 대해서는 "Effective Java"의 1조)를 읽을 수 있습니다. 저는 이 메소드를 거의 사용하지 않습니다. 일반적인 클래스에서 더 많은 객체를 생성해도 사용법에 약간만 주의를 기울이는 한 큰 영향을 미치지 않습니다. 괜찮아.

3부: 불변 객체를 재사용하는 것 외에도 수정되지 않는 것으로 알려진 가변 객체를 재사용할 수도 있습니다. 책에 쓰여진 예는 이해하기가 매우 어려우므로 시간을 내어 모두에게 비슷한 예를 생각해 냈습니다. 이것이 의미하는 바가 무엇인지 모르겠습니다. 조언!

부정적 예:

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 Type4f157ac1ec92f600c22109a8c35fc045)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和引用类型之间的差别变得模糊起来,但是并没有完全消除。它们在语义上还有着微妙的差别,在性能上也有着比较明显的差别。考虑下面的程序,它计算所有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虚拟机)具有高度优化的垃圾回收器,如果是轻量的对象池可能还不如垃圾回收器的性能。

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


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.