繁体   English   中英

哪个是好的做法 - 在方法中修改列表,或在方法中返回新列表?

[英]Which is good practice - Modifying a List in the method, or returning a new List in the method?

示例代码:

modifyMyList(myList);

public void modifyMyList(List someList){
    someList.add(someObject);
}

或者:

List myList = modifyMyList(myList);

public List modifyMyList(List someList){
    someList.add(someObject)
    return someList;
}

我相信还有第三个选项:您可以在 modifyMyList 方法中创建一个新列表并返回这个新列表...

(第三个选项在这里,我太懒了,但有人已经在答案中添加了它:)

List myList = modifyMyList(myList);

public List modifyMyList(List someList){
    List returnList = new ArrayList();
    returnList.addAll(someList);
    returnList.add(someObject);
    return Collections.unmodifiableList(returnList);
}

有什么理由我应该选择一个而不是另一个? 在这种情况下应该考虑什么?

我有一个(自我强加的)规则,即“永远不要在公共方法中改变方法参数”。 因此,在私有方法中,可以改变参数(我什至也尝试避免这种情况)。 但是在调用公共方法时,参数永远不应该被改变,并且应该被认为是不可变的。

我认为变异方法参数有点hacky,并且可能导致更难看到的错误。

众所周知,我对这条规则有例外,但我需要一个非常好的理由。

实际上没有功能上的区别。

当您想要返回列表时,您会知道其中的区别

List someNewList = someInstnace.modifyMyList(list);

第二个可能令人困惑,因为它意味着正在创建和返回一个新值 - 但事实并非如此。

一个例外是,如果该方法是“流利”API 的一部分,其中该方法是一个实例方法并正在修改其实例,然后返回该实例以允许方法链接:Java StringBuilder类就是一个例子。

但是,总的来说,我不会使用任何一种。

我会选择您的第三个选项:我编写一个方法来创建并返回一个带有适当更改的新列表。 就您的示例而言,这有点人为,因为该示例实际上只是在复制List.add() ,但是...

/** Creates a copy of the list, with val appended. */
public static <T> List<T> modifyMyList(List<T> list, T val) {
    List<T> xs = new ArrayList<T>(list);
    xs.add(val);
    return xs;
}

旁白:我不会,正如 Saket 所建议的那样,返回一个不可变的列表。 他关于不变性和并行性的论点是有效的。 但大多数情况下,Java 程序员希望能够修改集合,除非在特殊情况下。 通过让你的方法返回一个不可变的集合,你将它的可重用性限制在这种情况下。 (如果调用者愿意,他们总是可以使列表不可变:他们知道返回的值是一个副本,不会被其他任何东西触及。)换句话说:Java 不是 Clojure。 此外,如果需要考虑并行性,请查看 Java 8 和流(新类型 - 不是 I/O 流)。

这是一个不同的例子:

/** Returns a copy of a list sans-nulls. */
public static <T> List<T> compact(Iterable<T> it) {
    List<T> xs = new ArrayList<T>();
    for(T x : it)
        if(x!=null) xs.add(x);
    return xs;
}

请注意,我已经通用化了该方法,并使其更广泛地适用于采用Iterable而不是列表。 在实际代码中,我有两个重载版本,一个采用Iterable ,一个采用Iterator (第一个将通过使用可迭代的迭代器调用第二个来实现。)另外,我将其设为静态,因为您的方法没有理由成为实例方法(它不依赖于实例的状态)。

但是,有时,如果我正在编写库代码,并且不清楚变异实现还是非变异实现更普遍有用,我会同时创建两者。 这是一个更完整的例子:

/** Returns a copy of the elements from an Iterable, as a List, sans-nulls. */ 
public static <T> List<T> compact(Iterable<T> it) {
    return compact(it.iterator());
}
public static <T> List<T> compact(Iterator<T> iter) {
    List<T> xs = new ArrayList<T>();
    while(iter.hasNext()) {
        T x = iter.next();
        if(x!=null) xs.add(x);
    }
    return xs;
}

