views:

161

answers:

6
assertEquals(def.getMengs(), exp.getMengs());

fails, reporting: expected: java.util.HashSet<[...so geht die Legende ... ...legend has it ... ]> but was: java.util.HashSet<[...so geht die Legende ... ...legend has it ... ]>

Indeed, through the debugger I see that both sets contain only one Meaning with objId = 1 for both. I expected the following code in Meaning class (@Entity) to guarantee that the above works.

@Override
public boolean equals(Object object) {
   if (!(object instanceof Meaning)) {
        return false;
   }
   Meaning other = (Meaning) object;
   if (other != null && objId == other.objId) return true;
   return false;
}

@Override
public int hashCode() {
    int hash = 7;
    hash = 67 * hash + this.objId;
    return hash;
}

Indeed, this test passes:

 db.insert(admin);
    final Meaning meng = new Meaning(admin, new Expression("essen"));
    meng.setObjId(11);
    final Meaning meng1  = new Meaning(admin, new Expression("mangiare"));
    meng1.setObjId(11);
    assertEquals(meng,meng1);

So what could be my problem? Thery are both HashSets, they are both of the same size, and the objects inside them equals. Indeed

            assertEquals(def.getMengs().iterator().next(), exp.getMengs().iterator().next());

before it passes. However this won't (but I don't know why):

assertTrue(def.getMengs().containsAll(exp.getMengs()));

So, it's the problem.

Here's the test code:

try{
            db.insertWords(toEnumMap(mengs[i],admin));
        }catch(Exception e){
            fail(e.getMessage());
        }
        final Expression exp = db.get(Expression.class, mengs[i][0]);
        testGender(exp, mengs[i][2]);

        final Expression def = db.get(Expression.class, mengs[i][1]);
        assertNotNull(def);

        assertEquals(def.getMengs().iterator().next(), exp.getMengs().iterator().next());
        assertEquals(exp.getMengs().size(), def.getMengs().size());
        assertTrue(def.getMengs().containsAll(def.getMengs()));
        assertTrue(def.getMengs().containsAll(exp.getMengs()));
        assertEquals(def.getMengs(), exp.getMengs());

db.get just wraps em.find. InsertWords should be persisting def and exp.

public void insertWords(EnumMap<Input, MemoEntity> input) throws MultipleMengsException {
    insert(input.get(Input.expression)); //INSERT OR IGNORE
    final boolean isNewDef = insert(input.get(Input.definition));

    final Expression def = get(Expression.class, input.get(Input.definition).getId());
    final Expression exp = get(Expression.class, input.get(Input.expression).getId());
    final MUser usr = get(MUser.class, input.get(Input.user).getId());

    final Set<Meaning> mengs = getMengs(usr,def,isNewDef);
    if (mengs == null) {//is new to the user
        final Meaning meng = new Meaning(usr, exp, def);
        insert(meng);

    } else { //old meaning
        if (mengs.size() > 1) throw new MultipleMengsException(mengs);
        else{
            final Meaning meng = mengs.iterator().next();
            meng.addExp(exp);
            meng.setLastPublishedDate(null); //reschedule
        }
    }
    Logger.getAnonymousLogger().log(Level.INFO, "inserted pair <{0},{1}>", new String[]{exp.getExpression(), def.getExpression()});
}

public boolean insert(final MemoEntity entity) {
        if (em.find(entity.getClass(), entity.getId()) == null) {
            et.begin();
            em.persist(entity);
            et.commit();
            return true;
        }
        return false;
    }

public <MemoEntity> MemoEntity get(final Class<MemoEntity> entityClass, final Object primaryKey) {
        return em.find(entityClass, primaryKey);
    }
A: 

The problem is that containsAll compares references to the objects (using ==) instead of calling .equals. HashSet.equals uses containsAll internally according to the documentation.

A possible solution is to derive your own class from HashSet and override the containsAll method.

dark_charlie
-1 `.containsAll()` uses `.equals()` for comparisons
NullUserException
@NullUserException, so where is the problem? You said the constant is unnecessary, but it's not the problem.
simpatico
@simpatico is your `.equals()` working?
NullUserException
@NullUserException Yeah, doesn't the code above prove it?
simpatico
@simpatico It proved it worked for that particular case.
NullUserException
@NullUserException Are you sure containsAll uses equals() in all implementations? I couldn't find that anywhere, it would be nice of you if you could post a link.
dark_charlie
@dark http://java.sun.com/j2se/1.4.2/docs/api/java/util/Set.html#contains(java.lang.Object)
NullUserException
A: 

