2

Using Java 8, I want two combine to collections: one collection is a java.util.List; the other is an com.fasterxml.jackson.databind.node.ArrayNode.

Here's a non-Java 8 way of handling that:

private void mergeMemberAndRelationshipData(final List<JsonNode> members, final ArrayNode relationships) {

    assert members != null : "members cannot be null";
    assert relationships != null : "relationships cannot be null";

    for (int i = 0; i < members.size(); i++) {

        final ObjectNode relationship = (ObjectNode) relationships.get(i);
        final JsonNode member = members.get(i);

        if (member != null && !member.hasNonNull(ExceptionMapper.errorCodeStr)) {
            relationship.put(FIRST_NAME, this.jsonHelpers.safeGetString(member, FIRST_NAME));
            relationship.put(LAST_NAME, this.jsonHelpers.safeGetString(member, LAST_NAME));
            relationship.put(EMAIL, this.jsonHelpers.safeGetString(member, EMAIL));
            relationship.put(MEMBER_SINCE, this.jsonHelpers.safeGetString(member, MEMBER_SINCE));
        } else {
            LOGGER.error("No corresponding Member, {}, found for Relationship: {}", member, relationship);
        }
    }
}

I wanna stream. I like streams. Okay, then, let's give it a shot:

private void mergeMemberAndRelationshipData(final List<JsonNode> members, final ArrayNode relationships) {

    assert members != null : "members cannot be null";
    assert relationships != null : "relationships cannot be null";

    int i = 0;

    members.stream().forEach(member -> {
        final ObjectNode relationship = (ObjectNode) relationships.get(i++);

        if (member != null) {
            relationship.put(FIRST_NAME, this.jsonHelpers.safeGetString(member, FIRST_NAME));
            relationship.put(LAST_NAME, this.jsonHelpers.safeGetString(member, LAST_NAME));
            relationship.put(EMAIL, this.jsonHelpers.safeGetString(member, EMAIL));
            relationship.put(MEMBER_SINCE, this.jsonHelpers.safeGetString(member, MEMBER_SINCE));
        } else {
            LOGGER.warn("No corresponding member found for relationship: {}", relationship);
        }
    });
}

But no, that won't work: Local variable i defined in an enclosing scope must be final or effectively final. Yup, that makes sense. Kinda.

Alright, so I want to stream one collection, A, and for each object in A, do some operation on the corresponding object in collection, B. I need to know the index to do that.

How can I use a counter, declared outside the scope of an anonymous function, inside of that anonymous function?

I can solve this in a coupla inventive, creative ways:

private void mergeMemberAndRelationshipData(final List<JsonNode> members, final ArrayNode relationships) {

    assert members != null : "members cannot be null";
    assert relationships != null : "relationships cannot be null";

    int[] i = new int[1];
    i[0] = 0;

    members.stream().forEach(member -> {
        final ObjectNode relationship = (ObjectNode) relationships.get(i[0]);
        i[0] = i[0]++;

        if (member != null) {
            relationship.put(FIRST_NAME, this.jsonHelpers.safeGetString(member, FIRST_NAME));
            relationship.put(LAST_NAME, this.jsonHelpers.safeGetString(member, LAST_NAME));
            relationship.put(EMAIL, this.jsonHelpers.safeGetString(member, EMAIL));
            relationship.put(MEMBER_SINCE, this.jsonHelpers.safeGetString(member, MEMBER_SINCE));
        } else {
            LOGGER.warn("No correspodning member foudn for relationship: {}", relationship);
        }
    });
}

Or I can use an AtomicInteger thusly:

AtomicInteger i = new AtomicInteger(0);
...
final ObjectNode relationship = (ObjectNode) relationships.get(i.getAndIncrement());

But that's a misuse of AtomicInteger; that's meant for shared values amongst threads. Not quite what's happening here, and it's way too heavyweight for this to be an appropriate use (in my opinion, please feel free to disagree with me).

So, this is a very common thing to do (linking two ordered lists and operating on their elements). It's super easy pre-Java 8 (technically still is, but you pedants get my point, I'm sure). Is there a Java-8-ey way to accomplish this using streams?

For context, where i work, we use a micro-service architecture approach. This means that we have a bunch of services that are responsible for a discrete, encapsulated set of responsibilities (e.g., MemberService handles member CRUD, and CompanyService handles company CRUD). On top of all that, we have an orchestration/authentication/authorization service that makes these disparate calls and then stitches them together. So, in the method I've pasted, this is the orchestration layer, where I get my two datasets and now I want to merge them together. This is why I don't have a LinkedList or some other similar solution.

liltitus27
  • 1,670
  • 5
  • 29
  • 46
  • I use the array hack. It's the simplest workaround. – Bohemian Jul 07 '16 at 21:07
  • that's how it's seeming to me as well, it's just so ugly and a bastardization of the array. plus, it's tricking the compiler's safety checks, which is another reason i don't like it, even though it works. i like jay's solution below and will play around with it, since it seems hopeful. additionally, it's a functional programming solution using java 8 libs, which is what i'm after. i wanna be functional and learn these new, albeit odd, ways of solving common issues. – liltitus27 Jul 07 '16 at 21:11

3 Answers3

2

Using Streams puts you more in a functional programming paradigm. From that paradigm, it might be better to use a Map function on the collection, rather than mutate external state.

This isn't an exact refactor (no logging for null members, for example), but it might be a pseudo-code solution that moves you in the direction you are looking for.

private void mergeMemberAndRelationshipData(final List<JsonNode> members, final ArrayNode relationships) 
{       
    List<...{SomeJsonObject}...> relationshipList = members.stream()
        .filter(x -> x != null)
        .map(x -> //build individual relationship object)
        .collect(Collectors.toList());

    relationshipList.forEach(x -> //add to ArrayNode); 
}

An alternate approach, if you need to iterate through the two collections in step, is this:

    private void mergeMemberAndRelationshipData(final List<JsonNode> members, final ArrayNode relationships) 
    {       
        IntStream.range(0, members.size())
            .forEach(i -> 
                { 
                    if (members.get(i) != null)
                        //add data
                    else
                        //perform logging
                });
    }
Silas Reinagel
  • 4,155
  • 1
  • 21
  • 28
  • i played around with this approach, but it kept falling apart. for example, while i could add logging back in the filter function, i would lose the one-to-one relationship between the two lists, breaking their linkage. it's almost as if i need some sort of `Pair` here, but java refuses to implement one :( – liltitus27 Jul 07 '16 at 20:43
  • 1
    Ah, so you are essentially treating the two objects as an unofficial linked list? Let me see what I can come up with. – Silas Reinagel Jul 07 '16 at 20:46
  • I added an alternate solution. Hopefully the new one will match your use case. – Silas Reinagel Jul 07 '16 at 20:59
2

You want to look for zipping. Java initially had a zip utility built in, but it was removed. Here's a past stackoverflow response for zipping with streams with an example implementation: Zipping streams using JDK8 with lambda (java.util.stream.Streams.zip).

Beyond that to avoid keeping the index externally you can use an IntStream:

final List<String> a = Arrays.asList("a", "b", "c");
final List<String> b = Arrays.asList("1", "2", "3");
IntStream.range(0, Math.min(a.size(), b.size()))
   .mapToObj(i -> a.get(i) + "-" + b.get(i))
   .forEach(System.out::println);
Community
  • 1
  • 1
Jay Anderson
  • 937
  • 1
  • 8
  • 18
0

You cannot use a variable that is local to a method for this. However, you could simply use a class field:

private int i = 0;

private void mergeMemberAndRelationshipData(final List<JsonNode> members, final ArrayNode relationships) {

    assert members != null : "members cannot be null";
    assert relationships != null : "relationships cannot be null";



    members.stream().forEach(member -> {
        final ObjectNode relationship = (ObjectNode) relationships.get(i++);

        ...
    });
}

This is not thread safe, so you would need to ensure that if you have threads, each thread is working with it's own instance of this class, or you have proper locking mechanisms in place.

Martin Konecny
  • 57,827
  • 19
  • 139
  • 159
  • Why wouldn't you just create a mutable integer wrapper? A `new int[1]` would suffice. – Louis Wasserman Jul 07 '16 at 20:23
  • that's another option, but seems like a lot of boilerplate everytime i wanna use a counter in a stream loop. as using stream.foreach eventually replaces for each loops, it seems to me that there'd be an easy, non-boilerplate way to handle this situation. – liltitus27 Jul 07 '16 at 20:25