Core Java

How to design Classes and Interfaces

This article is part of our Academy Course titled Advanced Java.

This course is designed to help you make the most effective use of Java. It discusses advanced topics, including object creation, concurrency, serialization, reflection and many more. It will guide you through your journey to Java mastery! Check it out here!

1. Introduction

Whatever programming language you are using (and Java is not an exception here), following good design principles is a key factor to write clean, understandable, testable code and deliver long-living, easy to maintain solutions. In this part of the tutorial we are going to discuss the foundational building blocks which the Java language provides and introduce a couple of design principles, aiming to help you to make better design decisions.

More precisely, we are going to discuss interfaces and interfaces with default methods (new feature of Java 8), abstract and final classes, immutable classes, inheritance, composition and revisit a bit the visibility (or accessibility) rules we have briefly touched in part 1 of the tutorial, How to create and destroy objects.

2. Interfaces

In object-oriented programming, the concept of interfaces forms the basics of contract-driven (or contract-based) development. In a nutshell, interfaces define the set of methods (contract) and every class which claims to support this particular interface must provide the implementation of those methods: a pretty simple, but powerful idea.

Many programming languages do have interfaces in one form or another, but Java particularly provides language support for that. Let take a look on a simple interface definition in Java.

package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
    void performAction();
} 

In the code snippet above, the interface which we named SimpleInterface declares just one method with name performAction. The principal differences of interfaces in respect to classes is that interfaces outline what the contact is (declare methods), but do not provide their implementations.

However, interfaces in Java can be more complicated than that: they can include nested interfaces, classes, enumerations, annotations (enumerations and annotations will be covered in details in part 5 of the tutorial, How and when to use Enums and Annotations) and constants. For example:

package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";

    enum InnerEnum {
        E1, E2;
    }

    class InnerClass {
    }

    interface InnerInterface {
        void performInnerAction();
    }

    void performAction();
} 

With this more complicated example, there are a couple of constraints which interfaces implicitly impose with respect to the nested constructs and method declarations, and Java compiler enforces that. First and foremost, even if it is not being said explicitly, every declaration in the interface is public (and can be only public, for more details about visibility and accessibility rules, please refer to section Visibility). As such, the following method declarations are equivalent:

public void performAction();
void performAction(); 

Worth to mention that every single method in the interface is implicitly declared as abstract and even these method declarations are equivalent:

public abstract void performAction();
public void performAction();
void performAction(); 

As for the constant field declarations, additionally to being public, they are implicitly static and final so the following declarations are also equivalent:

String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT"; 

And finally, the nested classes, interfaces or enumerations, additionally to being public, are implicitly declared as static. For example, those class declarations are equivalent as well:

class InnerClass {
}

static class InnerClass {
} 

Which style you are going to choose is a personal preference, however knowledge of those simple qualities of interfaces could save you from unnecessary typing.

3. Marker Interfaces

Marker interfaces are a special kind of interfaces which have no methods or other nested constructs defined. We have already seen one example of the marker interface in part 2 of the tutorial Using methods common to all objects, the interface Cloneable. Here is how it is defined in the Java library:

public interface Cloneable {
} 

Marker interfaces are not contracts per se but somewhat useful technique to “attach” or “tie” some particular trait to the class. For example, with respect to Cloneable, the class is marked as being available for cloning however the way it should or could be done is not a part of the interface. Another very well-known and widely used example of marker interface is Serializable:

public interface Serializable {
} 

This interface marks the class as being available for serialization and deserialization, and again, it does not specify the way it could or should be done.

The marker interfaces have their place in object-oriented design, although they do not satisfy the main purpose of interface to be a contract.

4. Functional interfaces, default and static methods

With the release of Java 8, interfaces have obtained new very interesting capabilities: static methods, default methods and automatic conversion from lambdas (functional interfaces).

In section Interfaces we have emphasized on the fact that interfaces in Java can only declare methods but are not allowed to provide their implementations. With default methods it is not true anymore: an interface can mark a method with the default keyword and provide the implementation for it. For example:

package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
} 

Being an instance level, defaults methods could be overridden by each interface implementer, but from now, interfaces may also include static methods, for example:

package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
} 

