2

I have a Set<Client> which I am filling with results from a database query. The resulting rows contain different values for clients so that several rows contain values to be added to a single instance of Client.
The Clients are stored in the mentioned Set<Client> and currently, I am checking if the client is already present in that Set or needs to be created by the following stream:

Set<Client> clients = new TreeSet<>(); 
// yes, Client implements Comparable<Client>, has hashCode() and equals()

// some things that are irrelevant to this question happen here

// then iterating the database result
ResultSet rs = ps.executeQuery();

while (rs.next()) {
    int clientNumber = rs.getInt(...);
    String clientName = rs.getString(...);
    // get the client from the collection or create a new one if it isn't there
    Client client = clients.stream()
            .filter(c -> c.getNumber() == clientNumber)
            .findFirst()
            .orElse(new Client(clientNumber, clientName));

    // here I have to check again if the client is in the Set, how can I avoid that?
}

However, this does — of course — not add a possibly newly created Client to the Set<Client>, I need to check again if clients contains this instance afterwards in order to find out if I have to add this instance to clients or not. I couldn't even try anthing because I couldn't even add methods of Stream, like concat(...) at any position in the clients.stream()... statement.

That leads to the following question:

Is there any way at all that provides a possibility of directly adding a new instance of Client (or any object) to the Set while streaming this Set and the new instance is created by .orElse(new ...)?

I can imagine something like orElseAdd(new ...) but couldn't find anything similar.

deHaar
  • 17,687
  • 10
  • 38
  • 51
  • I assume `Client.equals` compares only `clientNumber`? In that case you don't need the stream at all, just `add` new clients into the set. The set will figure out whether it already exists or not. – Sweeper Aug 16 '19 at 09:33
  • 1
    I'm not sure to understand, why would you need to check if `new Client(clientNumber, clientName)` is in the set ? You already filtered it with `clientNumber`. In addition, you're using a set and have implemented the right methods, why do you verify the presence before adding a new element ? – Arnaud Claudel Aug 16 '19 at 09:34
  • @ArnaudClaudel The problem is the `orElse(new Client(clientNumber, clientName);` that will create a new instance if there is no matching instance found in the set, but it doesn't add it to the set afterwards. – deHaar Aug 16 '19 at 09:44
  • 2
    Because `Stream` are not meant for this kind of operations. And you are really duplicating the behavior of a `Set`, simply write `set.add()` – Arnaud Claudel Aug 16 '19 at 10:03
  • @ArnaudClaudel Thanks, it is definitely an ok idea to just add the resulting `Client` to the `Set`. – deHaar Aug 16 '19 at 10:16
  • 1
    But remove also the `Stream`, I think that you're misusing the `Set`. This is the first line of its javadoc `A collection that contains no duplicate elements. More formally, sets contain no pair of elements e1 and e2 such that e1.equals(e2), and at most one null element. As implied by its name, this interface models the mathematical set abstraction.` – Arnaud Claudel Aug 16 '19 at 10:19

4 Answers4

3

A Map provides a useful method of inserting something if it doesn't exist.

It also doesn't hurt to store clients in a map. It seems from your example that clientNumber is unique so that will be the key.

Map<Integer, Client> clients = new TreeMap<>();

...

Client client = clients.computeIfAbsent(clientNumber, key -> new Client(clientNumber, clientName));

When the lambda is executed, key is equal to clientNumber, so it can be ignored. Note here that the lambda is only executed when the client is not in the map, so don't fear about creting too many objects. Not that it matters since it's also creating boxed Integer objects if clientNumber is larger than the caching threshold (255 in my VM). Luckily Java is very efficient when dealing with short-lived objects.

Mark Jeronimus
  • 9,278
  • 3
  • 37
  • 50
  • Thanks, I have already thought about using a `Map`. And just out of interest: Why does a `Set` have no `computeIfAbsent`? Is that relying on a key? – deHaar Aug 16 '19 at 10:12
  • Yes. If you query the presence of an object in a `Set` you do it by-value, so you already have an object. There's no point in having a lambda that creates a new object. – Mark Jeronimus Aug 16 '19 at 10:19
2