/** In-place, mutating version of compact(). */
public static <T> void compactIn(Iterable<T> it) {
    // Note: for a 'fluent' version of this API, have this return 'it'.
    compactIn(it.iterator());
}
public static <T> void compactIn(Iterator<T> iter) {
    while(iter.hasNext()) {
        T x = iter.next();
        if(x==null) iter.remove();
    }
}

如果这是在真正的 API 中,我会检查 null 的参数并抛出 IllegalArgumentException。 (NOT NullPointerException - 尽管它经常用于此目的。NullPointerException 也因其他原因而发生,例如错误代码。IllegalArgumentException 更适合无效参数。)

(Javadoc 也会比实际代码多!)

第一个和第二个解决方案非常相似,第二个的优点是允许链接。 正如我们在这里看到的那样,“这是一个好的做法”的问题受到了争论: Java 中的方法链

所以真正的问题是在第一个具有可变列表的解决方案和第三个具有不可变列表的解决方案之间,同样,没有唯一的响应,返回不可变的字符串和使用可变但可变的字符串缓冲区之间的争论是一样的允许更好的性能。

如果您需要 API 的可靠性,并且没有性能问题,请使用 immutable(第三种解决方案)。 如果您的列表总是很小,请使用它。

如果您只需要性能,请使用可变列表(第一个解决方案)

我建议在方法中创建一个新列表并返回一个不可变列表。 这样,即使您在不可变列表中传递,您的代码也能正常工作。 当我们通常转向函数式编程并尝试跨多个处理器架构进行扩展时,创建不可变对象通常是一种很好的做法。

List myList = modifyMyList(myList);

public List modifyMyList(List someList){
    List returnList = new ArrayList();
    returnList.addAll(someList);
    returnList.add(someObject);
    return Collections.unmodifiableList(returnList);
}

正如我在其他答案中所说,我认为您不应该改变 list 参数。 但有时您也不想获取原始列表的副本并改变副本。

  • 原始列表可能很大,因此副本很昂贵
  • 您希望副本与原始列表的任何更新保持同步。

在这些情况下,您可以创建一个MergedList ,它是两个(或更多)列表的视图

import java.util.*;

public class MergedList<T> extends AbstractList<T> {
    private final List<T> list1;
    private final List<T> list2;

    public MergedList(List<T> list1, List<T> list2) {
        this.list1 = list1;
        this.list2 = list2;
    }

    @Override
    public Iterator<T> iterator() {
        return new Iterator<T>() {
            Iterator<T> it1 = list1.iterator();
            Iterator<T> it2 = list1.iterator();

            @Override
            public boolean hasNext() {
                return it1.hasNext() || it2.hasNext();
            }

            @Override
            public T next() {
                return it1.hasNext() ? it1.next() : it2.next();
            }
        };
    }

    @Override
    public T get(int index) {
        int size1 = list1.size();
        return index < size1 ? list1.get(index) : list2.get(index - size1);
    }

    @Override
    public int size() {
        return list1.size() + list2.size();
    }
}

你可以做的

public List<String> modifyMyList(List<String> someList){
    return new MergedList(someList, List.of("foo", "bar", "baz"));
}

两种方式都可以,因为在这种情况下,java 使用 List 的引用,但我更喜欢第二种方式,因为这种解决方案也适用于按值传递,而不仅仅是按引用传递。

在功能上两者是相同的。

但是,当您将方法公开为 API 时,第二种方法可能会给人一种印象,即它返回一个新的修改列表,而不是原始传递的列表。

虽然第一种方法会明确(当然基于方法命名约定)它将修改原始列表(相同对象)。

此外,第二种方法返回一个列表,因此理想情况下,即使传递的列表不是空的,调用者也应该检查空返回值(该方法可能返回空而不是修改后的列表)。

考虑到这一点,我通常更喜欢使用方法一。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM