使用列表参数作为 return 值多次调用 void 方法优于 return 列表的方法?

Multiple calls to a void method using list parameter as return value is better than a method that return a List?

简而言之,我的问题是:如果一个方法被多次调用,那么就内存消耗而言,将其设为 void 并使用 是否更好?列出 作为 return 其值的参数?万一真的省内存,那岂不是代码更难阅读的坏习惯?

让我举个例子来说明一下。假设我有一辆 class Car 并且每辆车必须属于一个 品牌 。我有一个方法 returns 品牌列表中的所有汽车,这个方法使用 foreach 和一个从一个品牌检索所有汽车的方法。喜欢下面的代码:

private List<Car> getCarsByBrands(List<Brand> brands) {
    List<Car> result = new Arraylist<>;
    for(Brand brand : brands) {
        result.add(getCarsBySingleBrand(brand))
    }
    return result;
}

private List<Car> getCarsBySingleBrand(Brand brand) {
    List<Car> result = new Arraylist<>;
    result.add(retrieveCarsByBrand(brand)) // retrieveCarsByBrand omitted
    return result;
}

我的一位同事辩护说 getCarsBySingleBrand 方法应该重写为 void 并使用 List 作为参数,此列表将包含所有汽车,如下所示:

private List<Car> getCarsByBrands(List<Brand> brands) {
    List<Car> result = new Arraylist<>;
    for(Brand brand : brands) {
        getCarsBySingleBrand(result, brand))
    }
    return result;
}

private void getCarsBySingleBrand(List<Car> cars, Brand brand) {
    cars.add(retrieveCarsByBrand(brand)) // retrieveCarsByBrand omitted
}

他认为这种方式消耗的内存更少,因为他不会在每次调用方法 getCarsBySingleBrand 时都创建一个列表。我认为这是一种不必要的优化,是一种不好的做法,因为代码更难理解。

他可能是对的。

让我们解释一下为什么

因此,在第一个示例中,您在 getCarsBySingleBrand 方法中为每个品牌创建新列表,并且您必须在结果列表中添加新创建列表中的汽车getCarsByBrands

result.add(getCarsBySingleBrand(brand))

在这一行中,您只需从 getCarsBySingleBrand 获取列表并将所有这些元素添加到结果中。

但在第二种情况下,您直接将汽车 添加到 getCarsByBrands list ,所以你做的操作更少。

此外,第一个示例消耗的内存略多一点是正确的。但我认为这里更多的问题只是操作,而不是内存。

结论

在我看来,第二种方法并不是一种糟糕的风格。因此,如果需要,您可以进行此类优化。

第二个选项可能更优化。

问题是第一种方法不仅需要为每个品牌制作另一个 List 对象,然后将其丢弃,但如果一个品牌有很多汽车,则 Java 将多次调整列表大小(初始化为默认大小 16)。调整大小操作需要复制数组。如果您多次调整大小,这可能会变得昂贵。

第二个选项只有一个列表,由于调整大小通常会使容量翻倍,因此它应该比第一个选项调整大小的次数更少。

但是,这将进入微优化。我不会担心这种事情,除非您注意到性能问题并进行分析以确定这是一个瓶颈。

如果您担心方法名称,我认为名称中包含 "get" 一词会使您失望,因为它通常意味着返回某些内容。将其命名为 addBrandOfCarsTo() 可能会使它读起来更像一个句子:

for(Brand brand : brands) {
    addBrandOfCarsTo(result, brand));
}

两种选择都不错,主要取决于您的喜好:

  • 有些人喜欢清晰(每个人都有自己的风格)。
  • 其他人更喜欢性能,无论它有多小。

如果您遇到这种情况,您也可以直接调用 retrieveCarsByBrand 并使用其结果:

private List<Car> getCarsByBrands(List<Brand> brands) {
    List<Car> result = new Arraylist<>;
    for(Brand brand : brands) {
        result.add(retrieveCarsByBrand(brand))
    }
    return result;
}

或简化getCarsBySingleBrand并使用其结果:

