7

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.

Community
  • 1
  • 1
José Pereda
  • 44,311
  • 7
  • 104
  • 132

2 Answers2

7

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 1
    This solution seems faster. But after all, you're also adding (and casting) float by float to `curr` with aditional array resizing and copy. Even if I increase the initial size of the array, minimizing the number of copies, it's around 40% faster (tested with 8M floats on Windows 7 64 bits i7, 32 GB RAM). What do you think is the reason for that? The `DoubleStream`? What initial size for the array do you recommend? – José Pereda Nov 17 '14 at 13:52
  • 3
    The other solution also has to deal with an unknown number of items, but the way it solves this is hidden in the `Collectors.toList()` part. But it collects the data into a `List` of `Float` instances before copying the entire data into a `float[]` array performing an unboxing operation. There is no ideal initial array size; it’s always a trade-off. However, you could add a constructor to `FaCollector` taking an initialSize hint and change the `FaCollector::new` to `()->new FaCollector(hint)` to provide an application-specific hint value,e.g. `listVertices.size()*3` for a sequential collection – Holger Nov 17 '14 at 15:45
  • 1
    Very nice. This is what I was referring to by "But maybe I am missing something"; as you say it avoids the collection in one structure (`List` in the implementation I had) before subsequent conversion to `float[]`. I always struggle with verifying support for parallel processing though: does the `DoubleStream` guarantee atomicity of `add` with respect to `join`? – James_D Nov 19 '14 at 18:40
  • 3
    @James_D: yes, that’s what `Stream.collect` is for. Concurrent collection will use the `Supplier` to create one instance per thread so the concurrent invocations of the *accumulator* `add` method won’t interfere (if its side-effects are restricted to its own instance) and the *combiner* `join` will be invoked when two threads are done with their instances. For ordered streams the implementation will also ensure that the instances are combined in a order maintaining way. – Holger Nov 19 '14 at 20:34
  • 3
    @James_D: Or [in the short words as used by the specification](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#collect-java.util.function.Supplier-java.util.function.BiConsumer-java.util.function.BiConsumer-): “Like reduce…, collect operations can be parallelized without requiring additional synchronization”    See also http://stackoverflow.com/q/22350288/2711488 – Holger Nov 19 '14 at 20:36
  • @Holger, why did you created the collector as a static class? Is there a way to create it as a public class and then reuse in other places? – marionmaiden Mar 15 '19 at 13:33
  • 1
    @marionmaiden when I test examples for Stackoverflow, I usually write everything into a single source file, so additional classes end up being nested classes and apparently, I forgot to remove the `static` when posting the answer. There’s nothing stopping you from re-using the class at other places, though, I’d rather create a factory method returning a `Collector` then, similar to the built-in collectors in the `Collectors` class. Then, you don’t need to export the type which is only used for temporary storage. – Holger Mar 15 '19 at 14:16
  • Ok. What bugs me is that adding the same code to a new java file, removing the static and adding the public access modifier doesn't seems to work... If I add the implements Collector the methods to override are completely different – marionmaiden Mar 15 '19 at 14:29
  • Nevermind. Just forgot to add the public access modifiers on methods as well – marionmaiden Mar 15 '19 at 14:34
  • 1
    @marionmaiden I just remembered that this is specifically for `DoubleStream`, so using the `Collector` interface is not an option here, unless you want to accept the boxing overhead. – Holger Mar 15 '19 at 15:08
4

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};
        }
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • 1
    Thanks, @James_D, I've got it working. The only thing is I can't use `Collector, float[]>`. It says "The type of collectingAndThen(Collector,Function) is erroneous". But this works `Collector`. (Using NetBeans 8.0.1, JDK8u25, Windows 7 64 bits) – José Pereda Nov 16 '14 at 01:50
  • 1
    Interesting. This compiles and runs fine for me in my IDE (Eclipse 4.4; JDK 8u40, Mac OS X 10.9.5) - however... it won't compile from the command line unless I make the change in your comment. (Runs fine from the command line if I use Eclipse to compile it, which is no surprise.) I guess I should update the answer to something that compiles from the command line, though I'd love to know where the discrepancy comes from. (Seems like it should be able to infer the supplier type correctly there, as it does in Eclipse.) – James_D Nov 16 '14 at 02:08
  • 1
    Maybe this gives you a clue: Removing the return value, NetBeans complains now about `ListIterator`: "ListIterator can't be converted to ListIterator". If I change it to `ListIterator`, and then cast `(float)iterator.next();`, it doesn't complain. Finally, the return value proposed is `Collector` – José Pereda Nov 16 '14 at 02:17
  • 1
    Well. Hmm. I guess I'm now coming to the conclusion that Eclipse is actually wrong here. The second type parameter is the type of the accumulator created by the collector. That's something that's actually created by the implementation of `Collectors.toList()`: and I suppose it could really be anything (any kind of buffer holding the correct type would work). `Collectors.toList()` returns a `Collector>` (i.e. it hides the implementation detail about what it uses for an accumulator). So the version accepted by `javac` makes sense. – James_D Nov 16 '14 at 02:26
  • 1
    Also, by using explict parameters, `(List floatList)->` the problem I've mentioned (Object/Float) is solved, and we can use only `Float`. But with a return value, this is not necessary. Testing it now with 2.6M vertices and 5M faces... works like a charm. Thank you, James. – José Pereda Nov 16 '14 at 02:39