2

I am trying to replace element in collection with new modified version. Below is short code that aims to demonstrate what I'd like to achieve.

The whole idea is that I have one object that consists of collections of other objects. At some point in time I am expecting that this objects in collections (in my example phones) might require some modifications and I'd like to modify the code in one place only.

I know that in order to update the object's attributes I can use setters while iterating through the collection as demonstrated below. But maybe there is better, more general way to achieve that.

public class Customer {
    private int id;
    private Collection<Phone> phoneCollection;
    public Customer() {
        phoneCollection = new ArrayList<>();
    }
//getters and setters    

}

and Phone class

public class Phone {
    private int id;
    private String number;
    private String name;
//getters and setters    
}

and

public static void main(String[] args) {
        Customer c = new Customer();

        c.addPhone(new Phone(1, "12345", "aaa"));
        c.addPhone(new Phone(2, "34567", "bbb"));
        System.out.println(c);

        Phone p = new Phone(2, "9999999", "new name");

        Collection<Phone> col = c.getPhoneCollection();
        for (Phone phone : col) {
            if (phone.getId() == p.getId()) {
//             This is working fine
//                phone.setNumber(p.getNumber());
//                phone.setName(p.getName());

//              But I'd like to replace whole object if possible and this is not working, at least not that way
                  phone = p;
            }
        }
        System.out.println(c);
    }
}

Is this possible to achieve what I want? I tried copy constructor idea and other methods I found searching the net but none of them was working like I would expect.

EDIT 1

After reading some comments I got an idea

I added the following method to my Phone class

public static void replace(Phone org, Phone dst){
    org.setName(dst.getName());
    org.setNumber(dst.getNumber());
}

and now my foreach part looks like that

    for (Phone phone : col) {
        if (phone.getId() == p.getId()) {
            Phone.replace(phone, p);
        }
    }

And it does the job. Now if I change the Phone class attributes I only need to change that method. Do you think it is OK solving the issue that way?

norbi771
  • 814
  • 2
  • 12
  • 29

5 Answers5

5

You should not modify the collection while you're iterating through it; that's likely to earn you a ConcurrentModificationException. You can scan the collection for the first object that matches your search criterion. Then you can exit the loop, remove the old object, and add the new one.

Collection<Phone> col = c.getPhoneCollection();
Phone original = null;
for (Phone phone : col) {
    if (phone.getId() == p.getId()) {
        original = phone;
        break;
    }
}
if (original != null) {
    Phone replacement = new Phone(original);
    replacement.setNumber(p.getNumber());
    replacement.setName(p.getName());
    col.remove(original);
    col.add(replacement);
}

Alternatively, you could declare a more specific type of collection, such as a List, that would allow you to work with indexes, which would make the replacement step much more efficient.

