Home >Java >javaTutorial >Detailed analysis of immutable objects in Java (with code)

Detailed analysis of immutable objects in Java (with code)

不言
不言forward
2019-04-13 09:51:172381browse

This article brings you a detailed analysis of immutable objects in Java (with code). It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.

Immutable objects must be familiar to most of my friends. In the process of writing code, everyone will 100% use immutable objects, such as the most common String objects, wrapper objects, etc. So why is Java What are the real intentions and considerations for designing the language in this way? Maybe some friends have not thought about these issues in detail. Today we will talk about topics related to immutable objects.

1. What is an immutable object

The following is the definition of immutable objects in the book "Effective Java":

Immutable Object: Once an object is created, all its state and properties will not change during its life cycle.

From the definition of immutable objects, it is actually relatively simple. After an object is created, no changes can be made to the object. For example, the following code:

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

Since ImmutableObject does not provide any setter method, and the member variable value is a basic data type, the getter method returns a copy of value, so once an ImmutableObject instance is created, the instance's The state can no longer be changed, making this class immutable.

Another example is the String we usually use most:

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

Output result:

 

From the output result It can be seen that after string replacement of str, the string object pointed to by str1 still has not changed.

2. In-depth understanding of immutability

Have we ever considered a question: Is it ok if the String and wrapper classes in Java are designed to be mutable? If the String object becomes mutable, what problems will it cause?

In this section we mainly talk about the meaning of the existence of immutable objects.

1) Make concurrent programming easier

When it comes to concurrent programming, many friends may find that the most distressing thing is how to handle mutually exclusive access to shared resources. , a little carelessness may lead to inexplicable problems after the code goes online, and most concurrency problems are not easy to locate and reproduce. Therefore, even very experienced programmers will be very careful and tread on thin ice when performing concurrent programming.

In most cases, for scenarios with mutually exclusive access to resources, locking is used to implement serial access to resources to ensure concurrency security, such as the synchronize keyword, Lock lock, etc. But the biggest difficulty with this solution is that you need to be very careful when locking and unlocking. If the locking or unlocking timing is slightly off, it may cause major problems. However, this problem cannot be discovered by the Java compiler, nor can it be discovered during unit testing and integration testing. The program can run normally even after it goes online, but it may Suddenly one day, it appeared out of nowhere.

However, humans are smart. Since it is so easy to cause problems when accessing shared resources in a serial manner, is there any other way to solve it? The answer is yes.

In fact, the root cause of thread safety problems is that multiple threads need to access the same shared resource at the same time.

If there are no shared resources, then the problem of multi-thread safety will naturally be solved. This is the idea adopted in providing the ThreadLocal mechanism in Java.

However, most of the time, threads need to use shared resources to communicate information. If the shared resource does not change at all after it is created, it is like a constant, and concurrent reading of the shared resource by multiple threads is There will be no online security issues because all threads can always obtain a consistent and complete resource state whenever they read the shared resource.

Immutable objects are objects that never change after they are created. This feature makes them inherently thread-safe and makes concurrent programming easier.

Let’s take a look at an example. This example comes from: 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;
    }
}

For example, a thread 1 executes the following code:

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

Then another thread 2 calls the color.set method after Statement 1 and before Statement 2:

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

Then the value of the variable myColorInt and the value of myColorName in thread 1 will not match. In order to avoid such a result, these two statements must be bound together for execution as follows:

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

If SynchronizedRGB is an immutable class, then this problem will not occur. For example, change SynchronizedRGB to The following implementation method:

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

Since the set method does not change the original object, but creates a new object, no matter how thread 1 or thread 2 calls the set method, concurrent access will not occur. data inconsistency problem.

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

The above is the detailed content of Detailed analysis of immutable objects in Java (with code). For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:segmentfault.com. If there is any infringement, please contact admin@php.cn delete