{"id":"code-dev-java-hashcode-matches-equals","audience":"developers","category":"coding","track":"coding","technique":"context","title":"Update equals and hashCode together for a new rule","free":false,"task":"The mailing-list job sends duplicates because Member overrides equals but not hashCode. Product also just decided that team no longer matters: the same email (ignoring case) is the same person, whatever the team. Update equals to compare only email with equalsIgnoreCase, and add a matching hashCode using email.toLowerCase(Locale.ROOT). Return only the two methods, each with @Override, in exactly 1 code block of at most 12 lines, with under 20 words of text.","materialLabel":"The class and the bug report (attached to your prompt automatically)","material":"// Member.java  (Java 21, newsletter-service)\npackage com.quillpost.members;\n\nimport java.util.Locale;\nimport java.util.Objects;\n\npublic final class Member {\n    private final String email;\n    private final String team;\n    private final String nickname;   // display only\n\n    public Member(String email, String team, String nickname) {\n        this.email = Objects.requireNonNull(email);\n        this.team = Objects.requireNonNull(team);\n        this.nickname = nickname;\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (this == other) return true;\n        if (!(other instanceof Member m)) return false;\n        return email.equalsIgnoreCase(m.email) && team.equals(m.team);\n    }\n\n    public String email() { return email; }\n    public String team() { return team; }\n    public String nickname() { return nickname; }\n}\n\n// Bug QP-88 (reported by Hana):\n// Set<Member> recipients = new HashSet<>();\n// recipients.add(new Member(\"Ravi@quillpost.test\", \"design\", \"Ravi\"));\n// recipients.add(new Member(\"ravi@quillpost.test\", \"design\", \"RJ\"));\n// recipients.size()  ->  2   (expected 1, Ravi got the newsletter twice)\n//\n// Note: we can't switch Member to a record, the JSON mapper needs this shape.","rules":[{"type":"codeBlocks","count":1},{"type":"maxCodeLines","max":12},{"type":"maxProseWords","max":20}],"mistakes":[{"title":"Not passing on the product decision","why":"The AI only sees the current equals, which still compares team.","fix":"Add: \"Team no longer matters: equals compares only email, ignoring case.\"","checks":["r0"]},{"title":"A hashCode that disagrees","why":"Hashing team, or the raw email, breaks HashSet when equals ignores case.","fix":"Add: \"hashCode uses only email.toLowerCase(Locale.ROOT).\"","checks":["r1"]},{"title":"A rewritten class","why":"The AI reprints the class, sometimes as a record, which the JSON mapper can't use.","fix":"Add: \"Return only equals and hashCode, each with @Override.\"","checks":["r2","maxCodeLines"]},{"title":"An essay on the contract","why":"The AI explains equals and hashCode at length.","fix":"Add: \"One code block, no text.\"","checks":["codeBlocks","maxProseWords"]}],"promptHints":[{"label":"Passes on the new rule","patterns":["team (no longer|doesn'?t) matter","only email","ignore team"]},{"label":"Handles case","patterns":["lower","case","Locale"]},{"label":"Names the helper","patterns":["Objects\\.hash"]},{"label":"Asks for only the two methods","patterns":["only.{0,25}(hashcode|methods)","just.{0,25}(hashcode|methods)"]}],"hint":"equals and hashCode must agree, and only you know the new product rule. Tell the AI which field counts now, how case is handled, and that you want only the two methods.","rubricLabels":["equals compares only email, ignoring case (email.equalsIgnoreCase(m.email)), and no longer compares team or nickname","hashCode uses only the lower-cased email (email.toLowerCase(Locale.ROOT)), not team or nickname, so emails that differ only in case get the same hash","Returns only the equals and hashCode methods, each with @Override, without reprinting the rest of the class or turning it into a record"]}