Maison  >  Article  >  Java  >  Analyse détaillée des objets immuables en Java (avec code)

Analyse détaillée des objets immuables en Java (avec code)

不言
不言avant
2019-04-13 09:51:172286parcourir

Cet article vous apporte une analyse détaillée des objets immuables en Java (avec code). Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.

Les objets immuables doivent être familiers à la plupart de mes amis. Dans le processus d'écriture de code, tout le monde utilisera à 100 % des objets immuables, tels que les objets String les plus courants, les objets wrapper, etc. Alors pourquoi Java ? sont les véritables intentions et considérations pour concevoir le langage de cette manière ? Peut-être que certains amis n'ont pas réfléchi à ces questions en détail. Aujourd'hui, nous parlerons de sujets liés aux objets immuables.

1. Qu'est-ce qu'un objet immuable ?

Voici la définition des objets immuables dans le livre "Effective Java" :

Objet immuable : Une fois un objet créé, tout son état et ses propriétés ne changeront pas au cours de son cycle de vie.

D'après la définition des objets immuables, c'est en fait relativement simple. Une fois qu'un objet est créé, aucune modification ne peut être apportée à l'objet. Par exemple, le code suivant :

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

Étant donné qu'ImmutableObject ne fournit aucune méthode de définition et que la valeur de la variable membre est un type de données de base, la méthode getter renvoie une copie de la valeur, donc une fois l'instance d'ImmutableObject est créée, l'état de l'instance ne peut plus être modifié, rendant cette classe immuable.

Un autre exemple est la chaîne que nous utilisons habituellement le plus :

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

Résultat de sortie :

 

De Il ressort de la sortie qu'après le remplacement de la chaîne de str, l'objet chaîne pointé par str1 n'a toujours pas changé.

2. Compréhension approfondie de l'immuabilité

Avons-nous déjà réfléchi à une question : est-il acceptable que les classes String et wrapper en Java soient conçues pour être mutables ? Si l’objet String devient mutable, quels problèmes cela entraînera-t-il ?

Dans cette section, nous parlons principalement du sens de l'existence d'objets immuables.

1) Rendre la programmation simultanée plus facile

En matière de programmation simultanée, de nombreux amis peuvent trouver que la chose la plus pénible est de savoir comment gérer l'accès mutuellement exclusif aux ressources partagées , un peu de négligence peut entraîner des problèmes inexplicables une fois le code mis en ligne, et la plupart des problèmes de concurrence ne sont pas faciles à localiser et à reproduire. Par conséquent, même les programmeurs très expérimentés seront très prudents et marcheront sur de la glace lorsqu’ils effectueront une programmation simultanée.

Dans la plupart des cas, pour les scénarios avec accès aux ressources mutuellement exclusifs, le verrouillage est utilisé pour implémenter un accès série aux ressources afin de garantir la sécurité de la concurrence, comme le mot clé synchroniser, le verrouillage Lock, etc. Mais la plus grande difficulté de cette solution est qu’il faut être très prudent lors du verrouillage et du déverrouillage. Si le timing de verrouillage ou de déverrouillage est légèrement décalé, cela peut entraîner des problèmes majeurs. Cependant, ce problème ne peut pas être découvert par le compilateur Java, ni lors des tests unitaires et des tests d'intégration. Même le programme peut s'exécuter normalement après sa mise en ligne. , mais cela peut arriver. Soudain, un jour, il est apparu de nulle part.

Cependant, les humains sont intelligents. Puisqu'il est si facile de causer des problèmes lors de l'accès à des ressources partagées de manière sérielle, existe-t-il un autre moyen de les résoudre ? La réponse est oui.

En fait, la cause première des problèmes de sécurité des threads est que plusieurs threads doivent accéder à la même ressource partagée en même temps.

S'il n'y a pas de ressources partagées, alors le problème de sécurité multi-thread sera naturellement résolu. C'est l'idée adoptée en Java pour fournir le mécanisme ThreadLocal.

Cependant, la plupart du temps, les threads doivent utiliser des ressources partagées pour communiquer des informations. Si la ressource partagée ne change pas du tout après sa création, il s'agit d'une lecture constante et simultanée de la ressource partagée. par plusieurs threads Il n'y aura pas de problèmes de sécurité en ligne car tous les threads peuvent toujours obtenir un état de ressource cohérent et complet à chaque fois qu'ils lisent la ressource partagée.

Les objets immuables sont des objets qui ne changent jamais après leur création. Cette fonctionnalité les rend intrinsèquement thread-safe et facilite la programmation simultanée.

Regardons un exemple. Cet exemple provient de : 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;
    }
}

Par exemple, un thread 1 exécute le code suivant. :

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

Puis un autre fil 2 a appelé la méthode color.set après l'instruction 1 et avant l'instruction 2 :

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

Puis dans le fil 1, la valeur de la variable myColorInt et myColorName Les valeurs ne correspondront pas. Afin d'éviter un tel résultat, ces deux instructions doivent être liées ensemble pour l'exécution comme suit :

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

Si SynchronizedRGB est une classe immuable, alors ce problème ne se produira pas, comme synchroniséRGB Modifiez ce qui suit implémentation :

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

Puisque la méthode set ne modifie pas l'objet d'origine, mais crée un nouvel objet, peu importe la façon dont le thread 1 ou le thread 2 appelle la méthode set, elle ne le fera pas. Il y a un problème de données incohérence causée par un accès simultané.

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer