user140547
user140547

Reputation: 8200

Java 8 - chaining constructor call and setter in stream.map()

I have a class

class Foo{
    String name;
    // setter, getter
}

which just has a default constructor.

Then, I am trying to create a List of Foo from some string:

Arrays.stream(fooString.split(","))
            .map(name -> {
                Foo x = new Foo();
                x.setName(name);
                return x;

            }).collect(Collectors.toList()));

Since there is no constructor which takes a name, I can't simply use a method reference. Of course, I could extract those three lines, with the constructor call and the setter, into a method but is there any better or concise way to do that? (without changing Foo, which is a generated file)

Upvotes: 36

Views: 34306

Answers (5)

Jaroslaw Pawlak
Jaroslaw Pawlak

Reputation: 5578

Another alternative that no one mentioned yet would be to subclass Foo class, however this may have some disadvantages - it's difficult to say whether it would be appropriate solution to your problem, as I don't know the context.

public class Bar extends Foo {

    public Bar(String name) {
        super.setName(name);
    }

}

Upvotes: 3

Philipp
Philipp

Reputation: 69663

.map(n -> new Foo() {{ name = n; }} )

This uses an initialization block to set an instance-variable.

There is however a caveat: The returned objects will not actually be of type Foo but of new, anonymous classes which extend Foo. When you follow the Liskov substitution principle this should not be a problem, but there are a few situations where it might be a concern.

Upvotes: 5

Holger
Holger

Reputation: 298263

If this happens repeatedly, you may create a generic utility method handling the problem of constructing an object given one property value:

public static <T,V> Function<V,T> create(
    Supplier<? extends T> constructor, BiConsumer<? super T, ? super V> setter) {
    return v -> {
        T t=constructor.get();
        setter.accept(t, v);
        return t;
    };
}

Then you may use it like:

List<Foo> l = Arrays.stream(fooString.split(","))
    .map(create(Foo::new, Foo::setName)).collect(Collectors.toList());

Note how this isn’t specific to Foo nor its setName method:

List<List<String>> l = Arrays.stream(fooString.split(","))
    .map(create(ArrayList<String>::new, List::add)).collect(Collectors.toList());

By the way, if fooString gets very large and/or may contain lots of elements (after splitting), it might be more efficient to use Pattern.compile(",").splitAsStream(fooString) instead of Arrays.stream(fooString.split(",")).

Upvotes: 34

Tunaki
Tunaki

Reputation: 137104

No, there is no better way.

The only alternative is, like you said in your question, to create a factory for Foo objects:

public class FooFactory {
    public static Foo fromName(String name) {
        Foo foo = new Foo();
        foo.setName(name);
        return foo;
    }
}

and use it like this:

Arrays.stream(fooString.split(",")).map(FooFactory::fromName).collect(toList());

If there are a lot of names to split, you can use Pattern.compile(",").splitAsStream(fooString) (and store the compiled pattern in a constant to avoid recreation) instead of Arrays.stream(fooString.split(",")).

Upvotes: 11

aleroot
aleroot

Reputation: 72636

In this case you don't have too many alternatives unless you add a constructor taking the name as parameter, or you create a static factory method that create your instance .

Upvotes: 8

Related Questions