The Fundamentals of Object-Oriented Design in Java
Object-oriented design, key methods of Java’s Object class, and implementing exception handling schemes will all be discussed in this article.
The purpose of this article is to introduce some design patterns-basically best practices for solving some of the most common problems encountered in software development. Additionally, we’ll discuss the design of safe programs — those that don’t become inconsistent over time — towards the end of the article. As a starting point, we will examine Java’s calling conventions and passing conventions.
Java Values
There is no complicated relationship between Java’s values and its type system. A Java value can be either a primitive or an object reference.
A primitive value cannot be altered, whereas a reference can; the value 2 always remains the same. A mutation of object contents, on the other hand, is a method of changing the contents of an object reference.
It is also important to note that variables can only contain values of the appropriate type. The contents of variables of reference type are not directly contained in them, but are only references to the memory locations containing the objects. Dereference operators and structs do not exist in Java.
C++ programmers often get confused between “contents of an object” and “references to an object,” so Java simplifies this concept. It is, however, impossible to completely hide the difference, so it is important for programmers to know how reference values work.
IS JAVA “PASS BY REFERENCE”?
There is a difference between Java’s “by reference” and the phrase “pass by reference.” “Pass by reference” refers to some programming languages’ method-calling conventions. Values, even primitive values, aren’t passed directly to methods in pass-by-reference languages. Values are never passed directly to methods but rather references to them. Therefore, even primitive types are visible when the method returns if its parameters are modified.
This is not the case with Java; it is a “pass-by-value” language. Reference types, on the other hand, pass a copy of the reference (as a value) instead of the reference itself. There is a difference between pass-by-reference and pass-by value. A reference type would be passed to a method as a reference if Java were a pass-by-reference language.
There is a very simple way to demonstrate Java’s pass-by-value nature. Even after manipulating(), variable c still holds a reference to a Circle object of radius 2 after the call to manipulate(). The Java language would instead hold a reference to a circle with radius 3 if it were a pass-by-reference language:
Object references can be viewed as one of Java’s possible kinds of values if we’re scrupulously careful about the distinction and refer to them as such. This point is ambiguous in some older texts.
Java.lang.Object’s important methods
It has been noted that all classes extend java.lang.Object, either directly or indirectly. There are a number of methods defined in this class that can be overridden by the classes you write. An example of overriding these methods can be found in Example 1. This example follows sections that explain why you might want to override the default implementation of each technique.
First, this example implements the Comparable interface parameterized, or generically. Secondly, the @Override annotation indicates (and verifies) that certain methods override Objects.
Example 1. Object methods that are overridden by a class
toString()
An object’s textual representation is returned by the toString() method. Methods such as System.out.println() and string concatenation automatically invoke the method. Textual representations of objects can be quite helpful for debugging or logging output, and they can even be used to generate reports.
It includes a hexadecimal representation of the hashCode() value of an object as well as the class name inherited from Object. Despite providing basic type and identity information for an object, this implementation is rarely useful. Rather than returning a human-readable string, the toString() method in Example 1 returns the value of each field in the Circle class.
equals()
It determines if two references refer to the same object by using the == operator. Using the equals() method will test whether two objects are equal. Classes can override equals() to define their own notion of equality. When two objects are considered equal, they have to be the same: Object.equals() is simply a method that uses the == operator.
When all the fields of two Circle objects are equal, the equals() method considers them equal. The equals() method performs a quick identity check with == as an optimization, and then checks the type of the other object with instanceof: a Circle can only be equal to another Circle, and throwing a ClassCastException is not acceptable. Furthermore, the instanceof test rules out null arguments as well: null arguments always result in a false value for instanceof.
hashCode()
HashCode() must be overridden whenever equals() is overridden. A hash table data structure uses this method to return an integer. In order for two objects to be equal, their hash codes must match.
Unequal objects should have unequal hash codes (to allow efficient operation of hash tables), though it is not required. HashCode() methods involving mildly tricky arithmetic or bit manipulation can be generated by this second criterion.
In contrast to object equality, Object.hashCode() returns a hash code based on the identity of the object rather than the equality of the object. The static method System.identityHashCode() provides the functionality of Object.hashCode() for identity-based hash codes.
Comparable::compareTo()
The compareTo() method is included in Example 1. As a standard method to implement, we include it in this section even though it is defined by java.lang.Comparable rather than by Object. By using compareTo(), Comparable and its instance methods can be compared to each other similar to how the <, <=, >, and >= operators compare numbers. We can use Comparable methods to determine whether one instance is less than, greater than, or equal to another if a class implements Comparable. As a result, Comparable instances can also be sorted.
The object does not declare a compareTo() method, so each class must determine whether and how its instances should be ordered and implement that ordering via a compareTo() method.
A Circle object is compared to a word on a page according to Example 1. In this method, circles are first ordered from top to bottom according to their y coordinates: circles with larger y coordinates are less significant than circles with smaller y coordinates. A circle whose y coordinate is the same as another is ordered from left to right if they have the same y coordinate. Circles with smaller x coordinates are smaller than circles with larger x coordinates. A circle’s radius is compared if its x and y coordinates are the same. Smaller circles have smaller radiuses. The three fields of two circles must be equal for two circles to be equal under this ordering. By definition, compareTo() and equals() define equality in the same order. (This is not strictly necessary, but it is very desirable).
An int value is returned by the compareTo() method. When this object is smaller than the object passed to it, compareTo() should return a negative number. 0 should be returned if both objects are equal. As long as this is greater than the method argument, compareTo() should return a positive number.
clone()
An object can be cloned using the clone() method, which returns another object with identical fields to the current type. Two reasons make this method unusual. As a first requirement, the class must implement the java.lang.Cloneable interface. The implements clause of a class signature lists the clonable interface, which does not define any methods. In addition to being a protected function, clone() has another unusual characteristic. Cloneability requires that you override the clone() method in your object to make it public so other classes can clone your object.
Circle objects in Example 1 cannot be cloned; instead, they can be made copies using the copy constructor:
Copy constructors are easier and safer to use than clone(), which can be difficult to implement correctly.
Object-Oriented Design Aspects
A number of object-oriented design techniques will be discussed in this section. In addition to the aforementioned Effective Java by Joshua Bloch, this is an unsatisfactory treatment and merely aims to showcase some examples.
Our discussion of Java’s object-oriented capabilities for modeling and domain object design begins with good practices for defining constants in Java. Lastly, we cover some common Java design patterns at the end of the section.
Constants
Interface definitions can contain constants, as noted earlier. The constants defined by an interface are inherited by all classes that implement it, so they can be used in any class that implements the interface. The constants do not have to be prefixed with the name of the interface or implemented in any way.
Constants that are used by more than one class are often defined once in an interface and then implemented by any classes that need them. In the case of network protocols, for example, this might occur when client and server classes use symbolic constants to denote connection and listening port numbers. For instance, consider java.io.ObjectStreamConstants, which specifies constants for the ObjectInputStream and ObjectOutputStream’s object serialization protocol.
It saves time by not having to specify the type that defines constants when constant definitions are inherited from an interface. It is not recommended to use this technique with ObjectStreamConstants.
A class signature should not declare constants as implementation details in the implements clause.
Instead of typing the full class name and constant name, you should define constants in a class and use them that way. With the import static declaration, you can save typing by importing the constants from their defining class.
Abstract Classes vs. Interfaces
A fundamental change has been made to Java’s object-oriented programming model with the advent of Java 8. Interfaces were pure API specifications before Java 8 and did not contain any implementation. If there are many implementations of the interface, this could often lead to duplication of code.
The result was the development of a coding pattern. An abstract class does not have to be completely abstract; it can contain partial implementations that can be used by subclasses. An abstract superclass may provide method implementations to numerous subclasses in some cases.
A primary implementation of the pattern consists of an abstract class along with an interface containing the API specification. The java.util.List class is paired with the java.util.AbstractList class in this example. ArrayList and LinkedList, which are included in the JDK as subclasses of AbstractList, are implementations of List. A second example would be:
This picture has been significantly changed by Java 8’s default methods.
If you are going to define an abstract type (like Shape) that will have many subtypes, you have to choose between interfaces and abstract classes. It is not always clear which to use due to the fact that they now have potentially similar features.
Classes that extend abstract classes can’t extend other classes, and interfaces can’t have nonconstant fields. Therefore, Java programs still have some limitations when it comes to object orientation.
The compatibility of interfaces versus abstract classes is another important difference. You break any classes that implemented the interface previously if you define it as part of a public API and then add a new mandatory method to it later — as such, any new interface methods must be declared as defaults and an implementation provided. A class that extends an abstract class can safely have nonabstract methods added without modifying other classes that extend that class.
The subclass methods always win when it comes to clashing with new methods of the same name and signature. In light of this, if the method names are “obvious” or if the method has multiple meanings, you should be careful when adding new methods.
When an API specification is needed, it is generally preferable to use interfaces. An implementation must implement the mandatory methods of the interface in order to be considered valid. The use of default methods should only be limited to methods that are truly optional, or which have only one possible implementation.
Last but not least, the older method of stating which methods are considered “optional” and throwing a java.lang.UnsupportedOperationException, when they are not implemented, is troublesome and should not be used anymore.
The default method can be used as a trait, but how?
Until Java 8, there was a strict single inheritance model. The implementation of methods could only be defined in a class, or inherited from the superclass hierarchy, except for Object.
A default method changes this picture since it allows implementations from multiple places to be inherited-whether they appear in superclasses or interfaces.
Mixins, which appear in some languages as a feature of trait languages, are effectively this pattern from C++.
Java compile-time errors result from conflicts between default methods from separate interfaces. Thus, multiple inheritances of implementations cannot conflict, since the programmer must manually resolve any clashes. Additionally, state does not inherit multiple times.
The Java language designers, however, are of the opinion that default methods do not qualify as full traits. The code included within the JDK, particularly interfaces within java.util.function (such as Function itself), undermines this view.
Consider the following code example:
It simplifies the java.util.function function types by eliminating generics and only dealing with ints as data types.
Functional composition methods (compose() and andThen()) present in this example demonstrates an important point: these functions will always be composed in the standard manner, and it is highly unlikely that they could be overridden.
In the limited domain provided by java.util.function, default methods can also be treated as stateless traits, which is also true for the function types within.
Class methods or instance methods?
Object-oriented programming relies heavily on instance methods. You shouldn’t, however, avoid class methods altogether. Class methods are perfectly reasonable in many situations.
The static keyword is used to declare class methods in Java, and the terms static method and class method are interchangeable.
The Circle class, for instance, might make it difficult for you to compute the area of a circle with a given radius without creating a Circle object to represent that circle. It is more convenient to use a class method in this case:
If the parameters of the methods differ, it is perfectly legal for a class to define more than one method with the same name. In this version of the area() method, the circle radius is specified as a parameter instead of an implicit this parameter. Instance methods with the same name are distinguished by this parameter.
Consider defining a method called bigger() that examines two Circle objects and returns whichever has a larger radius as an example of the choice between instance methods and class methods. As an instance method, bigger() can be written as follows:
As a class method, bigger() can be implemented as follows:
It is possible to determine which Circle object is bigger by using the instance method or the class method given two Circle objects, x and y. Both methods, however, have different invocation syntaxes:
Neither method is “more correct” than the other from an object-oriented design perspective. There is an asymmetry in the invocation syntax of the instance method, which is more formally object-oriented. In such a case, class methods are a better choice than instance methods. It will likely be more natural to choose one or the other, depending on the circumstances.
The System.out.println() function
To display output on the terminal window or console, we often use System.out.println(). Neither we nor those two periods in this method’s name have ever explained what those periods mean. Now that you understand the difference between classes and instances, you can better understand what is happening: System is a class. Out is a public class field. A println() method is provided by this field, which is a java.io.PrintStream object.
This can be shortened by using static imports by importing static java.lang.System.out–this will let us refer to the printing method as out.println(), but since it’s an instance method, we can’t further shorten it.
Inheritance vs. Composition
Aside from inheritance, we have other options when it comes to object-oriented design. Composition involves aggregating smaller components into larger conceptual units by referencing other objects. Delegation is a related technique in which a primary object of one type references a secondary object of another type, and forwards all operations to it. The employment structure of software companies is modeled using interface types, as shown in the following example:
No actual work is performed by the Manager object, which delegated the work() operation to their direct report. The delegating class may do some work, while only some calls are forwarded to the delegate object in variants of this pattern.
Decorator patterns are also useful, related techniques. It provides the capability of adding new functionality to objects, including during runtime. There is a slight overhead at design time due to some extra work. Taking an example from a taqueria, let’s see how the decorator pattern is applied to modeling burritos. The burrito’s price is the only aspect we’ve modeled to keep things simple:
Burritos come in two sizes and prices — these cover the basics. Let’s enhance this with jalapeo chilies and guacamole as optional extras. All of the optional decorating components will subclass an abstract base class, which is:
Here’s how it works:
JDK utility classes use the decorator pattern extensively.
Accessors and Field Inheritance
State inheritance in Java can be addressed in a variety of ways. In addition, fields can be marked as protected so that subclasses can access them directly (and even write to them). Accessor methods can also be provided to read (and write, if desired) the actual object fields while maintaining encapsulation.
The following code can be rewritten as an accessor method, instead of the preceding code:
In both cases, Java is legal, but there are a few differences between them. The best way to model object state is not to use fields that are writable outside of the class.
Therefore, it is unfortunate that the protected keyword in Java allows the declaring class to access fields (and methods) from subclasses as well as classes within the same package. This, coupled with the fact that anyone can write a class that belongs to any package (except the system package), makes protected inheritance of state in Java potentially problematic.
It is not possible in Java to make a member only visible to subclasses of the declaring class.
Unless the inherited state is declared final, in which case protected inheritance of state is perfectly acceptable, it is usually more beneficial to use accessor methods (either public or protected) to provide access to state for subclasses.
The singleton
One of the most well-known design patterns is the singleton pattern. A single instance of a class is needed or desired when a design issue arises. The singleton pattern can be implemented in several different ways in Java. The following form will be used to discuss a safe singleton in a slightly more verbose manner:
To be effective, singletons must not be created more than once, and references to them in an uninitialized state must not be possible (see later in this article for more on this important point). In order to accomplish this, we need to call a private constructor once. In our version of Singleton, the constructor is only called when the private static variable instance is created. In the private method init(), the only Singleton object is created and its initialization is performed.
As a result, getInstance() is the only static helper method that can be used to get a reference to the lone Singleton instance. Using this method, the active state of the object is checked by checking the flag initialized. An object reference to the singleton is returned if it is. Alternatively, if init() is called, then getInstance() activates the object and switches the flag to true, so that the Singleton will not have to be activated again when a reference is requested in the future.
Lastly, we should note that getInstance() is synchronized.
The singleton pattern is often overused due to its simplicity. The use of singleton classes can be useful when they are used correctly, but too many singletons in a program is a sign of badly engineered code.
Singletons have some drawbacks, including the difficulty of testing and separating them from other classes. In multithreaded code, it must also be used with caution. Despite this, developers need to be familiar with it to avoid accidentally reinventing it. Configuration management often uses the singleton pattern, but modern software usually provides the programmer with singletons automatically through a framework (often a dependency injection).