Advanced Java Class Design

Develop code that uses abstract classes and methods

An abstract class is a class that is declared abstract. Abstract classes cannot be instantiated, only subclassed.

abstract class Animal {
  public void eat() {}
}

An abstract method is a method that is declared without an implementation, like this:

public abstract void method(int arg);

If a class includes an abstract method, it has to be declared abstract. When an abstract class is subclassed, if the subclass doesn't provide an implementation for the inherited abstract methods, it has to be declared abstract as well.

abstract class Animal {
  public void eat() {
   /** Do something */
  }
  public abstract makeSound();
}
class Dog extends Animal {
  public makeSound() {
    System.out.println("Bark");
  }
}

If an abstract class has static members, you can use them with a class reference (AbstractClass.staticMethod()) as you would with any other class.

Abstract classes are similar to interfaces. However, with abstract classes, you can declare fields that are not static and final, and define public, protected, and private concrete methods. With interfaces, all fields are automatically public, static, and final, and all methods that you declare or define are public. In addition, you can extend only one class, whether or not it is abstract, whereas you can implement any number of interfaces.

Use abstract classes when:

  • You want to share code among several closely related classes.
  • You expect that classes that extend your abstract class have many common methods or fields, or require access modifiers other than public (such as protected and private).
  • You want to declare non-static or non-final fields. This enables you to define methods that can access and modify the state of the object to which they belong.

Use interfaces when:

  • You expect that unrelated classes would implement your interface. For example, the interfaces Comparable and Cloneable are implemented by many unrelated classes.
  • You want to specify the behavior of a particular data type, but not concerned about who implements its behavior.
  • You want to take advantage of multiple inheritance of type.

 

Develop code that uses final keyword

The final keyword can be used in variables, methods, and classes.

Final Variable

If you make a variable final, you cannot change its value after is initialized. It can only be initialized in the constructor or when it's declared. If it's static, it will be initialized in a static block or when it's declared.

If the variable holds an object, it cannot be assigned to other objects, but the attributes of that object can be changed.

class Car {  
 final int speed = 70; 
 void speedUp(){  
  speed = 90;  //Compile-time error
 }

Final Method

If you make a method of a class final, a subclass cannot override that method when inherited.

class Animal {  
  final public void eat() {
    System.out.println("eating");
  }  
}  
class Dog extends Animal {  
  void eat() { //Compile-time error
    System.out.println("eating");
  }  
}

Final Class

If you make any class final, you cannot extend it.

final class Animal {  
  public void eat() {
    System.out.println("eating");
  }  
}  
class Dog extends Animal { //Compile-time error  
}

 

Create inner classes including static inner class, local class, nested class, and anonymous inner class

An inner class is a class declared inside another class.

Static Inner Class

An inner class declared with the static keyword becomes a static inner class, for example:

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 nonstatic method of nested static class
       sc.print();
    }
}

The rules of a static nested class are:

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

Local Inner Class

A class created inside a method or a block is called local inner class. If you want to use a local inner class, you must instantiate it inside the method or block.

public class LocalClass{  
 private int n = 5;

 void result(){  
   // Local inner class
   class Cube {  
     int calc() {
        return n*n*n;
     }  
   }

   Cube c = new Cube();  
   System.out.println(c.calc());  
 }  

 public static void main(String args[]) {  
    LocalClass lc = new LocalClass();  
    lc.result();  
 }  
}

The rules of a local inner class are:

  • Local inner class cannot be invoked from outside the method.
  • Since java 8, it's possible to access the non-final local variables in a local inner class.

Nested class

A nested class or member inner class is a non-static class that is created inside a class but outside a method.

class OuterClass {
   // Nested class
   public class NestedClass {
       public void print() { 
         System.out.println("Message from nested class"); 
       }
    }
} 

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

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

The rules of a nested class are:

  • Nested classes need a reference of their outer class
  • Static and non-static members of the outer class are accessible in a nested class
  • If the nested class is used inside the class that defines it, the keyword this can be used to create an instance of the nested class (for example OuterClass.NestedClass nc = this.new NestedClass();)

