Content
- Introduction
- Interfaces
- Interface markers
- Functional Interfaces, Static Methods, and Default Methods
- Abstract classes
- Immutable (persistent) classes
- Anonymous classes
- Visibility
- Inheritance
- Multiple Inheritance
- Inheritance and Composition
- Encapsulation
- Final classes and methods
- What's next
- 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() {
}
}
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() {
}
}
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.
Runnable
The 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() {
}
public abstract void performAnotherAction();
}
In this example, the class
SimpleAbstractClass
is 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(
new Runnable() {
@Override
public void run() {
}
}
).start();
}
}
In this example,
Runnable
the 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( () -> { } ).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.
Visibility 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
Parent
and its subclass
Child
to demonstrate the difference in visibility levels and their effects.
package com.javacodegeeks.advanced.design;
public class Parent {
public static final String CONSTANT = "Constant";
private String privateField;
protected String protectedField;
private class PrivateClass {
}
protected interface ProtectedInterface {
}
public void publicAction() {
}
protected void protectedAction() {
}
private void privateAction() {
}
void packageAction() {
}
}
package com.javacodegeeks.advanced.design;
public class Child extends Parent implements Parent.ProtectedInterface {
@Override
protected void protectedAction() {
super.protectedAction();
}
@Override
void packageAction() {
}
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
Object
at 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() {
}
@Override
public void close() throws Exception {
}
}
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() {
}
}
public class B extends A implements AutoCloseable {
@Override
public void close() throws Exception {
}
}
public class C extends B implements Readable {
@Override
public int read(java.nio.CharBuffer cb) throws IOException {
}
}
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() {
}
@Override
default void close() throws Exception {
}
}
public class C implements DefaultMethods, Readable {
@Override
public int read(java.nio.CharBuffer cb) throws IOException {
}
}
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:
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:
interface E extends B, C {
}
The class
Vehicle
consists of an engine and wheels (plus many other parts left out for simplicity). However, it can be said that a class
Vehicle
is 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
GO TO FULL VERSION