private List<Car> getCarsByBrands(List<Brand> brands) {
    List<Car> result = new Arraylist<>;
    for(Brand brand : brands) {
        result.add(retrieveCarsByBrand(brand));
    }
    return result;
}

private List<Car> getCarsBySingleBrand(Brand brand) {
    return retrieveCarsByBrand(brand);
}

或者在清晰度和性能之间做出妥协,更改 getCarsBySingleBrand 的名称:

private List<Car> getCarsByBrands(List<Brand> brands) {
    List<Car> result = new Arraylist<>;
    for(Brand brand : brands) {
        getCarsBySingleBrand(result, brand))
    }
    return result;
}

private void addBrandCars(List<Car> cars, Brand brand) {
    cars.add(retrieveCarsByBrand(brand)) // retrieveCarsByBrand omitted


}

正如@dkatzel 所说,第二种选择可能更好。但这只是因为在第一个选项中,在方法 getCarsBySingleBrand(Brand brand) 中,您不必要地将 retrieveCarsByBrand(brand) 结果复制到一个全新的 ArrayList。这意味着在方法 getCarsBySingleBrand(Brand brand) returns:

之前存在 3 个不同的列表
  • 持有最终结果的那个
  • 包含一个品牌汽车的子列表,return编辑者retrieveCarsByBrand(brand)
  • 您用来复制 return 由 retrieveCarsByBrand(brand)
  • 编辑的列表的新 ArrayList

如果按照@Narkha 的建议,在第一个选项中,您通过不将 retrieveCarsByBrand(brand) 编辑的 return 汽车列表复制到新的 getCarsBySingleBrand(brand) 来简化方法 getCarsBySingleBrand(brand) =14=],或者只是内联 retrieveCarsByBrand(brand) 方法,那么 两个选项在内存消耗方面是相同的 。这是因为在这两种方法中,为单个品牌检索的汽车列表将被复制到包含每个品牌最终结果的列表中。这意味着只会同时存在 2 个列表:

  • 持有最终结果的那个
  • 包含一个品牌汽车的子列表,return编辑者retrieveCarsByBrand(brand)

正如其他人所说,几乎不需要这种优化。但是,如果您使用 Guava(您应该!),您应该知道有一种更优化的方法可以做到这一点。

private Iterable<Car> getCarsByBrands(List<Brand> brands) {
    Iterable<Iterable<Car>> carsGroupedByBrand = new Arraylist<>();
    for(Brand brand : brands) {
        carsGroupedByBrand.add(retrieveCarsByBrand(brand));
    }
    return Iterables.concat(carsGroupedByBrand);
}

此解决方案存储汽车子列表列表,其中每个子列表包含每个品牌的汽车,并在最后装饰汽车子列表列表为单个Iterable的汽车。如果你真的,真的需要return一个List而不是一个Iterable,那么你可以修改最后一行如下:

    return Lists.newArrayList(Iterables.concat(carsGroupedByBrand));

这里的关键词是decorates,意思是不执行任何子列表的实际复制。相反,创建了一个特殊用途的迭代器,它遍历第一个子列表的每个元素,当它到达终点时,它自动透明地 'jumps' 到第二个子列表的第一个元素,直到它到达终点,依此类推,直到到达最后一个子列表的最后一个元素。详情请参考 Guava 的 Iterables.concat(...) method documentation

我猜你可以同时使用两个世界。 第二个的唯一优点是它消耗更少的内存。如前所述,使用函数式方法而不是 void 方法会增加清晰度,并且有助于单元测试(您可以模拟返回值)。 以下是我的做法:

private List<Car> getCarsByBrands(List<Brand> brands) {
    List<Car> result = new Arraylist<>;
    for(Brand brand : brands) {
        getCarsBySingleBrand(result, brand))
    }
    return result;
}

private List<Car> addBrandCars(List<Car> cars, Brand brand) {
    cars.add(retrieveCarsByBrand(brand)) // retrieveCarsByBrand omitted
    return cars;
}