Java は不要なオブジェクトの作成を回避します

高洛峰
リリース: 2016-12-01 16:27:15
オリジナル
1170 人が閲覧しました

リトル アランは最近、「Effective Java」という本を読みました。この本には非常に豊富な内容が含まれています。私は Java 開発者として、この本を見逃したとしか言えません。残念なので、時間がある友達にはこの本を読んでもらうことをお勧めします。この本で紹介されている内容の多くはまだ理解できていませんし、日常の開発でそれらの多くを使用するとは限りません。そのため、すべてを詳しく説明することはしませんが、この本からできることだけを抜粋します。日常の開発におけるより実践的な部分と、初心者である小さなアランが理解できる部分。いくつかの非現実的で高度な部分については、小さなアランの仕事の経験と深い理解に基づいてゆっくりと整理することしかできません。役に立つと思っている友人たちへの私の思い。

「効果的な Java」の第 5 条: 不要なオブジェクトの作成を避ける

原文をいくつかのパートに分けて理解し、小さな目標を 1 つずつ達成し、最終的にこの作品の内容を完全に理解します。

パート 1: 一般的に、必要になるたびに同じ機能を持つ新しいオブジェクトを作成するよりも、オブジェクトを再利用する方が良いでしょう。再利用は迅速で人気があります。オブジェクトが不変であれば、いつでも再利用できます。

否定的な例:

String s = new String("Papapa"); //これは行わないでください!

このステートメントは実行されるたびに新しい String インスタンスを作成しますが、これらはオブジェクトを作成します。すべてのアクションは不要です。 String コンストラクターに渡されるパラメーター (「papapa」) は、それ自体 String インスタンスであり、コンストラクターによって作成されたすべてのオブジェクトと機能的に同等です。この使用法がループ内、または頻繁に呼び出されるメソッド内で行われる場合、何千もの不要な String インスタンスが作成されます。

改良版:

String s = "Papapa";

このバージョンでは、実行されるたびに新しい String インスタンスを作成するのではなく、String インスタンスを 1 つだけ使用します。さらに、同じ文字列リテラルが含まれている限り、同じ仮想マシンで実行されるすべてのコードでオブジェクトが再利用されることが保証されます。

拡張されたアイデア: ① Java 1.7 で実行する場合、Java はメソッド領域で実行するときに定数プールの最初のインスタンスを記録します。つまり、「pah pah pah」が定数プールに保存され、それを呼び出すと、 next time String s = "Papapa"; の場合、Java は新しいオブジェクトを再作成する代わりに、このオブジェクトへの参照を直接返します。これにより、メモリのオーバーヘッドが節約され、安心してループ内で使用できます。メソッド内で頻繁に使用されます。 String s = new String("PaPaPaPa"); は実際には 2 つのオブジェクトを作成します。1 つはヒープに保存され、もう 1 つは定数プールに保存されたオブジェクトです。 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 によって提供される静的ファクトリ メソッドを模倣して、そのような静的ファクトリ メソッドを定義することもできます。独自のクラスを実装するためには、オブジェクトを取得し、オブジェクトの繰り返し作成を避けてください。ただし、静的ファクトリ メソッドの使用については、あまり迷信深くならないでください (静的ファクトリ メソッドについては、次の記事を参照してください)。 「効果的な Java」)。個人的には、このメソッドを控えめに使用する場合は、使用方法に少し注意する限り、通常のクラスにさらにオブジェクトを作成しても大きな影響はありません。

パート 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 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虚拟机)具有高度优化的垃圾回收器,如果是轻量的对象池可能还不如垃圾回收器的性能。

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


関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!