One may say that providing an implementation in the interface defeats the whole purpose of contract-based development, but there are many reasons why these features were introduced into the Java language and no matter how useful or confusing they are, they are there for you to use.

The functional interfaces are a different story and they are proven to be very helpful add-on to the language. Basically, the functional interface is the interface with just a single abstract method declared in it. The Runnable interface from Java standard library is a good example of this concept:

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

The Java compiler treats functional interfaces differently and is able to convert the lambda function into the functional interface implementation where it makes sense. Let us take a look on following function definition:

public void runMe( final Runnable r ) {
    r.run();
} 

To invoke this function in Java 7 and below, the implementation of the Runnable interface should be provided (for example using Anonymous classes), but in Java 8 it is enough to pass run() method implementation using lambda syntax:

runMe( () -> System.out.println( "Run!" ) ); 

Additionally, the @FunctionalInterface annotation (annotations will be covered in details in part 5 of the tutorial, How and when to use Enums and Annotations) hints the compiler to verify that the interface contains only one abstract method so any changes introduced to the interface in the future will not break this assumption.

5. Abstract classes

Another interesting concept supported by Java language is the notion of abstract classes. Abstract classes are somewhat similar to the interfaces in Java 7 and very close to interfaces with default methods in Java 8. By contrast to regular classes, abstract classes cannot be instantiated but could be subclassed (please refer to the section Inheritance for more details). More importantly, abstract classes may contain abstract methods: the special kind of methods without implementations, much like interfaces do. For example:

package com.javacodegeeks.advanced.design;

public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }

    public abstract void performAnotherAction();
} 

In this example, the class SimpleAbstractClass is declared as abstract and has one abstract method declaration as well. Abstract classes are very useful when most or even some part of implementation details could be shared by many subclasses. However, they still leave the door open and allow customizing the intrinsic behavior of each subclass by means of abstract methods.

One thing to mention, in contrast to interfaces which can contain only public declarations, abstract classes may use the full power of accessibility rules to control abstract methods visibility (please refer to the sections Visibility and Inheritance for more details).

6. Immutable classes

Immutability is becoming more and more important in the software development nowadays. The rise of multi-core systems has raised a lot of concerns related to data sharing and concurrency (in the part 9, Concurrency best practices, we are going to discuss in details those topics). But the one thing definitely emerged: less (or even absence of) mutable state leads to better scalability and simpler reasoning about the systems.

Unfortunately, the Java language does not provide strong support for class immutability. However using a combination of techniques it is possible to design classes which are immutable. First and foremost, all fields of the class should be final. It is a good start but does not guarantee immutability alone.

package com.javacodegeeks.advanced.design;

import java.util.Collection;

public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection< String > collectionOfString;
} 

Secondly, follow the proper initialization: if the field is the reference to a collection or an array, do not assign those fields directly from constructor arguments, make the copies instead. It will guarantee that state of the collection or array will not be changed from outside.

public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection< String > collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
} 

And lastly, provide the proper accessors (getters). For the collection, the immutable view should be exposed using Collections.unmodifiableXxx wrappers.

public Collection<String> getCollectionOfString() {
    return Collections.unmodifiableCollection( collectionOfString );
} 

With arrays, the only way to ensure true immutability is to provide a copy instead of returning reference to the array. That might not be acceptable from a practical standpoint as it hugely depends on array size and may put a lot of pressure on garbage collector.

public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
} 

Even this small example gives a good idea that immutability is not a first class citizen in Java yet. Things can get really complicated if an immutable class has fields referencing another class instances. Those classes should also be immutable however there is no simple way to enforce that.

There are a couple of great Java source code analyzers like FindBugs) and PMD) which may help a lot by inspecting your code and pointing to the common Java programming flaws. Those tools are great friends of any Java developer.


 

7. Anonymous classes

In the pre-Java 8 era, anonymous classes were the only way to provide in-place class definitions and immediate instantiations. The purpose of the anonymous classes was to reduce boilerplate and provide a concise and easy way to represent classes as expressions. Let us take a look on the typical old-fashioned way to spawn new thread in Java:

package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
} 

In this example, the implementation of the Runnable interface is provided in place as anonymous class. Although there are some limitations associated with anonymous classes, the fundamental disadvantages of their usage are a quite verbose syntax constructs which Java imposes as a language. Even the simplest anonymous class which does nothing requires at least 5 lines of code to be written every time.

   new Runnable() {
      @Override
      public void run() {
      }
   } 

