【问题标题】:Correct way to develop generic modifiers in Java在 Java 中开发泛型修饰符的正确方法
【发布时间】:2014-08-06 01:34:01
【问题描述】:

在 Java 中,我们有这样的函数:

public Collection<String> removeNulls(Collection<String> input) {
    List<String> output = new ArrayList<>();
    // ...
    return output;
}

请注意,我不会以任何方式修改输入,因为很多时候我们碰巧使用ImmutableList 作为列表。更常见的是,我们尽量确保函数的不变性。然而,我在这里看到的一个陷阱是,我所有的方法都经常使用它们自己的Collection 实现。如果有人使用我的库正在使用LinkedListSetSortedSet,那么每当在他们的列表上调用我的函数时,诸如“查找是O(1)”或“列表总是排序”之类的假设就会中断:

Set<String> mySet = new SortedSet<>();
mySet.add(10);
mySet.add(9); // I know that my collection is now sorted
Collection<String> myFilteredSet = removeNulls(mySet); // It is no longer sorted
Set<String> mySet = Sets.newSortedSet(myFilteredSet); // Have to sort it again

如果是LinkedList,这会变得更加微妙,如果我的函数要返回List,那么我将继续使用该列表。它不会有任何编译时错误,我们也不会看到问题。在前一种情况下,问题至少是可见的,因为我仍然必须将返回的 Collection(或 List)转换为 Set,并且我知道发生了什么。这个问题只会在以后出现,如果有一些繁重的处理依赖于 LinkedList 正在使用。一种理想的方式(但这并不意味着“推荐”)是:

public List<String> removeNulls(List<String> input) {
    List<String> output = (List<String>)input.getClass().newInstance();
    // ...
    return output;
}

这样,我确保至少使用相同的类型实现。您认为这个问题的解决方案是什么?

编辑

澄清一下,我实际上没有 removeNulls 方法,这不是我想要的。这个问题与现实生活中的问题无关,而更像是一个“需要思考的问题”。换句话说,我的问题是,鉴于您给了我一个集合,我必须返回另一个以某种方式派生的集合并且不修改输入,我如何确保我的输出集合符合合同(或具体来说,输入集合的实现)?

【问题讨论】:

  • 如果您不能开发更统一的 API,最好将关键假设(尤其是那些您无法以编程方式强制执行的假设)作为文档中的先决条件,因此程序员当他们不正确地使用您的库函数时,没有人可以责怪他们,而是他们自己打破了这一先决条件。在我看来确实存在一些 API 设计问题......
  • 我认为关键是输入/原始集合可能不会更改并且调用者得到一个新列表,所以他最终得到了原始列表和不带空值的新列表,可能是原始列表列表可能和其他地方一样需要。
  • 您可以通过将签名更改为&lt;E, C extends Collection&lt;E&gt;&gt; C removeNulls(C input) 来确保结果集合在语法上与输入集合的类型相同,但您仍然必须通过反射来实例化结果集合。无论如何,真正的问题是您不知道如何正确处理元素。即使在这个简单的例子中,也可以想象一个集合,它在内部将nulls 存储在一个单独的集合中。然后,removeNulls 将以不同的方式实现。集合实现应该决定如何去做。通用解决方案无济于事。

标签: java oop generics collections


【解决方案1】:

我想,你想要的是用操作withoutNulls 修改 Collection 接口,并相应地实现它,但是由于你不能向现有的集合类“添加”新方法,所以你不走运。但是,由于您经常使用自己的集合实现,也许您可​​以扩展您的接口并提供一些方法来将常规集合类转换为您自己的。

类似这样的:

public interface BetterCollection<E> extends Collection<E> {
    BetterCollection<E> withoutNulls();
}

public class BetterLinkedList<E> extends LinkedList<E> implements BetterCollection<E> {
    public BetterLinkedList() {}

    public BetterLinkedList(LinkedList<E> input) {
        super(input);
    }

    @Override
    public BetterCollection<E> withoutNulls() {
        BetterCollection<E> result = new BetterLinkedList<E>();
        ...
        return result;
    }
}

我认为在 C# 中可以使用扩展方法,但是您在实现方面受到了一些限制(例如,您无法访问私有字段并且我不确定它们是否可以是虚拟的)。

Java 中的另一个解决方案是重载方法并让它们接受不同的集合类型,例如 removeNulls(LinkedList&lt;String&gt;)removeNulls(SortedSet&lt;String&gt;),但它显然也有其缺点。

