3

I would like to go through a list of objects and concatenate several Strings for each variable:

final StringBuilder xBuilder = new StringBuilder();
final StringBuilder yBuilder = new StringBuilder();
final StringBuilder zBuilder = new StringBuilder();

someObjects.forEach(obj -> {
  A a = obj.getA();
  xBuilder.append(obj.getX() + ",");
  yBuilder.append(obj.getY() + ",");
  zBuilder.append(a.getZ() + ",");
});

Is there any more effective way than creating several StringBuilders for each variable?

muzychuk
  • 288
  • 1
  • 2
  • 13
Peters_
  • 597
  • 2
  • 8
  • 28
  • I don't know if it is really more efficient, but an alternative may be a [`StringJoiner`](https://docs.oracle.com/javase/8/docs/api/java/util/StringJoiner.html). – deHaar Sep 03 '18 at 14:37
  • [Reflection](https://stackoverflow.com/questions/2466038/how-do-i-iterate-over-class-members?rq=1) could be what you want. – meowgoesthedog Sep 03 '18 at 14:38

4 Answers4

4

Well, you do want 3 separate Strings here, so looks just fine to create 3 StringBuilder(s).. You could alternatively use a StringJoiner though:

StringJoiner xJoiner = new StringJoiner(",");
someObjects.forEach(obj -> {
    xJoiner.join(obj.getX();
});
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • Don't use `forEach` for this. Use `someObjects.stream().map(SomeObject::getX).collect(joining(...))`. – Andy Turner Sep 03 '18 at 14:40
  • If there are 3 separate things to join, write a proper collector. – Andy Turner Sep 03 '18 at 14:41
  • 1
    @AndyTurner god no, *I* would not write a collector *just for that* – Eugene Sep 03 '18 at 14:42
  • "The behavior of this operation is explicitly nondeterministic. ... For any given element, the action may be performed at whatever time and in whatever thread the library chooses.". Really, `forEach` shouldn't be used for this. – Andy Turner Sep 03 '18 at 14:45
  • 1
    @AndyTurner you are quoting the documentation of `forEach` that derives from `stream`, this is not using that – Eugene Sep 03 '18 at 14:46
  • 4
    @AndyTurner or, instead of implementing a custom collector, just do three `someObjects.stream().map(SomeObject::getWhatEver).collect(joining(","))` operations. I don’t know why so many people are so preoccupied with trying to avoid iterations (over trivial collections). The costs of iterating are definitely not the most expensive thing here. It’s not even predictable, which variant will be faster… – Holger Sep 03 '18 at 15:13
  • 1
    @Holger because, at least in this case, it feels like a very natural thing to do – Eugene Sep 03 '18 at 18:32
  • 2
    @Eugene it “feels natural” due to the lang-standing preoccupation. If you get used to writing dedicated statements instead of loops doing multiple things at once, you may feel different. To name a different example, I was writing these well-known `collection.toArray(new ArrayType[collection.size()])` expressions for two decades, before learning that `collection.toArray(new ArrayType[0])` actually is faster. So switching to the variant that is simpler *and* faster was not easy, as the other felt “very natural”. But after some time of using the better variant, I got used to it. – Holger Sep 04 '18 at 07:50
  • @Holger that is understandable, even `java-11` `Collection::toArray` uses this trick. – Eugene Sep 04 '18 at 08:41
4

Is there any more effective way than creating several StringBuilders for each variable?

There is no more effective way than creating a something for each of the variables.

Don't use forEach here: there is no advantage over a plain old loop (see here for a question about the differences):

final StringBuilder xBuilder = new StringBuilder();
final StringBuilder yBuilder = new StringBuilder();
final StringBuilder zBuilder = new StringBuilder();

for (SomeObject obj : someObjects) {
  A a = obj.getA();

  // Use two appends, rather than concatentating then appending.
  xBuilder.append(obj.getX()).append(",");

  yBuilder.append(obj.getY()).append(",");
  zBuilder.append(a.getZ()).append(",");

}
Andy Turner
  • 137,514
  • 11
  • 162
  • 243
1

Collectors.joining() method will use StringJoiner internally to append Strings with passed delimiter like here ','.

someObjects.stream().collect(Collectors.joining(","));
0

As you're concatenating 3 different properties of the object, you need to use 3 different StringBuilder instances (or StringJoiner). You could iterate the list of objects 3 times, one for each property, or if you only want to iterate once (despite traversing a list is extremely cheap), you could use either your own approach or a traditional foreach.

Or you could also use the code I'm showing below, which is semantically equivalent to the other answers, except that it allows to decouple the concatenation process from the iteration (if you need this at all):

StringBuilder xBuilder = new StringBuilder(); // no need to use final since
StringBuilder yBuilder = new StringBuilder(); // these instances are 
StringBuilder zBuilder = new StringBuilder(); // effectively final

Consumer<SomeObject> action = ((Consumer<SomeObject>) obj -> 
    xBuilder.append(obj.getX() + ","))
        .andThen(obj -> yBuilder.append(obj.getY() + ","))
        .andThen(obj -> zBuilder.append(obj.getA().getZ() + ","));

Then, you can pass around action (i.e. to some method that doesn't have visibility over the builders) and simply do:

someObjects.forEach(action);
fps
  • 33,623
  • 8
  • 55
  • 110