跳转到主要内容

保存的帖子

在Java中的不可动加性

Dave Nicolette |龙头
Dave Nicolette 高级顾问
阅读: 在Java中的不可动加性

此帖子不是关于如何在Java中设计不可变的对象的教程。这更像是哀叹,或者也许是延伸的抱怨。为什么Java应用程序似乎比以其他语言写的那些更难以在Java中设计不可变的对象而不是任何其他语言的影响

那又怎样?

我猜第一个问题是“那又怎样?”不管怎么说,谁在乎不变性?

在创建之后无法修改一个不可变的对象。当需要一个新值时,接受的做法是制作具有新值的对象的副本。

功能语言通过设计支持无情。使用面向对象或过程语言的时,必须设计访问数据结构的对象和程序代码,以提供防止不需要的修改。

不变的物体为建立可靠应用提供了许多优点。由于我们不需要编写防御或保护代码来保持应用程序状态一致,我们的代码可以更简单,更简洁,更少错误,而不是定义可变对象时的错误。

不可变对象的一些主要好处是:

  • 线程安全
  • 原子性故障
  • 没有隐藏的副作用
  • 防止空引用错误的保护
  • 缓解的缓存
  • 预防身份突变
  • 避免在方法之间的时间耦合
  • 支持参考透明度
  • 保护从实例化逻辑上 - 无效的对象
  • 防止现有对象的意外损坏

请注意,不可变形作为一个设计目标,它并不意味着不可能改变一个对象。这意味着在给定的编程语言中访问对象的正常机制不允许修改。绕过一般的机制总是可能的,但是不建议在业务应用程序代码中这样做(尽管这对于某些其他类型的解决方案是正常的)。亚博vip9通道

为什么这对Java来说是个问题?

不变性通常是大多数类别的软件都是一个好主意。它不仅仅是java或只是面向对象的设计。就像任何其他任何一个好主意一样,它不是一个“规则”或“教条”。软件需要做任何必要的事情来解决手头的问题。有很多情况下有很多情况下可以有变形实体。仍然,一般来说,永恒性帮助我们建设居所代码。那么,为什么要选Java呢?

我们花在现有代码库上的时间要比花在新开发上的时间多得多。Java可能是我们在生产环境中发现的最常见的语言。我们在野外发现的大多数Java代码都不遵守使用不可变对象的指导原则,这并不是因为作者仔细考虑了可变性的权衡。如果说有什么区别的话,那似乎是因为他们已经做到了这样做了。与可变性相关的运行时问题在生产Java应用程序中很常见。

为什么Java似乎比其他语言更容易受到这个问题的影响?毕竟,有足够的信息有关不变性的价值和有关如何在Java中设计不可变量的对象的指导。Oracle的官方Java教程包括一个课上不变性。开发人员如何在不工作这些教程的情况下学习Java?他们是自由的,他们直接来自马的嘴巴。学习语言的明显起点。

许多开发人员也分享了他们关于Java中不可变对象的知识。比如大卫·奥米拉一个好的解释在JavaRanch上,它直接基于基本的Java教程(没有属性;啧啧,啧啧)。在我看来,他的演练和示例比Java教程中的更清楚。这只是一个例子;丰富的信息。

特别是,没有明显的理由说明为什么Java应用程序比用任何其他语言编写的应用程序遭受更大程度的不变性问题……或者真的有这种情况吗?

我将提出以下可能的原因:

  • 大多数Java程序员认为它是“必要的”或“必需”为类中的所有字段编写Getter和Setter方法。它们似乎也相信任何名称以“get”或“set”开始的方法,不允许包含除了检索或修改字段的值之外的任何逻辑。这是他们最初学会编写Java代码的方式,他们从不质疑它。实际上,这几乎没有比宣布每个公共场地更好。这种做法可能是一个回归java beans公约,不适当地应用于不应该是Java bean的类。
  • 基于java的webapp框架中使用的依赖注入容器的早期实现要求类必须用无参数构造函数来定义。Java程序员已经养成了编写无参数构造函数或允许默认的无参数构造函数保持不变的习惯,即使在解决方案设计不包含此类需求的情况下也是如此。
  • 许多程序员夸大了对象创建成本的看法。他们脱离可读性,可靠性,简单性和可居住的代码的其他理想特征,以避免实例化新对象。

