Java中将不会有不可变的集合-现在或现在都不会

大家好!

今天,请您注意一篇关于Java的基本问题之一-可变性,以及它如何影响数据结构的结构以及如何使用它们的精心撰写的文章的翻译。 资料取材于Nicolai Parlog的博客,我们真正尝试保留其辉煌的文学风格。 尼古拉斯本人的特点是摘自哈布雷(Habé)JUG.ru公司博客摘录 。 让我们完整地引用这段话:


尼古拉·帕洛格(Nikolay Parlog)就是这样的媒体佬,他对Java功能进行评论。 但是他不是同时来自Oracle,因此这些评论出奇的坦率和易于理解。 有时在他们被解雇之后,却很少。 Nikolay将讨论Java的未来,以及新版本中的内容。 他擅长于谈论趋势以及关于大世界的信息。 他是一个很好的阅读和博学的同伴。 每当您学习新内容时,即使简单的报告也很令人愉快。 而且,尼古拉斯无所不知。 也就是说,即使根本不是您的话题,您也可以访问任何报告并享受它。 他在教书。 他为曼宁出版社(Manning Publishing House)撰写了“ Java模块系统”,在codefx.org上维护有关软件开发的博客,并且长期参与多个开源项目。 他是自由职业者,可以在会议上立即聘用。 是的,一个非常昂贵的自由职业者。 这是报告

我们阅读并投票。 谁特别喜欢该帖子-我们还建议您查看读者对原始帖子的评论。

波动性不好,对吗? 因此, 不变性是良好的 。 不变性特别有用的主要数据结构是集合:在Java中,它是一个列表( List ),一个集合( Set )和一个字典( Map )。 但是,尽管JDK带有不可变(或不可修改?)集合,但是类型系统对此一无所知。 JDK中没有ImmutableList ,而Guava的这种类型对我来说似乎完全没有用。 但是为什么呢? 为什么不只是将Immutable...添加到此混合中,而不是说应该混合呢?

什么是不可变集合?


在JDK术语中,“ 不可变 ”和“ 不可修改 ”一词的含义在过去几年中发生了变化。 最初,“不可修改的”被称为不允许可变性(mutability)的实例:为了响应不断变化的方法,他抛出了UnsupportedOperationException 。 但是,可以用另一种方式更改它-可能是因为它只是可变集合的包装。 这些视图反映在Collections::unmodifiableListunmodifiableSetunmodifiableMap方法及其JavaDoc中

最初,术语“ 不可变 ”是指Java 9集合工厂方法返回的集合 。 集合本身无法进行任何更改(是的,存在反射 ,但是不算在内),因此,似乎它们可以证明其名称合理。 las,因此经常会造成混乱。 假设有一种方法可以在屏幕上显示不可变集合中的所有元素-它会始终提供相同的结果吗? ?? 还是不行

如果您没有立即回答“ 否” ,则意味着您只是对这里可能出现的混乱情况有所了解。 “ 秘密代理的不可变集合 ”-听起来与“ 不可变的秘密代理的不可变集合 ”非常相似但是这两个实体可能并不相同。 一个不可变的收藏集不能使用插入/删除/清除操作等进行编辑,但是,如果秘密特工是可变的(尽管间谍电影中的角色发展非常糟糕,以至于您根本不相信它),那并不意味着它整个秘密特工的收藏是不变的。 因此,现在有一种趋势是将此类集合命名为不可修改的而不是不可变的 ,这已在新版JavaDoc中得到了体现

本文讨论的不可变集合可能包含可变元素。

我个人不喜欢这样的术语修订。 我认为,“不变的收藏”一词仅意味着收藏本身不宜更改,而不能描述其中包含的要素。 在这种情况下,还有一个更积极的观点:Java生态系统中的“不变性”一词并没有完全变成废话。

一种或另一种方式,在本文中,我们将讨论不可变集合 ,其中...

  • 集合中包含的实例在设计阶段确定
  • 这些副本-数量均匀,既不减少也不增加
  • 没有关于这些元素的可变性的陈述。