【讨论】:

    【解决方案2】:

    removeNulls 方法是一个非常“简单”的示例,很难想象您可能会使用哪些其他类型的“修饰符”。

    根据您的描述,新的Streams API in Java 8 可能会很好地支持您的实际意图。例如,从Stream 中删除null 元素可以很容易地通过

    // Filter out all null elements
    collection.stream().filter((e) -> e != null).... // Further operations
    

    这里的好处是您的实际问题是在内部处理的:流的characteristics(尽可能)在操作期间保留。例如,之前排序的流将保持排序状态。


    对于 Java-8 之前的版本,处理这种情况的另一种方法是指定输出集合(或至少提供这样做的选项)。例如:您可以将方法编写为

    public <T> Collection<T> removeNulls(Collection<? extends T> input, Collection<T> output)
    {
        if (output == null) 
        {
            // Create some default collection or
            // report an error...
        }
        for (T t : input)
        {
            if (t != null)
            {
                output.add(t);
            }
        }
        return output;
    }
    
    // Convenience method
    public <T> Collection<T> removeNulls(Collection<? extends T> input)
    {
        return removeNulls(input, new ArrayList<T>());
    }
    

    被称为

    Set<String> mySet = new SortedSet<>();
    mySet.add(10);
    mySet.add(9); // I know that my collection is now sorted
    Collection<String> myFilteredSet = removeNulls(mySet, new SortedSet<>());
    

    (注意:这里甚至可以进一步利用类型推断和泛型的力量——这只是为了展示基本思想)


    无论如何,我认为在 JavaDoc 中明确指定任何假设作为前置条件和后置条件是非常重要的,例如

    /** 
     * Creates a new collection with the same contents as the given collection,
     * but without any <code>null</code> entries
     *
     * The order of output elements will be the same as the input elements
     * or alternatively:
     * No assumptions may be made about the order of the output collection
     *
     * @return The filtered collection
     */
    

    【讨论】:

      【解决方案3】:

      简答题使用 Scala。

      更严肃的回答:看看 Scala 集合库,尤其是 BuildFrom

      基本思想是您提供一个能够在可能的情况下创建正确返回集合的工厂。您基于输入参数的类创建集合的想法是朝着正确方向迈出的良好一步。

      完整的 Scala 版本会相当复杂,因为用户必须自己提供正确的 BuildFrom,并且在 98% 的情况下不能依靠 Scalas 隐式魔法来提供正确的 BuildFrom。但是研究它会给你很多想法,你可能会担心什么以及如何解决它。到底要不要做,就看你自己了。

      【讨论】:

        【解决方案4】:

        我认为你的方法需要改变。

        如果您以后需要调整它们,那么创建不可变集合几乎没有意义。在暴露给外界之前(如果有必要),集合应该是不可变的。因此,您应该只对可修改的集合进行操作,并允许您的方法调整参数。

        如果您从其他地方收到不可变集合,请将其转换为可修改集合,然后按照上述方法。

        【讨论】:

          【解决方案5】:

          对于“排序”的属性,根据

          public Collection<String> someFilter(Collection<String> input)
          

          可能会以允许您保证“排序”属性不违反的方式编写,即过滤器是“稳定的”。

          例如,迭代输入集合并将通过过滤器的任何内容存储在 ArrayList 中并返回“稳定”的内容。

          (当然,可能会有你无法保证的转换。)

          另一个属性是保存“对象身份”而不是复制,但这有点深奥;-)

          【讨论】:

          • 虽然这保留了排序后的属性,但它违反了当你放入一个集合时你想要得到一个集合的期望。
          • 我为什么要这样?而且,更重要的是,谁会做出极端的努力来遵守这一点?如果“设置保留”或“列表保留”是有趣的属性 - 很好,并在类型中显示它。但是一般的“收藏类型保留” - 我不知道......
          • “我为什么要这样?”出于同样的原因,人们可能期望排序保留。 “谁会做出极大的努力[..]?” Scala 的人做到了。
          • 我并不是说你的解决方案是错误的或什么的。只是指出限制。
          猜你喜欢
          • 2011-04-13
          • 1970-01-01
          • 2015-08-09
          • 2013-12-15
          • 2022-12-13
          • 1970-01-01
          • 2016-06-22
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多