NO.17 不可变对象:海枯石烂,此心不渝 | Java敲黑板系列

开场白

老铁 :对待爱情,您希望有一位对你忠贞不渝的另一半;对待友情,您希望能有一个友情比金坚的知己。在这到处充满变化的世界,我们心里始终怀揣着对一些美好事物能够“不变”的期许。一位哲人曾经说过:“我们无法踏进同一条河流”,看来不变已经成为了一种奢望;在编程的世界里,对象无处不在,那么我们能得到不可变的对象吗?如果答案是肯定的话,那么我们如何创建不可变对象了?在“唯一不变的就是变化”的逻辑体系里,为什么我需要不可变对象了?以下我们将一一揭晓答案。

什么是不可变对象(What)?

从对象的动态演变过程中,我们可以将对象分为两类:不可变对象与可变对象。

不可变对象:对象实例一旦创建完成后,就不能改变其成员变量的值。比如Java中的String字符串类型、Integer、Double等数字类型、Color类型等都属于此类。
可变对象:相对于不可变对象,可变对象实例创建后可以改变其成员变量的值,我们开发过程中大部分对象都是可变对象。

如何创建不可变对象(How)?

在回答该问题时,我们先看代码1,该代码为JDK中String的源码,为了说明问题,文中对完整的源码进行了删减:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = "".value;
    }

    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        //深拷贝
        this.value = Arrays.copyOf(value, value.length);
    }


    /**
     * Allocates a new string that contains the sequence of characters
     * currently contained in the string buffer argument. The contents of the
     * string buffer are copied; subsequent modification of the string buffer
     * does not affect the newly created string.
     *
     * @param  buffer
     *         A {@code StringBuffer}
     */
    public String(StringBuffer buffer) {
        //深拷贝
        synchronized(buffer) {
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        }
    }

    /**
     * Allocates a new string that contains the sequence of characters
     * currently contained in the string builder argument. The contents of the
     * string builder are copied; subsequent modification of the string builder
     * does not affect the newly created string.
     *
     * <p> This constructor is provided to ease migration to {@code
     * StringBuilder}. Obtaining a string from a string builder via the {@code
     * toString} method is likely to run faster and is generally preferred.
     *
     * @param   builder
     *          A {@code StringBuilder}
     *
     * @since  1.5
     */
    public String(StringBuilder builder) {
        //深拷贝
        this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }

    //......
}

Java中的String对象是经典的不可变对象。从上述源码中,我们可以得到以下如何定义一个不可变对象的步骤;按照以下步骤,老铁们在实际开发中也可以很容易的去实现一个不可变对象。

敲黑板

步骤0:声明class为final;以此设置该类不能被继承,进而限制继承类对父类的数据进行修改
步骤1:将class中的所有数据声明为private,进而限制调用方法直接修改成员数据
步骤2:对象成员的赋值操作,均在构造函数中进行
步骤3:在构造函数中,如果传入值包括了可变对象,则clone(深拷贝)先
步骤4:只提供取值方法(getter),不运行存在设值函数(setter)
步骤5:从getter函数返回前,如果返回值为可变对象,则clone(深拷贝)先

上述步骤3与步骤5中为什么需要clone?(什么是clone?请移步:NO.3 厉害了,clone哥 )请老铁们深入思考,并欢迎老铁们将心得写在留言区,与大家共同交流讨论,具体答案将在明日更文中阐述。

为什么需要不可变对象(Why)?

根据上面的描述,我们知道了不可变对象在其全生命周期中的内容是不能被更改的,这个特性让他们天生具备了“多线程安全”。在多线程运行环境中,如果一个线程在另外一个线程读取某个对象数据时正好修改那份数据,那么往往就需要进行同步控制。而如果该对象为不可变对象,那么一切并发所带来的困扰和性能开销都会降低到最低。为此,在并发编程中,一种被普遍认可的原则就是要尽可能的使用不可变对象来创建简单、稳定的代码。

此外,使用不可变对象时,也存在着另外一种声音:认为使用可变对象会降低程序性能。这个顾虑是有道理的:结合不可变对象的应用场景,由于一旦不可变对象被创建完成以后,其内容就不能被修改,为此经常是该对象用完即走,走了即扔,为了满足业务功能就需要创建大量不可变对象,进而为JVM垃圾回收器带来了很多性能上的损耗。

上述问题有解决的方法吗?老铁们可以先思考10秒,然后接着看下文。

上述问题是可以通过缓存的方法来解决。由于不可变对象的不变性,其对象实例是可以被重复使用,我们可以把这些对象缓存起来以便复用。比如查看JDK源代码我们就可以发现String、Integer、Double都提供了静态工厂方法valueOf,它可以从上述缓存中返回一个已经存在的不可变对象,而不是重新再创建一个新对象,从而很好的解决了上述问题。

在实际开发过程中,我们需要结合具体的业务场景合理使用不可变对象。特别是在并发开发环境中,如果构建对象不是性能瓶颈的话,还是极力推荐使用不可变对象。不可变对象的优点包括以下几个方面:

敲黑板

  1. 不可变对象具有线程安全特性:不需要额外的同步控制,不可变对象可以在代码中任意共享。
  2. 由于不需要做额外的同步控制,不可变对象简化了并发程序的开发,降低了并发程序的逻辑复杂度。
  3. 由于减少了同步控制带来的性能开销,提高了整体程序的运行性能。
  4. 为了减少对象创建所带来的性能开销,可采用缓存的方法进行优化。

转载自公众号:代码荣耀
图1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值