Java Beans约定

根据这一点规范“JavaBeans API的目标是为Java定义软件组件模型,因此第三方ISV可以创建和将可以通过最终用户组成的Java组件。”在Java语言的历史上很早地制定了建立申请的方法;Java Bean规范文档的“当前”版本于1997年8月8日日期,并适用于Java 1.1。

Java Beans惯例被生产可视化GUI应用程序构建器的公司所接受。这些工具允许开发人员通过拖放GUI小部件(AWT组件)到可视工作区来创建一个“屏幕”。AWT组件被实现为Java bean。这使工具能够使用Java反射API.发现名称以“set”开头的任何方法,以便开发人员可以通过添加文本、图标、操作等自定义每个小部件。多年来,这一直是构建基于swing的独立应用程序和Java applet的流行方式。这个概念在现场被证明是成功的。

但是Sun Microsystems对于Java bean有更广阔的视野,而不仅仅是GUI小部件。规范中写道:“一些JavaBean组件更像常规的应用程序,然后可以将它们组合成复合文档。所以一个电子表格Bean可能被嵌入到一个Web页面中。”事实上,我见过(也写过)将Swing组件组合成GUI应用程序中的类似电子表格的元素的解决方案。总的来说,这种方法从未被用于一般的应用程序开发。

Java Bean现象获得了很多关注。Sun微系统公司将这一理念扩展到了前端之外,并将其以以下形式应用到中间层和后端应用程序中企业Java bean(ejb)。就像我们工作领域中的许多想法一样,ejb当时看起来一定是一个好主意。

随着更简单和/或更健壮的替代方案的出现,Java bean的使用逐渐减少。Java bean约定的主要设计目标是(根据规范):

  • 组件粒度
  • 可移植性
  • 统一的、高质量的API
  • 简单

所有这些目标通常都是通过今天的其他方式实现的。组件粒度简单通过一般接受的软件设计原则支持,例如坚硬的抓牢可移植性统一的、高质量的APIAPI设计原则支持目标,自Java Beans会议的出现以来已经成熟,包括谷歌的API设计指南swift.org的API指南, 这休息API指南和heroku的十二因素应用设计指南

Java Swing是一个基于事件的框架。Java bean就是要在这个框架中工作。一颗豆子有三个方面:

  • 属性
  • 方法
  • 事件

豆的属性是它的字段。的方法是使GUI构建器工具能够获取和设置字段的操作。豆子会燃烧并做出反应活动来响应用户界面上的手势。

如果您正在编写与此不同的代码,那么Java Beans约定可能是不相关的。

当豆子不是豆子的时候

现在,大多数Java程序员例行地将类的字段声明为私有,并通过公共的getter和setter方法公开它们,就像在Java Bean中所做的那样。大多数应用程序不是事件驱动的,并且大多数生产应用程序中的类不会触发和响应事件。

以下是Java类的示例,其中包含其所有字段已声明公众:

public class Shoe {public shostyle style;公共双倍尺寸;公共ShowWidth宽度;公共int脚跟;公共[]availableColors颜色;公共字符串photoFilename;}

我们假设存在ShoeStyle和Shoewidth类型,这很可能是枚举,如果这是一个真实的应用程序。

您可以在应用程序中使用该类定义。没有什么淘气或邪恶的。编译器不关心是否将所有字段声明为public。也许你也不在乎,如果这门课只是一堆田地。如果类有任何行为,或者如果在运行时修改字段有任何负面影响,那么您可能会有点关心。

下面是一个典型的Java程序员可能会如何编写它:

公共班鞋{私人鞋石风格;私人双倍尺寸;私人湿润宽度;私人INT脚跟;私人清单可用性ecolors;私有串Photofilename;public double getsize(){返回大小;}公共void seteze(双倍尺寸){this.size = size;}等。}

现在这些字段被声明为私有的,因此我们对从这个类创建的对象的健壮性有了一种安全感。我们还为所有字段设置了公共设置,所以我们知道我们的安全感是错误的。getter方法的名称揭示了字段的内部名称,因此客户机代码必须了解更多关于类内部的信息。任何字段名的更改都需要更改其getter和setter方法名,因此需要更改使用该类的所有客户端代码。

