为了账号安全,请及时绑定邮箱和手机立即绑定

Java流图中可重用的单实例包装器/对象

Java流图中可重用的单实例包装器/对象

潇潇雨雨 2023-01-05 10:09:53
似乎这个问题应该已经有了答案,但我找不到重复的答案。无论如何,我想知道社区如何看待这样的Stream.map用例?Wrapper wrapper = new Wrapper();list.stream()    .map( s -> {        wrapper.setSource(s);        return wrapper;    } )    .forEach( w -> processWrapper(w) );    public static class Source {    private final String name;            public Source(String name) {        this.name = name;    }            public String getName() {        return name;    }}    public static class Wrapper {    private Source source = null;            public void setSource(Source source) {        this.source = source;    }            public String getName() {        return source.getName();    }}public void processWrapper(Wrapper wrapper) {}我不是这种用法的忠实粉丝,map但它可能有助于在处理大型流时提高性能并避免Wrapper为每个Source.这肯定有其局限性,例如对于并行流和终端操作(如collect.更新 - 问题不在于“如何做”,而是“我可以这样做吗”。例如,我可以有一个仅适用于 Wrapper 的代码,我想在其中调用它,forEach但又想避免为每个Source元素创建它的新实例。基准测试结果使用可重复使用的包装纸显示大约8倍的改进-基准 (N) 模式 Cnt 得分误差单位BenchmarkTest.noReuse 10000000 平均 5 870,253 ± 122,495 毫秒/操作BenchmarkTest.withReuse 10000000 平均 5 113.694 ± 2.528 毫秒/操作
查看完整描述

3 回答

?
呼如林

TA贡献1798条经验 获得超3个赞

您的方法恰好有效,因为流管道仅包含无状态操作。在这种情况下,顺序流评估可能一次处理一个元素,因此对包装器实例的访问不会重叠,如此处所示。但请注意,这不是保证的行为。

它绝对不适用于像sorted和这样的有状态操作distinct。它也不能用于归约操作,因为它们总是必须至少保存两个元素进行处理,其中包括reduceminmax。在 的情况下collect,这取决于特定的CollectorforEachOrdered由于需要缓冲,因此不适用于并行流。

请注意,即使您使用TheadLocal创建线程受限包装器,并行处理也会出现问题,因为无法保证在一个工作线程中创建的对象保持在该线程的本地。工作线程可能会在获取另一个不相关的工作负载之前将部分结果移交给另一个线程。

所以这个共享的可变包装器在特定实现的顺序执行中与一组特定的无状态操作一起工作,比如mapfilterforEachfindFirst/Any, 。all/any/noneMatch您无法获得 API 的灵活性,因为您必须限制自己,不能将流传递给期望 a 的任意代码,Stream也不能使用任意Collector实现。您也没有接口的封装,因为您假设了特定的实现行为。

换句话说,如果你想使用这样一个可变的包装器,你最好使用一个实现特定操作的循环。您确实已经有了这种手动实施的缺点,那么为什么不实施它来获得优势呢。


另一个要考虑的方面是,你从重用这样一个可变包装器中得到了什么。它仅适用于类似循环的用法,在这种情况下,临时对象在应用逃逸分析后可能会被优化掉。在这种情况下,重用对象、延长它们的生命周期实际上可能会降低性能。

当然,对象标量化不是一种有保证的行为。可能存在一些场景,例如超过 JVM 内联限制的长流管道,其中对象不会被删除。但是,临时对象并不一定很昂贵。

这已在此答案中进行了解释。临时对象的分配成本很低。垃圾收集的主要成本是由仍然存在的对象引起的。这些需要遍历,并且在为新分配腾出空间时需要移动这些。临时对象的负面影响是它们可能会缩短垃圾收集轮次之间的时间。但这是分配率和可用分配空间的函数,所以这确实是一个可以通过投入更多 RAM 来解决的问题。更多的 RAM 意味着 GC 周期之间的时间更长,GC 发生时死对象更多,这使得 GC 的净成本更小。

尽管如此,避免过度分配临时对象是一个有效的问题。IntStreamLongStream和的存在DoubleStream表明。但这些是特殊的,因为使用原始类型是使用包装器对象的可行替代方案,而且没有重用可变包装器的缺点。它的不同之处还在于它适用于原始类型和包装类型在语义上等同的问题。相反,您想解决操作需要包装器类型的问题。对于原始流也适用,当您需要对象来解决问题时,没有办法绕过装箱,这将为不同的值创建不同的对象,而不是共享可变对象。

因此,如果您同样遇到一个问题,即存在语义等效的包装对象避免替代方案而没有实质性问题,例如在可行Comparator.comparingInt的情况下使用而不是Comparator.comparing,您可能仍然会喜欢它。但只有那时。


简而言之,大多数时候,这种对象重用的节省(如果有的话)并不能证明其缺点。在特殊情况下,在有益且重要的情况下,使用循环或完全控制的任何其他构造可能会更好,而不是使用Stream.


查看完整回答
反对 回复 2023-01-05
?
米脂

TA贡献1836条经验 获得超3个赞

你可以有一些方便的功能,也可以有线程安全的版本来并行工作。


Function<T,U> threadSafeReusableWrapper(Supplier<U> newWrapperInstanceFn, BiConsumer<U,T> wrapFn) {

   final ThreadLocal<T> wrapperStorage = ThreadLocal.withInitial(newWrapperInstanceFn);

   return item -> {

      T wrapper = wrapperStorage.get();

      wrapFn.consume(wrapper, item);

      return wrapper;

   }

}


Function<T,U> reusableWrapper(U wrapper, BiConsumer<U,T> wrapFn) {

   return item -> {

      wrapFn.consume(wrapper, item);

      return wrapper;

   };

}


list.stream()

    .map(reusableWrapper(new Wrapper(), Wrapper::setSource))

    .forEach( w -> processWrapper(w) );

list.stream()

    .map(threadSafeReusableWrapper(Wrapper::new, Wrapper::setSource))

     .parallel()

    .forEach( w -> processWrapper(w) );

但是,我认为不值得。这些包装器是短暂的,所以不太可能离开年轻一代,所以会很快被垃圾收集。不过,我认为这个想法值得用微基准库 JMH检查


查看完整回答
反对 回复 2023-01-05
?
LEATH

TA贡献1936条经验 获得超6个赞

尽管这是可能的,但引用流外的对象会使代码在风格上功能性降低。可以使用辅助函数简单地实现一个封装得更好的非常接近的等价物:


public class Context {


    private static final Wrapper WRAPPER = new Wrapper();


    private static void helper(Source source) {

        WRAPPER.setSource(source);

        processWrapper(WRAPPER);

    }


    public static void main(String[] args) {

        List<Source> list = Arrays.asList(new Source("Foo"), new Source("Baz"), new Source("Bar"));

        list.stream().forEach(Context::helper);

}


查看完整回答
反对 回复 2023-01-05
  • 3 回答
  • 0 关注
  • 85 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信