Re: [EqualsVerifier] toString throws ClassCastException when field id is null

250 views
Skip to first unread message

Jan Ouwens

unread,
Apr 30, 2013, 2:02:39 PM4/30/13
to equalsv...@googlegroups.com
Hi Nathan,

From the information you send me, I can't figure it out either, I'm afraid...
I see you use the .debug() method. Can you send me the console output that this gives? Also, it would help if you can send me a self-contained class that reproduces the problem. Right now, I'm not sure what the type of id is (it's obviously not an int since it can be null), or how some of the other types you use will influence things (especially EmailContact looks interesting).


Regards,
Jan




On 29 April 2013 21:39, Nathan Anderson <nat...@melinate.com> wrote:
I'm getting the following error and I have not been able to find the cause. I don't see anything that could cause a ClassCastException. This error occurs when using EqualsVerifier 1.1.3 and 1.2. I can make the error "go away" buy suppressing null warnings, but that doesn't seem like a solution.

Thanks for any help,
Nathan

-----------------------------------------------------------------------------------
java.lang.AssertionError: toString throws ClassCastException when field id is null.
at nl.jqno.equalsverifier.util.Assert.fail(Assert.java:80)
at nl.jqno.equalsverifier.NullChecker$NullPointerExceptionFieldCheck.exceptionThrown(NullChecker.java:111)
at nl.jqno.equalsverifier.NullChecker$NullPointerExceptionFieldCheck.handle(NullChecker.java:102)
at nl.jqno.equalsverifier.NullChecker$NullPointerExceptionFieldCheck.execute(NullChecker.java:84)
at nl.jqno.equalsverifier.FieldInspector.check(FieldInspector.java:37)
at nl.jqno.equalsverifier.NullChecker.check(NullChecker.java:45)
at nl.jqno.equalsverifier.EqualsVerifier.verifyWithoutExamples(EqualsVerifier.java:374)
at nl.jqno.equalsverifier.EqualsVerifier.performVerification(EqualsVerifier.java:362)
at nl.jqno.equalsverifier.EqualsVerifier.verify(EqualsVerifier.java:334)
at com.sumware.hynts.model.ModelTests.testCustomer(ModelTests.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

-----------------------------------------------------------------------------------
Here is the relevant portions of my Customer object:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((customerNumber == null) ? 0 : customerNumber.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Customer other = (Customer) obj;
if (customerNumber == null) {
if (other.customerNumber != null)
return false;
} else if (!customerNumber.equals(other.customerNumber))
return false;
return true;
}

@Override
public String toString() {
String toString = "Customer [id=" + (id == null ? "null" : id.toString()) + ", createdTimestamp=" + createdTimestamp
+ ", customerNumber=" + customerNumber + ", lastName=" + lastName + ", firstName=" + firstName + ", emailContact="
+ (emailContact == null ? "null" : emailContact.getEmailAddress()) + ", phoneContacts=" + toString(phoneContacts)
+ "]";
System.out.println(toString);
return toString;
}

private String toString(Map<PhoneType, PhoneContact> phoneContacts) {
StringBuilder builder = new StringBuilder();
if (phoneContacts != null) {
builder.append("[");
for (Entry<PhoneType, PhoneContact> entry : phoneContacts.entrySet()) {
if (builder.length() > 1) {
builder.append(", ");
}

builder.append(entry.getKey()).append("=");
if (entry.getValue() != null) {
builder.append(entry.getValue().getPhoneNumber());
if (!Strings.isNullOrEmpty(entry.getValue().getPhoneExtention())) {
builder.append("x").append(entry.getValue().getPhoneExtention());
}
}
}
builder.append("]");
}
return builder.toString();
}

-----------------------------------------------------------------------------------
Here is my test:

@Test
public void testCustomer() throws Exception {
EqualsVerifier.forClass(Customer.class)
.usingGetClass()
.withPrefabValues( EmailContact.class,
new EmailContact("te...@test.com"),
new EmailContact("te...@test.com"))
.debug()
.verify();
}

--
You received this message because you are subscribed to the Google Groups "equalsverifier" group.
To unsubscribe from this group and stop receiving emails from it, send an email to equalsverifie...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Nathan Anderson

unread,
Apr 30, 2013, 8:17:59 PM4/30/13
to equalsv...@googlegroups.com
Hi Jan, 

Thanks for the reply. The error message I pasted is the output from .debug().  Here are the classes in question:


I tried adding some null safety to the toString method (even though I'm thinking it shouldn't matter).  I also tried making the equals() and hashCode() for EmailContact exclude any reference to Customer.

Hopefully that info helps...

Thanks,
Nathan

Jan Ouwens

unread,
May 1, 2013, 4:58:29 PM5/1/13
to equalsv...@googlegroups.com
Hi Nathan,

OK, this one took a while to figure out, but I found it.

In order to "do its thing", EqualsVerifier creates instances of objects on the fly, bypassing the constructor and filling the fields with "best guess" values. As a rule, this works fine, but it has a hard time with the phoneContacts map. Due to type erasure, it doesn't know the appropriate types of the keys and values, and simply puts in strings. Obviously, this does not go well when iterating over the values and calling methods on them... The exception gets thrown at line 175 of your Customer pastebin. It calls entry.getValue().getPhoneNumber(). But entry.getValue() is now a String... obviously, this is Not Good™.

Unfortunately, because of the aforementioned type erasure, there's no real way to solve this. There are a few ways to work around it, though, especially since the problem, in this case, only occurs in toString.

On my end, I can (and probably will) remove the toString checks. In version 1.2, I changed the way errors are reported, so that something sensible is printed even if toString throws an exception. So it's kind of pointless to check toString for exceptions.

In the mean time, there are also two things you could do:

* Add prefabValues for the Map:

Map<PhoneType, PhoneContact> red = new HashMap<PhoneType, PhoneContact>();
Map<PhoneType, PhoneContact> black = new HashMap<PhoneType, PhoneContact>();
black.put(new PhoneType(1), new PhoneContact("x", "y", new PhoneType(1)));

EqualsVerifier
    .forClass(Customer.class)
    .usingGetClass()
    .withPrefabValues(EmailContact.class,
        new EmailContact("te...@test.com"),
        new EmailContact("te...@test.com"))
    .withPrefabValues(Map.class, red, black)
    .suppress(Warning.NONFINAL_FIELDS)
    .debug().verify();

* Use PhoneContact's toString method in Customer's toString, by replacing lines 175-178 with the following line:

builder.append(entry.getValue());


I realize that neither option may be exactly what you want. I've created an issue ( https://code.google.com/p/equalsverifier/issues/detail?id=79 ) that you can follow if you like. It could be a while, though, before I get around to actually fixing it.


Hope this helps!
Jan

Nathan Anderson

unread,
May 1, 2013, 5:16:53 PM5/1/13
to equalsv...@googlegroups.com
Glad you were able to find the problem, thanks for your help.  I forgot to include the code for PhoneType, but I'm guessing you figured out that it is simply an Enum (MOBILE, HOME, etc). I don't expect I'd run across this issue many other places in my project, so adding prefab values wouldn't be a big deal. I'll have to examine the implications of making the proposed toString change...

Thanks again for the explanation.

Nathan

Tim van Heugten

unread,
May 2, 2013, 3:25:54 AM5/2/13
to equalsv...@googlegroups.com
Hi Jan,

I'm not sure this is what you need, or whether you are not already aware of this, but since I recently needed to figure out the generic type arguments myself I thought maybe I should share here how to do it.
It is in fact possible to find out the type arguments using reflection (and reflection is already used extensively right?).

public class TypeArgumentsDetector {

    private Map<String, TypeArgumentsDetector> phoneContacts;
    private List<Long> longs;
    private Long id;

    public static void main(String[] args) {
        detectType(TypeArgumentsDetector.class);
    }

    private static void detectType(Class<TypeArgumentsDetector> clazz) {
        for (Field field : clazz.getDeclaredFields()) {
            Type type = field.getGenericType();
            if (type instanceof ParameterizedType) {
                Type[] types = ((ParameterizedType)type).getActualTypeArguments();
                System.out.println(field.getType() + " of: " + Arrays.toString(types));
            } else {
                System.out.println(type);
            }
        }
    }
}

The elements in the Type[] types are Class instances, so the can be cast to apply further reflection operations on them.

Best,

Tim



Groet,

Tim van Heugten

Jan Ouwens

unread,
May 2, 2013, 4:01:23 PM5/2/13
to equalsv...@googlegroups.com
Hi Tim,

I was vaguely aware that this is possible, but I admit I never actually tried it. Looks interesting, thanks!
However, I'm not sure how much it'll help me. Of course this works for types I know in advance, like Map and List, because, once I know the generic types, I know how to populate them: using "put" for Maps, and using "add" for Lists.
For user-defined generic classes, where I don't know in advance how they should be populated (do I use the constructor? do I use a setter?), this approach may not work. But I'd have to try it to be sure. Anyway thanks for the suggestion!


Jan

Reply all
Reply to author
Forward
0 new messages