🌀 Chapter 2 - Design Patterns and Principles ✍️
Interfaces are classes which implicitly abstract
and contain:
Methods which are implicitly public abstract
✅
public static
methods which have implementations✅
public default
methods which have implementations✅
Variables which are public static final
✅
You CAN extend multiple interfaces💡
However, if you have ⚠️ default methods⚠️ with the same name you will encounter the diamond problem so the compiler prevents that:
interface InterfaceA {
default void sameNameMethod () {} }
interface InterfaceB {
default void sameNameMethod () {} }
interface InterfaceC extends InterfaceA , InterfaceB {}
// ^^^^^^^^^^ compiler error
🟥 2.2 Functional Interfaces
A FUNCTIONAL INTERFACE is an interface with a single abstract method
All functional interfaces can be implemented with a lambda
We can use @FunctionalInterface
to ensure that an interface is a valid functional interface
Abstract classes with a single method can NOT be functional interfaces
@ FunctionalInterface
/*^^^^^^^^^^^^^^^^^ compiler error */
abstract class AbstractClass { }
Functional Interfaces can be implemented as lambdas💡
There are rules for syntax of lambdas:
If there are multiple parameters, they must be in round brackets✅
The types of the parameters are optional, if provided then they must be in round brackets✅
If one of the parameter types are specified, then all parameters must have type specified✅
If you want to use return
, then you must use braces will regular java syntax✅
Here are some examples:
@ FunctionalInterface
public interface VoidInterface {}
// MAIN METHOD
VoidInterface v1 = () -> ;
/* ^^ COMPILER ERROR */
VoidInterface v2 = () -> {};
VoidInterface v3 = () -> {return ;} ;
The Predicate
Interface is a functional interface defined as:
public interface T Predicate <T > {
void test (T t );
}
You can use this interface in a method and call the test(T t)
method which return true/false
This method parameter can then be passed as a lambda
Polymorpism is the property of a single interface being able to support multiple underlying forms
It enables subtypes of a class to be passed into a method
⭐ Casting Object References ⭐
The compiler will prevent casts to unrelated types but casting to unrelated types can still occur at runtime
You can implicitly cast an object to it superclass
A DESIGN PRINCIPLE is an established idea or practice which is applied throughout an application. It leads to code which is easier to maintain and reuse
Encapsulation, Inheritance and Composition are all design principles💡
⭐ Applying Has-a
Relationship ⭐
This is also known as the object-composition test
Object Composition is constructing a class using references to other classes to reuse their functionality
It can be use to simulate polmorphic behaviour which can not be achieve multiple inheritance
A design pattern is an established solution to a commonly occuring development problem. E.g. the MVC model for web applications
We shall look at CREATIONAL PATTERNS which manage creation of objects
The Singleton pattern let's us create an object in memory only once in the application
A singleton requires the following:
A private static final
instance
A public static getInstance()
method
A private
constructor
Methods which modify fields must be synchronized
E.g.
public class Singleton {
private int field ;
private Singleton () {}
private static Singleton instance = new Singleton ();
public static Singleton getInstance () {
return instance ;
}
public synchronized void setField (int field ) {
this .field = field ;
}
}
We can also employ lazy instantiation to singletons too:
public class LazySingleton {
private static LazySingleton instance ;
private LazySingleton () {}
public static LazySingleton getInstance () {
if (instance == null ) {
instance = new LazySingleton (); // NOT THREAD SAFE
}
return instance ;
}
}
Lazy instantiation prevents us from making the instance final
public class LazySingleton {
private static final LazySingleton instance ; // COMPILER ERROR
private LazySingleton () {}
public static LazySingleton getInstance () {
if (instance ==null )
instance = new LazySingleton ();
// ^^^^^^^^ COMPILER ERROR
return instance ;
}
}
We can circumvent this compiler limitation, by makking the getInstance()
method synchronized:
public class LazySingleton {
private static LazySingleton instance ;
private LazySingleton () {}
public static synchronized LazySingleton getInstance () {
if (instance ==null )
instance = new LazySingleton ();
return instance ;
}
}
An immutability strategy can be implemented by having:
A public constructor for setting all the fields
Mark all instance variables as private final
Define no setters (impossible due to final
modifier)
Do no allow referenced objects to be modified or accessed directly
Make class final to prevent method overriding
public final class ImmutableClass {
private final int field ;
public ImmutableClass (int field ) {
this .field = field ;
}
}
We need to be sure not to expose mutable objects directly:
final class BadImmutableClass {
private final List <String > list ;
public BadImmutableClass (List <String > list ) {
this .list = new ArrayList <>(list );
}
public List getList () { // makes class immutable
return list ;
}
@ Override
public String toString () {
return "BadImmutableClass [list=" + list + "]" ;
}
public static void main (String [] args ) {
BadImmutableClass immutable = new BadImmutableClass (Arrays .asList ("hello" ));
immutable .getList ().remove (0 );
System .out .println (immutable .toString ());
// BadImmutableClass [list=[]]
}
}
We can fix this problem by returning a new reference of the list:
public List getList () {
return new ArrayList <>(list ); // makes it immutable again!
}
The Builder Pattern enables us to construct objects without having to specify all fields in a large constructor
Also it enables us to add fields without having to force users of the constructor to update their code!
A class which adopts the builder pattern has:
Setter methods to set the fields
A build()
method which calls the constructor
class Animal {
private int age ;
private String species ;
// constructor
}
public class AnimalBuilder {
private int age ;
private String species ;
public AnimalBuilder setAge (int age ) {
this .age = age ;
return this ;
}
public AnimalBuilder setSpecies (String species ) {
this .species = species ;
return this ;
}
public Animal build () {
return new Animal (age , species );
}
}
Suppose we need a specific instance, but we only have the information at runtime.
The factory method let's us return a specific instance using polymorphism
public abstract class Food {
private int quantity ;
public Food (int quantity ) { this .quantity = quantity ; }
}
class Hay extends Food {
public Hay (int quantity ) { super (quantity ); }
}
class Pellets extends Food {
public Pellets (int quantity ) { super (quantity ); }
}
class Fish extends Food {
public Fish (int quantity ) { super (quantity ); }
}
class FoodFactory {
public static Food gettFood (String animal ) {
switch (animal ) {
case "zebra" : return new Hay (1000 );
case "rabbit" : return new Pellets (5 );
case "goat" : return new Pellets (30 );
case "polar bear" : return new Fish (10 );
}
}
}