How to write immutable Java classes? -- Core Java Best Practices -- Part 2
This is an extension to Java OO Interview Questions and Answers where demonstrated a simple example as to how to design good OO classes, but those classes were not using the best practices defined in Core Java Best Practices -- Part 1. The following questions were raised.
1) Why does the Employee class need to be mutable?
2) Why aren't the roles defensively copied?
3) Why would the Employee need to know how to add and remove roles?
4) Waiter and Manager are placed in a collection but don't override hashcode and equals. That will cause the contains method on a List to not behave as expected.
5) You check if the role is null then throw an IllegalArgumentException, that should instead be a NullPointerException.
6) The code that checks for null roles being added is duplicated, thus defeating the DRY principle.
Here are the classes with the best practices.
Step 1: Immutable Employee class
package com.mycompany.app15; import groovy.transform.Immutable; import java.util.Collection; import java.util.Collections; @Immutable public class Employee { private final String name; private final Collection<Role> roles; public Employee(String name, Collection<Role> roles) { this.name = name; this.roles = roles; } public String getName() { return name; } public Collection<Role> getRoles() { return Collections.unmodifiableCollection(roles); //returns immutable collection } }
Step 2: Role interface same as before.
package com.mycompany.app15; public interface Role { public String getName(); public void perform(); }
Step 3: The Immutable Manager and Waiter classes. They also have the equals( ) and hashCode( ) methods added as they are add to a collection.
package com.mycompany.app15; import groovy.transform.Immutable; @Immutable public final class Waiter implements Role { private final String roleName; public Waiter(String roleName) { this.roleName = roleName; } @Override public String getName() { return this.roleName; } @Override public void perform() { System.out.println(roleName); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((roleName == null) ? 0 : roleName.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; Waiter other = (Waiter) obj; if (roleName == null) { if (other.roleName != null) return false; } else if (!roleName.equals(other.roleName)) return false; return true; } @Override public String toString() { return "Waiter [roleName=" + roleName + "]"; } }
package com.mycompany.app15; import groovy.transform.Immutable; @Immutable public final class Manager implements Role { private final String roleName; public Manager(String roleName) { this.roleName = roleName; } @Override public String getName() { return this.roleName; } @Override public void perform() { System.out.println(roleName); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((roleName == null) ? 0 : roleName.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; Manager other = (Manager) obj; if (roleName == null) { if (other.roleName != null) return false; } else if (!roleName.equals(other.roleName)) return false; return true; } @Override public String toString() { return "Manager [roleName=" + roleName + "]"; } }Step 3: Finally, the modified Restaurant class.
package com.mycompany.app15; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class Restaurant { public static void main(String[] args) { Role waiter = new Waiter("waiter"); Role manager = new Manager("manager"); List<Role> roles = new ArrayList<Role>(); roles.add(waiter); roles.add(manager); Employee emp1 = new Employee("Bob", roles); List<Role> roles2 = new ArrayList<Role>(); roles2.add(waiter); Employee emp2 = new Employee("Jane", roles2); System.out.println(emp1.getName() + " has roles "); Collection<Role> rolesRetrieved = emp1.getRoles(); for (Role role : rolesRetrieved) { role.perform(); } System.out.println(emp2.getName() + " has roles "); rolesRetrieved = emp2.getRoles(); for (Role role : rolesRetrieved) { role.perform(); } //can't add new roles now as getRoles return unmodifiable collection emp1.getRoles().add(waiter); //java.lang.UnsupportedOperationException is thrown } }
Q. Can you still make any further improvements?
A. Yes, use log4j instead of System.out.println().
Labels: Best Practice, Core Java