Java の不変オブジェクトの詳細な分析 (コード付き)

不言
リリース: 2019-04-13 09:51:17
転載
2359 人が閲覧しました

この記事では、Java の不変オブジェクトの詳細な分析 (コード付き) を紹介します。これには一定の参考価値があります。必要な友人は参照できます。お役に立てば幸いです。

不変オブジェクトは、ほとんどの友人にとってよく知られているはずです。コードを書く過程では、誰もが 100% 不変オブジェクト (最も一般的な String オブジェクト、ラッパー オブジェクトなど) を使用します。このように言語を設計する本当の意図と考慮事項は何ですか?もしかしたら、これらの問題について詳しく考えていない友人もいるかもしれませんが、今日は不変オブジェクトに関連するトピックについて話します。

1. 不変オブジェクトとは何ですか

次は、書籍『Effective Java』における不変オブジェクトの定義です:

不変オブジェクト: オブジェクトが作成されると、そのすべての状態とプロパティはそのライフ サイクル中に変更されません。

不変オブジェクトの定義は、実際には比較的単純ですが、オブジェクトの作成後は、そのオブジェクトに変更を加えることはできません。たとえば、次のコード:

public class ImmutableObject {
    private int value;
    
    public ImmutableObject(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return this.value;
    }
}
ログイン後にコピー

ImmutableObject には setter メソッドが提供されておらず、メンバー変数の値は基本データ型であるため、getter メソッドは値のコピーを返すため、ImmutableObject インスタンスが作成されると、 、インスタンス 状態を変更できなくなり、このクラスは不変になります。

別の例は、通常最もよく使用する文字列です:

public class Test {
    public static void main(String[] args) {
        String str = "I love java";
        String str1 = str;

        System.out.println("after replace str:" + str.replace("java", "Java"));
        System.out.println("after replace str1:" + str1);
    }
}
ログイン後にコピー

出力結果:

出力より結果 str の文字列置換後、str1 が指す文字列オブジェクトはまだ変更されていないことがわかります。

2. 不変性についての深い理解

「Java の String クラスとラッパー クラスが変更可能になるように設計されていても大丈夫でしょうか?」という質問を考えたことはありますか? String オブジェクトが変更可能になった場合、どのような問題が発生しますか?

このセクションでは、主に不変オブジェクトの存在の意味について説明します。

1) 同時プログラミングを容易にする

同時プログラミングに関して言えば、共有リソースへの相互排他的アクセスをどのように処理するかが最も面倒なことであると多くの友人が感じるかもしれません。 . 、少しの不注意によって、コードがオンラインになった後に不可解な問題が発生する可能性があり、ほとんどの同時実行性の問題を特定して再現するのは簡単ではありません。したがって、非常に経験豊富なプログラマであっても、同時プログラミングを実行するときは細心の注意を払い、薄氷を踏むことになります。

ほとんどの場合、リソースへの相互排他的アクセスを伴うシナリオでは、同時実行性のセキュリティを確保するために、同期キーワード、Lock ロックなどのロックがリソースへのシリアル アクセスを実装するために使用されます。ただし、このソリューションの最大の難点は、ロックとロック解除の際に細心の注意を払う必要があることです。ロックやロック解除のタイミングが少しでもずれると大きな問題が発生する可能性がありますが、Javaコンパイラでは発見できず、単体テストや結合テストでも発見できず、オンライン化後もプログラムは正常に動作します。 、しかし、それはある日突然、どこからともなく現れたかもしれません。

しかし、人間は賢いものです。共有リソースにシリアルにアクセスすると問題が発生しやすいため、他に解決方法はあるのでしょうか?答えは「はい」です。

実際、スレッド セーフティの問題の根本原因は、複数のスレッドが同じ共有リソースに同時にアクセスする必要があることです。

共有リソースがなければ、マルチスレッドの安全性の問題も自然に解決するという考えが、Java で ThreadLocal 機構を提供する際に採用されました。

