Java Class Design

Implement encapsulation

Encapsulation means keeping the internal workings of your code safe from other programs that use it, allowing only what you choose to be accessed. This makes the code that uses the encapsulated code much simpler and easier to maintain since much of the complexity of the latter is hidden.

The main benefit of encapsulation is that it protects a class from being used in a way that it wasn't meant to be. By controlling the way the code is used, it can only ever do what you want it to do.

For example, if we set a variable like this:

car.model = 2015;

We can't prevent an invalid assignment like this:

car.model = 2343242;

To implement encapsulation, we set up the class so only its methods can refer to its instance variables. External code will access these private instance variables only through public get/set methods. This is a convention used in reusable components called JavaBeans. The rules are:

  • Instance variables are private
  • Getter methods begin with get if the property is not a boolean, otherwise, they begin with is
  • Setter methods begin with set
  • The method name starts with get/is/set followed by the name of the instance variable with its first letter in uppercase

So if we have a class like the following:

public class Car {
  int model;
  String name;
  String color;
}

The encapsulated version would look like this:

public class Car {
  private int model;
  private String name;
  private String color;

  public int getModel() {
    return model;
  }
  public String getName() {
    return name;
  }
  public String getColor() {
    return color;
  }
  public void setModel(int model) {
    this.model = model;
  }
  public void setName(int name) {
    this.name = name;
  }
  public void setColor(int color) {
    this.color = color;
  }
}

Notice the use of this in the setter methods. The parameter name can be anything, but if it's the same as the instance variable's name, this (that references the instance) must be used to differentiate between them.

Of course, just like that, in the surface this code does the same as the non-encapsulated version, but by using a method instead of getting/setting the instance variable directly, we can add something like a validation without breaking the external code:

public void setModel(int model) {
  if(model >= 2000 && model <= 2015) {
    this.model = model;
  } else {
    this.model = 2000;
  }
}

Now, if we set an invalid value:

car.setModel(2343242);

We'll get a default and valid value that will protect the class from being used in a way it wasn't meant to be.

Encapsulation can also be used with constructors and methods of a class. The key thing is to restrict the access to any member of the class that can break things when something changes or that doesn't want to be exposed.

 

Implement inheritance including visibility modifiers and composition

Inheritance

With inheritance, you created classes based on the other classes so they can get the attributes and methods from the base class to reuse them and even redefine them.

The keyword extends is used to make a new class that derives from an existing class. The base class is called the superclass and the new class is called the subclass.

Let's begin for example with this class:

public class Animal {
  public void eat() { /* Do something */ }
}

A subclass can be:

public class Dog extends Animal {
}

By inheriting from Animal, the Dog class automatically gets the method eat(). We can add a method to the class:

public class Dog extends Animal {
  public void bark() { /* Do something */ }
}

So now Dog has two methods, eat() that is inherited from Animal and bark() that is specific to the Dog class. Animal still has one method, inheritance only works from the superclasses to the subclasses.

Not everything can be inherited, and this depends directly from the used visibility modifiers.

When a subclass extends a superclass in Java, all protected and public attributes and methods of the superclass are inherited by the subclass. Attributes and methods with default (package) access modifiers can be accessed by subclasses only if the subclass is located in the same package as the superclass. Private attributes and methods of the superclass are not inherited.

Modifier Inherited
public Yes
protected Yes
default Only for subclasses in the same package
private No

When you create a subclass, the methods in the subclass cannot have less accessible access modifiers assigned to them than they had in the superclass. For instance, if a method in the superclass is public then it must be public in the subclass too.

In Java, a class is only allowed to inherit from a single superclass (singular inheritance). In some programming languages, like C++, it is possible for a subclass to inherit from multiple superclasses (multiple inheritance).

Composition

You do composition by having an instance of another class as a field of your class instead of extending.

When to use which?

  • If there is an IS-A relation, inheritance is likely to be preferred.
  • If there is a HAS-A relationship, composition is preferred.

