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.

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
Vivek T S
  • 65
  • 8

4 Answers4

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.

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
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(" ");
        }
    }
    
}

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;
OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
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;
}
Eritrean
  • 15,851
  • 3
  • 22
  • 28