Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Count and print unique list items in one chain using Java Streams

I'm trying to achieve this using only functional programming constructs (Streams, Collectors, lambda expressions).

Let's say list is a String[]:

{"Apple", "Samsung", "LG", "Oppo", "Apple", "Huawei", "Oppo"}

I want to print out a distinct list of brand names from this array, and number them, i.e.:

1. Apple
2. Huawei
3. LG
4. Oppo
5. Samsung

I can print out the unique elements (sorted):

Stream.of(list)
    .distinct()
    .sorted()
    .forEach(System.out::println);

But this does not show the preceding counter. I tried Collectors.counting() but that, of course, didn't work.

Any help from FP experts?


Edit: I understand some other questions have asked about iterating over a stream with indices. However, I can't simply do:

IntStream.range(0, list.length)
        .mapToObj(a -> (a+1) + ". " + list[a])
        .collect(Collectors.toList())
        .forEach(System.out::println);

because the original array contains duplicate elements. There might also be other map and filter operations I may need to perform on the stream before I print the result.

like image 826
light Avatar asked Jun 06 '19 13:06

light


3 Answers

Edit after Holger’s comment: If you just want the distinct brands in the order encountered in the array, all you need to do is to omit the sort operation.

    String[] list = {"Apple", "Samsung", "LG", "Oppo", "Apple", "Huawei", "Oppo"};
    Stream.of(list)
            .distinct()
            .forEach(System.out::println);

Output is:

Apple
Samsung
LG
Oppo
Huawei

As Holger said, distinct maintains encounter order if the stream had encounter order before the distinct operation (which your stream has).

I actually need the preceding counter (1. 2. 3. 4. 5.).

My preferred way of doing it is using an enhanced for loop and no lambdas:

    int counter = 0;
    for (String brand : new LinkedHashSet<>(Arrays.asList(list))) {
        counter++;
        System.out.println("" + counter + ". " + brand);
    }

Output is:

1. Apple
2. Samsung
3. LG
4. Oppo
5. Huawei

Further comment from you:

I'm wondering what's the "ideal FP" solution.

I’m not necessarily convinced that any good purely functional programming solution exists. My attempt would be, inspired from the linked original question and its answers:

    List<String> distinctBrands
            = new ArrayList<>(new LinkedHashSet<>(Arrays.asList(list)));
    IntStream.range(0, distinctBrands.size())
            .mapToObj(index -> "" + (index + 1) + ". " + distinctBrands.get(index))
            .forEach(System.out::println);

A LinkedHashSet maintains insertion order and drops duplicates. Output is the same as before.

like image 150
Ole V.V. Avatar answered Sep 30 '22 10:09

Ole V.V.


A simpler solution could be to use a Set as a collection to ensure unique strings are accessed and incrementing the index along with for all such elements:

String[] list = {"Apple", "Samsung", "LG", "Oppo", "Apple", "Huawei", "Oppo"};
Set<String> setOfStrings = new HashSet<>(); // final strings
AtomicInteger index = new AtomicInteger(1); // index
Arrays.stream(list)
        .filter(setOfStrings::add) // use the return type of Set.add
        .forEach(str -> System.out.println(index.getAndIncrement() + ". " + str));

Edit: (Thanks to Holger) And instead of the index maintained as a separate variable, one can use the size of the collection as:

Arrays.stream(list)
        .filter(setOfStrings::add) // use the return type of Set.add
        .forEach(str -> System.out.println(setOfStrings.size() + ". " + str)); // use size for index
like image 25
Naman Avatar answered Sep 30 '22 10:09

Naman


You can do this in a functional style, without side-effects, by setting up one stream that sorts the items and a second stream for the line numbers, then combining the streams with zip.

This example uses guava's zip function. Zip is a very common utility for functional programming.

import java.util.stream.Stream;
import com.google.common.collect.Streams;

public class ZipExample {
    public static void main(String[] args) {
        String[] a = {"Apple", "Samsung", "LG", "Oppo", "Apple", "Huawei", "Oppo"};
        Stream<String> items = Stream.of(a).sorted().distinct();
        Stream<Integer> indices = Stream.iterate(1, i -> i + 1);
        Streams.zip(items, indices, 
            (item, index) -> index + ". " + item)
            .forEach(System.out::println);
    }
}

It prints out

1. Apple
2. Huawei
3. LG
4. Oppo
5. Samsung

Rewritten as one expression it would look like:

Streams.zip(
    Stream.of(a).sorted().distinct(), 
    Stream.iterate(1, i -> i + 1), 
    (item, index) -> index + ". " + item)
    .forEach(System.out::println);
like image 45
Nathan Hughes Avatar answered Sep 30 '22 11:09

Nathan Hughes