尺度不变性是指什么不变_不变性的来龙去脉

尺度不变性是指什么不变

因此,在我的第一篇文章中,我谈到了一些构建器模式,并提到了一个非常强大但却被忽视的概念:不变性。

什么是不可变类? 这只是一个其实例无法修改的类。 类属性的每个值都在其声明或其构造函数中设置,并且在对象的整个生命周期中保留这些值。 Java有很多不变的类,例如String ,所有装箱的原语( DoubleIntegerFloat等), BigIntegerBigDecimal等。 这有一个很好的理由:不可变类比可变类更易于设计,实现和使用。 一旦实例化它们,它们只能处于一种状态,因此它们不易出错,并且,正如我们在本文后面会看到的那样,它们更加安全。

您如何确保类是不变的? 只需遵循以下5个简单步骤:

  1. 不要提供任何可修改对象状态的公共方法 ,也称为增变器(例如setter)。
  2. 防止扩展类 。 这不允许任何恶意或粗心的类扩展我们的类并损害其不变的行为。 这样做的通常且更简单的方法是将类标记为final ,但是我将在本文中提及另一种方法。
  3. 将所有字段定为最终值 。 这是让编译器为您强制执行第1点的方法。 此外,它清楚地使任何看到您的代码的人都知道,您不希望这些字段在设置后更改其值。
  4. 将所有字段设为私有 。 无论您是否考虑了不变性,这一点都应该很明显,并且您应该遵循它 ,但是我只是为了以防万一。
  5. 永远不要提供对任何可变属性的访问 。 如果您的类具有一个可变对象作为其属性之一(例如ListMap或您的域问题中的任何其他可变对象),请确保该类的客户端永远无法获得对该对象的引用。 这意味着您永远不要从访问器(例如,getter)直接返回对它们的引用,并且永远不要在构造函数中使用从客户端作为参数传递的引用来初始化它们。 在这种情况下,您应该始终制作防御性副本。

有很多理论知识,没有代码,因此让我们看看一个简单的不可变类是什么样子,以及它如何处理我之前提到的5个步骤:

public class Book {
    private final String isbn;
    private final int publicationYear;
    private final List reviews;
    private Book(BookBuilder builder) {
        this.isbn = builder.isbn;
        this.publicationYear = builder.publicationYear;
        this.reviews = Lists.newArrayList(builder.reviews);
    }
    public String getIsbn() {
        return isbn;
    }
    public int getPublicationYear() {
        return publicationYear;
    }
    public List getReviews() {
        return Lists.newArrayList(reviews);
    }
    public static class BookBuilder {
        private String isbn;
        private int publicationYear;
        private List reviews;
        public BookBuilder isbn(String isbn) {
            this.isbn = isbn;
            return this;
        }
        public BookBuilder publicationYear(int year) {
            this.publicationYear = year;
            return this;
        }
        public BookBuilder reviews(List reviews) {
            this.reviews = reviews == null ? new ArrayList() : reviews;
            return this;
        }
        public Book build() {
            return new Book(this);
        }
    }
}

我们将在这个非常简单的课程中讲解重点。 首先,您可能已经注意到,我再次使用了构建器模式。 这不仅是因为我是它的忠实拥护者,而且还因为我想说明一些我不想在之前的文章中没有首先对不变性概念有基本了解的观点。 现在,让我们看一下我提到的5个步骤,您需要遵循这些步骤使一个类不可变,并查看它们是否对本书示例有效:

    • 不要提供任何修改对象状态的公共方法 。 请注意,类上的唯一方法是其私有构造函数和其属性的获取器,但没有更改对象状态的方法。
    • 防止扩展类 。 这很棘手。 我提到确保这一点的最简单方法是将班级定为最终班,但Book班显然不是最终班。 但是,请注意,唯一可用的构造函数是private 。 编译器确保没有公共或受保护的构造函数的类不能被子类化。 因此,在这种情况下,不需要在类声明中使用final关键字,但是无论如何将其包括在内只是个好主意,以使看到您代码的任何人都可以清楚地知道。
    • 将所有字段定为最终值 。 非常简单,该类上的所有属性都声明为final
    • 永远不要提供对任何可变属性的访问 。 这实际上很有趣。 请注意Book类如何具有一个List <String>属性,该属性被声明为final并且其值在类构造函数上设置。 但是,此列表是可变对象。 也就是说,虽然评论参考一旦设置便无法更改,但列表的内容可以更改。 引用相同列表的客户端可以添加或删除元素,结果,在创建Book对象后更改其状态。 因此,请注意,在Book构造函数中,我们不直接分配引用。 相反,我们使用Guava库通过调用“ this.reviews = Lists.newArrayList(builder.reviews); ”来复制列表this.reviews = Lists.newArrayList(builder.reviews); ”。 在getReviews方法上可以看到相同的情况,在该方法中,我们返回列表的副本而不是直接引用。 值得注意的是,此示例可能有点简化,因为评论列表只能包含不可变的字符串。 如果列表的类型是可变的类,那么您还必须复制列表中的每个对象,而不仅仅是列表本身。