There's also a concept named shadowing. If a declaration of a type (such as a member variable or a parameter name) in a particular scope (such as an inner class or a method definition) has the same name as another declaration in the enclosing scope, then the declaration shadows the declaration of the enclosing scope, for example:

public class Test {
    public int x = 10;

    class Inner {
        public int x = 1;
        void m(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("Test.this.x = " + Test.this.x);
        }
    }

    public static void main(String... args) {
        Test t = new Test();
        Test.Inner i = t.new Ineer();
        i.m(5);
    }
}

This example defines three variables named x, the member variable of the class Test, the member variable of the inner class Inner, and the parameter in the method m. The variable x defined as a parameter of the method shadows the variable of the inner class. So, when you use the variable x in the method m, it refers to the method parameter. To refer to the member variable of the inner class Inner, use the keyword this to represent the enclosing scope. To refer to member variables that enclose larger scopes, use the class name to which they belong. The output of the example is:

x = 5
this.x = 1
Test.this.x = 10

Anonymous Inner Class

Anonymous Inner classes are classes without a name (but not without type) used to override a method of class or interface. They can't have constructors.

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

The code:

    Animal a = new Animal() { //This is the only case where you can use the keyword 'new' with an interface
      public void eat() { System.out.println("eat"); }  
    };

Represents:

  • A class that is created but have its name decided by the compiler
  • The class implements the Animal interface and provides the implementation of the eat method.
  • An object is created and referred by the a variable of Animal type.

The rules to access variables in anonymous classes are:

  • An anonymous class has access to the members of its enclosing class.
  • An anonymous class cannot access local variables in its enclosing scope that are not declared as final or effectively final.
  • Like a nested class, a declaration of a variable in an anonymous class shadows any other declarations in the enclosing scope that have the same name.

 

Use enumerated types including methods, and constructors in an enum type

Enumerated types (or enums) are classes that can be used to define a set of constants. They are type-safe, meaning that you cannot assign anything else other than the predefined constants to an enum variable.

Here's an example:

public enum Colors {
    RED,
    BLUE,
    BLACK
}

You can refer to the constants in the enum like this:

Colors color = Colors.BLUE;

Enums extend from java.lang.Enum implicitly, so they cannot extend another class. But they can implement an interface and override any method like a normal class.

You can specify values of enum constants at the creation time, but you need to define a constructor for this and, optionally, a method to get these values, for example:

public enum Colors {
    RED("#ff0000"),
    BLUE("#3366cc"),
    BLACK("#000000");

    private String hexValue;

    private Colors(String hexValue) {
      this.hexValue = hexValue;
    }

    public String getHexValue() {
      return value;
    }
}

And the method is used like this:

String value = Colors.BLUE.getHexValue();

If an enum contains attributes and methods, their definition must always come after the list of constants in the enum and the list of constants must be terminated by a semicolon.

The constructor of an enum must be private, any other access modifier will result in compilation error. For this reason, you cannot create an instance of an enum by using the new operator.

Enums can be used in if statements in this way:

Colors color = Colors.BLUE;

if( color == Colors.RED) {
  /** Do something */
} else if( color == Colors.BLUE) {
  /** Do something */
} else if( color == Colors.BLACK) {
  /** Do something */
}

And in switch statements like this:

Colors color = Colors.BLUE;

switch (color) {
    case RED: /** Do something */; break;
    case BLUE: /** Do something */; break;
    case BLACK: /** Do something */; break;
}

You can also get all the possible values of an enum type by calling the static values() method:

for (Colors c : Colors.values()) {
    System.out.println(c);
}

The order of the values is exactly the same in which they were defined.

 

Develop code that declares, implements and/or extends interfaces and use the atOverride annotation

Interfaces

An interface is like an abstract class, except that it cannot contain an implementation of the methods, only their signature (return type, name, parameters, and exceptions).

An interface is declared using the interface keyword. Just like classes, an interface can be declared public or with package scope (no access modifier).

public interface Vehicle {
    public String serie = "XXX";
    public void start();
}

The variables declared in an interface are public, static and final by default. The methods are public and abstract by default.

Before you can use an interface, it must be implemented by some class:

public class Truck implements Vehicle {
    public void start() {
        System.out.println("Starting truck...");
    }
}