For example:

class Toy {} 

class Animal{} 

// Cat is an Animal, so Cat class extends Animal class.
class Cat extends Animal { 
 // Cat has a Toy, so Cat has an instance of Toy as its member.
 private Toy toy; 
}

Favoring composition over inheritance is a popular object-oriented design principle that helps to create flexible and maintainable code. For example, composition facilitates testing. If one class is composed of another class, you can easily create a mock object representing the composed class.

 

Implement polymorphism

With inheritance, you can take advantage of polymorphism. Polymorphism is based on the fact that you can use a variable of a superclass type to hold a reference to an object whose class is any of its subclasses, for example:

Animal animal = new Dog();

This way, polymorphism allows the program to act differently based on the object performing the action. For example:

class Animal {
  public void eat() {
    System.out.println("Animal Food");
  }
}
class Dog extends Animal {
  public void eat() {
    System.out.println("Dog Food");
  }
}
class Cat extends Animal {
  public void eat() {
    System.out.println("Cat Food");
  }
}
public class Test {
  public static void main(String args[]) {
    Animal a = new Dog();
    System.out.println(a.eat());
    a = new Cat();
    System.out.println(a.eat());
  }
}

The first System.out.println(a.eat()); will print Dog Food since in that moment, the a reference is holding a Dog object. In the second call, it will print Cat Food, since now the reference holds a Cat object.

So polymorphism can help make code easier to change. If you have a fragment of code that uses a variable of a superclass type, such as Animal, you could later create a brand new subclass with another behavior, such as Spider, polymorphism will ensure that Spider's implementation of Animal's method gets executed and the fragment will work without change.

 

Override hashCode, equals, and toString methods from Object class

equals

From javadoc:

public boolean equals(Object obj) Indicates whether some other object is "equal to" this one.
The equals method implements an equivalence relation on non-null object references:

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.
  • The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).

It is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

hashCode

From javadoc:

public int hashCode() Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by HashMap. The general contract of hashCode is:

  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
  • If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
  • It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
  • As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by Java.)

The relation between the two methods is:

Whenever a.equals(b), then a.hashCode() must be same as b.hashCode().

toString

From javadoc:

public String toString() Returns a string representation of the object. In general, the toString method returns a string that "textually represents" this object. The result should be a concise but informative representation that is easy for a person to read. It is recommended that all subclasses override this method. The toString method for class Object returns a string consisting of the name of the class of which the object is an instance, the at-sign character '@', and the unsigned hexadecimal representation of the hash code of the object. In other words, this method returns a string equal to the value of:

getClass().getName() + '@' + Integer.toHexString(hashCode())

The following is an example of an implementation of these methods:

 public class Person {
   private final String lastName;
   private final String firstName;
   private final boolean female;

   @Override
   public boolean equals(Object obj)
   {
      if (obj == null)
      {
         return false;
      }
      if (getClass() != obj.getClass())
      {
         return false;
      }
      final Person other = (Person) obj;
      if ((this.lastName == null) ? (other.lastName != null) : !this.lastName.equals(other.lastName))
      {
         return false;
      }
      if ((this.firstName == null) ? (other.firstName != null) : !this.firstName.equals(other.firstName))
      {
         return false;
      }
      if (this.female != other.female)
      {
         return false;
      }
      return true;
   }

   @Override
   public int hashCode()
   {
      int hash = 3;
      hash = 19 * hash + (this.lastName != null ? this.lastName.hashCode() : 0);
      hash = 19 * hash + (this.firstName != null ? this.firstName.hashCode() : 0);
      hash = 19 * hash + (this.female ? 1 : 0);
      return hash;
   }

   @Override
   public String toString()
   {
      return  "Person{" + "lastName=" + lastName + ", firstName=" + firstName
            + ", female=" + female +  '}';
   }
 }

 

Create and use singleton classes and immutable classes

Singleton

