如何传达插入顺序在地图中的重要性?


24

我正在从数据库中获取一组元组,并将其放入地图中。数据库查询成本很高。

映射中的元素没有明显的自然顺序,但是插入顺序仍然很重要。对地图进行排序将是一项繁重的操作,因此鉴于查询结果已经按照我想要的方式进行了排序,因此我想避免这样做。因此,我只是将查询结果存储到中LinkedHashMap,然后从DAO方法返回地图:

public LinkedHashMap<Key, Value> fetchData()

我有一种方法processData应该在地图上进行一些处理-修改一些值,添加一些新的键/值。定义为

public void processData(LinkedHashMap<Key, Value> data) {...}

但是,一些短毛猫(Sonar等)抱怨说,“数据”的类型应该是诸如“地图”之类的接口,而不是实现“ LinkedHashMap”squid S1319)。
所以基本上说我应该有

public void processData(Map<Key, Value> data) {...}

但是我希望方法签名说映射顺序很重要 -这对算法很重要processData-这样我的方法就不会仅传递任何随机映射。

我不想使用SortedMap,因为它(来自的javadocjava.util.SortedMap)“是根据其键的自然顺序排序的,或者是由通常在排序的地图创建时提供的Comparator来排序的。”

我的键没有自然的顺序,创建比较器不执行任何操作似乎很冗长。

而且我仍然希望它是一个地图,以利用它put来避免重复的键等。如果没有,则data可能是一个List<Map.Entry<Key, Value>>

那么我怎么说我的方法想要一个已经排序的地图?可悲的是,没有java.util.LinkedMap接口,否则我会使用它。

Answers:


56

所以用LinkedHashMap

是的,您应该尽可能使用Map特定的实现,是的,这最佳实践。

就是说,这是一个奇怪的特定情况,Map实际执行很重要。当您使用时Map,对于代码中99.9%的情况,这不是正确的,但是在这种情况下,您的情况就是在这种0.1%的情况下。Sonar不知道这一点,因此Sonar只是告诉您避免使用特定的实现,因为在大多数情况下它是正确的。

我会争辩说,如果您可以为使用特定实现提供理由,请不要尝试将口红涂在猪身上。您需要一个LinkedHashMap,而不是一个Map

这就是说,如果您是编程的新手,并且迷失了这个答案,请不要认为这会违反最佳实践,因为事实并非如此。但是,当不接受将一种实现替换为另一种实现时,您唯一可以做的就是使用该特定实现,并且该死于Sonar。


1
我喜欢实用的方法。
Vidar S. Ramdal

20
我几乎完全同意这个答案。我只是说你不该死在声纳上。您始终可以将其配置为忽略该特定错误/警告。见stackoverflow.com/questions/10971968/...
弗拉基米尔Stokic

11
if you are new to programming and stumble upon this answer, don't think this allows you to go against best practice because it doesn't.-好的建议,如果有“最佳实践”之类的话。更好的建议:学习如何做出正确的决定。 如果可行,请遵循该练习,但要让工具和权限指导您的思考过程,而不是命令它。
罗伯特·哈维

13
注意:当声纳向您报告某事时,您可以将其关闭为“将无法解决”,并留下注释,说明您为什么不解决。因此,不仅声纳将停止打扰您,而且您还将追踪为什么这样做。
Walfrat

2
我认为使这成为一般原则的例外的方面是LinkedHashMap具有特定于该实现的契约,并且未在任何接口中表示。这不是通常的情况。因此,表达对合同的依赖的唯一方法是使用实​​现类型。
Dana

21

您正在对抗三件事:

首先是Java的容器库。分类法中没有任何方法可以确定类是否以可预测的顺序进行迭代。没有IteratesInInsertedOrderMap可以实现的接口LinkedHashMap,这使得类型检查(以及使用行为相同的替代实现)变得不可能。那可能是设计使然,因为它的实质是您真的应该能够处理行为像抽象的对象Map