If your phone IDs are unique to each phone, you should consider using a Map<Integer, Phone> that maps each phone ID to the corresponding phone. (Alternatively, you could use some sort of third-party sparse array structure that doesn't involve boxing each ID into an Integer.) Of course, if your IDs aren't unique, then you might want to modify the above to gather a secondary collection of all matching phones (and reconsider the logic of your existing code as well).

Ted Hopp
  • 232,168
  • 48
  • 399
  • 521
  • Would be slightly cleaner IMHO if creating the replacement was done outside the loop in your 2nd if. Maybe renaming original to found? – user949300 Apr 19 '16 at 18:57
  • Would also be simpler using an iterator to delete inside the iteration whilst avoiding a `ConcurrentModificationException`. – MeetTitan Apr 19 '16 at 18:58
  • you may earn `UnsupportedOperationException` because not all subclasses implement the `remove` method (which is `default` and throws this exception) – Andrew Tobilko Apr 19 '16 at 18:59
  • @user949300 - Agreed. I made that change. – Ted Hopp Apr 19 '16 at 18:59
  • 1
    @AndrewTobilko - True. But OP's code indicates that the collection is actually an `ArrayList`, which does support `remove`. – Ted Hopp Apr 19 '16 at 19:00
  • @MeetTitan - yes, that would be more efficient. It would need an explicit iterator rather than an enhanced `for` loop. Also, as a matter of personal style, I'd prefer to keep the remove/add pair together in the code. If efficiency is an issue, a `Map` is the way to go. – Ted Hopp Apr 19 '16 at 19:02
  • Could you see my EDIT 1 and eventually comment on that please? – norbi771 Apr 19 '16 at 19:45
  • @norbi771 - Re your edit 1: that will work perfectly well if you don't mind changing the original `Phone` object already in the collection. My understanding was that you wanted to avoid that. (Perhaps because the objects in the collection are also used elsewhere?) Of course, your original code is doing pretty much the same thing. (BTW, I'd rename the method from `replace()` to something closer to what's going on--perhaps `set()` or even `setNameAndNumber()`.) – Ted Hopp Apr 19 '16 at 21:08
  • @Ted Hopp - thanks for your comment. I couldn't find good name but you're right I definitely will rename that method. What I wanted to express in my problem description is that what the code is supposed to do is not limited to update the name and the number. Actually, since I have many collections of objects in my customer object, I was looking for something opposite, the general solution that could be simply applied to any collection I have. That's way the controller code would be simple and would not change if for some reason I would have to change attributes of object in collection. – norbi771 Apr 20 '16 at 06:26
1

You can also use a Set (HashSet), this is only when you don't want to do the way Mike suggested.

Use the Phone as an item in the set. Don't forget to implement hashCode() and equals() in Phone. hashCode() should return the id, as it is supposed to be unique.

Since you are concerned about replacing the item, here's how HashSet will help you :

  1. Create an instance of your object.
  2. Remove the object you want to replace from the set.
  3. Add the new object (you created in step 1) back to the set.

Both these operations 2 & 3 are guaranteed in O(1) / constant time.

You don't need to maintain a map for this problem, that's redundant.

If you want to get the object from the collection itself and then modify it, then HashMap would be better, search is guaranteed in O(1) time.

Partha
  • 310
  • 3
  • 17
  • This would only help if he were trying to negate duplicates in his collection. – MeetTitan Apr 19 '16 at 19:22
  • Well using a HashMap is essentially the same thing. No duplicates allowed. – Partha Apr 19 '16 at 20:38
  • But what advantages would a set have over a list in this case unless the goal was to eliminate duplicate elements? – MeetTitan Apr 19 '16 at 23:34
  • In order to use a HashMap, a key-value pair has to be maintained. This mean's the Phone object is now exposed with having its Id/phone number as a Key and the object itself as a value. This problem doesn't need a key-value pair implementation, that's redundant and unnecessary. Instead, the developer just needs to find the object and replace it with a new/updated one. Since Set internally uses HashMap as a backing structure, this can be achieved in O(1) time.When using list, unless the specific index the object is present at is known beforehand(in this case it isn't), the cost of search is O(n) – Partha Apr 20 '16 at 05:05
  • All of what you say is correct, except for your last couple sentences. HashMaps do indeed have O(1) (amortized) access time, and by extension HashSets do too; however since there is no way to access an element at any given position in a set, you still need to perform the O(n) search to receive said object from your set to do anything with it. In other words you wouldn't be able to pull any object from the set without either a linear search, or having a reference to the object beforehand, which negates our problem entirely. – MeetTitan Apr 20 '16 at 16:58
  • So, what I was getting at is the "behind-the scenes" part here. The developer needn't find an object here, which is obviously O(n) for a set, instead, he can just call remove and then add - both operations happen in O(1) time. So, the headache of finding the object and modifying it is gone. The problem statement is not about modifying an existing object in the collection. Its about replacing it. If it was just about modifying, then I agree HashMap is a better choice. – Partha Apr 21 '16 at 16:35
  • I'm well aware of the "behind-the-scenes" action here. You also must be aware of the `Set` interface contract that `HashSet` is bound to. How do you propose calling `Set.remove(Object o)` without a reference to the object to be removed? A O(n) search is needed to retrieve the reference to even begin the O(1) removal process, as an object reference is needed to remove from a set. – MeetTitan Apr 21 '16 at 17:39
  • That object is the one you want to replace an existing object with. So, you create that and pass on that object. This is only in the replace scenario (not the modify scenario) I am talking about, since set doesn't really provide a replaceIfFound() – Partha Apr 21 '16 at 18:43
  • I'd like to see the `hashCode()` method you have in mind to make this work properly. – MeetTitan Apr 21 '16 at 19:01
  • It will return the id of the Phone object, as per the design, the id is supposed to be unique. – Partha Apr 21 '16 at 20:30
  • So you're never explicitly calling remove, simply relying on the duplicate removal of the set? That would work, but that did not seem like what you were getting at when you said removal from a HashSet is constant time. If you update your answer to reflect our conversation, it'll score an upvote from me. :) – MeetTitan Apr 21 '16 at 21:56
  • Thanks :) If you see my comment above - where I was explaining "behind-the-scenes" I did mention about remove and then add. Also, removal from HashSet is O(1) anyway. In our case, this is constant time when we dont have to look up for that object, just replace it. Replacing = remove +add in the set. – Partha Apr 21 '16 at 22:19
