'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 I may need to perform on the stream before I print the result.



Solution 1:[1]

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.

Solution 2:[2]

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

Solution 3:[3]

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

Solution 4:[4]

With Guava:

Streams.mapWithIndex(Stream.of(list).distinct(), (s, i) -> (i + 1) + ". " + s)
       .forEach(System.out::println);

Output:

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4 ZhekaKozlov