ただし、ほとんどの場合、スレッドは情報を通信するために共有リソースを使用する必要があります。共有リソースが作成後にまったく変更されない場合、共有リソースは一定であり、同時に読み取られることになります。複数のスレッドによる共有リソースの読み取りは、すべてのスレッドが常に一貫した完全なリソース状態を取得できるため、オンライン セキュリティの問題は発生しません。

不変オブジェクトは、作成後に決して変更されないオブジェクトです。この機能により、オブジェクトは本質的にスレッドセーフになり、同時プログラミングが容易になります。

例を見てみましょう。この例は http://ifeve.com/immutable-objects/

public class SynchronizedRGB {
    private int red;  // 颜色对应的红色值
    private int green; // 颜色对应的绿色值
    private int blue;  // 颜色对应的蓝色值
    private String name; // 颜色名称

    private void check(int red, int green, int blue) {
        if (red < 0 || red > 255 || green < 0 || green > 255 
                || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red, int green, int blue, String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red, int green, int blue, String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }
}
ログイン後にコピー

から引用しています。たとえば、スレッド 1 は次のコードを実行します。 ##

SynchronizedRGB color =  new SynchronizedRGB(0, 0, 0, "Pitch Black");
int myColorInt = color.getRGB();      // Statement1
String myColorName = color.getName(); // Statement2
ログイン後にコピー

次に、別のスレッド 2 がステートメント 1 の後、ステートメント 2 の前に color.set メソッドを呼び出します。

color.set(0, 255, 0, "Green");
ログイン後にコピー

この場合、変数 myColorInt の値とスレッド 1 の myColorName の値は一致しません。このような結果を回避するには、これら 2 つのステートメントを次のようにバインドして実行する必要があります:

synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
}
ログイン後にコピー

SynchronizedRGB が不変クラスである場合、この問題は発生しません。たとえば、SynchronizedRGB を次の実装に変更します。メソッド:

public class ImmutableRGB {
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red, int green, int blue) {
        if (red < 0 || red > 255 || green < 0 || green > 255
                || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red, int green, int blue, String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public ImmutableRGB set(int red, int green, int blue, String name) {
        return new ImmutableRGB(red, green, blue, name);
    }

    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }
}
ログイン後にコピー

set メソッドは元のオブジェクトを変更するのではなく、新しいオブジェクトを作成するため、スレッド 1 またはスレッド 2 がどのように set メソッドを呼び出しても、同時アクセスは発生しません。


2)消除副作用

很多时候一些很严重的bug是由于一个很小的副作用引起的,并且由于副作用通常不容易被察觉,所以很难在编写代码以及代码review过程中发现,并且即使发现了也可能会花费很大的精力才能定位出来。

举个简单的例子:

class Person {
    private int age;   // 年龄
    private String identityCardID;  // 身份证号码

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getIdentityCardID() {
        return identityCardID;
    }

    public void setIdentityCardID(String identityCardID) {
        this.identityCardID = identityCardID;
    }
}


public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        jack.setAge(101);
        jack.setIdentityCardID("42118220090315234X");

        System.out.println(validAge(jack));
    
    // 后续使用可能没有察觉到jack的age被修改了
    // 为后续埋下了不容易察觉的问题

    }

    public static boolean validAge(Person person) {
        if (person.getAge() >= 100) {
            person.setAge(100);  // 此处产生了副作用
            return false;
        }
        return true;
    }

}
ログイン後にコピー

validAge函数本身只是对age大小进行判断,但是在这个函数里面有一个副作用,就是对参数person指向的对象进行了修改,导致在外部的jack指向的对象也发生了变化。

如果Person对象是不可变的,在validAge函数中是无法对参数person进行修改的,从而避免了validAge出现副作用,减少了出错的概率。

3)减少容器使用过程出错的概率

我们在使用HashSet时,如果HashSet中元素对象的状态可变,就会出现元素丢失的情况,比如下面这个例子:

class Person {
    private int age;   // 年龄
    private String identityCardID;  // 身份证号码

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getIdentityCardID() {
        return identityCardID;
    }