0

Instead of a list, use a map with the Phone's id as the key. Then your code looks like this:

public static void main(String[] args) {
        Customer c = new Customer();

        c.addPhone(new Phone(1, "12345", "aaa"));
        c.addPhone(new Phone(2, "34567", "bbb"));
        System.out.println(c);

        Phone p = new Phone(2, "9999999", "new name");

        Map<Integer, Phone> phoneMap = c.getPhoneMap();
        phoneMap.put(p.getId(), p);

        System.out.println(c);
}
MeetTitan
  • 3,383
  • 1
  • 13
  • 26
Mike M.
  • 9
  • 2
  • I love the idea, but why not simply `phoneMap.put(p)`? – user949300 Apr 19 '16 at 18:52
  • I have lists (or collections) in my entities and would like to stick with that. I am looking for simplest repeatable method to update object composed of some basic attributes and many collections / lists. These lists are bound to JSF components. – norbi771 Apr 19 '16 at 19:04
  • The phone map should be `Map` rather than `Map` since phone IDs are `int` values, not strings. – Ted Hopp Apr 19 '16 at 19:27
  • @TedHopp, I made that edit for him. I made the assumption that the ID was a String based on http://stackoverflow.com/a/2853253/4307644, however scrolling up I can see I've made the decision in error. I've edited his answer to reflect this. – MeetTitan Apr 19 '16 at 19:51
-1

If you take the object out from the collection and update its properties, it will get reflected in the same object in collection too.. Hence, you dont have to technically replace object after updating it. As "Mike M." pointed out, you can use hashmap to retrieve the object quickly without iteration and update the object values.

Maheswaran
  • 19
  • 4
  • That's what OP is already doing. The question is specifically about _not_ mutating the existing object in the collection. – Ted Hopp Apr 19 '16 at 19:07
-2

If order matters to you, you can change Collection to List (Since you're always using an ArrayList anyway) and then:

int index = col.indexOf(phone);
col.remove(phone);
col.add(p, index);
Zircon
  • 4,677
  • 15
  • 32
  • 1
    Trouble is, he doest have a Phone to remove, just an ID of the phone. – user949300 Apr 19 '16 at 18:50
  • He's using an enhanced `for` loop, which gives him a `Phone`. It's also possible to use `col.remove(index);` – Zircon Apr 19 '16 at 18:51
  • But `col.indexOf(theNewPhone)` will always return -1. Unless he overrides equals for Phone. – user949300 Apr 19 '16 at 18:54
  • @Zircon, what happens if there are duplicates? It is a `List` after all. – MeetTitan Apr 19 '16 at 18:54
  • He's iterating over the whole `List`, so they would all eventually be replaced anyway. In any case, my solution won't work because it causes a `ConcurrentModificationException` I think. – Zircon Apr 19 '16 at 19:00
  • @Zircon - The `Collection` interface does not have a `remove(int)` method. That only exists for ordered collections (such as `List`). – Ted Hopp Apr 19 '16 at 19:09