Hermetization

This is one writer's word for creating classes that don't unnecessarily leak state and open themselves to dirty programming practices. This code is an example of such I took from a DZone article by Bartłomiej Słota.

Hermetization is the essence of hiding and securing internals. This makes the code here a pretty good example to follow for providing an immutable class. The features that includes are:

Customer.java:
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

public class Customer implements Serializable
{
  private final long                  id;
  private final String                name;
  private final ZonedDateTime         creationDate;
  private final ZonedDateTime         activationDate;
  private       List< ContactPerson > contactPeople;

  private Customer( long id, String name, ZonedDateTime creationDate,
                      ZonedDateTime activationDate, List< ContactPerson > contactPeople )
  {
    this.id             = id;
    this.name           = Objects.requireNonNull( name, "Name cannot be null or empty" );
    this.creationDate   = creationDate;
    this.activationDate = activationDate;

    List< ContactPerson > cp = Objects.requireNonNull( contactPeople, "There must be a list of contacts" );
    this.contactPeople  = new ArrayList<>( Objects.requireNonNullElse( cp, new ArrayList<>() ) );

    if( name.length() < 1 )
      throw new IllegalArgumentException( "Name cannot be empty" );
  }

  public static Customer valueOf( String name, List< ContactPerson > contactPeople )
  {
    return new Customer( Sequence.nextValue(), name, ZonedDateTime.now(), null, contactPeople );
  }

  public long getId() { return id; }
  public String getName() { return name; }
  public ZonedDateTime getCreationDate() { return creationDate; }
  public List< ContactPerson > getContactPeople() { return Collections.unmodifiableList( contactPeople ); }

  public Customer activate()
  {
    return new Customer( this.id, this.name, this.creationDate, ZonedDateTime.now(),
        new ArrayList<>( this.contactPeople ) );
  }

  public boolean isActive() { return this.activationDate != null; }

  public Customer addContactPerson( ContactPerson contactPerson )
  {
    final List< ContactPerson > newContactPersonList = new ArrayList<>( this.contactPeople );
    newContactPersonList.add( contactPerson );
    return new Customer( this.id, this.name, this.creationDate, this.activationDate, newContactPersonList );
  }

  public Customer removeContactPerson( long contactPersonId )
  {
    final List< ContactPerson > newContactPersonList = new ArrayList<>( this.contactPeople );
    newContactPersonList.removeIf( it -> it.getId() == contactPersonId );
    return new Customer( this.id, this.name, this.creationDate, this.activationDate, newContactPersonList );
  }

  @Override public String toString()
  {
    return "Customer {"
        + "id=" + id
        + ", name='" + name
        + '\''
        + ", creationDate=" + creationDate
        + ", activationDate=" + activationDate
        + ", contactPeople=" + contactPeople
        + '}';
  }

  @Override public boolean equals( Object o )
  {
    if( this == o )
      return true;
    if( isNull( o ) || getClass() != o.getClass() )
      return false;

    final Customer customer = ( Customer ) o;

    if( id != customer.id )
      return false;
    if( ( nonNull( name ) )
        ? !name.equals( customer.name )
        : customer.name != null )
      return false;
    if( ( nonNull( creationDate ) )
        ? !creationDate.equals( customer.creationDate )
        : customer.creationDate != null )
      return false;
    if( ( nonNull( activationDate ) )
        ? !activationDate.equals( customer.activationDate )
        : customer.activationDate != null )
      return false;
    return( nonNull( contactPeople ) )
        ? contactPeople.equals( customer.contactPeople )
        : customer.contactPeople != null;
  }

  @Override public int hashCode()
  {
    int result = ( int ) ( id ^ ( id >>> 32 ) );
    result = 31 * result + ( nonNull( name )           ? name.hashCode()           : 0 );
    result = 31 * result + ( nonNull( creationDate )   ? creationDate.hashCode()   : 0 );
    result = 31 * result + ( nonNull( activationDate ) ? activationDate.hashCode() : 0 );
    result = 31 * result + ( nonNull( contactPeople )  ? contactPeople.hashCode()  : 0 );
    return result;
  }

  private void readObject( ObjectInputStream s ) throws IOException, ClassNotFoundException
  {
    s.defaultReadObject();

    if( isNull( contactPeople ) )
      throw new NotSerializableException( "Contact people list cannot be null" );

    if( StringUtils.isEmpty( name ) )
      throw new NotSerializableException( "Name cannot be empty" );

    contactPeople = new ArrayList<>( contactPeople );
  }
}
ContactPerson.java:
import java.io.Serializable;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

