Generics and streams: How to make this into a `StreamTuple::new` statement?

I am working on a small library which emulates multi-value return values in Streams. For this to work well, I would like to be able to use StreamTuple::new instead of StreamTuple::create or id -> new StreamTuple<>(id, id). I tried various modifications, but my generics-fu is not good enough to figure out how to change the source to allow this.

public class StreamTuple<L, R> {

    protected final L left;
    protected final R right;

    public StreamTuple(L left, R right) {
        this.left = left;
        this.right = right;
    }

    /**
     * Suitable for method::references.  Value is set to id.
     */

    public static <L> StreamTuple<L, L> create(L left) {
        return new StreamTuple<>(left, left);
    }
    ....

Calling code snippet:

    Map<String, String> m = Stream.of("1", "2", "3")
//          .map(StreamTuple::create)
            .map(id -> new StreamTuple<>(id, id))
            .collect(toMap(StreamTuple::left, StreamTuple::right));

Suggestions? Or can't it be done?


EDIT: I would like an additional constructor that only takes one argument and returns a StreamTuple where L and R is the same type. The current code does not reflect that because it was a while ago I couldn't make it work and I thought I replicated it. I can only find out how to provide one of L / R and that leaves the other one open. How to write a single argument constructor for this?

Upvotes: 4

Views: 141

Answers (5)

PaoloC
PaoloC

Reputation: 4115

StramTuple::new will be translated differently based on the FunctionalInterface expected by the accepting method.

expecting Function (T -> R) ==> ::new translates to new StreamTuple(t)

expecting BiFunction (T,U -> R) ==> ::new translates to new StreamTuple(t,u)

expecting Supplier (() -> R) ==> ::new translates to new StreamTuple()

Since Stream#map is expecting a Function, this cannot translate to the 2-args constructor. So your create factory method seems a good way to go.

Having a 1-arg contructor (like the below) behaving like the create method is not possible since compiler will complain because R is not bound

public StreamTuple(L both) { //will fail compilation
    this.left = both;
    this.right = both;
}

BUT since you want a 1-arg constructor and its argument must be assignable to both R and L, then you have 2 options: 1) one extends the other (e.g. R is assignable to L)

public class StreamTuple<L, R extends L> {

    protected final L left;
    protected final R right;

    public StreamTuple(R both) {
        this.left = both;
        this.right = both;
    }
}

2) if R and L are not related, then you need Object with casts on getters:

public class StreamTuple<L, R> {

    protected final Object left, right;

    public StreamTuple(Object o) {
        this.left = this.right = o;
    }

    public <L> L getLeft() { return (L) left; }
    public <R> R getRight() { return (R) right; }
}

Upvotes: 1

tsolakp
tsolakp

Reputation: 5948

Have you considered extending StreamTuple in order to define single argument constructor?

 Map<String, String> m = Stream.of("1", "2", "3")
.map( UnaryStreamTuple<String>::new )
.collect( Collectors.toMap(UnaryStreamTuple::left, UnaryStreamTuple::right) );

public class UnaryStreamTuple<T> extends StreamTuple<T, T> {

    public UnaryStreamTuple(T left) {
        super(left, left);
    }

    public UnaryStreamTuple(T left, T right) {
        super(left, right);
    }        
}

Upvotes: 2

Radiodef
Radiodef

Reputation: 37875

There's no way to do this with a constructor, because there's no way to specify that the argument is a subtype of both L and R (i.e. conjunction/intersection). The language has a construct for such a requirement, but an intersection between type variables is not allowed:

// not allowed (causes a compilation error)
//                 vvvvv
public <LR extends L & R> StreamTuple(LR val) {
    this.left = val;
    this.right = val;
}

That might work in a future version of Java, however.

You have to use a factory method or, I suppose, what tsolakp suggested. I think the factory makes more sense.

Upvotes: 1

John Kugelman
John Kugelman

Reputation: 361909

It can't be done since you're passing id twice. StreamTuple::create is the way to go.

Upvotes: 1

rgettman
rgettman

Reputation: 178303

Stream's map method requires a Function that takes in exactly one parameter and returns something else.

However, what you are attempting to use is StreamTuple::new, and that takes in 2 parameters, e.g. a BiFunction, so that doesn't match.

I suppose you could use flatMap to have two copies of each stream element, then write a custom Collector to collect these items 2 at a time, but that sounds like a lot more work than referencing your create method, which already matches.

What will work best is what you've done -- a factory method that takes exactly one parameter and returns the desired StreamTuple.

Upvotes: 1

Related Questions