JavaRush /Java Blog /Random EN /Designing Classes and Interfaces
fatesha
Level 22

Designing Classes and Interfaces

Published in the Random EN group
Designing Classes and Interfaces (Article Translation) - 1

Content

  1. Introduction
  2. Interfaces
  3. Interface markers
  4. Functional Interfaces, Static Methods, and Default Methods
  5. Abstract classes
  6. Immutable (persistent) classes
  7. Anonymous classes
  8. Visibility
  9. Inheritance
  10. Multiple Inheritance
  11. Inheritance and Composition
  12. Encapsulation
  13. Final classes and methods
  14. What's next
  15. Download source code

1. INTRODUCTION

Regardless of which programming language you use (and Java is no exception), following good design principles is key to writing clean, understandable, and verifiable code; as well as making it long-lived, easily supporting problem solving. In this part of the tutorial, we are going to discuss the fundamental building blocks that the Java language provides and introduce a couple of design principles in an effort to help you make better design decisions. More specifically, we are going to discuss interfaces and interfaces using default methods (a new feature of Java 8), abstract and final (final) classes, immutable classes, inheritance, composition and revisit the rules of visibility (or accessibility), which we briefly touched on in Part 1 lesson"How to create and destroy objects" .

2. INTERFACES

In object-oriented programming , the concept of interfaces forms the basis for the development of contracts . In a nutshell, interfaces define a set of methods (contracts) and every class that needs to support that specific interface must provide an implementation of those methods: a fairly simple but powerful idea. Many programming languages ​​have interfaces in one form or another, but Java in particular provides language support for this. Let's take a look at a simple interface definition in Java.
package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
void performAction();
}
In the snippet above, the interface we named SimpleInterface, only declares one method named performAction. The main difference between interfaces and classes is that interfaces delineate what a contact should be (declare a method), but do not provide their implementation. However, interfaces in Java can be more complex: they can include nested interfaces, classes, counts, 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();
}
In this more complex example, there are several restrictions that interfaces implicitly impose on nested constructs and method declarations that are enforced by the Java compiler. First of all, even if it's not explicitly declared, every method declaration in an interface is public (and can only be public). Thus the following method declarations are equivalent:
public void performAction();
void performAction();
It's worth mentioning that every single method in an interface is implicitly declared abstract , and even these method declarations are equivalent:
public abstract void performAction();
public void performAction();
void performAction();
As for declared constant fields, in addition to being public , they are also implicitly static and marked as final . Therefore, the following declarations are also equivalent:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
And finally, nested classes, interfaces, or counts, in addition to being public , are also implicitly declared as static . For example, these declarations are also equivalent to:
class InnerClass {
}

static class InnerClass {
}
The style you choose is your personal preference, but knowing these simple properties of interfaces can save you from unnecessary typing.

3. Marker interface

A marker interface is a special kind of interface that has no methods or other nested constructs. As the Java library defines it:
public interface Cloneable {
}
Interface markers are not contracts per se, but kind of a useful technique to "attach" or "attach" some specific feature to a class. For example, with respect to Cloneable , the class is marked as cloneable, however, the way this can or should be implemented is not part of the interface. Another very famous and widely used example of a marker interface is Serializable:
public interface Serializable {
}
This interface marks a class as being suitable for serialization (serialization) and deserialization (deserialization), and again, it does not specify how this can or should be implemented. Interface markers have their place in object-oriented programming, although they do not satisfy the main purpose of an interface to be a contract. 

4. FUNCTIONAL INTERFACES, DEFAULT METHODS AND STATIC METHODS

Since the releases of Java 8, interfaces have received some very interesting new features: static methods, default methods, and automatic conversion from lambdas (functional interfaces). In the interfaces section, we emphasized the fact that interfaces in Java can only declare methods, but do not provide implementations. With a default method, things are different: an interface can mark a method with the default keyword and provide an implementation for it. For example:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
}
Being at the instance level, default methods could be overridden by each interface implementation, but now interfaces can also include static methods, for example: package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}
It can be said that providing an implementation in an interface defeats the whole purpose of contract programming. 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 and your use. Functional interfaces are a different story and have proven to be very useful additions to the language. Basically, a functional interface is an interface with just one abstract method declared in it. RunnableThe standard library interface is a very good example of this concept.
@FunctionalInterface
public interface Runnable {
    void run();
}
The Java compiler handles functional interfaces differently and can turn a lambda function into an implementation of a functional interface where it makes sense. Let's consider the following function description: 
public void runMe( final Runnable r ) {
    r.run();
}
To call this function in Java 7 and below, an implementation of the interface must be provided Runnable(for example, using anonymous classes), but in Java 8 it is enough to pass the implementation of the run() method using the lambda syntax:
runMe( () -> System.out.println( "Run!" ) );
Also, the @FunctionalInterface annotation (annotations will be covered in detail in part 5 of the tutorial) hints that the compiler can check if an interface contains only one abstract method, so any changes made to the interface in the future will not violate that assumption.

