【问题标题】:Reason not to use a guarded/constrained collection不使用受保护/受约束的集合的原因
【发布时间】:2015-10-23 06:06:41
【问题描述】:

是否有任何理由/论据不实现基于谓词/约束来限制其成员的 Java 集合?

鉴于这种功能应该经常是必需的,我期待它已经在 apache-commons 或 Guava 等集合框架上实现。但是虽然apache indeed had it、Guava deprecated its version of it 并建议不要使用类似的方法。

Collection interface contract 声明一个集合可以对其元素设置任何限制,只要它被正确记录,所以我无法理解为什么不鼓励使用受保护的集合。还有什么其他选择可以确保 Integer 集合在不隐藏整个集合的情况下从不包含负值?

【问题讨论】:

  • @dcsohl 对于Collections.unmodifiable*(...) 集合可以给出相同的论点(尽管有人可能会争论这是否不仅仅是某些特定语言缺陷的暗示)。 @biziclop 有趣的是,add 方法已经允许在 "...元素的某些属性阻止它被添加到此集合" 的情况下抛出 IllegalArgumentException。所以我也没有看到不使用这样一个集合的深刻技术原因。
  • 弃用注释表明Preconditions 是约束的替代品。
  • 这样的List 可能效率低下。例如Collections.swap 将检查这两个项目的谓词,即使它们都已经在List 中。您无法覆盖此行为,因为它是 static。例如,这可能会对Collections.sort 的性能产生重大影响。
  • @biziclop 实际上,add() 在这种情况下更进一步。但是我看不到一种方法可以让我既公开一个漂亮的集合接口又将验证封装在正确的类中。无论如何,我都必须公开类似集合的行为,所以我宁愿利用 Collection/List 接口。
  • @gcscaglia 好吧,如果你愿意,你真的可以。只是无论出于何种原因,Guava 开发人员都没有发现它是一个足够常见的问题(或足够好的解决方案),无法将其保留在他们的库中。

标签: java collections constraints predicate


【解决方案1】:

这只是一个偏好问题 - 查看关于 checking before vs checking after 的线程 - 我认为这就是它归结为的原因。也只检查 add() 我只对不可变对象足够好。

【讨论】:

  • 关于不可变对象的要点。尽管您可以争辩 HashSet 也只能用于不可变对象(或至少具有不可变 hashCode() 的对象),但它是类库的一部分。
  • 哎呀,我真的错过了添加后对象发生变化的可能性。在我的用例中,它的一种可能性。正整数只是一个例子。 Ty 指出来。
【解决方案2】:

几乎不可能有一个(“可接受”)答案,所以我只是补充一些想法:

如 cmets 中所述,Collection#add(E) 已经允许抛出 IllegalArgumentException,原因

如果元素的某些属性阻止它被添加到此集合中

因此可以说,在设计集合接口时明确考虑了这种情况,并且没有明显、深刻、纯技术(接口合同相关)的理由不允许创建这样的集合。


但是,在考虑可能的应用程序模式时,人们很快就会发现这样一个集合的观察行为可能是……至少可以说是违反直觉的。

dcsohl 在 cmets 中已经提到了一个,并提到了这样的集合只是另一个集合上的 视图 的情况:

List<Integer> listWithIntegers = new ArrayList<Integer>();

List<Integer> listWithPositiveIntegers = 
    createView(listWithIntegers, e -> e > 0);

//listWithPositiveIntegers.add(-1); // Would throw IllegalArgumentException
listWithIntegers.add(-1); // Fine

// This would be true:
assert(listWithPositiveIntegers.contains(-1));

但是,有人可能会争辩说

  • 这样的集合不一定必须只是一个视图。相反,可以强制只创建具有此类约束的集合
  • 行为与Collections.unmodifiableCollection(Collection) 的行为相似,这是人们普遍预期的。 (虽然它服务于一个更广泛和无所不在的用例,即通过访问器方法返回一个可修改版本的集合来避免暴露类的内部状态)

但在这种情况下,“不一致”的可能性要高得多。

例如,考虑调用Collection#addAll(Collection)。它还允许抛出IllegalArgumentException“如果指定集合的​​元素的某些属性阻止它被添加到此集合中”。但是不能保证原子性之类的事情。用这种方式表述:当抛出此类异常时,没有指定集合的​​状态。想象这样一个案例:

List<Integer> listWithPositiveIntegers = createList(e -> e > 0);

listWithPositiveIntegers.add(1); // Fine
listWithPositiveIntegers.add(2); // Fine
listWithPositiveIntegers.add(Arrays.asList(3,-4,5)); // Throws

assert(listWithPositiveIntegers.contains(3)); // True or false?
assert(listWithPositiveIntegers.contains(5)); // True or false?

(可能很微妙,但可能是个问题)。

当集合创建后条件发生变化时,所有这一切可能会变得更加棘手(无论它是否只是一个视图)。例如,可以想象这样的一系列调用:

List<Integer> listWithPredicate = create(predicate);
listWithPredicate.add(-1); // Fine 
someMethod();
listWithPredicate.add(-1); // Throws

someMethod()中,有一个像这样的无辜行

predicate.setForbiddingNegatives(true);

其中一个 cmets 已经提到了可能的性能问题。这当然是真的,但我认为这并不是一个真正的strong 技术论点:无论如何,Collection 接口的任何方法的运行时都没有正式的复杂性保证。您不知道collection.add(e) 通话需要多长时间。对于LinkedList,它是 O(1),但对于 TreeSet,它可能是 O(n log n)(谁知道此时 n 是什么)。

也许性能问题和可能的不一致可以被认为是更一般的陈述的特殊情况:

这样的集合将允许在许多操作期间基本上执行任意代码 - 取决于谓词的实现。

这实际上可能具有任意含义,并且无法对算法、性能和确切行为(就一致性而言)进行推理。


底线是:有很多可能理由不使用这样的集合。但我想不出一个强有力的、普遍的技术原因。因此,此类集合可能存在应用案例,但应牢记注意事项,考虑到此类集合打算如何使用

【讨论】:

    【解决方案3】:

    我会说这样的集合会有太多的责任,违反了SRP。

    我在这里看到的主要问题是使用集合的代码的可读性和可维护性。假设您有一个只允许添加正整数 (Collection&lt;Integer&gt;) 的集合,并且您在整个代码中使用它。然后要求发生变化,您只能向其中添加奇数正整数。因为没有编译时检查,所以在代码中找到向该集合添加元素的所有事件要比使用单独的封装该集合的包装类要困难得多。

    虽然当然还没有接近这样一个极端,但它与对应用程序中的所有对象使用 Object 引用有些相似。

    更好的方法是利用编译时检查并遵循成熟的 OOP 原则,如类型安全和封装。这意味着创建一个单独的包装类或为集合元素创建一个单独的类型。

    例如,如果您真的想确保只在上下文中使用正整数,您可以创建一个单独的类型PositiveInteger extends Number,然后将它们添加到Collection&lt;PositiveInteger&gt;。通过这种方式,您可以获得编译时安全性,并且将PositiveInteger 转换为OddPositiveInteger 需要的工作量要少得多。

    枚举是首选专用类型而不是运行时约束值(常量字符串或整数)的一个很好的例子。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-06-12
      • 2011-12-14
      • 2011-06-22
      • 2014-06-26
      • 2023-04-01
      • 2011-08-03
      • 1970-01-01
      相关资源
      最近更新 更多