Luckily, with Java 8, lambdas and functional interfaces all this boilerplate is about to gone away, finally making the Java code to look truly concise.

package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
} 

8. Visibility

We have already talked a bit about Java visibility and accessibility rules in part 1 of the tutorial, How to design Classes and Interfaces. In this part we are going to get back to this subject again but in the context of subclassing.

ModifierPackageSubclassEveryone Else
publicaccessibleaccessibleAccessible
protectedaccessibleaccessiblenot accessible
<no modifier>accessiblenot accessiblenot accessible
privatenot accessiblenot accessiblenot accessible

Table 1

Different visibility levels allow or disallow the classes to see other classes or interfaces (for example, if they are in different packages or nested in one another) or subclasses to see and access methods, constructors and fields of their parents.

In next section, Inheritance, we are going to see that in action.

9. Inheritance

Inheritance is one of the key concepts of object-oriented programming, serving as a basis of building class relationships. Combined together with visibility and accessibility rules, inheritance allows designing extensible and maintainable class hierarchies.

Conceptually, inheritance in Java is implemented using subclassing and the extends keyword, followed by the parent class. The subclass inherits all of the public and protected members of its parent class. Additionally, a subclass inherits the package-private members of the parent class if both reside in the same package. Having said that, it is very important no matter what you are trying to design, to keep the minimal set of the methods which class exposes publicly or to its subclasses. For example, let us take a look on a class Parent and its subclass Child to demonstrate different visibility levels and their effect:

package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    public void childAction() {
        this.protectedField = "value";
    }
}

Inheritance is a very large topic by itself, with a lot of subtle details specific to Java. However, there are a couple of easy to follow rules which could help a lot to keep your class hierarchies concise. In Java, every subclass may override any inherited method of its parent unless it was declared as final (please refer to the section Final classes and methods).

However, there is no special syntax or keyword to mark the method as being overridden which may cause a lot of confusion. That is why the @Override annotation has been introduced: whenever your intention is to override the inherited method, please always use the @Override annotation to indicate that.

Another dilemma Java developers are often facing in design is building class hierarchies (with concrete or abstract classes) versus interface implementations. It is strongly advised to prefer interfaces to classes or abstract classes whenever possible. Interfaces are much more lightweight, easier to test (using mocks) and maintain, plus they minimize the side effects of implementation changes. Many advanced programming techniques like creating class proxies in standard Java library heavily rely on interfaces.

10. Multiple inheritance

In contrast to C++ and some other languages, Java does not support multiple inheritance: in Java every class has exactly one direct parent (with Object class being on top of the hierarchy as we have already known from part 2 of the tutorial, Using methods common to all objects). However, the class may implement multiple interfaces and as such, stacking interfaces is the only way to achieve (or mimic) multiple inheritance in Java.

package com.javacodegeeks.advanced.design;

public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
} 

Implementation of multiple interfaces is in fact quite powerful, but often the need to reuse an implementation leads to deep class hierarchies as a way to overcome the absence of multiple inheritance support in Java.

public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}

And so on… The recent Java 8 release somewhat addressed the problem with the introduction of default methods. Because of default methods, interfaces actually have started to provide not only contract but also implementation. Consequently, the classes which implement those interfaces are automatically inheriting these implemented methods as well. For example:

package com.javacodegeeks.advanced.design;

public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
} 

Be aware that multiple inheritance is a powerful, but at the same time a dangerous tool to use. The well known “Diamond of Death” problem is often cited as the fundamental flaw of multiple inheritance implementations, so developers are urged to design class hierarchies very carefully. Unfortunately, the Java 8 interfaces with default methods are becoming the victims of those flaws as well.

interface A {
    default void performAction() {
    }
}

interface B extends A {
    @Override
    default void performAction() {
    }
}

interface C extends A {
    @Override
    default void performAction() {
    }
} 

For example, the following code snippet fails to compile:

// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
} 

At this point it is fair to say that Java as a language always tried to escape the corner cases of object-oriented programming, but as the language evolves, some of those cases are started to pop up.

11. Inheritance and composition

