>  기사  >  Java  >  Java의 불변 객체에 대한 자세한 분석(코드 포함)

Java의 불변 객체에 대한 자세한 분석(코드 포함)

不言
不言앞으로
2019-04-13 09:51:172343검색

이 기사는 Java의 불변 객체(코드 포함)에 대한 자세한 분석을 제공합니다. 이는 특정 참조 가치가 있습니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

불변 객체는 대부분의 친구들에게 친숙할 것입니다. 코드를 작성하는 과정에서 모든 사람은 가장 일반적인 String 객체, 래퍼 객체 등과 같은 불변 객체를 100% 사용하게 됩니다. 그렇다면 왜 Java 언어가 여기에 설계되어 있습니까? , 진짜 의도와 고려 사항은 무엇입니까? 어쩌면 일부 친구들은 이러한 문제에 대해 자세히 생각하지 않았을 수도 있습니다. 오늘은 불변 객체와 관련된 주제에 대해 이야기하겠습니다.

1. 불변 객체란 무엇인가

책 《Effective Java》에 나오는 불변 객체에 대한 정의는 다음과 같습니다.

Immutable Object(불변 객체): 객체가 생성되면 객체의 모든 상태는 개체 및 속성은 수명 동안 변경되지 않습니다.

불변 객체의 정의에 따르면 실제로는 상대적으로 간단합니다. 즉, 객체가 생성된 후에는 객체를 변경할 수 없습니다. 예를 들어 다음 코드는

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이 가리키는 문자열 개체는 여전히 Nothing입니다. 변경되었습니다.

2. 불변성에 대한 심층적인 이해

Java의 문자열 및 래퍼 클래스가 변경 가능하도록 설계되어도 괜찮을까요?라는 질문을 생각해 본 적이 있나요? String 객체가 변경 가능해지면 어떤 문제가 발생합니까?

이 섹션에서는 주로 불변 객체의 존재 의미에 대해 이야기합니다.

1) 동시 프로그래밍을 더 쉽게 만들기

동시 프로그래밍에 관해 많은 친구들이 가장 고민하는 것은 공유 리소스에 대한 상호 배타적인 액세스를 처리하는 방법이라고 생각할 수 있습니다. 주의하지 않으면 코드가 망가질 수 있습니다. 설명할 수 없는 문제가 발생하며 대부분의 동시성 문제는 찾아내고 재현하기가 쉽지 않습니다. 따라서 경험이 많은 프로그래머라도 동시 프로그래밍을 수행할 때는 매우 조심스럽고 위험한 상황에 처하게 됩니다.

대부분의 경우 리소스에 대한 상호 배타적 액세스 시나리오의 경우 잠금은 동기화 키워드, 잠금 잠금 등과 같은 동시성 보안을 보장하기 위해 리소스에 대한 직렬 액세스를 구현하는 데 사용됩니다. 하지만 이 솔루션의 가장 큰 어려움은 잠금 및 잠금 해제 시 매우 주의해야 한다는 것입니다. 잠금 또는 잠금 해제 타이밍이 약간 어긋나면 큰 문제가 발생할 수 있습니다. 그러나 이 문제는 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 값이 일치하지 않습니다. 이러한 결과를 방지하려면 이 두 문을 다음과 같이 실행하기 위해 함께 바인딩해야 합니다.

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

동기화된 RGB가 불변 클래스인 경우 이 문제는 발생하지 않습니다. 예를 들어 동기화된 RGB를 다음 구현 방법으로 변경합니다.

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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제