1

I couldn't wrap my head around writing the below condition using Java Streams. Let's assume that I have a list of elements from the periodic table. I've to write a method that returns a String by checking whether the list has Silicon or Radium or Both. If it has only Silicon, method has to return Silicon. If it has only Radium, method has to return Radium. If it has both, method has to return Both. If none of them are available, method returns "" (default value).

Currently, the code that I've written is below.

String resolve(List<Element> elements) {
    AtomicReference<String> value = new AtomicReference<>("");
    elements.stream()
            .map(Element::getName)
            .forEach(name -> {
                if (name.equalsIgnoreCase("RADIUM")) {
                    if (value.get().equals("")) {
                        value.set("RADIUM");
                    } else {
                        value.set("BOTH");
                    }
                } else if (name.equalsIgnoreCase("SILICON")) {
                    if (value.get().equals("")) {
                        value.set("SILICON");
                    } else {
                        value.set("BOTH");
                    }
                }
            });
    return value.get();
}

I understand the code looks messier and looks more imperative than functional. But I don't know how to write it in a better manner using streams. I've also considered the possibility of going through the list couple of times to filter elements Silicon and Radium and finalizing based on that. But it doesn't seem efficient going through a list twice.

NOTE : I also understand that this could be written in an imperative manner rather than complicating with streams and atomic variables. I just want to know how to write the same logic using streams.

Please share your suggestions on better ways to achieve the same goal using Java Streams.

1
  • Element { String name; int atomicNumber; } Commented Mar 4, 2022 at 18:29

4 Answers 4

1

It could be done with Stream IPA in a single statement and without multiline lambdas, nested conditions and impure function that changes the state outside the lambda.

My approach is to introduce an enum which elements correspond to all possible outcomes with its constants EMPTY, SILICON, RADIUM, BOTH.

All the return values apart from empty string can be obtained by invoking the method name() derived from the java.lang.Enum. And only to caver the case with empty string, I've added getName() method.

Note that since Java 16 enums can be declared locally inside a method.

The logic of the stream pipeline is the following:

  • stream elements turns into a stream of string;
  • gets filtered and transformed into a stream of enum constants;
  • reduction is done on the enum members;
  • optional of enum turs into an optional of string.

Implementation can look like this:

public static String resolve(List<Element> elements) {
    return elements.stream()
            .map(Element::getName)
            .map(String::toUpperCase)
            .filter(str -> str.equals("SILICON") || str.equals("RADIUM"))
            .map(Elements::valueOf)
            .reduce((result, next) -> result == Elements.BOTH || result != next ? Elements.BOTH : next)
            .map(Elements::getName)
            .orElse("");
}

enum

enum Elements {EMPTY, SILICON, RADIUM, BOTH;
    String getName() {
        return this == EMPTY ? "" : name(); // note name() declared in the java.lang.Enum as final and can't be overridden
    }
}

main

public static void main(String[] args) {
    System.out.println(resolve(List.of(new Element("Silicon"), new Element("Lithium"))));
    System.out.println(resolve(List.of(new Element("Silicon"), new Element("Radium"))));
    System.out.println(resolve(List.of(new Element("Ferrum"), new Element("Oxygen"), new Element("Aurum")))
                .isEmpty() + " - no target elements"); // output is an empty string
}

output

SILICON
BOTH
true - no target elements

Note:

  • Although with streams you can produce the result in O(n) time iterative approach might be better for this task. Think about it this way: if you have a list of 10.000 elements in the list and it starts with "SILICON" and "RADIUM". You could easily break the loop and return "BOTH".
  • Stateful operations in the streams has to be avoided according to the documentation, also to understand why javadoc warns against stateful streams you might take a look at this question. If you want to play around with AtomicReference it's totally fine, just keep in mind that this approach is not considered to be good practice.

I guess if I had implemented such a method with streams, the overall logic would be the same as above, but without utilizing an enum. Since only a single object is needed it's a reduction, so I'll apply reduce() on a stream of strings, extract the reduction logic with all the conditions to a separate method. Normally, lambdas have to be well-readable one-liners.

Sign up to request clarification or add additional context in comments.

Comments

0

Collect the strings to a unique set. Then check containment in constant time.

Set<String> names = elements.stream().map(Element::getName).map(String::toLowerCase).collect(toSet());
boolean hasSilicon = names.contains("silicon");
boolean hasRadium = names.contains("radium");
String result = ""; 
if (hasSilicon && hasRadium) {
  result = "BOTH";
} else if (hasSilicon) {
  result = "SILICON";
} else if (hasRadium) {
  result = "RADIUM";
}
return result;

5 Comments

Is there any other way than using if-else construct?
What do you mean? Your own question is using if-else. I don't see any other way to check one string or the other
@VivekTS: Not really, no. Certainly nothing as efficient.
@LouisWasserman There might be a way to use XOR and a bitmap of the periodic table, maybe?
I would have tried a lookup in a Map<Set<String>, String>.
0

i have used predicate in filter to for radium and silicon and using the resulted set i am printing the result.


import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class Test {

    public static void main(String[] args) {
        List<Element> elementss = new ArrayList<>();
        Set<String> stringSet = elementss.stream().map(e -> e.getName())
            .filter(string -> (string.equals("Radium") || string.equals("Silicon")))
            .collect(Collectors.toSet());
        if(stringSet.size()==2){
            System.out.println("both");
        }else if(stringSet.size()==1){
            System.out.println(stringSet);
        }else{
            System.out.println(" ");
        }
    }
    
}

Comments

0

You could save a few lines if you use regex, but I doubt if it is better than the other answers:

String resolve(List<Element> elements) {
    String result = elements.stream()
                            .map(Element::getName)
                            .map(String::toUpperCase)
                            .filter(str -> str.matches("RADIUM|SILICON"))
                            .sorted()
                            .collect(Collectors.joining());

    return result.matches("RADIUMSILICON") ? "BOTH" : result;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.