第二个信念是,您的短毛猫所说的必须被视为福音,而无视其所说的话是不好的。与这些天来的良好做法相反,棉绒警告不应该成为良好代码的障碍。他们会提示您对您编写的代码进行推理,并根据您的经验和判断来确定警告是否合理。不合理的警告是为什么几乎每个静态分析工具都提供一种机制来告知您您已经检查了代码,认为自己在做的事情还可以,并且他们将来不应该抱怨它。

第三,这可能就是它的实质,LinkedHashMap可能是完成这项工作的错误工具。映射旨在用于随机访问,而非有序访问。如果processData()只是简单地按顺序遍历记录,而不必按键查找其他记录,则您将强制执行的特定实现Map来完成的工作List。另一方面,如果您确实需要两者,LinkedHashMap则它是正确的工具,因为众所周知,它可以做您想要的事情,而您在要求它方面是很合理的。


2
“ LinkedHashMap可能是该工作的错误工具”。也许吧。当我说我需要一个时OrderedMap,我也可以说UniqueList。只要是具有定义的迭代顺序的某种集合,就会覆盖插入时的重复项。
Vidar S. Ramdal

2
@ VidarS.Ramdal数据库查询将是清除重复项的理想位置。如果您的数据库无法做到这一点,那么Set在构建列表时,您始终可以仅保留关键字的临时名称,以发现它们。
Blrfl

哦,我知道我引起了混乱。是的,数据库查询结果不包含重复项。但是processData修改地图,替换一些值,引入一些新的键/值。因此processData,如果它在以外的其他设备上运行,可能会引入重复项Map
Vidar S. Ramdal

7
@ VidarS.Ramdal:听起来您需要编写自己的UniqueList(或OrderedUniqueList)并使用它。这非常容易,并且可以使您的预期用途更加明确。
TMN

2
@TMN是的,我已经开始朝这个方向思考。如果您想发表您的建议作为答案,那肯定会得到我的支持。
Vidar S. Ramdal

15

如果您得到的LinkedHashMap仅仅是覆盖重复项的功能,但是您确实将其用作List,那么我建议最好将这种用法与您自己的自定义List实现进行交流。您可以将其基于现有Java集合类,并简单地重写any addremove方法来更新后备存储并跟踪密钥以确保唯一性。给它一个独特的名称,例如,ProcessingList将使您清楚地知道,提供给您processData方法的参数需要以特定方式处理。


5
无论如何,这可能是个好主意。哎呀,您甚至可以创建一个单行文件ProcessingList作为其别名LinkedHashMap-您只要以后保持公共接口完整,就可以随时决定将其替换为其他文件。
CompuChip

11

我听到您说“我的系统的一部分生成了LinkedHashMap,而在系统的另一部分中,我只需要接受由第一部分生成的LinkedHashMap对象,因为由其他进程生成的对象将可以。”不能正常工作。”

这使我认为这里的问题实际上是您尝试使用LinkedHashMap,因为它主要适合您要查找的数据,但是实际上,除了您创建的实例之外,它不能用任何其他实例替代。您真正想做的是创建自己的接口/类,这是您的第一部分创建而第二部分使用的。它可以包装“真实的” LinkedHashMap,并提供Map getter或实现Map接口。

这与CandiedOrange的回答有些不同,我建议封装实际Map(并根据需要委派调用),而不是扩展它。有时它是那些风格的圣战之一,但对我来说肯定不是“带有更多其他内容的地图”,而是“我的有用状态信息包,我可能会在内部用地图表示”。

如果您有两个这样需要传递的变量,则可能无需考虑太多就可以为它创建一个类。但是有时即使只有一个成员变量,拥有一个类也很有用,因为这在逻辑上是同一件事,而不是“值”,而是“我以后需要做的事情的结果”。


我喜欢这种想法-我去过那里:) MyBagOfUsefulInformation需要一个方法(或构造函数)来填充它:MyBagOfUsefulInformation.populate(SomeType data)。但是data将需要是排序的查询结果。那么SomeType,如果不是的话,那将是什么LinkedHashMap呢?我不知道我能够打破这一抓22
王庙S. Ramdal