A class that implements an interface must implement all the methods declared in the interface. The only exception is default methods. Java 8 introduces this feature, which provides the flexibility to allow an interface to define an implementation which will be used as default in the situation where a class fails to provide an implementation for that method.

This is made by adding the keyword default before the method's access modifier and adding its implementation inside the interface itself:

interface Vehicle {
    default public void start() {
        System.out.println("Default start");
    }
}

class Car implements Vehicle {
    // valid in Java 8
}

Once a class implements an interface, an instance of that class can be assigned to a reference of the interface type:

Vehicle truck = new Truck();

A class can implement multiple interfaces. In that case, the class must implement all the methods declared in all the interfaces implemented:

interface Run {
    public void run();
}
interface Sleep {
    public void sleep();
}
public class Man
    implements Run, Sleep {

    public void run() {
        System.out.println("run");
    }

    public void sleep() {
        System.out.println("sleep");
    }
}

An interface cannot extend from another class or implement another interface. It can only extend another interface(s):

interface Run {
    public void run();
}
interface Sleep {
    public void sleep();
}
interface Behavior extends Run, Sleep {
}

public class Man
    implements Behavior {

    public void run() {
        System.out.println("run");
    }

    public void sleep() {
        System.out.println("sleep");
    }
}

If a class implements Behavior, it has to implement all methods defined in both Run and Sleep interfaces.

atOverride Annotation

When you use the @Override annotation, the compiler checks to make sure you're actually overriding a method. For example, if you misspell the method name or not match the parameters correctly, you will be warned that you're not actually overriding a method of the superclass.

The most common use case for @Override is with Object methods:

public class Test {
  @Override
  public String toString() {
    /** Do something */
  }
  @Override
  public boolean equals(Object) {
    /** Do something */
  }
  @Override
  public int hashCode() {
    /** Do something */
  }
}

 

Create and use Lambda expressions

Think about an interface with one method, like the one used to create threads:

public interface Runnable() {
  public void run();
}

You can use an anonymous class to implement that interface:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("run");
    }
}).start();

Lambda expressions can be used in cases like this, where you have what is called a functional interface, an interface with just one method declared in it.

A lambda expression has the following syntax:

parameter -> expression body
  • The type declaration is optional. The compiler can inference the type from the value of the parameter.
  • Parentheses around the parameter are optional. The parentheses are required only for multiple parameters.
  • Curly braces in the expression body are optional. - The braces are required only if the expression body contains more than one statement.
  • A return keyword in the expression body is optional. The compiler automatically returns a value if the expression body has a single expression to return the value.

Here are some examples of Lambda expressions.

(int a, int b) -> {  return a * b; }
() -> System.out.println("Hi");
String s -> { System.out.println(s); }
(a) -> a
() -> return 100;

Using a lambda expression, a thread can be started in this way:

new Thread(
    () -> System.out.println("run");
).start();

In this case, the lambda expression replaces the method run(). Since this method has no parameters, the parentheses have no content in between. That is to signal that the lambda takes no parameters. Also, with lambda expressions, the type can be inferred from the surrounding code, so there is no need to reference the Runnable interface.

Lambda expressions are objects. You can assign a lambda expression to a variable and pass it around like this:

Runnable r = () -> System.out.println("run");
new Thread(r).start();

So lambda expressions only work with one-method interfaces (called functional interfaces). There's even an annotation to make the compiler check if an interface has more than one method:

@FunctionalInterface
public interface FuncInterface { //Generates a compile-time error
    public void doSomething();
    public void doMoreSomething();
}

Here's another example of the use of a lambda expression:

@FunctionalInterface
interface MathFunction {
    public int operation(int a, int b);
}

public class Test {
   public static void main(String args[]) {
      MathFunction multiply = (a, b) -> a * b;
      MathFunction divide = (a, b) -> a / b;

      System.out.println("4 * 2 = " + multiply.operation(4, 2));       
      System.out.println("4 / 2 = " + divide.operation(4, 2));  
   }
}

In summary, lambda expressions are basically used to define an implementation of a functional interface instead of using an anonymous class.

 

Content