5. ABSTRACT CLASSES

Another interesting concept supported by the Java language is the concept of abstract classes. Abstract classes are somewhat similar to interfaces in Java 7 and are very close to the default method interface in Java 8. Unlike regular classes, you cannot instantiate an abstract class, but it can be subclassed (refer to the Inheritance section for more details). ). More importantly, abstract classes can contain abstract methods: a special kind of method with no implementation, just like an interface. 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 SimpleAbstractClassis declared abstract and contains one declared abstract method. Abstract classes are very useful, most or even some of the implementation details can be shared with many subclasses. However, they still leave the door ajar and allow you to customize the behavior inherent in each of the subclasses using abstract methods. Worth mentioning, unlike interfaces, which can only contain public declarations, abstract classes can use the power of accessibility rules to control the visibility of an abstract method.

6. UNMODABLE CLASSES

Immutability is becoming more and more important in software development these days. The rise of multi-core systems has raised many questions about data sharing and concurrency. But one problem has definitely arisen: little (or even no) mutable state leads to better extensibility (scalability) and simpler reasoning about the system. Unfortunately, the Java language does not provide decent support for class immutability. However, by using a combination of techniques, it becomes possible to design classes that are immutable. First of all, all fields of the class must be final (marked as final ). This is a good start, but no guarantees. 
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;
}
Second, watch out for proper initialization: if a field is a collection or array reference, don't assign those fields directly from constructor arguments, instead make copies of it. This will ensure that the state of the collection or array will not be changed 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 finally, providing proper access (getters). For collections, the immutable kind must be provided as a wrapper  Collections.unmodifiableXxx: With arrays, the only way to provide true immutability is to provide a copy instead of returning a reference to the array. This may be unacceptable from a practical point of view, as it is very dependent on the size of the array and can put a huge amount of pressure on the garbage collector.
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
Even this small example gives a good idea that immutability is not yet a first class citizen in Java. Things can get complicated if the immutable class has a field that refers to an object of another class. Those classes should also be immutable, however there is no way to enforce this. There are several decent Java source code analyzers, like FindBugs and PMD, that can be of great help by checking your code and pointing out common flaws in Java programming. These tools are great friends of any Java developer.

7. ANONYMOUS CLASSES

In the pre-Java 8 era, anonymous classes were the only way to ensure that classes were instantiated quickly and immediately instantiated. The purpose of anonymous classes was to reduce boilerplate and provide a concise and easy way to represent classes as a record. Let's take a look at the typical old-fashioned way to spawn a 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, Runnablethe interface implementation is provided immediately as an anonymous class. Although there are some limitations associated with anonymous classes, the main disadvantage of using them is the very verbose construction syntax that Java as a language requires. Even just an anonymous class that does nothing requires at least 5 lines of code each time it is written.
new Runnable() {
   @Override
   public void run() {
   }
}
Fortunately, with Java 8, lambda and functional interfaces, all these stereotypes will soon go away, finally writing Java code will look really concise.
package com.javacodegeeks.advanced.design;

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

8. VISIBILITY

We already talked a little about the visibility and accessibility rules in Java in part 1 of the tutorial. In this part, we are going to return to this topic again, but in the context of subclassing. Designing Classes and Interfaces (Article Translation) - 2Visibility at various levels allows or prevents classes from viewing other classes or interfaces (for example, if they are in different packages or nested within each other) or subclasses from seeing and accessing their parent's methods, constructors, and fields. In the next section, inheritance, we'll see this in action.

9. INHERITANCE

Inheritance is one of the key concepts of object-oriented programming, which acts as the basis for building a relationship class. Combined with visibility and accessibility rules, inheritance allows classes to be designed in a hierarchy that is extensible and maintainable. Conceptually, inheritance in Java is implemented using subclassing and the extends keyword., along with the parent class. A subclass inherits all public and protected members of its parent class. Also, a subclass inherits the package-private members of the parent class if both (the subclass and the class) are in the same package. That being said, it's very important, no matter what you're trying to design, to stick to the minimum set of methods that a class exposes publicly or for its subclasses. For example, let's look at a class Parentand its subclass Childto demonstrate the difference in visibility levels and their effects.
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 big topic in its own right, with a lot of subtle Java-specific details. However, there are a few rules that are easy to follow that can go a long way in keeping class hierarchies short. In Java, each subclass can override any inherited methods of its parent if it has not been declared final. However, there is no special syntax or keyword to mark a method as being overridden, which can lead to confusion. This is why the @Override annotation was introduced : whenever your goal is to override an inherited method, please use the @Override annotationto sum it up briefly. Another dilemma that Java developers constantly face in design is the construction of class hierarchies (with concrete or abstract classes) versus implementing interfaces. It is highly recommended to favor interfaces over classes or abstract classes where possible. Interfaces are lighter, easier to test and maintain, plus they minimize the side effects of implementation changes. Many advanced programming techniques, such as creating proxy classes in the Java standard library, rely heavily on interfaces.