为什么不能MyBagOfUsefulInformation由DAO创建或在系统中生成任何数据?为什么您需要将基础映射完全暴露给Bag的生产者和消费者之外的其余代码?

根据您的体系结构,您可以使用私有/受保护/仅打包的构造函数来强制该对象只能由您想要的生产者创建。或者您可能只需要按照惯例进行操作,即只能由正确的“工厂”创建它。

是的,通过将MyBagOfUsefulInformation参数作为参数传递给DAO方法,我最终做了一些类似的事情:softwareengineering.stackexchange.com/a/360079/52573
Vidar S. Ramdal,2017年

4

LinkedHashMap是您要查找的唯一具有插入顺序功能的Java映射。因此,放弃依赖倒置原则是很诱人的,甚至是可行的。但是首先,请考虑遵循它。这是SOLID会要求您执行的操作。

注意:将名称替换Ramdal为描述性名称,以表明该接口的使用者是该接口的所有者。这使它决定插入顺序是否重要。如果您只是这样称呼,InsertionOrderMap您真的错过了重点。

public interface Ramdal {
    //ISP asks for just the methods that processData() actually uses.
    ...
}

public class RamdalLinkedHashMap extends LinkedHashMap implements Ramdal{} 

Ramdal<Key, Value> ramdal = new RamdalLinkedHashMap<>();

ramdal.put(key1, value1);
ramdal.put(key2, value2);

processData(ramdal);

这是一个大设计吗?也许取决于您是否认为您还需要实现的可能性LinkedHashMap。但是,如果您不仅仅因为这会带来巨大的痛苦而遵循DIP,我认为没有比这更痛苦的了。这是我希望不可触及的代码实现的接口所不希望使用的模式。实际上,最痛苦的部分是想起好名字。


2
我喜欢命名!
Vidar S. Ramdal

1

感谢您的许多好建议和深思熟虑。

我最终扩展了创建一个新的地图类,并创建了processData一个实例方法:

class DataMap extends LinkedHashMap<Key, Value> {

   processData();

}

然后,我重构了DAO方法,以便它不返回地图,而是将target地图作为参数:

public void fetchData(Map<Key, Value> target) {
  ...
  // for each result row
  target.put(key, value);
}

因此,填充DataMap和处理数据现在是一个两步过程,这很好,因为算法中还包含其他变量,这些变量来自其他地方。

public DataMap fetchDataMap() {
  var dataMap = new DataMap();
  dao.fetchData(dataMap);
  return dataMap;
}

这样,我的Map实现即可控制如何将条目插入其中,并隐藏了订购要求-现在是的实现细节DataMap


0

如果您想传达使用的数据结构是有原因的,请在方法签名上方添加注释。如果将来有其他开发人员遇到此代码行并注意到工具警告,则他们可能也会注意到该注释,并避免“解决”该问题。如果没有评论,那么什么也不会阻止他们更改签名。

在我看来,抑制警告不如发表评论,因为抑制本身并没有说明警告被抑制的原因。也可以将警告抑制和评论结合起来使用。


0

因此,让我尝试在此处了解您的情况:

...插入顺序很重要...对地图进行排序将是一项繁重的工作...

...查询结果已经按照我想要的方式排序

现在,您当前正在执行的操作:

我正在从数据库中获取一组元组,并将其放入地图中...

这是您当前的代码:

public void processData(LinkedHashMap<Key, Value> data) {...}

我的建议是执行以下操作:

  • 使用依赖项注入并将一些MyTupleRepository注入到处理方法中(MyTupleRepository是由通常从数据库中检索元组对象的对象实现的接口);
  • 在处理方法的内部,将来自存储库(又称DB,已返回有序数据)的数据放入特定的LinkedHashMap集合中,因为这是处理算法的内部细节(因为它取决于数据在数据结构中的排列方式) );
  • 请注意,这几乎是您已经在做的事情,但是在这种情况下,可以在处理方法内完成。您的存储库在其他地方实例化(您已经有一个返回数据的类,此示例中为存储库)

代码示例

public interface MyTupleRepository {
    Collection<MyTuple> GetAll();
}

