OOP Concepts
OOP Concepts
Lesson 1
Advanced Programming Concepts:
Object-oriented Programming (OOP) Principles and Design Patterns
1. Encapsulation: Bundling the data (attributes) and the methods (functions) that
operate on the data into a single unit, called a class. It also involves restricting access
to certain components to prevent unintended interference and misuse of data.
2. Inheritance: Allowing a new class (child class) to inherit properties and behaviors
from an existing class (parent class), promoting code reusability.
3. Polymorphism: Enabling a single interface to represent different underlying forms
(data types). It allows for one method to be used for different types of objects.
4. Abstraction: Hiding the complex reality while exposing only the necessary parts. It
simplifies the interface by abstracting away unnecessary details, so users interact with
simplified representations.
2. Methods:
Getter and Setter methods allow controlled access to the private attributes.
getName() and setName() to get and set the name.
getAge() and setAge() to get and set the age.
getAddress() and setAddress() to get and set the address.
~~~
Java Code with Encapsulation:
Explanation of Encapsulation:
1. Private Attributes:
o The name, age, and address attributes are private. This means these fields
cannot be accessed directly from outside the class.
2. Public Getter and Setter Methods:
o We provide public getter methods (getName(), getAge(), getAddress()) to allow
access to the private attributes.
o We also provide public setter methods (setName(), setAge(), setAddress()) to
allow modification of the attributes, but with conditions and control (e.g., the
setAge() method checks if the age is positive).
3. Control over Data:
o By encapsulating the data and providing controlled access through methods, we
can ensure that the data remains valid and doesn't get set to invalid or
unexpected values (like a negative age).
~~~
Using the Person Class:
Output:
Name: John Doe
Age: 25
Address: 123 Main St
Name: Jane Smith
Age: 30
Address: 456 Elm St
Age must be positive.
Key Points:
1. Encapsulation allows us to hide the internal details of the Person class, like the name,
age, and address, from the outside world. This way, we can control how these
attributes are accessed and modified.
2. The setter methods (e.g., setAge()) can include validation logic to ensure that only
valid data is being set. In this example, we made sure that age cannot be negative.
3. The getter methods (e.g., getName()) allow safe access to the private attributes.
4. Data Integrity is maintained because we control how the data is accessed and
changed.
~~~
This is a simple demonstration of encapsulation in Java. It is a powerful mechanism that
enhances data security, code maintainability, and flexibility. By hiding the internal workings
and exposing only necessary functionality through methods, we can ensure that our objects
are used in a controlled way.
Inheritance is another fundamental concept in Object-Oriented Programming (OOP). It
allows a new class (called a subclass or child class) to inherit properties and behaviors
(fields and methods) from an existing class (called a superclass or parent class). This helps
to promote code reusability and creates a natural hierarchy between classes.
In Java, inheritance is achieved using the extends keyword.
Key Concepts:
1. Superclass: The class whose properties and behaviors are inherited. Also called the
parent class.
2. Subclass: The class that inherits the properties and behaviors from the superclass.
Also called the child class.
3. extends Keyword: Used by a subclass to inherit from a superclass.
Java does not support multiple inheritance (i.e., a subclass cannot directly inherit from more
than one superclass) to avoid ambiguity.
Output:
Name: Buddy
Age: 3
Buddy barks
Buddy is fetching the ball
Explanation:
1. Inheritance: The Dog class inherits from the Animal class, so it automatically has the
name and age attributes and the sound() method from the Animal class.
2. The Dog class has its own constructor and method (fetch()).
3. The sound() method is overridden in the Dog class to provide a more specific
implementation for dogs.
4. The super(name, age) call is used to invoke the constructor of the Animal class to
initialize the inherited attributes.
~~~
Example 2: Multilevel Inheritance
In multilevel inheritance, a subclass inherits from another subclass, forming a chain of
inheritance.
// Base class (Parent Class)
class Animal {
String name;
@Override
public void sound() {
System.out.println(name + " barks");
}
}
@Override
public void sound() {
System.out.println(name + " yaps");
}
}
Output:
Max yaps
Explanation:
Multilevel Inheritance: The Puppy class inherits from the Dog class, which in turn
inherits from the Animal class. Each subclass can override the methods of its
superclass, resulting in a method resolution order where the most specific method is
called.
The Puppy class calls its own sound() method, which overrides the sound() method
from Dog and Animal.
~~~
Example 3: Hierarchical Inheritance
In hierarchical inheritance, multiple subclasses inherit from a single superclass.
// Subclass 1
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
// Subclass 2
class Cat extends Animal {
@Override
public void sound() {
System.out.println("Cat meows");
}
}
Output:
Dog barks
Cat meows
Explanation:
Hierarchical Inheritance: Both Dog and Cat inherit from the Animal class, but each
provides its own implementation of the sound() method.
This allows multiple subclasses to share the same functionality from the superclass
but customize it as needed.
~~~
Advantages of Inheritance:
1. Code Reusability: Common code in the superclass can be reused by subclasses,
reducing redundancy.
2. Method Overriding: Subclasses can override superclass methods to provide specific
behavior, enhancing flexibility.
3. Extensibility: New functionality can be added by creating subclasses, which can
extend the functionality of existing code.
4. Simplified Code: It creates a natural hierarchy between related classes, making the
system easier to understand and maintain.
Conclusion:
Inheritance allows classes to inherit features (fields and methods) from other classes.
It helps to achieve code reuse, extendibility, and organization.
In Java, inheritance is implemented using the extends keyword, and classes can inherit
from one superclass at a time (single inheritance), forming a natural hierarchy.
class Calculator {
Explanation:
We have overloaded the add() method with different parameter lists.
The method is called with two integers, three integers, and two doubles. The correct
method is selected at compile time based on the arguments passed.
~~~
Runtime Polymorphism (Method Overriding)
At runtime, the JVM determines which method to call based on the object's type (i.e.,
whether it's an instance of the subclass or superclass).
// Superclass
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
// Subclass 1
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
// Subclass 2
class Cat extends Animal {
@Override
public void sound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
// Creating objects of subclass types
Animal myAnimal = new Animal();
Animal myDog = new Dog();
Animal myCat = new Cat();
Output:
Animal makes a sound
Dog barks
Cat meows
Explanation:
The Animal class has a method sound(), and both the Dog and Cat subclasses override
this method.
The method call myAnimal.sound(), myDog.sound(), and myCat.sound() is determined
at runtime. Even though all three variables are of type Animal, the method
corresponding to the actual object type (whether Dog or Cat) is called, which is
runtime polymorphism.
Key Points:
Compile-time polymorphism (method overloading) is resolved at compile time.
Runtime polymorphism (method overriding) is resolved at runtime based on the
object's actual type.
In both cases, polymorphism allows us to write more flexible and reusable code.
Conclusion:
Polymorphism allows for flexibility in programming, as it enables methods or functions
to behave differently based on the object type. It enhances code maintainability and
readability, as you can reuse method names while changing behavior for different object
types.
Explanation:
The Animal class is abstract and has one abstract method sound().
The Dog and Cat classes inherit from Animal and provide their own implementation of
the sound() method.
The user of the Dog or Cat classes doesn’t need to know how the sound() method is
implemented, only that it will make a sound.
~~~
2. Using Interfaces
An interface is another way to achieve abstraction. Unlike abstract classes, an interface
cannot contain any concrete methods (methods with a body). An interface defines only
method signatures, and the classes that implement the interface must provide the
implementation of the methods.
Example: Interface
// Interface
interface Shape {
// Abstract method (does not have a body)
void draw();
}
Explanation:
The Shape interface defines a method draw().
Both the Circle and Rectangle classes implement the Shape interface and provide
their own implementation of the draw() method.
The user of the Shape interface doesn’t need to know how the draw() method works,
just that it will be able to draw the shape.
Benefits of Abstraction:
1. Reduces Complexity: Abstraction hides unnecessary details, simplifying the code for
the user.
2. Improves Code Reusability: With abstract classes and interfaces, you can write
reusable and modular code.
3. Encourages Loose Coupling: Abstraction allows changes to the implementation
without affecting the interface or the users of the class.
4. Enhances Maintainability: By focusing on essential functionality and hiding the
complexity, the code is easier to maintain and extend.
Summary:
Abstraction allows you to focus on what an object does rather than how it does it.
It is achieved through abstract classes and interfaces in Java.
Abstraction hides the implementation details and exposes only the necessary
functionalities to the user.
Design patterns are common solutions to recurring problems in software design. They
provide standardized ways to solve problems and help developers write code that is
reusable, maintainable, and scalable. In Java (or any other object-oriented programming
language), design patterns are particularly useful for organizing code and creating systems
that are easier to extend and maintain.
1. Reusability: Design patterns solve common problems in a standardized way. Once you
learn a pattern, you can apply it across different projects.
2. Scalability: Patterns are often designed to allow easy expansion or modification of a
system.
3. Maintainability: By using well-understood solutions, design patterns make it easier to
maintain and extend code.
4. Communication: Design patterns provide a common vocabulary for developers to
discuss solutions and ideas.
Singleton
Purpose: Ensures that a class has only one instance and provides a global point of
access to that instance.
When to Use: When you need to control access to shared resources, like a database
connection or configuration.
Explanation:
1. Private static variable (instance):
o The instance variable is private and static, meaning there will be only one
instance of the Singleton class, and it will be shared across the entire
application.
o The static keyword ensures that the instance is tied to the class itself, rather
than to any individual object. So, no matter how many times you call
getInstance(), you'll always get the same Singleton instance.
2. Private constructor (private Singleton()):
o The constructor is marked as private to prevent external classes from creating
new instances of the Singleton class directly. This enforces the rule that only
one instance of Singleton will exist.
o You can't instantiate Singleton outside of the Singleton class itself.
3. Public static method (getInstance()):
o This method provides the global access point for obtaining the single instance of
Singleton.
o The getInstance() method checks if the instance is null. If it is, it creates a new
Singleton instance. This ensures that only one instance is created during the
lifetime of the application (i.e., lazy initialization).
o If the instance already exists, it simply returns the existing instance.
4. Example method (showMessage()):
This is a regular instance method in the Singleton class, which demonstrates
some functionality of the singleton. In this case, it prints a message to the
o
console.
o This method can be called on the single instance of Singleton.
Overall, the Singleton pattern is most effective when you truly need to manage global state
or resources and are sure that a single instance is sufficient for the task at hand.
Example:
This Java code gets an input string the user and displaying it five times in the screen
using Singleton design pattern.
import java.util.Scanner;
scanner.close();
}
}
Code Explanation
1. Singleton Class: The StringDisplay class is designed to ensure that only one instance
of it can exist. This is achieved by:
o A private static variable instance that holds the single instance of the class.
A public static method getInstance() that checks if the instance is null; if so, it
creates a new instance.
o
2. Setting the Message: The setMessage(String message) method allows us to set the
string that we want to display.
3. Displaying the Message: The displayMessage() method contains a loop that prints the
stored message five times to the console.
We retrieve the singleton instance of StringDisplay, set the user input as the
message, and then call displayMessage() to print it five times.
o
This implementation not only adheres to the Singleton pattern but also provides a clear and
efficient way to manage the display of a string input by the user.
Purpose: Defines an interface for creating objects, but lets subclasses alter the
type of objects that will be created.
•
Example: A method that creates different types of shapes (Circle, Square, etc.)
depending on the input.
•
Explanation:
import java.util.Scanner;
// Shape interface
interface Shape {
void draw();
}
// Circle class implementing Shape
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
// Square class implementing Shape
class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
// ShapeFactory class
abstract class ShapeFactory {
public abstract Shape createShape();
}
// CircleFactory class
class CircleFactory extends ShapeFactory {
@Override
public Shape createShape() {
return new Circle();
}
}
// SquareFactory class
class SquareFactory extends ShapeFactory {
@Override
public Shape createShape() {
return new Square();
}
}
// Client code
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Enter the shape you want to create (Circle/Square): ");
String shapeType = scanner.nextLine();
ShapeFactory factory;
Shape shape = null;
switch (shapeType.toLowerCase()) {
case "circle":
factory = new CircleFactory();
shape = factory.createShape();
break;
case "square":
factory = new SquareFactory();
shape = factory.createShape();
break;
default:
System.out.println("Invalid shape type.");
return;
}
shape.draw();
scanner.close();
}
Code Explanation
In the provided code, Factory Method Design Pattern was implement to create different
shapes. Here’s a breakdown of the components:
1. Shape Interface: This is a common interface for all shapes. It declares a
method draw() that each shape class must implement.
2. Circle and Square Classes: These classes implement the Shape interface. Each class
provides its own implementation of the draw() method, which outputs a message
indicating the shape being drawn.
3. ShapeFactory Abstract Class: This abstract class defines a method createShape(String
shapeType). Subclasses will implement this method to create specific shapes.
4. ConcreteShapeFactory Class: This class extends ShapeFactory and provides the
implementation for the createShape method. It checks the input string and returns an
instance of the corresponding shape (Circle or Square). If the input does not match
any known shape, it returns null.
5. Client Code (ShapeFactoryDemo): This is the entry point of the application. It creates
an instance of ConcreteShapeFactory and uses it to create shapes based on user input.
The draw() method is then called on each shape to demonstrate the functionality.
This design pattern not only simplifies the process of object creation but also adheres to the
Open/Closed Principle, allowing for easy extension of new shapes without modifying existing
code.