    public void setIdentityCardID(String identityCardID) {
        this.identityCardID = identityCardID;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }

        if (!(obj instanceof  Person)) {
            return false;
        }
        Person personObj = (Person) obj;
        return this.age == personObj.getAge() && this.identityCardID.equals(personObj.getIdentityCardID());
    }

    @Override
    public int hashCode() {
        return age * 37 + identityCardID.hashCode();
    }
}


public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        jack.setAge(10);
        jack.setIdentityCardID("42118220090315234X");

        Set<Person> personSet = new HashSet<Person>();
        personSet.add(jack);

        jack.setAge(11);

        System.out.println(personSet.contains(jack));

    }
}
ログイン後にコピー

输出结果:

  

所以在Java中,对于String、包装器这些类,我们经常会用他们来作为HashMap的key,试想一下如果这些类是可变的,将会发生什么?后果不可预知,这将会大大增加Java代码编写的难度。

三.如何创建不可变对象

通常来说,创建不可变类原则有以下几条:

1)所有成员变量必须是private

2)最好同时用final修饰(非必须)

3)不提供能够修改原有对象状态的方法

最常见的方式是不提供setter方法

如果提供修改方法,需要新创建一个对象,并在新创建的对象上进行修改

4)通过构造器初始化所有成员变量,引用类型的成员变量必须进行深拷贝(deep copy)

5)getter方法不能对外泄露this引用以及成员变量的引用

6)最好不允许类被继承(非必须)

JDK中提供了一系列方法方便我们创建不可变集合,如:

Collections.unmodifiableList(List<? extends T> list)
ログイン後にコピー

另外,在Google的Guava包中也提供了一系列方法来创建不可变集合,如:

ImmutableList.copyOf(list)
ログイン後にコピー

这2种方式虽然都能创建不可变list,但是两者是有区别的,JDK自带提供的方式实际上创建出来的不是真正意义上的不可变集合,看unmodifiableList方法的实现就知道了:

可以看出,实际上UnmodifiableList是将入参list的引用复制了一份,同时将所有的修改方法抛出UnsupportedOperationException。因此如果在外部修改了入参list,实际上会影响到UnmodifiableList,而Guava包提供的ImmutableList是真正意义上的不可变集合,它实际上是对入参list进行了深拷贝。看下面这段测试代码的结果便一目了然:

public class Test {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        System.out.println(list);

        List unmodifiableList = Collections.unmodifiableList(list);
        ImmutableList immutableList = ImmutableList.copyOf(list);

        list.add(2);
        System.out.println(unmodifiableList);
        System.out.println(immutableList);

    }

}
ログイン後にコピー

输出结果:

四.不可变对象真的"完全不可改变"吗?

不可变对象虽然具备不可变性,但是不是"完全不可变"的,这里打上引号是因为通过反射的手段是可以改变不可变对象的状态的。

大家看到这里可能有疑惑了,为什么既然能改变,为何还叫不可变对象?这里面大家不要误会不可变的本意,从不可变对象的意义分析能看出来对象的不可变性只是用来辅助帮助大家更简单地去编写代码,减少程序编写过程中出错的概率,这是不可变对象的初衷。如果真要靠通过反射来改变一个对象的状态,此时编写代码的人也应该会意识到此类在设计的时候就不希望其状态被更改,从而引起编写代码的人的注意。下面是通过反射方式改变不可变对象的例子:

public class Test {
    public static void main(String[] args) throws Exception {
        String s = "Hello World";
        System.out.println("s = " + s);

        Field valueFieldOfString = String.class.getDeclaredField("value");
        valueFieldOfString.setAccessible(true);

        char[] value = (char[]) valueFieldOfString.get(s);
        value[5] = &#39;_&#39;;
        System.out.println("s = " + s);
    }

}
ログイン後にコピー

输出结果:

【相关推荐:Java视频教程

以上がJava の不変オブジェクトの詳細な分析 (コード付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:segmentfault.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート