【问题标题】:How to get an ObservableFloatArray from a Stream?如何从流中获取可观察的浮点数组?
【发布时间】:2014-11-15 22:24:17
【问题描述】:

这与one 的问题几乎相同,但方向相反。

我知道 Java 8 中没有 FloatStream,float[] 的用例也不多,但我有一个:

在JavaFX 3D 中处理TriangleMesh,您必须为整个网格顶点的3D 坐标提供ObservableFloatArray

作为一些计算的结果,我会将所有这些坐标都放在List 中,并将它们一次添加到网格中,我将调用triangleMesh.getPoints().addAll(),使用以下方法之一:

  • addAll(ObservableFloatArray src)
  • addAll(float... 元素)
  • addAll(ObservableFloatArray src, int srcIndex, int length)
  • addAll(float[] src, int srcIndex, int length)

其中ObservableFloatArray 可以使用FXCollections.observableFloatArray()FXCollections.observableFloatArray(ObservableFloatArray array)FXCollections.observableFloatArray(float... values) 创建。

假设我对每个顶点都有这个 pojo:

private class Vertex {
    private final float x;
    private final float y;
    private final float z;

    public Vertex(float x, float y, float z){
        this.x=x; this.y=y; this.z=z;
    }

    public float[] getCoordenates(){
        return new float[]{x,y,z};
    }
}

在执行一些计算后,我得到了List<Vertex> listVertices。我需要生成float[] arrayVertices 才能最终调用triangleMesh.getPoints().addAll(arrayVertices);

现在这就是我正在做的事情:

listVertices.forEach(vertex->triangleMesh.getPoints().addAll(vertex.getCoordenates()));

但这会在添加到可观察数组的每个新顶点上触发关联的侦听器,并且对于大量顶点,这会影响性能。

如果FloatStreamflatMapToFloat() 存在,我会这样做:

float[] arrayVertices = listVertices.stream()
            .map(vertex->FloatStream.of(vertex.getCoordenates()))
            .flatMapToFloat(f->f).toArray();
triangleMesh.getPoints().addAll(arrayVertices);

就像我实际上对面部索引的 int[] 列表所做的那样:

int[] arrayFaces = listFaces.stream()
            .map(face->IntStream.of(face.getFaceIndices()))
            .flatMapToInt(i->i).toArray();
triangleMesh.getFaces().addAll(arrayFaces);

但据我所知,没有办法使用流。

提前感谢任何涉及流的可能解决方案。

【问题讨论】:

    标签: java-8 javafx-8


    【解决方案1】:

    请记住,Stream 定义的是操作而不是存储。因此,对于大多数操作,在使用 CPU 寄存器时,使用 float 仅比 double 值提供一点好处。对于可以使用 SSE 或 GPU 加速的操作,理论上可能会有改进,但这与这里无关。

    因此您可以使用DoubleStream 进行该操作,您唯一需要的是能够将DoubleStream 收集到float[] 数组中的收集器:

    float[] arrayVertices = listVertices.stream()
        .flatMapToDouble(vertex->DoubleStream.of(vertex.x, vertex.y, vertex.z))
        .collect(FaCollector::new, FaCollector::add, FaCollector::join)
        .toArray();
    
    static class FaCollector {
        float[] curr=new float[64];
        int size;
        void add(double d) {
            if(curr.length==size) curr=Arrays.copyOf(curr, size*2);
            curr[size++]=(float)d;
        }
        void join(FaCollector other) {
            if(size+other.size > curr.length)
                curr=Arrays.copyOf(curr, size+other.size);
            System.arraycopy(other.curr, 0, curr, size, other.size);
            size+=other.size;
        }
        float[] toArray() {
            if(size!=curr.length) curr=Arrays.copyOf(curr, size);
            return curr;
        }
    }
    

    这支持并行处理,但是,对于仅包含数据复制的操作,并行处理没有任何好处。

    【讨论】:

    • 这个解决方案似乎更快。但毕竟,您还通过浮点数向curr 添加(和强制转换)浮点数,并进行额外的数组调整大小和复制。即使我增加了数组的初始大小,最小化了副本的数量,它也快了大约 40%(在 Windows 7 64 位 i7、32 GB RAM 上使用 8M 浮点数进行测试)。你认为这是什么原因? DoubleStream?您建议数组的初始大小是多少?
    • 另一个解决方案也必须处理未知数量的项目,但它解决这个问题的方式隐藏在Collectors.toList() 部分。但它会将数据收集到Float 实例中的List 中,然后将整个数据复制到执行拆箱操作的float[] 数组中。没有理想的初始数组大小;这总是一个权衡。但是,您可以将构造函数添加到 FaCollector 以获取 initialSize 提示并将 FaCollector::new 更改为 ()->new FaCollector(hint) 以提供特定于应用程序的提示值,例如listVertices.size()*3 用于顺序收集
    • 非常好。这就是我所说的“但也许我错过了什么”;正如您所说,在随后转换为float[] 之前,它避免了一个结构中的集合(在我的实现中为List<Float>)。不过,我一直在努力验证对并行处理的支持:DoubleStream 是否保证add 相对于join 的原子性?
    • @James_D:是的,这就是Stream.collect 的用途。并发集合将使用 Supplier 为每个线程创建一个实例,因此 accumulator add 方法的并发调用不会干扰(如果它的副作用仅限于它自己的实例)和combiner join 将在两个线程完成其实例时被调用。对于有序流,实现还将确保实例以保持顺序的方式组合。
    • @James_D: 或in the short words as used by the specification: “就像reduce ...,收集操作可以并行化,无需额外同步”另见stackoverflow.com/q/22350288/2711488
    【解决方案2】:

    我认为没有任何办法可以解决您必须创建一个数据结构(例如 double[]List<Float>)然后将其映射到 float[] 的事实。 (但也许我错过了一些东西。)

    如果您想使用类似Stream 的 API 执行此操作,您可以使用Collector 在最后进行映射:

    import java.util.List;
    import java.util.ListIterator;
    import java.util.Random;
    import java.util.stream.Collector;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    import java.util.stream.Stream;
    
    import javafx.scene.shape.TriangleMesh;
    
    public class StreamFloatsTest {
    
        public static void main(String[] args) {
    
            // The following declaration works in Eclipse 4.4
            // however it won't compile from the command line:
            // Collector<Float, List<Float>, float[]> toFloatArray =
    
            // This declaration works:
    
            Collector<Float, ?, float[]> toFloatArray =
                    Collectors.collectingAndThen(Collectors.toList(), floatList -> {
                        float[] array = new float[floatList.size()];
                        for (ListIterator<Float> iterator = floatList.listIterator(); iterator.hasNext();) {
                            array[iterator.nextIndex()] = iterator.next();
                        }
                        return array ;
                    });
    
    
            // Random vertex list for demo purposes:
            Random rng = new Random();
            List<Vertex> vertices = IntStream.range(0, 100)
                    .mapToObj(i -> new Vertex(rng.nextFloat(), rng.nextFloat(), rng.nextFloat()))
                    .collect(Collectors.toList());
    
            float[] vertexArray = vertices.stream()
                    .flatMap(v -> Stream.of(v.getX(), v.getY(), v.getZ()))
                    .collect(toFloatArray);
    
    
            TriangleMesh mesh = new TriangleMesh();
    
            mesh.getPoints().addListener(obs -> System.out.println("mesh invalidated"));
            mesh.getPoints().addListener((array, sizeChanged, from, to) -> System.out.println("mesh changed"));
            mesh.getPoints().addAll(vertexArray);
    
        }
    
        public static class Vertex {
            private final float x ;
            private final float y ;
            private final float z ;
            public Vertex(float x, float y, float z) {
                this.x = x ;
                this.y = y ;
                this.z = z ;
            }
            public float getX() {
                return x;
            }
            public float getY() {
                return y;
            }
            public float getZ() {
                return z;
            }
            public float[] getCoordinates() {
                return new float[] {x, y, z};
            }
        }
    }
    

    【讨论】:

    • 谢谢,@James_D,我已经成功了。唯一的问题是我不能使用Collector&lt;Float, List&lt;Float&gt;, float[]&gt;。它说“collectingAndThen(Collector,Function) 的类型是错误的”。但这有效Collector&lt;Float, ?, float[]&gt;。 (使用 NetBeans 8.0.1、JDK8u25、Windows 7 64 位)
    • 有趣。这在我的 IDE(Eclipse 4.4;JDK 8u40,Mac OS X 10.9.5)中对我来说编译和运行良好 - 但是......除非我在您的评论中进行更改,否则它不会从命令行编译。 (如果我使用 Eclipse 编译它,从命令行运行良好,这不足为奇。)我想我应该将答案更新为从命令行编译的东西,尽管我很想知道差异来自哪里。 (似乎它应该能够在那里正确推断供应商类型,就像在 Eclipse 中一样。)
    • 也许这会给你一个线索:删除返回值,NetBeans 现在抱怨ListIterator&lt;Float&gt;:“ListIterator 无法转换为 ListIterator”。如果我将其更改为ListIterator&lt;Object&gt;,然后投射(float)iterator.next();,它不会抱怨。最后提出的返回值为Collector&lt;Object, ?, float[]&gt;
    • 好吧。唔。我想我现在得出的结论是 Eclipse 在这里实际上是错误的。第二个类型参数是收集器创建的累加器的类型。这实际上是由 Collectors.toList(): 的实现创建的,我想它实际上可以是任何东西(任何类型的缓冲区都可以保存正确的类型)。 Collectors.toList() 返回一个 Collector&lt;T, ?, List&lt;T&gt;&gt;(即它隐藏了关于它用于累加器的实现细节)。所以javac接受的版本是有道理的。
    • 另外,通过使用显式参数,(List&lt;Float&gt; floatList)-&gt; 解决了我提到的问题(Object/Float),我们只能使用Float。但是对于返回值,这不是必需的。现在用 2.6M 顶点和 5M 面对其进行测试......就像一个魅力。谢谢你,詹姆斯。
    猜你喜欢
    • 2020-01-29
    • 1970-01-01
    • 2022-11-12
    • 1970-01-01
    • 1970-01-01
    • 2021-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多