public final class ContactPerson implements Serializable
{
  final private long   id;
  final private String email;

  private ContactPerson( long id, String email )
  {
    this.id    = id;
    this.email = email;
  }

  public static ContactPerson valueOf( String email )
  {
    return new ContactPerson( Sequence.nextValue(), email );
  }

  public long getId() { return id; }
  public String getEmail() { return email; }

  @Override public String toString() { return "ContactPerson{" + "email='" + email + '\'' + '}'; }

  @Override public boolean equals( Object o )
  {
    if( this == o )
      return true;
    if( isNull( o ) || getClass() != o.getClass() )
      return false;

    final ContactPerson that = ( ContactPerson ) o;

    if( id != that.id )
      return false;
    return nonNull( email ) ? email.equals( that.email ) : isNull( that.email );
  }

  @Override public int hashCode()
  {
    int result = ( int ) ( id ^ ( id >>> 32 ) );
    result = 31 * result + ( nonNull( email ) ? email.hashCode() : 0 );
    return result;
  }

  public ContactPerson copy()
  {
    return new ContactPerson( this.id, this.email );
  }
}
Sequence.java:
import java.util.concurrent.atomic.AtomicLong;

public class Sequence
{
  private static AtomicLong currentValue = new AtomicLong( 0L );

  public static long nextValue()
  {
    return currentValue.incrementAndGet();
  }

  public static long currentValue()
  {
    return currentValue.get();
  }
}

Rules to follow for maintaining hermetization in code:

Appendix: more on @Override equals()

Imagine a class for exam scores.

import static java.util.Objects.isNull;

public class Exam
{
  private final int id;
  private final int score;

  public Exam( int id, int score )
  {
    this.id = id;
    this.score = score;
  }

  @Override
  public boolean equals( Object o )
  {
    if( o == this )
      return true;

    if( isNull( o ) || getClass() != o.getClass() )
      return false;

    if( !( o instanceof Exam ) )
      return false;

    Exam other = ( Exam ) o;

    return( other.id == id && other.score == score );
  }
}

We would test this thus the various aspects of equality:

@Test
public void test()
{
  Exam x         = new Exam( 1, 97 );
  Exam y         = new Exam( 1, 97 );
  Exam z         = new Exam( 1, 97 );
  Exam different = new Exam( 5, 89 );

  // difference comparison
  System.out.println( x.equals( different ) ); // false

  // reflexive
  System.out.println( x.equals( x ) );         // true

  // symmetric
  System.out.println( x.equals( y ) );         // true
  System.out.println( y.equals( x ) );         // true

  // transitive
  System.out.println( x.equals( y ) );         // true
  System.out.println( y.equals( z ) );         // true
  System.out.println( x.equals( z ) );         // true

  // consistent
  System.out.println( x.equals( y ) );         // true
  System.out.println( x.equals( y ) );         // true

  // null
  System.out.println( x.equals( null ) );      // false
}

Appendix: more on @Override hashCode()

Hash codes for an object must (should) be constant while the data that is factored into the hash code remains unchanged. This usually means that the hash code for a given object remains constant as long as the state of that object is unchanged.

Hash codes must (should) be equal for objects that are equal as compared by the equals() method.

Hash codes for two (different) objects may not be unequal if the two are not equal according to their equals() methods. However, algorithms and data structures that rely upon hash codes usually perform better when unequal objects also result in unequal hash codes. (So be clever.)

Hash codes should be an ordered summation of the values of the object's fields as shown in code above. This is accomplished by multiplying each component (field) in the product/addition (per Bloch's Effective Java, page 52 in the Third Edition). Use the factor 31. This was chosen, apparently, because 31 is an odd prime. If it were even and the multiplication overflowed, information would be lost, because multiplication by 2 is equivalent to simple shifting. Besides, primes are magic—like eyes of newts and lark vomit. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance (on some architectures).

In the Exam class above, the hashCode() might be coded:

@Override
public int hashCode()
{
  return( 31 * id ) + score;
}

Note that Objects offers a method that reduces the clutter of implementing hashCode() and improves readability (though not without a price in a) the processing of a variable number of arguments and in the boxing that has to be done when instance variables are primitives).

@Override
public int hashCode()
{
  return Objects.hash( id, score );
}