//Concrete implementation of data access object, that retrieves 
//your tuples from DB; this data is already ordered by the query
public class DbMyTupleRepository implements MyTupleRepository { }

//Injects some abstraction of repository into the processing method,
//but make it clear that some exception might be thrown if data is not
//arranged in some specific way you need
public void processData(MyTupleRepository tupleRepo) throws DataNotOrderedException {

    LinkedHashMap<Key, Value> data = new LinkedHashMap<Key, Value>();

    //Represents the query to DB, that already returns ordered data
    Collection<MyTuple> myTuples = tupleRepo.GetAll();

    //Optional: this would throw some exception if data is not ordered 
    Validate(myTuples);

    for (MyTupleData t : myTuples) {
        data.put(t.key, t.value);
    }

    //Perform the processing using LinkedHashMap...
    ...
}

我想这将摆脱声纳警告,并在签名中指定处理方法所需数据的特定布局。


嗯,但是如何实例化存储库?这难道不只是将问题转移到其他地方(MyTupleRepository创建的地方?)
Vidar S. Ramdal

我想我会遇到与Peter Cooper的答案相同的问题。
Vidar S. Ramdal

我的建议涉及应用依赖注入原理。在这个例子中 MyTupleRepository是一个接口,该接口定义了检索您提到的元组(查询数据库)的功能。在这里,您将此对象注入到处理方法中。您已经有一些返回数据的类。这仅在接口中对其进行抽象,然后将对象注入到'processData'方法中,该方法在内部使用LinkedHashMap,因为这本质上是处理的一部分。
艾默生·卡多佐

我编辑了答案,试图更加清楚我的建议。
艾默生·卡多佐

-1

这个问题实际上是将您的数据模型汇总为一堆的问题。您需要一次解开它们。当您尝试简化每个难题时,更自然,更直观的解决方案将会消失。

问题1:您不能依赖数据库顺序

您对数据排序的描述不清楚。

  • 最大的潜在问题是您没有通过ORDER BY子句在数据库中指定显式排序。如果不是因为它看起来太昂贵,则说明您的程序存在错误。如果您不指定结果,则允许数据库以任何顺序返回结果;您不能仅仅因为您多次运行查询并且看起来就那样而依赖于它按顺序同时返回数据。顺序可能会更改,因为行在磁盘上进行了重新排列,或者某些行被删除并且新的行取代了它们,或者添加了索引。您必须指定某种ORDER BY子句。没有正确性,速度是毫无价值的。
  • 您也不清楚插入顺序的含义。如果您在谈论数据库本身,则必须有一个实际跟踪此列的列,并且它必须包含在您的ORDER BY子句中。否则,您将遇到错误。如果这样的列还不存在,那么您需要添加一个。像这样的列的典型选项是插入时间戳记列或自动递增键。自动递增密钥更可靠。

问题2:提高内存排序效率

一旦你确定它保证在你期望的那样,你可以利用这个事实在内存中进行的顺序来返回的数据进行排序很多更有效率。只需在查询的结果集中添加row_number()dense_rank()列(或数据库的等效项)即可。现在,每一行都有一个索引,可以直接指示顺序应该是什么,您可以在内存中按此顺序对其进行排序。只要确保为索引指定一个有意义的名称即可(例如sortedBySomethingIndex)。

中提琴 现在,您不必再依赖数据库结果集的顺序了。

问题3:您是否甚至需要在代码中执行此处理?

SQL实际上非常强大。这是一种了不起的声明性语言,可让您对数据进行大量转换和聚合。如今,大多数DB甚至都支持跨行操作。它们被称为窗口或分析函数:

您是否还需要像这样将数据拉入内存?还是可以通过使用窗口函数来完成SQL查询中的所有工作?如果您可以在数据库中完成所有(或什至只是很大一部分)工作,那就太好了!您的代码问题消失了(或更简单)!

问题4:您在做什么 data

假设您无法在数据库中完成所有操作,那么让我直接讲一下。你把数据作为地图(由你键入的东西不要在想排序),那么你迭代在它插入顺序,并通过更换一些键的值,并添加修改的地方地图新的?

对不起,这到底是什么?

呼叫者不必为此担心。您创建的系统非常棒脆弱。只需要犯一个愚蠢的错误(甚至像我们所做的那样,甚至是您自己做的)就可以做出一个小的错误更改,整个过程像一副纸牌一样崩溃。

这是一个更好的主意:

  • 让您的功能接受 List
  • 有几种方法可以解决订购问题。
    1. 快速应用失败。如果列表未按功能要求的顺序抛出错误。(注意:您可以使用问题2中的排序索引来确定它是否是。)
    2. 自己创建一个排序的副本(再次使用问题2的索引)。
    3. 找出一种顺序构建地图本身的方法。
  • 在函数内部构造您需要的映射,因此调用者不必关心它。
  • 现在,遍历任何具有顺序表示的内容,然后执行必须要做的事情。
  • 返回地图,或将其转换为适当的返回值

一种可能的变化是构造一个排序的表示形式,然后创建要索引的键的映射。这样一来,您就可以修改已排序的副本,而不会意外创建重复副本。

也许这更有意义:摆脱data参数,而processData实际上获取其自己的数据。然后,您可以记录正在执行的操作,因为它对数据的获取方式有非常特定的要求。换句话说,使函数成为自己的整个过程,而不仅仅是整个过程。相互依赖性太强,无法将逻辑拆分成较小的块。(在过程中更改函数的名称。)

也许这些都不适合您的情况。没有问题的全部细节,我也不知道。但是当我听到一个设计时,我确实知道它是一种脆弱而令人困惑的设计。

摘要

我认为这里的问题最终在于细节所在。当我开始遇到这样的麻烦时,通常是因为我没有正确表示我要解决的问题的数据。最好的解决方案是找到一个更好的表示形式,然后我的问题就变得很简单(也许不容易,但很简单)要解决。

寻找一个知道这一点的人:您的工作是将您的问题简化为一系列简单明了的问题。然后,您可以构建健壮,直观的代码。跟他们讲话。好的代码和好的设计会让您认为任何白痴都可以想到它们,因为它们既简单又直接。也许有一位资深的开发人员具有您可以与之交谈的心态。


“什么意思是没有自然顺序,但是插入顺序很重要?您是说将数据插入数据库表中的顺序是重要的,但是没有列可以告诉您插入什么顺序的东西吗?” 问题是这样的:“对地图进行排序将是一项繁重的操作,因此鉴于查询结果已经排序,因此我想避免这样做”。这清楚地意味着可计算的明确的顺序的数据,因为否则排序就不可能,而不是重,但该定义的次序是对键的自然顺序不同。
Jules

2
换句话说,OP正在处理类似的查询结果select key, value from table where ... order by othercolumn,并且需要在处理过程中保持顺序。该插入顺序他们指的是插入顺序进入他们的地图,在他们的查询,而不是使用的顺序定义的插入顺序到数据库。通过使用LinkedHashMap,可以清楚地看出这一点,它是具有键和值对的a Map和a 特征的数据结构List
Jules

@Jules,我会整理一下该部分,谢谢。(我实际上记得曾经读过这篇文章,但是当我在编写问题时检查内容时,我找不到它。查询以及它们是否具有显式排序。他们还说“插入顺序很重要”。关键是,即使排序很繁琐,如果您不明确地告诉数据库,您也不能依靠数据库来魔术地正确排序。而且,如果要在数据库执行此操作,则可以使用“索引”使其在代码中高效。
jpmc26

*写出答案(认为我应该尽快上床睡觉。)
jpmc26

是的,@ Jules是正确的。有一个order by在查询子句,但它是不平凡的(只是order by column),所以我想避免重新实现Java中的排序。虽然SQL 功能强大的(我们在这里谈论一个Oracle 11g数据库),在本质processData算法使得它更容易在Java中表达。是的,“插入顺序”是指“ 地图插入顺序”,即查询结果顺序。
Vidar S. Ramdal
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.