Fortunately, inheritance is not the only way to design your classes. Another alternative, which many developers consider being better than inheritance, is composition. The idea is very simple: instead of building class hierarchies, the classes should be composed from other classes.

Let us take a look on this example:

public class Vehicle {
    private Engine engine;
    private Wheels[] wheels;
    // ...
} 

The Vehicle class is composed out of engine and wheels (plus many other parts which are left aside for simplicity). However, one may say that Vehicle class is also an engine and so could be designed using the inheritance.

public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
} 

Which design decision is right? The general guidelines are known as IS-A and HAS-A principles. IS-A is the inheritance relationship: the subclass also satisfies the parent class specification and a such IS-A variation of parent class. Consequently, HAS-A is the composition relationship: the class owns (or HAS-A) the objects which belong to it. In most cases, the HAS-A principle works better then IS-A for couple of reasons:

  • The design is more flexible in a way it could be changed
  • The model is more stable as changes are not propagating through class hierarchies
  • The class and its composites are loosely coupled compared to inheritance which tightly couples parent and its subclasses
  • The reasoning about class is simpler as all its dependencies are included in it, in one place

However, the inheritance has its own place, solves real design issues in different way and should not be neglected. Please keep those two alternatives in mind while designing your object-oriented models.

12. Encapsulation

The concept of encapsulation in object-oriented programming is all about hiding the implementation details (like state, internal methods, etc.) from the outside world. The benefits of encapsulation are maintainability and ease of change. The less intrinsic details classes expose, the more control the developers have over changing their internal implementation, without the fear to break the existing code (a real problem if you are developing a library or framework used by many people).

Encapsulation in Java is achieved using visibility and accessibility rules. It is considered a best practice in Java to never expose the fields directly, only by means of getters and setters (if the field is not declared as final). For example:

package com.javacodegeeks.advanced.design;

public class Encapsulation {
    private final String email;
    private String address;

    public Encapsulation( final String email ) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }
} 

This example resembles what is being called JavaBeans in Java language: the regular Java classes written by following the set of conventions, one of those being allow the access to fields using getter and setter methods only.

As we already emphasized in the Inheritance section, please always try to keep the class public contract minimal, following the encapsulation principle. Whatever should not be public, should be private instead (or protected / package private, depending on the problem you are solving). In long run it will pay off, giving you the freedom to evolve your design without introducing breaking changes (or at least minimize them).

13. Final classes and methods

In Java, there is a way to prevent the class to be subclassed by any other class: it should be declared as final.

package com.javacodegeeks.advanced.design;

public final class FinalClass {
} 

The same final keyword in the method declaration prevents the method in question to be overridden in subclasses.

package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
} 

There are no general rules to decide if class or method should be final or not. Final classes and methods limit the extensibility and it is very hard to think ahead if the class should or should not be subclassed, or method should or should not be overridden. This is particularly important to library developers as the design decisions like that could significantly limit the applicability of the library.

Java standard library has some examples of final classes, with most known being String class. On an early stage, the decision has been taken to proactively prevent any developer’s attempts to come up with own, “better” string implementations.

14. What’s next

In this part of the tutorial we have looked at object-oriented design concepts in Java. We also briefly walked through contract-based development, touched some functional concepts and saw how the language evolved over time. In next part of the tutorial we are going to meet generics and how they are changing the way we approach type-safe programming.

15. Download the Source Code

This was a lesson on How to design Classes and Interfaces.

Andrey Redko

Andriy is a well-grounded software developer with more then 12 years of practical experience using Java/EE, C#/.NET, C++, Groovy, Ruby, functional programming (Scala), databases (MySQL, PostgreSQL, Oracle) and NoSQL solutions (MongoDB, Redis).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Dimitar
8 years ago

This tutorial from oracle better explains the purpose of default methods:
https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

“Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.”

ceph3us
ceph3us
8 years ago

there is no such thing like immutable object – when u can write into memory – only rom in certain conditions is immutable! second thing see reflections :) how many times did u face a security manager throwing ex of illegal access? I haven’t seen any (on every vm I used &) since I use java

Gagandeep Singh
Gagandeep Singh
7 years ago
Reply to  ceph3us

Immutable Objects are better used during Concurrent access across multiple threads to keep the anomalies away. It’s all about code design… if you consider it worthy, you will write one day.. :-)

Back to top button