10. MULTIPLE INHERITANCE

Unlike C++ and some other languages, Java does not support multiple inheritance: in Java, each class can only have one direct parent (with the class Objectat the top of the hierarchy). However, a class can implement multiple interfaces, and thus stacking interfaces is the only way to achieve (or simulate) 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
    }
}
Implementing multiple interfaces is actually quite powerful, but often the need to reuse an implementation over and over leads to deep class hierarchies as a way to overcome Java's lack of support for multiple inheritance. 
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 release of Java 8 somewhat addresses the issue with default method injection. Because of the default methods, interfaces actually provide not only a contract, but also an implementation. Therefore, classes that implement these interfaces will also automatically inherit these implemented methods. 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
    }
}
Keep in mind that multiple inheritance is a very powerful but also dangerous tool. The well-known "diamond of death" problem is often cited as a major flaw in the implementation of multiple inheritance, so developers are forced to design class hierarchies quite carefully. Unfortunately, Java 8 interfaces with default methods also fall prey to these defects.
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 will fail to compile:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
At this point, it's fair to say that Java as a language has always tried to avoid the corner cases of object-oriented programming, but as the language evolves, some of those cases have suddenly appeared. 

11. INHERITANCE AND COMPOSITION

Fortunately, inheritance is not the only way to design your class. Another alternative that many developers think is much better than inheritance is composition. The idea is very simple: instead of creating a hierarchy of classes, they need to be composed from other classes. Let's look at this example:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
The class Vehicleconsists of an engine and wheels (plus many other parts left out for simplicity). However, it can be said that a class Vehicleis also an engine, so it can be designed using inheritance. 
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}
What is the correct design decision? The general core guidelines are known as the IS-A (is) and HAS-A (contains) principles. IS-A is an inheritance relationship: a subclass also satisfies the class specification of the parent class and the variation of the parent class. subclass) extends the parent. If you want to know if one entity extends another, check against - IS-A (is).") Therefore, HAS-A is a composition relationship: a class owns (or contains) an object that In most cases, HAS-A works better than IS-A for a number of reasons: 
  • Design is more flexible;
  • The model is more stable because changes don't propagate through the class hierarchy;
  • A class and its composition are loosely coupled compared to composition, which tightly couples the parent and its subclass.
  • The logical flow of thought in a class is simpler, since all of its dependencies are included in it, in one place. 
Be that as it may, inheritance has its place, solves a number of existing design problems in various ways, so it should not be neglected. Please keep these two alternatives in mind when designing your object-oriented model.

12. ENCAPSULATION.

The concept of encapsulation in object-oriented programming is to hide all implementation details (like mode of operation, internal methods, etc.) from the outside world. The benefits of encapsulation are maintainability and ease of change. The internal implementation of the class is hidden, working with the data of the class occurs exclusively through the public methods of the class (a real problem if you are developing a library or frameworks (structures) used by many people). Encapsulation in Java is achieved through visibility and accessibility rules. In Java, it's considered best to never expose fields directly, only via getters and setters (unless the fields are marked 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 is reminiscent of what is called JavaBeans in the Java language: standard Java classes are written according to a set of conventions, one of which allows access to fields only through getter and setter methods. As we already emphasized in the inheritance section, please always keep the minimum publicity contract in the class using the principles of encapsulation. Anything that shouldn't be public should become private (or protected/ package private, depending on the problem you're solving). This will pay off in the long run by giving you the freedom to design without making breaking changes (or at least minimizing them). 

13. FINAL CLASSES AND METHODS

In Java, there is a way to prevent a class from becoming a subclass of another class: the other class must be declared final. 
package com.javacodegeeks.advanced.design;

public final class FinalClass {
}
The same final keyword in the method declaration prevents the method from being overridden in subclasses. 
package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
}
There are no general rules to decide whether a class or methods should be final or not. Final classes and methods limit extensibility and it is very difficult to think ahead whether a class should or should not be inherited, or whether a method should or should not be overridden in the future. This is especially important for library developers, as design decisions like this could severely limit the library's applicability. The Java Standard Library has several examples of final classes, the best known being the String class. Early on, this decision was made to prevent any attempt by developers to come up with their own, "better" implementation of string. 

14. WHAT'S NEXT

In this part of the tutorial, we covered the concepts of object-oriented programming in Java. We also briefly went over contract programming, touched on some functional concepts, and saw how the language has evolved over time. In the next part of the tutorial, we're going to meet generics and how they change the way we approach type safety in programming. 

15. DOWNLOAD SOURCE CODE

You can download the source here - advanced-java-part-3 Source: How to design Classes an
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION