Heim  >  Artikel  >  Java  >  Detaillierte Analyse unveränderlicher Objekte in Java (mit Code)

Detaillierte Analyse unveränderlicher Objekte in Java (mit Code)

不言
不言nach vorne
2019-04-13 09:51:172288Durchsuche

Dieser Artikel bietet Ihnen eine detaillierte Analyse unveränderlicher Objekte in Java (mit Code). Ich hoffe, dass er für Freunde hilfreich ist.

Unveränderliche Objekte müssen den meisten meiner Freunde bekannt sein. Beim Schreiben von Code verwendet jeder zu 100 % unveränderliche Objekte, wie z. B. die häufigsten String-Objekte, Wrapper-Objekte usw. Warum ist Java also was? Sind die wahren Absichten und Überlegungen für die Gestaltung der Sprache auf diese Weise? Vielleicht haben einige Freunde nicht im Detail über diese Themen nachgedacht. Heute werden wir über Themen im Zusammenhang mit unveränderlichen Objekten sprechen.

1. Was ist ein unveränderliches Objekt?

Das Folgende ist die Definition unveränderlicher Objekte im Buch „Effective Java“:

Unveränderliches Objekt: Sobald ein Objekt erstellt wurde, ändern sich sein Zustand und seine Eigenschaften während seines Lebenszyklus nicht.

Aus der Definition unveränderlicher Objekte geht hervor, dass es nach der Erstellung eines Objekts eigentlich relativ einfach ist. Zum Beispiel der folgende Code:

public class ImmutableObject {
    private int value;
    
    public ImmutableObject(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return this.value;
    }
}

Da ImmutableObject keine Setter-Methode bereitstellt und der Wert der Mitgliedsvariablen ein Basisdatentyp ist, gibt die Getter-Methode eine Kopie des Werts zurück, sobald also eine ImmutableObject-Instanz erstellt wird , Der Status der Instanz kann nicht mehr geändert werden, wodurch diese Klasse unveränderlich wird.

Ein weiteres Beispiel ist der String, den wir normalerweise am häufigsten verwenden:

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);
    }
}

Ausgabeergebnis:

 

Aus der Ausgabe Ergebnis Es ist ersichtlich, dass sich das String-Objekt, auf das str1 zeigt, nach dem String-Ersetzen von str immer noch nicht geändert hat.

2. Vertieftes Verständnis der Unveränderlichkeit

Haben wir jemals über eine Frage nachgedacht: Ist es in Ordnung, wenn die String- und Wrapper-Klassen in Java veränderbar sind? Welche Probleme wird es verursachen, wenn das String-Objekt veränderbar wird?

In diesem Abschnitt sprechen wir hauptsächlich über die Bedeutung der Existenz unveränderlicher Objekte.

1) Erleichtern Sie gleichzeitiges Programmieren

Wenn es um gleichzeitiges Programmieren geht, stellen viele Freunde möglicherweise fest, dass das Beunruhigendste darin besteht, wie mit dem sich gegenseitig ausschließenden Zugriff auf gemeinsam genutzte Ressourcen umgegangen wird . Ein wenig Nachlässigkeit kann zu unerklärlichen Problemen führen, nachdem der Code online geht, und die meisten Parallelitätsprobleme sind nicht leicht zu lokalisieren und zu reproduzieren. Daher werden selbst sehr erfahrene Programmierer bei der gleichzeitigen Programmierung sehr vorsichtig sein und sich auf dünnem Eis bewegen.

In den meisten Fällen wird in Szenarien mit sich gegenseitig ausschließendem Zugriff auf Ressourcen das Sperren verwendet, um den seriellen Zugriff auf Ressourcen zu implementieren und die Sicherheit der Parallelität zu gewährleisten, z. B. das Synchronisierungsschlüsselwort, Lock Lock usw. Die größte Schwierigkeit bei dieser Lösung besteht jedoch darin, dass Sie beim Ver- und Entriegeln sehr vorsichtig sein müssen. Wenn der Zeitpunkt des Sperrens oder Entsperrens geringfügig abweicht, kann dies zu größeren Problemen führen. Dieses Problem kann jedoch weder vom Java-Compiler noch während des Unit-Tests und des Integrationstests erkannt werden. Das Programm kann auch nach dem Online-Betrieb normal ausgeführt werden , aber es könnte sein, dass es eines Tages plötzlich aus dem Nichts erschien.

Gibt es eine andere Möglichkeit, das Problem zu lösen, da der serielle Zugriff auf gemeinsam genutzte Ressourcen so anfällig ist? Die Antwort ist ja.

Tatsächlich liegt die Hauptursache für Thread-Sicherheitsprobleme darin, dass mehrere Threads gleichzeitig auf dieselbe gemeinsam genutzte Ressource zugreifen müssen.

Wenn keine gemeinsam genutzten Ressourcen vorhanden sind, wird das Problem der Multi-Thread-Sicherheit auf natürliche Weise gelöst. Dies ist die in Java übernommene Idee, den ThreadLocal-Mechanismus bereitzustellen.

Meistens müssen Threads jedoch gemeinsam genutzte Ressourcen verwenden, um Informationen zu kommunizieren. Wenn sich die gemeinsam genutzte Ressource nach ihrer Erstellung überhaupt nicht ändert, handelt es sich um ein konstantes und gleichzeitiges Lesen der gemeinsam genutzten Ressource durch mehrere Threads ist Es gibt keine Online-Sicherheitsprobleme, da alle Threads immer einen konsistenten und vollständigen Ressourcenstatus erhalten können, wenn sie die gemeinsam genutzte Ressource lesen.

Unveränderliche Objekte sind Objekte, die sich nach ihrer Erstellung nie ändern. Diese Funktion macht sie von Natur aus threadsicher und erleichtert die gleichzeitige Programmierung.

Sehen wir uns ein Beispiel an: 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;
    }
}

Thread 1 führt beispielsweise den folgenden Code aus:

SynchronizedRGB color =  new SynchronizedRGB(0, 0, 0, "Pitch Black");
int myColorInt = color.getRGB();      // Statement1
String myColorName = color.getName(); // Statement2

Dann ruft ein anderer Thread 2 die Methode color.set nach Anweisung 1 und vor Anweisung 2 auf:

color.set(0, 255, 0, "Green");

Dann stimmen der Wert der Variablen myColorInt und der Wert von myColorName in Thread 1 nicht überein. Um ein solches Ergebnis zu vermeiden, müssen diese beiden Anweisungen zur Ausführung wie folgt miteinander verbunden werden:

synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
}

Wenn SynchronizedRGB eine unveränderliche Klasse ist, tritt dieses Problem beispielsweise nicht auf. Ändern Sie SynchronizedRGB in die folgende Implementierung Methode:

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;
    }
}

Da die Set-Methode das ursprüngliche Objekt nicht ändert, sondern ein neues Objekt erstellt, tritt kein Problem mit der Dateninkonsistenz beim gleichzeitigen Zugriff auf, unabhängig davon, wie Thread 1 oder Thread 2 die Set-Methode aufruft.

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视频教程

Das obige ist der detaillierte Inhalt vonDetaillierte Analyse unveränderlicher Objekte in Java (mit Code). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen