How to get an ObservableFloatArray from a Stream?

2019-01-20 00:09发布

问题:

This is nearly the same question as this one, but in the opposite direction.

I know there's no FloatStream in Java 8, and there're no many use cases for float[], but I have one:

Dealing with TriangleMesh in JavaFX 3D, you have to provide an ObservableFloatArray for the 3D coordinates of the vertices of the whole mesh.

As a result of some calculations I'll have all these coordinates in a List, and to add all of them at once to the mesh, I'll call triangleMesh.getPoints().addAll(), using one of the following methods:

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

where ObservableFloatArray can be created using FXCollections.observableFloatArray(), FXCollections.observableFloatArray(ObservableFloatArray array) or FXCollections.observableFloatArray(float... values).

Let's say I have this pojo for each vertex:

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};
    }
}

and after performing some calculations I have List<Vertex> listVertices. I'll need to generate float[] arrayVertices to finally call triangleMesh.getPoints().addAll(arrayVertices);.

For now this is what I'm doing:

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

But this triggers the associated listener on every new vertex added to the observable array, and for high number of vertices this affects performance.

Should FloatStream and flatMapToFloat() exist, I'd do something like this:

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

like I actually do with a list of int[] for face indices:

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

But as far as I know, there's no way using streams.

Thanks in advance for any possible solution involving streams.

回答1:

Keep in mind that a Stream defines an operation rather that a storage. So for most operations, using a float provides only little benefit over double values when CPU registers are used. There might be a theoretical improvement for operations that could be accelerated using SSE or GPU, but that’s not relevant here.

So you can use a DoubleStream for that operation, the only thing you need is a collector capable of collecting a DoubleStream into a float[] array:

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;
    }
}

This supports parallel processing, however, for an operation that merely consist of data copying only, there is no benefit from parallel processing.



回答2:

I don't think there's any way around the fact that you have to create one data structure (e.g. a double[] or a List<Float>) and then map it into a float[]. (But maybe I am missing something.)

If you want to do this using a Stream-like API, you can use a Collector to do the mapping at the end:

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};
        }
    }
}