2

Java stream on specific fields in a custom class object

I have an ArrayList of Train objects.

Each Train has three fields: source, destination, cost.

I want to get all the place names, i.e. all distinct sources + destinations.

I am using the below code, but as it can be observed, I'm using two streams to retrieve the data.

List<String> destinations = list.stream()
    .map(x -> x.getDestination())
    .distinct()
    .collect(Collectors.toList());

List<String> sources = List.stream()
    .map(x -> x.getSource())
    .distinct()
    .collect(Collectors.toList());

I was wondering how I could accomplish the same thing in a single stream? Can it be done using flatMap, or there's another way to achieve this?

List<String> allPlaces = ?

Also, is this possible to use Train class without getters?

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
potato
  • 37
  • 3
  • Have you tried mapping to a `Pair` type? – tenuki Sep 05 '22 at 17:46
  • Do you want 2 separate lists(1 for sources, 1 for destinations) or 1(for both)? – rostIvan Sep 05 '22 at 17:48
  • @rostIvan i wanted a single list of just distinct places – potato Sep 05 '22 at 17:51
  • 1
    @potato, please see the Mureinik answer. It is possible without the getters of the Train class if instance variables are accessible – rostIvan Sep 05 '22 at 17:54
  • @rostIvan It's not a good practice in Java to access instance variables directly from outside the class. – Alexander Ivanchenko Sep 05 '22 at 18:55
  • @AlexanderIvanchenko, sure, but the question is not about good practices but rather a possibility – rostIvan Sep 05 '22 at 19:11
  • @rostIvan It implies both. The spirit of StackOverfow is to be the source of hi-quality answers. It seems like background OP is Python, hence it's important to explain how we approach Encapsulation in Java apart from saying that it is technically not to declare getters. Also, as replay to the solution you've recommended, there's a better alternative for addressing this problem - [*see*](https://stackoverflow.com/a/73613240/17949945). – Alexander Ivanchenko Sep 05 '22 at 19:37

2 Answers2

6

You had the right idea with flatMap - you can map a train to a stream that contains the source and destination, and then flatMap it to you "main" stream:

List<String> allPlaces =
    trains.stream()
          .flatMap(t -> Stream.of(t.getSource(), t.getDestination()))
          .distinct()
          .collect(Collectors.toList());
Mureinik
  • 297,002
  • 52
  • 306
  • 350
  • wow ! exactly what i wanted, thanks a lot! will have to take a look at the Stream.of method – potato Sep 05 '22 at 17:52
  • @potato Note that for each element of the stream, `flatMap` requires a new nested stream to be created, because that's how it consumes flattened elements. In addition `Stream.of()` required wrapping elements with an array (that how *varargs* works in Java). These overheads can be avoided by using method `Stream.mapMulty()`. [*See*](https://stackoverflow.com/a/73613240/17949945). – Alexander Ivanchenko Sep 05 '22 at 18:52
2

In this case, we can utilize Java 16 method mapMulti(), which is functionally similar to flatMap(). It's meant for transforming a single stream element into a group of elements.

Here's how implementation might look like:

List<String> places = trains.stream()
    .<String>mapMulti((train, consumer) -> {
        consumer.accept(train.getSource());
        consumer.accept(train.getDestination());
    })
    .distinct()
    .toList();

Contrary to flatMap() it doesn't consume a stream, but operates via Consumer. mapMulti() a recommended alternative to flatMap() for situations when a new stream flatMap() requires would contain only a few elements (like in this case when we have only two elements: source and destination).

A quote from the API Note:

This method is preferable to flatMap in the following circumstances:

  • When replacing each stream element with a small (possibly zero) number of elements. Using this method avoids the overhead of creating a new Stream instance for every group of result elements, as required by flatMap.

Addressing peripheral question:

Also is this possible without the getters methods of class Train?

Sure, you can. But it's not a recommended practice to access instance fields directly. In Java we're using access modifier to hide and protect member-variables within the class, that's one of the aspects of Encapsulation.

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • I agree with you that in cases like this using `mapMulti` is preferred over `flatMap`. It might be that the OP wasn't running Java 16 or perhaps better understood the other solution. – WJS Sep 06 '22 at 15:41