让我们详细谈谈现在我们将练习添加不可变集合的事实。 准确地说-不可变的列表。 关于列表的所有说明都同样适用于其他类型的集合。

开始添加不可变的集合!

让我们创建ImmutableList接口,并使其相对于List ,呃……是什么? 是超型还是亚型? 让我们来谈谈第一个选项。



精美地讲, ImmutableList没有ImmutableList方法,因此使用它总是安全的,对吗? 那么?! 不,长官

 List<Agent> agents = new ArrayList<>(); // ,  `List`  `ImmutableList` ImmutableList<Agent> section4 = agents; //    section4.forEach(System.out::println); //    `section4` agents.add(new Agent("Motoko"); //  "Motoko" – ,      ?! section4.forEach(System.out::println); 

此示例表明,可以将这样一个不完全不变的列表传输到API,该列表的操作可以建立在不变性之上,因此,使该类型名称可能暗示的所有保证都无效。 这是给您的食谱,可能会导致灾难。

好的,然后ImmutableList扩展List 。 也许吧



现在,如果API期望一个不可变的列表,那么它将收到这样的列表,但是有两个缺点:

  • 不可变列表仍应提供修改方法(因为它们是在超类型中定义的),并且唯一可能的实现将引发异常
  • ImmutableList实例也是List实例,并且当分配给这样的变量,作为这样的参数传递或以这种类型返回时,逻辑上是假定允许可变性。

因此,事实证明,您只能在本地使用ImmutableList ,因为它通过API边界作为List传递,这需要您采取超人的预防措施,否则会在运行时爆炸。 它不像扩展ImmutableListList那样糟糕,但是这样的解决方案仍然远非理想。

当我说Guava的ImmutableList类型实际上是无用的时,这正是我想到的。 这是一个很棒的代码示例,在处理本地不可变列表时非常可靠(这就是我积极使用它的原因),但是,依靠它,很容易超越不可渗透的,有保证的已编译城堡,城堡的墙壁由不可变类型组成,并且仅在此范围内不变类型可以充分发挥其潜力。 这总比没有好,但作为JDK解决方案效率低下。

如果ImmutableList无法扩展List ,并且解决方法仍然不起作用,那么应该如何使其全部起作用?

不变性是一个特征


我们在前两次尝试添加不可变类型时遇到的问题是我们的误解,认为不可变性只是缺少某种东西:我们获取一个List ,从其中删除更改的代码,得到一个ImmutableList 。 但是,实际上,所有这一切都不是那样。

如果仅从List删除修改方法,则会得到一个只读列表。 或者,遵循上述公式,可以将其称为UnmodifiableList它仍然可以更改,只是您不会更改。

现在,我们可以在此图片中添加另外两件事:

  • 我们可以通过添加适当的方法使其变得可变
  • 我们可以通过添加适当的保证使其不可变。

不变性不是没有可变性,而是保证不会有任何变化
在这种情况下,重要的是要了解在两种情况下我们都在谈论完整功能-不变性不是没有更改,而是保证不会有更改。 现有功能可能不一定是有用的东西,它还可以保证代码中不会发生不好的事情-在这种情况下,例如,考虑线程安全性。

显然,可变性和不变性相互冲突,因此我们不能同时使用上述两个继承层次结构。 类型从其他类型继承功能,因此,无论您如何削减它们,如果其中一个类型从其他类型继承,它将包含两个功能。

很好, ListImmutableList不能互相扩展。 但是我们是通过使用UnmodifiableList来到这里的,实际上这两种类型都具有相同的API(只读),这意味着它们应该扩展它。



虽然,我不会确切地称呼这些名称,但这种层次结构是合理的。 例如,在Scala中, 实际上是这样做的 。 区别在于,共享超类型(我们称为UnmodifiableList )定义了可变方法,这些方法返回修改后的集合并保持原始状态不变。 因此,一个不可变的列表证明是持久的,并为可变变体提供了两组修改方法-继承来接收修改后的副本,以及为更改而拥有的副本。