Is there any way at all that provides a possibility of directly adding a new instance ... to the Set?

See this now:

For example, it is not generally permissible for one thread to modify a Collection while another thread is iterating over it. In general, the results of the iteration are undefined under these circumstances. Some Iterator implementations (including those of all the general purpose collection implementations provided by the JRE) may choose to throw this exception if this behavior is detected. Iterators that do this are known as fail-fast iterators, as they fail quickly and cleanly, rather that risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that this exception does not always indicate that an object has been concurrently modified by a different thread. If a single thread issues a sequence of method invocations that violates the contract of an object, the object may throw this exception. For example, if a thread modifies a collection directly while it is iterating over the collection with a fail-fast iterator, the iterator will throw this exception.

Quoting from ConcurrentModificationException.

Thing is: adding to a Set while iterating it is simply not a good idea. Most likely, an instance of the above exception would be thrown at you. See here for some more thoughts.

Also, note, conceptually: would you want a freshly added object to show up in that stream, too? What if your Set is sorted, and the new object goes somewhere in the set ... that was "already" streamed?!

Long story short: what you intend to do is bad practice. Rather collect all new Client objects in a separate set, and process that one by itself later on.

And note: it isn't mandatory, but the whole "streaming" API suggests you to follow functional programming paradigms. In functional programming, you don't modify things, you rather create new things. So, as said: collect new objects in a new set, and then finally, create a new merged set that contains old and new objects.

Community
  • 1
  • 1
GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • Thanks, I know about the concurrent modification problem, but I thought there might be some method that, instead of returning a new `Client` at the end of the stream, just adds a new one. – deHaar Aug 16 '19 at 10:14
2

If the process of creating a client is light enough to not worry about creating a dup, you can just let Set control it for you.

Set<Client> clients = new TreeSet<>(); 
ResultSet rs = ps.executeQuery();

while (rs.next()) {
    int clientNumber = rs.getInt(...);
    String clientName = rs.getString(...);
    clients.add(new Client(clientNumber, clientName))
}
1

If you are really wants to use TreeSet to maintain the sorting order of clients and wants to add the non existing client to clients collection set then try the following

Step 1

Since TreeSet maintains the sorted order, the Client object need to implement java.lang.Comparable's compareTo() method like the following:

public class Client implements Comparable<Client>{

    private int clientNumber;
    private String clientName;

    public Client(int clientNumber, String clientName) {
        this.clientNumber = clientNumber;
        this.clientName = clientName;
    }

    @Override
    public int compareTo(Client client) {
        return clientNumber - client.getClientNumber();
    }

    // getters and setters
}

Step 2

Since Set handles the duplicates internally just use

while (rs.next()) {
    int clientNumber = rs.getInt(...);
    String clientName = rs.getString(...);
    clients.add(new Client(clientNumber, clientName));
}

Step 3

For List collection Stream, Filter and add the non existing client to our clients collection set using the following way

while (rs.next()) {
    int clientNumber = rs.getInt(...);
    String clientName = rs.getString(...);
    clients.addAll(Lists.newArrayList(new Client(clientNumber, clientName)).stream()
            .filter(client -> !clients.contains(client))
            .collect(toList()));
}
Nidhish Krishnan
  • 20,593
  • 6
  • 63
  • 76
  • Thanks, I know about **Step 1** and have implemented `compareTo(Client other)` (a bit differently, but with the same functionality). I will check out **Step 2**. – deHaar Aug 16 '19 at 12:49
  • I could, but I can just tell you: I check if `this.number == other.getNumber()` and `return 0` if yes, otherwise I check if `this.number > other.getNumber()` and then `return 1` and `else return -1;` but I think that does not really matter if I have `equals()` and `hashCode()` as well. – deHaar Aug 16 '19 at 13:11
  • It looks fine for me....but what issue that causes you when simply use `clients.add(new Client(clientNumber, clientName));` – Nidhish Krishnan Aug 16 '19 at 13:19
  • There is no real issue with that (as I realized after some answers and comments were given here). I was checking if I got a `Client` that was in the `Set` before or a new one, but that is just not necessary if I use a `TreeSet`. – deHaar Aug 16 '19 at 13:48