听起来有很多繁忙的工作等着你去做。我不知道你怎么想,但我还是不要了。

一种稍微面向对象的方法可能是:

公共班鞋{私人鞋石风格;私人双倍尺寸;私人湿润宽度;私人INT脚跟;私人清单可用性ecolors;私有串Photofilename;公共鞋子(鞋形风格,双倍尺寸,彩色宽度,INT脚跟,列表可用ecolors,photofilename){if(style == null || width == null || availablecolors == null || size <1 ||尺寸> 15 ||脚跟<1 ||脚跟> 12){抛出newrestalargumentexception();}这个.Style =风格;这个.size = double;这。赫尔=脚跟; this.width = width; this.availableColors = availableColors; this.photoFilename = photoFilename; } public double size() { return size; } public boolean comesIn(Color color) { return availableColors.contains(color); } public Shoe addColor(Color color) { List newAvailableColors = availableColors; newAvailableColors.add(color); return new Shoe(style, size, width, heel, newAvailableColors, photoFilename); }等。}

这个版本有一个构造函数,可以确保不会创建逻辑上无效的实例。它阻止Shoe对象被创建,除非所有的强制字段都被填充(它允许photoFilename的空引用,表明这是一个可选值)。

它具有访问器(显示的一个是size()方法),但不会命名它们“getthis()”和“getThat()”。这样做的目的是,因为这不是java bean。其他方法以暗示其正常使用的方式命名。

代码可以更漂亮吗?确定。比如,那些神奇的数字就不是那么好。它们可以被int常量代替,如MIN_SHOE_SIZE等等,或者被枚举或类代替,这些枚举或类知道它们自己的上界和下界,并确保只能实例化有效的对象。这只是一个简单的例子;我们可不想忘乎所以。

要查明鞋子是否有红色,则不会调用“shoe.getavailablecolors()”并挖掘列表。你刚写,“shoe.comesin(color.red)”。您不必知道该类将可用颜色维护为列表,以及您不应该必须知道这点。客户机代码变得更加松散耦合更能表达意图:

public void expressextreeshoecolorbias (Shoe Shoe) {if (Shoe . comesin (Color.RED)) {System.out。println(“让我们去购物吧!”);} else {System.out.}println(“赤脚吧!”);}}

没有setter。要向鞋子添加一个属性,我们创建一个具有新属性的shoe新实例(如addColor()方法所示)。

你可以做更多的事情;这是为了说明一个不是Java Bean的对象不需要看起来像Java Bean并且不能从中受益看起来像一个Java Bean。

结论:过度使用的吸气器和定居者似乎是Java Beans和EJB的时代的持有。

依赖注入

依赖注入容器的早期实现无法追逐一系列依赖关系并解决所有构造函数参数。它们需要开发人员在可能注入的类上提供一个arg arg构造函数。

缺点是可以实例化逻辑上无效的对象;也就是说,缺少要使用的对象所需值的对象。在Java世界中,这导致了我们亲爱的朋友的频繁,unannounced访问,NullPointerException。其他朋友不时地掉下来,例如神秘的间歇运行时错误,即我不能重现,而且我的机器上的旧作品,而且没有在测试环境中,但他的堂兄,但单位测试通过了。

这种早期的限制并没有持续多久。现在的容器可以实例化任何对象。您可以编写结构良好、面向对象的代码,不允许创建无效对象。提供无参数构造函数和依赖setter注入的习惯可以而且应该被打破。

害怕行走对象实例化开销

Java运行时使用参考访问内存中的对象。这和不变性有什么关系?

假设我们想要两束花,每人两束。第一束花应该有6瓣和9瓣。第二束花应该有4片花瓣和8片花瓣。(很浪漫,不是吗?)在这个代码运行之后…