If you hash by objId, then it shouldn't be mutable (as the method setObjId suggests). In particular, if you insist on having that method, you shouldn't call it after putting an object in a hashset.

hmm..I've added the setObjId method just for testing, now after I've had the problem. I agree it shouldn't be mutable.However you are not answering the question at all, are you?
simpatico
*If* you change objId after putting it in a HashSet (which I don't know if you do because you don't include all the code), *then* that is the problem (so I *am* answering the question, conditionally :) ).
I never change the objId, except in that test code (which is not part of the problem).
simpatico
Then the problem is likely in some code that you omitted from the question. BTW, if 'other' is an instance of 'Meaning' then it's not 'null', so you have an extra check.
I've posted all the code that could contain the problem.
simpatico
+1  A: 

If

assertTrue(def.getMengs().containsAll(exp.getMengs()));

passes, then the most likely explanation is that def.getMengs() contains all elements from exp.getMengs() plus some other not in the latter.

Try reversing it, i.e.

assertTrue(exp.getMengs().containsAll(def.getMengs()));

or simply

assertEqual(exp.getMengs().size(), def.getMengs().size());

Edit

I see I've misread your question. However, this does clarify the situation. The equals method checks for 3 things. 1) That both are of type Set. 2) Same size and 3) That "a" contains all elements from "b".

You seem to be failing that last one. Indeed, since doing containsAll on a HashSet with itself is failing it must be the equals method on Meaning. Reading the code (Java 6) for the containsAll and contains methods on Sets clearly shows that the hashCode method is not used for this purpose.

Kris
No it doesn't. The debugger showed that they have the same size, and indeed the size assert passes.
simpatico
Interestingly this fails too: assertTrue(def.getMengs().containsAll(def.getMengs()));
simpatico
+3  A: 

HashCode and equals for entities are best written without using the surrogate ID for comparison, but rather implement comparison using the business attributes of the object or just the natural keys. See this SO question for details.

EDIT: As to why this doesn't work, my only guess is that you are modifying the object after adding it to the HashSet, in particular changing the ID. This will cause the contains method to fail, since it uses the hashCode to locate the object as a key in the hashmap, and if the ID has changed, it will be looking in the wrong place in the underlying hashMap.

mdma
you don't answer the question.
simpatico
@simpatico - I was just getting round to it - checking JDK sources. please see my edit.
mdma
I don't see how changing the object (other than the hashcode or objId) cause the problem. I'll post the test code.
simpatico
I say changing the objid is what would cause the problem.
mdma
Wait, I guess you are smelling in the right place. ObjId is:@Id @GeneratedValue(strategy = GenerationType.AUTO)Which I don't control. You say it could be that JPA changes it (like having it 0 at first and then set it when persisting, and then the object in cache will not have the correct hash code?)But if equals is used then only objIds matter.
simpatico
But equals is not used, at least not before hashCode. HashCode is used under the hood to locate the key in the underlying hashMap. The properties used to compute the hashCode must not change after an object is added to a HashMap/HashSet. This is why my original advice is not to use the objectID in the hashCode/equals method for entities.
mdma
hmm...indeed if I don't ovverride hashCode() the test passes!
simpatico
Yeah, but then what should I use? I don't understand, you want me to re-implement equals and hashcode or not?I don't understand your suggestion.
simpatico
Re-implement equals and hashCode to use the other properties of the object, but not objID.
mdma
and what if I cannot? I.e. the only way to distinguish two is by the objId?
simpatico
That's pretty unusual to have an object with no business properties. What odes it mean? Even so, you can work this by ensuring they are added to the map after they have been persisted. For an object with just a persistent ID, persisting them is what gives them their identity. However, typical use is that Entities have business keys that define them, and the ID is surrogate/neutral.
mdma
Well, they don't uniquely identify each instance. Anyway not overriding them seems to do just as well. However then trying to empty the database (even with cascade all) will not work, saying it would violate a foreign key constraint.
simpatico
+1  A: 

Is your objId an Integer or Long? Then your problem is probably related to autoboxing in the equals method:

objId == other.objId

This will work for small constants like in your first test since these are cached. Generally you should use the equals method in this case. That line in the equals method could better be written as:

 return objId == null ? this == other : objId.equals(other.objId);
Jörn Horstmann
A: 

You are using hibernate entities, so you should never refer to member variables directly, as they may be lazily loaded from the database. so, your equals/hashCode method should use getObjId() instead of objId.

also, as mentioned in another post, if your objId is an object type (Integer, Long), you should not be using "==".

james