Singleton is a design pattern that provides a way for a class to create only one object from that class.

The key for a Singleton class is to make the constructor private, have a static instance of itself and a method to access it:

public class Singleton {
  //Create an object
   private static final Singleton instance = new Singleton();

   //Make the constructor private so that this class cannot be instantiated
   private Singleton(){}

   //Get the only object available
   public static Singleton getInstance(){
      return instance;
   }
}

The line private static final Singleton instance = new Singleton(); is only executed when the class Singleton is actually used. This guaranteed the instantiation to be thread safe. To use the singleton class:

Singleton s = Singleton.getInstance();

The other ways to build a Singleton class are:

Using an Enum

public enum Singleton{
    INSTANCE;
}

Locking /Lazy loading with Double checked Locking

public class Singleton{
     private static volatile Singleton instance;

     private Singleton(){}

     public static Singleton getInstance(){
         if(instance == null){
            synchronized(Singleton.class){
                //double checking Singleton instance
                if(instance == null){
                    instance = new Singleton();
                }
            }
         }
         return instance;
     }
}

Immutable objects

Immutable objects are simply objects whose state (data) cannot change after construction, for examples the String class. They are useful in concurrent applications, since they cannot change state, they cannot be corrupted by threads.

There are several ways for creating immutable objects:

  • Don't provide "setter" methods — methods that modify fields or objects referred to by fields.
  • Make all fields final and private.
  • Don't allow subclasses to override methods. The simplest way to do this is to declare the class as final.
  • Make the class a Singleton
  • If the instance fields include references to mutable objects, don't allow those objects to be changed:
    • Don't provide methods that modify the mutable objects.
    • Don't share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods.

 

Develop code that uses static keyword on initialize blocks, variables, methods, and classes

A static member belongs to the class rather than to an instance of the class.

Static Initialize Blocks

A static block is used to initialize a static variable or execute some initialize code since the block is executed at the time of the class loading, before any constructors or methods.

class Block {  
  static {  
    System.out.println("static block executed"); 
  }

  Block() {
    System.out.println("constructor executed"); 
  }

  public static void main(String args[]) {
    new Block();
    System.out.println("main method executed"); 
  }
}

The output is:

static block executed
constructor executed
main method executed

Static blocks are executed in the order they are defined.

Statics Variables

A static variable is used to refer a common property of all objects or instances (something that is not unique for each object) of a class. It's initialized at the time of class loading.

public class Man {
  String name;
  static String gender = "M";

  Man(String name) {
    this.name = name;
  }

  void display() {
    System.out.println(name+" "+name+" "+gender+" "+gender);
  }  

  public static void main(String args[]) {
    Man m1 = new Man("Bob");
    Man m2 = new Man("Richard");

    m1.display();
    m2.display();
  }
}

The output is:

Bob M
Richard M

Static Method

A static method also belongs to the class rather than object of a class and can be invoked without the need for creating an instance of a class. The only restrictions are:

  1. A static method can only access another static member.
  2. this and super cannot be used in a static method.

    public class Square {  
     static int calculate(int x){  
     return x*x;  
    }  
    
    public static void main(String args[]){  
     int result = Square.calculate(9);  
     System.out.println(result);  
    }  
    }
    

Static Classes

We can define a class within another class. Such a class is called a nested class. We can't make a top level class static. Only nested classes can be static.

class OuterClass {
   // Static nested class
   public static class NestedStaticClass{
       public void print() { 
         System.out.println("Message from nested static class"); 
       }
    }
} 

public class Test {
    public static void main(String args[]) {
       // create instance of nested Static class
       OuterClass.NestedStaticClass sc = new OuterClass.NestedStaticClass();

       // call non static method of nested static class
       sc.print();
    }
}

The characteristics of a static nested class are:

  • Nested static classes don't need a reference to their outer class (the enclosing class)
  • Only static members of the outer class are accessible in a nested static class

 

Content