public class Flower {private int petalCount;public setPetalCount(int petalCount){这个。petalCount = petalCount;}} public class Bouquet {private List flowers;public Bouquet() {flowers = new ArrayList();} public void setFlowers(List flowers) {this。花=花;}} public class SomeClientClass {. . .public List makebouquet(){//构建第一个花束Flower flower1 = new Flower();flower1.setPetalCount (6);花花2 = new花();flower2.setPetalCount (9); List flowers = new ArrayList(); flowers.add(flower1); flowers.add(flower2); Bouquet bouquet1 = new Bouquet(); bouquet1.setFlowers(flowers); // build second bouquet flower1.setPetalCount(4); flower2.setPetalCount(8); Bouquet bouquet2 = new Bouquet(); bouquet2.setFlowers(flowers); List bouquets = new ArrayList(); bouquets.add(bouquet1); bouquets.add(bouquet2); } }

两束花都有4瓣和8瓣引用命名Flower1.花朵2.指向同一花的花。Bouquet1和Bouquet2都在看相同的参考。因此,当我们修改Flower1和Flower2以设置Bouquet2的花瓣计数时,那些在Bouquet1中可以看到这些变化。

对Bouquet对象做一个浅拷贝不会导致对flower1和flower2的不同引用:

公共班级花束{。。。公共花束克隆(){Bouquet newbouquet = new bouquet();newbouquet.setFlowers(this.flowers);返回newbouet;}}

引用flower1和flower2仍然指向内存中的相同对象。

根据字段的数据类型,创建对象的安全副本可能涉及创建深红色对象中的某些字段。根据应用程序的设计方式,可能需要几层深度副本,以确保在对象实例化后,栈中某处的数据值不会被修改。(时髦的人称之为隐藏的副作用;我们没有预料到的应用程序状态的变化。)

公共班级花束{。。。公共花束克隆(){Bouquet newbouquet = new bouquet();List NewFlowers = new arraylist();for(花朵花:this.flowers){flower newflower = new flower();newflower.setpetalcount(flower.getpetalcount());Newflowers.add(Newflower);newbouquet.setFlowers(NewFlowers);返回newbouet; } }

即使是这个琐碎的例子看起来比如实例化新的Flower对象会产生大量的计算开销。想象一个更现实的例子,其中包含许多不平凡的对象。看起来很吓人,是吧?

看起来吓人,但不是。

许多Java开发人员过度担心对象实例化的性能影响。问题是如此普遍Java教程在不可改性的情况下明确调用它:“程序员通常不愿意使用不可变的物体,因为他们担心创建新对象的成本而不是更新到位。对象创建的影响通常被高估,并且可以被一些与不可变物体相关的效率抵消。这些包括由于垃圾收集而下降的开销,并消除了保护可变物体免受损坏所需的代码。“

程序员真正应该担心的是I/O开销。在循环中编写的数据库查询比在内存中实例化一个对象的开销要大得多。无论如何,网络延迟很可能会吞噬应用程序中的任何计算开销。实例化开销不是避免创建不可变对象的有效理由。

如果你的申请真正严重的表现要求,然后我的第一个问题是,“你为什么选择这个应用程序的Java?”Java是一般商业应用程序编程的一种很好的语言,其性能特征适用于此类应用。亚博vip9通道Java是设计用于真正严格的性能要求。

为什么我一直在写"真正严重"因为大多数Java程序员似乎对“高性能”有一个不切实际的概念。高性能软件并不是为了将一条记录从后端数据存储中取出并在屏幕上显示其字段。高性能软件用于快速搜索海量数据存储和实时巡航导弹制导。如果你正在开发一个web应用程序或一个面向商业的微服务,你不会有真正严亚博vip9通道格的性能要求。

克服它。字符串是实习。Pseudo-primitive类型等int同样管理。Java编译器比手工优化代码更好。jvm对内存使用和垃圾收集的优化要比手动更好。不要为了获得无关紧要的性能改进而牺牲可靠性和简单的设计。

结论

不变性的好处是完全成熟和众所周知的。我认为我们需要理由是公平的在我们的解决方案中使用不可变量,而不是使用它们的肯定原因。

下一个>敏捷中的约束理论

评论(1)

  1. Thais Manfrin.
    回复

    很有启发性的内容。: -)
    关于EJB的一个说明:关于它如何发展和影响Java开发的非常好的解释(我从不喜欢仅仅为了公开它们的私有字段和公共getter /setter,但在那个时候,没有it专业人员可以对此持不同意见:-))。

    回复

发表评论

您的电子邮件地址不会被公开。必需的地方已做标记*