tags:

views:

152

answers:

4

I have a List of Foo Objects. If a name appears multiple times, I want to do something to the first Item with that name.

HashMap<String, Foo> map = new HashMap<String, Foo>();
for (Foo bar: this.FooList)
{
    if (!map.containsKey(bar.getName()))
    {
        map.put(bar.getName(), bar);
    }
    else
    {
        map.get(bar.getName()).doSomeThing();
    }
}

But this is not working, because every name (unique or not) gets thrown in that map. Does the HashMap check only for reference equality and not equality on key objects?

A: 

What exactly is the problem? The code adds the Foo if it hasn't been encountered before, and invokes an action on the Foo if it has been encountered before. What were you expecting it to do?

To answer your question at the end, HashMap relies on the hashCode and equals methods of the key objects.

UPDATE: Are you trying to invoke doSomeThing only on repeated Foos? If the name property of a Foo defines it's identity (and you properly implemented hashCode() and equals()) you should use R. Bemrose's solution. If name is not a unique property of Foos, the following code might help:

final Map<String, Foo> firstFooByName = new HashMap<String, Foo>();
final List<String> dupeNames = new ArrayList<String>();
for (final Foo foo: myFoos) {
    final String fooName = foo.getName();
    if (!firstFooByName.containsKey(fooName)) {
        firstFooByName.add(fooName)
    } else {
        dupeNames.add(fooName);
    }
}

for (final String fooName : dupeNames) {
    firstFooByName.get(fooName).doSomeThing();
}
Hank Gay
The call to `Map#containsKey()` is unnecessary here. Just call `Map#put()` and test the return value against null -- assuming that none of the values in the map are permitted to be null. That saves one hash lookup, bucket traversal, and key comparison per item, as the same work done by `containsKey()` must be done to prepare for `put()`.
seh
I think `containsKey` is the most straightforward way to capture the programmer's intent, and it doesn't rely on implicit constraints on the allowed values (since some `Map` implementations allow `null` values). Unless I had profiler output proving this was a hotspot, I wouldn't waste my time "optimizing" it into a less clear version.
Hank Gay
With more time to reflect, my suggestion above is inappropriate. I was thinking of `Set#add()` and `ConcurrentMap#putIfAbsent()`, both of which are idempotent. The original question here wanted to add the item to the map only if the key didn't yet exist; my misguided proposal above would mistakenly replace the value associated with a given key, only after which the caller could detect that the key had been present before. Sorry for the confusion.
seh
A: 

HashMap uses the .equals() method on the key object.

Peter Recore
and equals for String checks the value equality rather than the reference equality, so things *should* work as expected.
R. Bemrose
HashMap will use a combination of `hashcode` and `equals`.
pjp
+1  A: 

This is the code you need:

 HashMap<String, Foo> map = new HashMap<String, Foo>();
 for (Foo bar: this.FooList)
 {
  if (!map.containsKey(bar.getName()))
  {
   map.put(bar.getName(), bar);
  }
  else
  {
   Foo foo = map.get(bar.getName());
   if (foo != null)
    foo.doSomeThing();
   map.put(bar.getName(), null);
  }
 }

Here's a testbed for it:

import java.util.HashMap;
import java.util.ArrayList;

public class Example
{
    public static void main(String[] args)
    {
     new Example().run();
    }

    private ArrayList<Foo> FooList = new ArrayList<Foo>();

    public void run()
    {
     FooList.add(new Foo("abc", 1));
     FooList.add(new Foo("abc", 2));
     FooList.add(new Foo("def", 3));
     FooList.add(new Foo("abc", 4));
     FooList.add(new Foo("abc", 5));
     FooList.add(new Foo("ghi", 6));
     FooList.add(new Foo("def", 7));

     HashMap<String, Foo> map = new HashMap<String, Foo>();
     for (Foo bar: this.FooList)
     {
      if (!map.containsKey(bar.getName()))
      {
       map.put(bar.getName(), bar);
      }
      else
      {
       Foo foo = map.get(bar.getName());
       if (foo != null)
        foo.doSomeThing();
       map.put(bar.getName(), null);
      }
     }
    }

    class Foo
    {
     public Foo(String name, int i)
     {
      this.name = name;
      this.i = i;
     }

     public String getName()
     {
      return name;
     }

     public void doSomeThing()
     {
      System.out.println(getName() + " " + i);
     }

     private String name;
     private int i;
    }
}

Output is:

abc 1
def 3
Mark Byers
Why not use the interface type `Map<String,Foo>` in your declaration?
pjp
What are the cases for `foo` being `null` in the `else` case. I'd expect a `NullPointerException` in `bar.getName()` if `bar` was `null`. The only other reasons would be inconsistent `Map.get()` and `Map.containsKey()` methods unless the map is being modified by multiple threads.
pjp
pjp: No reason for using HashMap: I just copied his code. You're right that it could be changed to Map.
Mark Byers
Thanks. I'm not sure why it's working now ... but thanks ;)
Marcel Benthin
+1  A: 

Hmmm... if Foo implements equals() so that it only checks name, you could always do something like this:

Set<Foo> set = new HashSet<Foo>();
for (Foo bar: this.FooList)
{
    if (!set.add(bar)) {
       bar.doSomething();
    }
}

Which works because set.add(bar) will run bar.equals against every element already in the set, and return false if any of them are equal.

Edit: Since this is a HashSet, you should also implement hashCode(). Heck, you should always implement hashCode() if you're overriding equals anyway.

R. Bemrose
A couple of caveats: it needs to implement `hashCode()` and `equals()`; the equality checks must depend only on the `name` property.
Hank Gay
Yeah, I should have mentioned `hashCode()` since this is a `HashSet`. Whoops. I kind of assumed that if `equals()` was implemented, it would just be on name, which is kind of a naive assumption.
R. Bemrose