Table of contents
Inheritance
Inheritance in Java mirrors how real-world objects relate. Think of a hierarchy where a base class (like
Vehicle
) shares common traits with subclasses (likeCar
andMotorcycle
). Subclasses inherit features from the base class, such as methods likestart()
andstop()
in our example.Understanding Method Variations
Overridden Methods: Subclasses can modify the behavior of inherited methods, like a
Car
defining howstart()
operates differently.Specialized Methods: Subclasses can introduce new methods specific to their type, such as
accelerate()
in aCar
.Inherited Methods: Subclasses automatically inherit methods from their superclass, inheriting functionalities like
stop()
fromVehicle
.
Private Members and Their Scope
Private members (variables and methods) belong exclusively to their class and aren't inherited by subclasses. This keeps sensitive data within its class, promoting data encapsulation.
Java's Universal Base
Every Java class implicitly extends the
Object
class. This universal inheritance ensures all objects have basic functionalities liketoString()
andequals()
.Upcasting:
Upcasting allows treating a subclass object as its superclass type. While this offers flexibility in handling objects, it limits access to superclass-specific features until explicitly accessed through downcasting.
There are specific rules regarding what a superclass type can and cannot access, especially in the context of overridden, specialized, and inherited members. Here's a breakdown:
Inherited Variables:
Superclass reference can directly access its own variables.
Subclass inherits non-private variables from the superclass but cannot access private variables directly.
Overridden Methods:
Superclass can call overridden methods but executes the overridden version defined in the subclass.
@Override ensures that the compiler checks whether the method signature matches that of a method in the superclass. This helps catch errors where there might be typos in patent class method names
Superclass cannot access subclass-specific variables/methods directly.
Coming from a C++ background , we can understand that in java all methods are by default virtual (late binding understands the context of the object, rather than context of the caller)
Specialized Methods:
Superclass cannot access specialized methods (methods unique to subclasses) directly.
Subclass-specific variables/methods are hidden from the superclass type unless downcasted to the subclass type.
Constructor Dynamics
Constructors don't participate in inheritance; however, a subclass constructor implicitly calls its superclass constructor using
super()
. If a subclass constructor callsthis()
instead, it overrides the implicitsuper()
call, skipping the superclass's default constructor.- this is used to implement constructor chainingUtilizing
this()
andsuper()
this()
: Used within a class to invoke another constructor within the same class, aiding in constructor overloading and chaining.super()
: Invokes the superclass constructor, ensuring proper initialization of superclass attributes before subclass-specific tasks.
Access Boundaries: Super's Limitations
The
super
keyword can't directly access variables or methods of a grandparent class (super.super.a
is invalid). This emphasizes encapsulation and hierarchical structure, maintaining code integrity.
Role of Access Specifiers in Inheritance
While overriding a function
you cannot reduce the visibility of the methods you are overriding
you can increase the visibility of methods you are overriding
you cannot change the return type . Except in one case , when the return type is a covariant return type, is-a relationship between types
you cannot change parameter list of a a function you want to override, if you do it would be overloaded instead
NOTE:
Overloading - compile time VS Overriding - runtime
Compile time polymorphism - method overloading and constructor overloading
Runtime polymorphism - method overriding and Polymorphic Method Invocation
Polymorphism
same code doing multiple things according to context , for example animal.speak() will return "Woof" or "Meow" according to the type of instance object , and if instance object has overriden the base class speak implementation
Abstraction
in java we can have method signatures only without body , such methods should be declared as abstract
Abstract methods can only be made in abstract class
Any class with even one abstract method ,MUST be declared as abstract
Abstract class can have both abstract as well as concrete methods
An abstract method must either be overridden or the inheriting class should also be made abstract
abstractions are used to define the specification but not the details
you cannot instantiate objects of an abstract class
A class can be abstract, a method can be abstract , but never a variable
An abstract class can have a constructor
you cannot make constructor abstract , it has a default body with super () and not even inherited so there is no point of it being overriden as it is with other methods which are abstract in parent class
Abstract class can have a constructor and it gets executed because of super() call present in the child class constructor
Final keyword in java
final variable cannot be re-assigned
final methods although participate in inheritance , cannot be overriden
final class cannot be inherited from ie We cannot inherit from final class , so it can also cannot be made abstract (what would even be the point ?)
Abstract class / methods can never be final
Static keywords in java
static keywords are initialised once for the class and are not instance specific
static methods although participate in inheritance, cannot be overriden, but ! if you try to do so,it will become a specialised method of child class and it will hide the parents inherited implementation- This is called method hiding
Interface in Java
used to provide 100% abstraction and loose coupling
interface keyword need not be accompanied by class keyword
you cannot instantiate object of an interface
unlike inheritance from parent class/abstract class where `extends` keyword is used here 'implements' keyword is used
All methods in an interface are by-default public and abstract
Since the type is abstract , you cannot give them any defination/body -IDEALLY. but in newer versions of java this is possible
Since you cannot reduce visibiliity and abstract default type is public, the overridden methods are also public and obviously same return type
Like earlier we are allowed to do upcasting of implementors to interface type - to achieve runtime polymorphism . using type interface we can access overriden methods but bit specialised methods in implementors(will have to do downcasting if that functionality is needed )
multiple inheritance is not allowed in java , but you can implement multiple interfaces
One interface in java can
extend
another interface , but one interface cannot 'implement' another interface`implements` means that we are promising implementation
We can have variables inside an interface , but all variables by default are marked as public static final , hence none of the classes can modify it
Reference to parent interface can only access methods override specific to it , While upcasting to a parent (interface) type , the parent ref pointing to child instance , it can only access methods provided by it and implemented by child
A class can extend and implement at the same time , but it should extend first then implement
An interface with no-body is called marker interface,developer need not implement this , example of this is serializable interface
Abstract classes vs Interface
Interfaces have 100% abstraction , whereas abstract classes can have partial
All methods in interface are by default are, public and abstract , and variables are public static final, no such implicit behavious in abstract classes
only one abstract classs can be extended at a time , but multiple interfaces can be implemented at once
New changes in interfaces
(JAVA 8) In an interface we can now have a method with body/implementation by declaring method as default. - Default methods in an interface. They get inherited in the implementor class
Its not compulsory for implementing class to override this default method - its not abstract. but obviously it can be overridden if required
(JAVA 8) An interface can have static methods also, they will NOT get inherited in implementing classes but can be invoked using interface type
(JAVA 9)Now Suppose some code is common across all methods of the interface , for this purpose private methods as also allowed in interfaces now
Lambda functions
There are three ways to implement an interface
using the `implements` keyword
Creating an anonymous inner class
Use Lambda functions , which are just syntactic sugar for anonymous inner classes
In java 8 a new operator is introduced , lambda operator/arrow operator
Lambda expression is like an anonymous method, an un-named overriding method, a method with no name,
There is left side and right side of the operator , left side consists of parameter list , right side is the body, return type is implicitly defined
Since there is no name , they cannot be called independently
You can write lambda only if there is functional interface. An interface with only single abstract method is called functional interface @FunctionalInterface. The annotation ensures no one can add another method in functional interface
When you don't want to re-use implementation
You cannot write lama independently. Can only be written if there is a functional interface
Can Lambda be written for abstract classes ? no
the syntax of lambda interface can be simplified much further
The Lambda kind of allowing us to instantiate an functional interface on the go .. , indirectly. Syntax rules same as arrow functions in javascript
Association/Composition
In Java, composition/aggregation and inheritance are two different ways of achieving code reuse and structuring classes.
Composition/Aggregation:
Dependent object: This refers to an object that is part of another object's structure. For example, a
Car
class may have aEngine
object as a dependent object.Target object: This is the object that contains the dependent object. In the
Car
example, theCar
class is the target object.Dependency Injection: This is a technique where a dependent object is injected into the target object rather than being created within it. This can be done using setter methods (
setter injection
) or constructor arguments (constructor injection
).Advantages:
Flexibility: Allows for changing the dependent object at runtime without affecting the target object's behavior.
Modularity: Encourages loosely coupled classes, making the codebase easier to maintain and test.
Interface coupling: It's recommended to couple the target object to an interface rather than a concrete implementation. This promotes code flexibility and scalability.
Correct Way for Composition:
- To follow best practices, the target object should be coupled to an interface representing the dependent object's behavior rather than directly to a concrete class. This promotes flexibility and allows for easy switching of implementations.
Interface for Payment Strategy:
// Step 1: Define PaymentStrategy interface
public interface PaymentStrategy {
void pay(double amount);
}
Concrete Payment Strategy Implementations:
// Step 2: Implement Concrete Payment Strategies
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " via Credit Card");
// Payment logic for Credit Card
}
}
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " via PayPal");
// Payment logic for PayPal
}
}
Payment Context Coupled to Interface:
// Step 3: Use PaymentStrategy interface in PaymentContext
public class PaymentContext {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void processPayment(double amount) {
paymentStrategy.pay(amount);
}
}
Usage Example:
public class Main {
public static void main(String[] args) {
PaymentContext paymentContext = new PaymentContext();
// Using Composition and Dependency Injection (Strategy pattern)
PaymentStrategy creditCardPayment = new CreditCardPayment();
paymentContext.setPaymentStrategy(creditCardPayment);
paymentContext.processPayment(100.00);
PaymentStrategy payPalPayment = new PayPalPayment();
paymentContext.setPaymentStrategy(payPalPayment);
paymentContext.processPayment(50.00);
}
}
Explanation:
Interface for Payment Strategy:
- We define the
PaymentStrategy
interface, which represents the behavior of different payment methods.
- We define the
Concrete Payment Strategy Implementations:
- Concrete classes like
CreditCardPayment
andPayPalPayment
implement thePaymentStrategy
interface, providing specific payment logic.
- Concrete classes like
Payment Context Coupled to Interface:
- The
PaymentContext
class is coupled to thePaymentStrategy
interface rather than specific payment classes. This promotes flexibility and allows for easy switching of payment implementations.
- The
Usage Example:
- We create instances of concrete payment strategies (
CreditCardPayment
,PayPalPayment
) and inject them into thePaymentContext
using setter injection (setPaymentStrategy
). This follows best practices by coupling to an interface, not concrete classes.
- We create instances of concrete payment strategies (