最后一点说明了为什么不可变的类导致更简洁的设计和更易于阅读的代码。 您只需共享那些不可变的对象,而不必担心防御性副本。 实际上,绝对不要制作任何副本,因为对象的任何副本都将永远等于原始副本。 一个必然的结论是,不变的对象只是简单的。 他们只能处于一种状态,并且一生都保持这种状态。 您可以使用类构造函数检查所有不变量(即需要在该类上有效的条件,例如其属性之一的值范围),然后可以确保这些不变量保持真实状态而无需付出任何努力您或您的客户。

不变对象的另一个巨大好处是它们本质上是线程安全的。 它们不能被同时访问对象的多个线程破坏。 到目前为止,这是在应用程序中提供线程安全性的最简单且不易出错的方法。

但是,如果您已经有一个Book实例并且想要更改其属性之一的值怎么办? 换句话说,您想要更改对象的状态。 在不可变的类上,按照定义,这是不可能的。 但是,与软件中的大多数事情一样,总有一种解决方法。 在这种情况下,实际上有两个。

第一种选择是在Book类上使用Fluent Interface技术,并具有类似于setter的方法,这些方法实际上创建一个对象,该对象的所有属性都具有相同的值,但要更改的对象除外。 在我们的示例中,我们将必须在Book类中添加以下内容:

private Book(BookBuilder builder) {
        this(builder.isbn, builder.publicationYear, builder.reviews);
    }
    private Book(String isbn, int publicationYear, List reviews) {
        this.isbn = isbn;
        this.publicationYear = publicationYear;
        this.reviews = Lists.newArrayList(reviews);
    }
    public Book withIsbn(String isbn) {
        return new Book(isbn,this.publicationYear, this.reviews);
    }

请注意,我们添加了一个新的私有构造函数,可以在其中指定每个属性的值,并修改了旧的构造函数以使用新的构造函数。 此外,我们添加了一个新方法,该方法返回一个新的Book对象,该对象具有我们想要的isbn属性值。 相同的概念适用于该类的其余属性。 之所以称为功能方法,是因为方法无需修改即可返回对其参数进行操作的结果。 这与程序命令式方法形成对比,在方法式命令式方法中,方法将一个过程应用于其操作数,从而更改其状态。

这种生成新对象的方法显示了不可变类的唯一真正缺点:它们要求我们为所需的每个不同值创建一个新对象,这会在性能和内存消耗方面产生可观的开销。 如果要更改对象的几个属性,则会放大此问题,因为在每个步骤中都将生成一个新对象,并且最终会丢弃所有中间对象,而仅保留最后一个结果。

我们可以在构建器模式的帮助下为多步操作提供更好的选择,例如我在上一段中描述的操作。 基本上,我们向构建器添加一个新的构造器,该构造器采用一个已经创建的实例来设置其所有初始值。 然后,客户端可以以通常的方式使用构建器来设置所有所需的值,然后使用build方法来创建最终对象。 这样,我们避免只使用我们需要的某些值来创建中间对象。 在我们的示例中,此技术在生成器方面看起来像这样:

public BookBuilder(Book book) {
    this.isbn = book.getIsbn();
    this.publicationYear = book.getPublicationYear();
    this.reviews = book.getReviews();
}

然后,在我们的客户上,我们可以:

Book originalBook = getRandomBook();

Book modifiedBook = new BookBuilder(originalBook).isbn('123456').publicationYear(2011).build();

现在,显然构建器不是线程安全的,因此您必须采取所有通常的预防措施,例如不与多个线程共享构建器。

我提到过这样一个事实,即我们必须为状态的每个更改都创建一个新对象,这可能会增加性能,这是不可变类的唯一真正的缺点。 但是,对象创建是JVM不断改进的方面之一。 实际上,除特殊情况外,对象创建比您想象的要高效得多。 无论如何,提出一个简单明了的设计通常是一个好主意,然后仅在进行测量后才重构性能。 在尝试猜测代码花费大量时间的十分之九的时间中,有九次会发现自己错了。 此外,不变对象可以自由共享而不必担心后果,这一事实使您有机会鼓励客户端尽可能重用现有实例,从而大大减少了创建对象的数量。 一种常见的方法是为最常见的值提供公共静态最终常量。 此技术在JDK上大量使用,例如在Boolean.FALSEBigDecimal.ZERO中

总结一下这篇文章,如果您想从中学到一些东西,那就这样吧: 除非有充分的理由使它们可变,否则类应该是不可变的 。 不要为每个类属性自动添加设置器。 如果出于某种原因您绝对不能使您的类不可变,那么请尽可能限制其可变性。 一个对象可以处于的状态越少,就越容易考虑该对象及其不变量。 并且不必担心不变性的性能开销,很有可能您不必担心它。

参考: JCG合作伙伴 Jose Luis在开发上的 不变性的来龙去脉

翻译自: https://www.javacodegeeks.com/2013/01/the-ins-and-outs-of-immutability.html

尺度不变性是指什么不变

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值