当前位置: 首页 > 知识库问答 >
问题:

在字节码级别上理解Java8流

樊熠彤
2023-03-14

为了使之具体化,请考虑下面的示例,在这里我需要查找所有名称中包含“fish”一词的鱼,然后将每个匹配的鱼的第一个字母大写。(是的,我知道哈格鱼不是真的鱼,但我没有匹配的鱼名了。)

List<String> fishList = Arrays.asList("catfish", "hagfish", "salmon", "tuna", "blowfish");

// Pre Java-8 solution
List<String> hasFishList = new ArrayList<String>();

for (String fish : fishList) {
    if (fish.contains("fish")) {
        String fishCap = fish.substring(0, 1).toUpperCase() + fish.substring(1); 
        hasFishList.add(fishCap);
    }
}

// Java-8 solution using streams
List<String> hasFishList = fishList.stream()
    .filter(f -> f.contains("fish"))
    .map(f -> f.substring(0, 1).toUpperCase() + f.substring(1))
    .collect(Collectors.toList());

对于这两种方法在字节码级别的本质上可能存在的差异,您可能有的任何洞察力都是很好的。一些实际的字节代码就更好了。

共有1个答案

宗政英才
2023-03-14

随着时间的推移,答案已经变得相当多了,所以我将以一个总结开始:

  • API真正执行的流的跟踪乍一看很吓人。许多调用和对象创建。但是,请注意,为集合中的所有元素重复的唯一部分是do-while循环的主体。因此,除了一些常量开销之外,每个元素的开销是~6个虚拟方法调用(invokeinterface指令-我们的2个lambdas和4个accept()对接收器的调用)。
  • 给流API调用的lambdas被转换为包含实现和invokedynamic指令的静态方法。它不是创建一个新对象,而是给出了如何在运行时创建lambda的处方。之后对创建的lambda对象调用lambda方法没有什么特别之处(invokeinterface指令)。
  • 您可以观察流是如何被懒洋洋地评估的。filter()map()将它们的操作包装在statelessop的匿名子类中,这些子类又扩展了referencePipelineabstractpipelinebasestream。实际计算是在执行collection()时完成的。
  • 您可以看到流如何真正使用spliterator而不是iterator。注意检查isParallel()的许多分叉-并行分支将利用spliterator的方法。
  • 创建的新对象相当多,至少有13个。如果在循环中调用此类代码,可能会遇到垃圾收集问题。对于一次执行,应该没有问题。
  • 我希望看到两个版本的基准比较。Streams版本可能会慢一些,与“Java7版本”的差异会随着鱼的数量的增加而减少。另请参阅相关的SO问题。

下面的伪代码通过使用流执行版本捕获跟踪。有关如何读取跟踪的说明,请参阅本文底部。

Stream stream1 = fishList.stream();
    // Collection#stream():
    Spliterator spliterator = fishList.spliterator();
        return Spliterators.spliterator(fishList.a, 0);
            return new ArraySpliterator(fishList, 0);
    return StreamSupport.stream(spliterator, false)
        return new ReferencePipeline.Head(spliterator, StreamOpFlag.fromCharacteristics(spliterator), false)
Predicate fishPredicate = /* new lambda f -> f.contains("fish") */
Stream stream2 = stream1.filter(fishPredicate);
    return new StatelessOp(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SIZED) { /* ... */ }
Function fishFunction = /* new lambda f.substring(0, 1).toUpperCase() + f.substring(1) */
Stream stream3 = stream2.map(fishFunction);
    return new StatelessOp(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) { /* ... */ }
Collector collector = Collectors.toList();
    Supplier supplier = /* new lambda */
    BiConsumer accumulator = /* new lambda */
    BinaryOperator combiner = /* new lambda */
    return new CollectorImpl<>(supplier, accumulator, combiner, CH_ID);
List hasFishList = stream3.collect(collector)
    // ReferencePipeline#StatelessOp#collect(Collector):
    List container;
    if (stream3.isParallel() && /* not executed */) { /* not executed */ }
    else {
    /*>*/TerminalOp terminalOp = ReduceOps.makeRef(collector)
            Supplier supplier = Objects.requireNonNull(collector).supplier();
            BiConsumer accumulator = collector.accumulator();
            BinaryOperator combiner = collector.combiner();
            return new ReduceOp(StreamShape.REFERENCE) { /* ... */ }
    /*>*/container = stream3.evaluate(terminalOp);
            // AbstractPipeline#evaluate(TerminalOp):
            if (linkedOrConsumed) { /* not executed */ }
            linkedOrConsumed = true;
            if (isParallel()) { /* not executed */ }
            else {
            /*>*/Spliterator spliterator2 = sourceSpliterator(terminalOp.getOpFlags())
                    // AbstractPipeline#sourceSpliterator(int):
                    if (sourceStage.sourceSpliterator != null) { /* not executed */ }
                    /* ... */
                    if (isParallel()) { /* not executed */ }
                    return spliterator;
            /*>*/terminalOp.evaluateSequential(stream3, spliterator2);
                    // ReduceOps#ReduceOp#evaluateSequential(PipelineHelper, Spliterator):
                    ReducingSink sink = terminalOp.makeSink()
                        return new ReducingSink()
                    Sink sink = terminalOp.wrapAndCopyInto(sink, spliterator)
                        Sink wrappedSink = wrapSink(sink)
                            // AbstractPipeline#wrapSink(Sink)
                            for (/* executed twice */) { p.opWrapSink(p.previousStage.combinedFlags, sink) }
                                return new Sink.ChainedReference(sink)
                        terminalOp.copyInto(wrappedSink, spliterator);
                            // AbstractPipeline#copyInto()
                            if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
                            /*>*/wrappedSink.begin(spliterator.getExactSizeIfKnown());
                            /*>*/ /* not important */
                            /*>*/supplier.get() // initializes ArrayList
                            /*>*/spliterator.forEachRemaining(wrappedSink)
                                    // Spliterators#ArraySpliterator#foreachRemaining(Consumer):
                                    // ... unimportant code
!!                                  do {
                                    /*>*/action.accept((String)a[i])
                                    } while (++i < hi) // for each fish :)
                            /*>*/wrappedSink.end() // no-op
                            } else { /* not executed */}
                        return sink;
                    return sink.get()
            }
    /*>*/if (collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) { return container; }
    /*>*/else { /* not executed */ }

感叹号指向实际的工作:fishlistspliterator中的do-while循环。下面是do-while循环的更详细跟踪:

do {
/*>*/action.accept((String)a[i])
    if (predicate.test(u)) { downstream.accept(u); }  // predicate is our fishPredicate
        downstream.accept(mapper.apply(u)); // mapper is our fishFunction
            accumulator.accept(u)
                // calls add(u) on resulting ArrayList
} while (++i < hi) // for each fish :)

让我们看看执行的代码的相关部分在字节码中是什么样子的。有趣的是

fishList.stream().filter(f -> f.contains("fish")).map(f -> f.substring(0, 1).toUpperCase() + f.ubstring(1)).collect(Collectors.toList());

是翻译过来的。你可以在Pastebin上找到完整的版本。在这里,我将只关注
筛选器(f->f.contains(“fish”)):

invokedynamic #26,  0         // InvokeDynamic #0:test:()Ljava/util/function/Predicate; [
    java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    (Ljava/lang/Object;)Z, 
    FishTest.lambda$fish8$0(Ljava/lang/String;)Z, 
    (Ljava/lang/String;)Z
  ]
invokeinterface #27,  2       // InterfaceMethod java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
  
new FishTest$1                        // create new instance of Predicate
dup
invokespecial FishTest$1.<init>()V    // call constructor

字节码的其他部分就不那么有趣了。do-while循环除了明显的循环之外,还包含一个invokeinterface指令,该指令在相应的使用者上调用accept()accept()调用沿着接收器传播,沿途调用lambda。这里没有什么特别之处,lambda调用和通过接收器的传播都是简单的invokeinterface指令。

缩进用于在缩进代码上方显示调用的展开体。使用/*>*/进行代码乞讨表示当前调用的继续(当需要更好的可读性时)。因此调用

Objects.requireNonNull(new Object());

将在跟踪伪代码中写入为:

Object o = new Object(); // extracted variable to improve visibility of new instance creation
Objects.requireNonNull(o);
    // this is the body of Objects.requireNonNull():
    if (o == null) {
    /*>*/throw new NullPointerException(); // this line is still part of  requireNonNull() body
    }
    return o;
 类似资料:
  • 除了基本的读写操作, ByteBuf 还提供了它所包含的数据的修改方法。 随机访问索引 ByteBuf 使用zero-based 的 indexing(从0开始的索引),第一个字节的索引是 0,最后一个字节的索引是 ByteBuf 的 capacity - 1,下面代码是遍历 ByteBuf 的所有字节: Listing 5.6 Access data ByteBuf buffer = ...;

  • 问题内容: 我遇到了一些有关JVM / JIT活动的参考,其中似乎在编译字节码和解释字节码之间有区别。该特定注释声明的字节码在前10000次运行时进行解释,然后进行编译。 “编译”和“解释”字节码之间有什么区别? 问题答案: 解释字节码基本上是逐行读取字节码,不进行任何优化或任何操作,然后对其进行解析并实时执行。由于许多原因,这种方法效率低下,其中包括Java字节码设计得不能快速解释的问题。 编译

  • 我正在使用java 11处理一些依赖项并编译到旧版本。我将一个依赖项迁移到Java11并正常工作,但我们仍然必须在Java8上运行Tomcat 7或8。是否可以使用标志来编译使用,或并在8上运行的代码? 发布标志表明应该可以: --发布版本 针对特定 VM 版本的公共、受支持和记录的 API 进行编译。支持的版本目标为 6、7、8 和 9。 这个项目是一个依赖项,独立运行在SprinBoot2.1

  • 主要内容:1 Java8 Base64编码解码的介绍,2 基本编码和解码,3 URL和文件名的编码解码,4 MIME,5 Base64的内部类,6 Base64的方法,7 Base64.Decoder的方法,8 Base64.Encoder的方法,9 Java Base64案例:基本编码和解码,10 Java Base64案例:URL编码和解码,11 Java Base64案例:MIME编码和解码1 Java8 Base64编码解码的介绍 Java提供了一个Base64类来处理加密。您可以使用提

  • 问题内容: 我经常卡在没有源的Java类文件中,并且试图理解我手头的问题。 请注意,反编译器是有用的,但在所有情况下都不足够… 我有两个问题 有哪些工具可用来查看Java字节码(最好从linux命令行中获得) 什么是熟悉Java字节码语法的良好参考 问题答案: 与其直接查看Java字节码(需要熟悉Java虚拟机及其操作),不如尝试使用Java反编译实用程序。反编译器将尝试从指定文件创建源文件。 该