Java呢? 通过向其添加新的超类型和同级结构,是否有可能使这种层次结构现代化?

是否可以改善不可修改且不可更改的收藏?
当然,添加UnmodifiableListImmutableList并创建上述继承层次结构没有问题。 问题是,从短期和中期来看,它将几乎无用。 让我解释一下。

ImmutableListUnmodifiableListImmutableListList作为类型-在这种情况下,API将能够清楚地表达它们的需求和提供的内容。

 public void payAgents(UnmodifiableList<Agent> agents) { //      , //         } public void sendOnMission(ImmutableList<Agent> agents) { //   ( ), //  ,     } public void downtime(List<Agent> agents) { //       , //        ,      } public UnmodifiableList<Agent> teamRoster() { //   ,     , //      ,     -  } public ImmutableList<Agent> teamOnMission() { //    ,      } public List<Agent> team() { //    ,    , //        } 

但是,除非您从头开始该项目,否则您可能会具有这样的功能,并且它将看起来像这样:

 //   ,  `Iterable<Agent>` //  ,   ,        public void payAgents(List<Agent> agents) { } public void sendOnMission(List<Agent> agents) { } public void downtime(List<Agent> agents) { } //      , //    ,  `List`     public List<Agent> teamRoster() { } // ,     `Stream<Agent>` public List<Agent> teamOnMission() { } public List<Agent> team() { } 

这不好,因为为了使我们刚刚介绍的新系列有用,我们需要与他们合作! (phe) 上面的代码让人想起应用程序代码,因此,重构会ImmutableList UnmodifiableListImmutableList ,您可以实现它,如上面的清单所示。 这可能是很大的工作量,并且在您需要组织旧代码与更新代码的交互时会造成混乱,但至少看来是可行的。

框架,库和JDK本身呢? 这里的一切看起来都黯淡无光。 尝试将参数或返回类型从List更改为ImmutableList会导致与源代码不兼容 ,即 现有的源代码将无法与新版本一起编译,因为这些类型彼此无关。 同样,将返回类型从List更改为新的超类型UnmodifiableList会导致编译错误。

随着新类型的引入,有必要在整个生态系统中进行更改和重新编译。

但是,即使将参数类型从List扩展到UnmodifiableList ,也会遇到问题,因为这样的更改会导致字节码级别的不兼容 。 当源代码调用方法时,编译器将通过以下方式将此调用转换为引用目标方法的字节码:

  • 声明目标实例的类的名称
  • 方法名称
  • 方法参数类型
  • 方法返回类型

方法的参数或返回类型的任何更改都将导致字节码在引用该方法时指示错误的签名。 结果,在执行期间将发生NoSuchMethodError错误。 如果您所做的更改与源代码兼容(例如,如果您正在谈论缩小返回类型或扩展参数的类型),那么重新编译就足够了。 但是,随着影响深远的变化,例如引入新的集合,一切都不那么简单:为了整合这些变化,您需要重新编译整个Java生态系统。 这是丢失的业务。

在不破坏兼容性的情况下利用此类新集合的唯一可能方法是使用新名称复制每个现有方法,更改API,然后将旧版本标记为不受欢迎。 您能想象这样一项任务多么艰巨和几乎无限大吗?

倒影


当然,不可变的集合类型是我很想拥有的一件好事,但是我们不太可能在JDK中看到类似的东西。 ListImmutableList永远不能相互扩展(实际上,它们都扩展了相同的列表类型UnmodifiableList ,这是只读的),这使得很难将此类类型集成到现有的API中。

在类型之间任何特定关系的上下文之外,更改现有方法签名总是充满问题,因为此类更改与字节码不兼容。 引入它们时,至少需要重新编译,这是一个灾难性的变化,将影响整个Java生态系统。

因此,我相信这样的事情将不会发生-永远不会。

Source: https://habr.com/ru/post/zh-CN470149/


All Articles