3

I was wondering if I can join two pre-sorted streams (in a single pass). For example, if I have the following Java classes:

public class Person { // constructor & getters ommitted
  private String name;
  private int salary;
  private List<Car> cars;
  public void addCar(Car c) { cars.add(c); }
}

public class Car { // constructor & getters ommitted
  private String owner;
  private String brand;
}

And I have pre-sorted streams, as in:

Stream<Person> clients = Arrays.asList(
  new Person("Anne", 500), 
  new Person("Johnny", 340)
  ).stream();

Stream<Car> cars = Arrays.asList(
  new Car("Johnny", "Mazda"), 
  new Car("Johnny", "Fiat"), 
  new Car("Mary", "Volvo")
  ).stream();

I wanted to apply addCar() to Johnny with the "Mazda" and "Fiat". There's no car for Anne, and Mary is not a client.

Is it possible to join both streams with a single pass on the streams?

I've seen solutions where one stream is walked multiple times, but since they are pre-ordered, I guess there could be a chance of doing it in one pass.

EDIT: The expected result of the operation would be to call addCar() twice for "Johnny": once with the "Mazda", once with the "Fiat".

Joe DiNottra
  • 836
  • 8
  • 26

2 Answers2

3

I am afraid it's not possible to efficiently solve this task using Stream API. But you can still do it using iterators.

public static void addCars(Stream<Person> clients, Stream<Car> cars) {
    Iterator<Person> clientsIt = clients.iterator();
    Iterator<Car> carsIt = cars.iterator();
    Person client = null;
    while (carsIt.hasNext()) {
        Car car = carsIt.next();
        while (client == null || !client.getName().equals(car.getOwner())) {
            if(!clientsIt.hasNext()) return;
            client = clientsIt.next();
        }
        client.addCar(car);
    }
}
IlyaMuravjov
  • 2,352
  • 1
  • 9
  • 27
  • I was afraid it wasn't possible. Thanks. +1 – Joe DiNottra Mar 19 '20 at 19:59
  • @Naman The complexity is `O(cars + clients)`. The outer loop is executed at most `cars.size()` times since `carsIt.next()` is called on each iteration. The inner loop is executed at most `clients.size()` times **in total** since `clientsIt.next()` is called on each iteration, and the same `clientsIt` is used every time. This is the fastest solution because it only iterates once over `cars` and once over `clients`, and no overhead is caused by using containers like `HashMap`. – IlyaMuravjov Mar 20 '20 at 16:13
1

A precompute can help. Something like

Map<String, List<Car>> cars = Stream.of(
        new Car("Johnny", "Mazda"),
        new Car("Johnny", "Fiat"),
        new Car("Mary", "Volvo"))
        .collect(Collectors.groupingBy(Car::getOwner));

Stream.of(new Person("Anne", 500),
        new Person("Johnny", 340))
        .forEachOrdered(p -> cars.getOrDefault(p.getName(), 
                Collections.emptyList()).forEach(p::addCar));
Naman
  • 27,789
  • 26
  • 218
  • 353