0% found this document useful (0 votes)
35 views33 pages

Summary OOP

its a summery about OOP and JAVA

Uploaded by

nzmbrm
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
35 views33 pages

Summary OOP

its a summery about OOP and JAVA

Uploaded by

nzmbrm
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 33

Compile errors

1. incompatible
2. ";" expected
3. cannot find symbols

Runtime errors
1. ClassCastException
2. IllegalArgumentException
3. ArrayIndexOutOfBoundsException

Static & Nested Classes

static class variable

public class MyClass {


// Static variable
public static int staticVariable = 0;

// Instance method to modify the static variable


public void incrementStaticVariable() {
staticVariable++;
}

public static void main(String[] args) {


// Increment the static variable using static method
MyClass.staticVariable++;

// Print the static variable


System.out.println("Static variable: " + MyClass.staticVariable); // Output: Static variable:

// Create an instance of MyClass


MyClass obj1 = new MyClass();

// Increment the static variable using instance method


obj1.incrementStaticVariable();

// Print the static variable again


System.out.println("Static variable: " + MyClass.staticVariable); // Output: Static variable:
}
}

non-static inner-classes

public class OuterClass {


private String outerField = "Outer Field";

// Member inner class


class InnerClass {
void display() {
System.out.println("Outer field is: " + outerField);
}
}

public static void main(String[] args) {


OuterClass outer = new OuterClass();
InnerClass inner = outer.new InnerClass();
inner.display();
}
}

inner-class static & inner-class method non static

// Example 1
public class OuterClass {
private static String outerField = "Outer Field";

// Static nested class


public static class StaticNestedClass {
public void display() {
System.out.println("Outer field is: " + outerField);
}
}

public static void main(String[] args) {


StaticNestedClass nested = new StaticNestedClass();
nested.display();
}
}

// Example 2
public class OuterClass {
private static String staticOuterField = "Static Outer Field";
private String instanceOuterField = "Instance Outer Field";

// Static nested class


public static class StaticNestedClass {
public void display() {
// Can access static members of the outer class
System.out.println("Accessing: " + staticOuterField);

// Cannot access instance members of the outer class directly


// System.out.println("Accessing: " + instanceOuterField); // This will cause a compile-ti
}
}
}

public class Main {


public static void main(String[] args) {
// Creating an instance of the static nested class
OuterClass.StaticNestedClass nestedInstance = new OuterClass.StaticNestedClass();
nestedInstance.display();
}
}

inner-class & inner-class method both static

// Example 1
public class OuterClass {
private static String outerField = "Outer Field";

// Static nested class


public static class StaticNestedClass {
static void display() {
System.out.println("Outer field is: " + outerField);
}
}

public static void main(String[] args) {


StaticNestedClass.display(); // Call the static method directly without creating an instance
}
}

// Example 2
public class OuterClass {
private static String staticOuterField = "Static Outer Field";
private String instanceOuterField = "Instance Outer Field";

// Static nested class


public static class StaticNestedClass {
public static void display() {
// Can access static members of the outer class
System.out.println("Accessing: " + staticOuterField);
}
}
}

public class Main {


public static void main(String[] args) {
// Calling the static display method of the static nested class
OuterClass.StaticNestedClass.display();
}
}
Threads & Runnable
// Task to print numbers
class NumberPrinter implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Number: " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore interrupted status
System.out.println("NumberPrinter interrupted");
}
}
}
}

// Task to print letters


class LetterPrinter implements Runnable {
@Override
public void run() {
for (char ch = 'A'; ch <= 'E'; ch++) {
System.out.println("Letter: " + ch);
try {
Thread.sleep(1500); // Sleep for 1.5 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore interrupted status
System.out.println("LetterPrinter interrupted");
}
}
}
}

public class MultiThreadExample {


public static void main(String[] args) {
// Create Runnable objects
Runnable numberTask = new NumberPrinter();
Runnable letterTask = new LetterPrinter();

// Create Thread objects


Thread numberThread = new Thread(numberTask);
Thread letterThread = new Thread(letterTask);

// Start the threads


numberThread.start();
letterThread.start();
}
}

OOP vs structured programming languages (C)


1. Programming Paradigm
Object-Oriented Programming (OOP): OOP languages, such as Java, C++, and Python, are based on the concept
of "objects." These objects are instances of classes, the classes are templates (blueprints) to create objects. OOP
is centered around four main principles: encapsulation, inheritance, polymorphism, and abstraction. These
principles help manage and manipulate data by modeling it closer to how you might think about objects in the real
world.

Note: Modularity and code reuse: OOP promotes modularity and code reuse through encapsulation,
inheritance, and polymorphism, making it easier to maintain and extend large and complex programs.

Polymorphism: When you define a supertype for a group of classes, any subclass of that supertype can be
substituted where the supertype is expected.

class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
}

class Dog extends Animal {


@Override
public void makeSound() {
System.out.println("Bark");
}

public void sleep() {


System.out.println("Dog is sleeping");
}
}

//Compile Time: At compile time, the compiler only knows that animal is of type Animal. It does not kn
//Runtime: At runtime, when the line Animal animal = new Dog(); is executed, an object of type Dog is
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // Upcasting: Dog object, Animal reference

//
animal.makeSound(); // Outputs "Bark" because the actual object is a Dog

// This line would cause a compile-time error:


// animal.sleep(); // Animal class does not have a sleep method

// Runtime check using instanceof


if (animal instanceof Dog) { // True at runtime
Dog dog = (Dog) animal; // Downcasting
dog.sleep(); // Now we can call sleep(), outputs "Dog is sleeping"
}
}
}
Advantages of polymorphism

// Define a Notification interface (reduces coupling)


interface Notification {
void send(String message);
}

// Polymorphism allows for the use of a single interface


// or abstract class to represent multiple types (CODE REUSE)
class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending email: " + message);
}
}

// New notification type can be added easily (FLEXIBILITY)


class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending push notification: " + message);
}
}

// NotificationService depends on Notification interface (REDUCES COUPLING)


class NotificationService {
private Notification notification;

// Constructor injects a Notification (reduces coupling)


public NotificationService(Notification notification) {
this.notification = notification;
}

public void notifyUser(String message) {


notification.send(message); // Single method to handle all notifications (code reuse)
}
}

public class Main {


public static void main(String[] args) {
// Creating service with different notifications (flexibility)
NotificationService emailService = new NotificationService(new EmailNotification());
NotificationService pushService = new NotificationService(new PushNotification());

// Notify users with different services (code reuse)


emailService.notifyUser("Your order has been shipped.");
pushService.notifyUser("You have a new message.");
}
}

Structured Programming: Structured programming, exemplified by languages like C, emphasizes a logical structure
in the code. It uses functions or procedures to perform tasks. The focus is on improving clarity, quality, and
development time by using blocks of code (like functions and control structures such as loops and if-else
statements) that can execute sequentially, conditionally, or repeatedly.
In Java, three things can prevent a class from being subclassed (inheritance):

Access Control: A non-public class (default access) can only be subclassed by classes within the same
package.
Final Modifier: A class declared as final cannot be subclassed.
Private Constructors: A class with only private constructors cannot be subclassed.

2. Code Organization

OOP: Code in OOP is organized around objects and classes. Classes define the attributes and behaviors of
objects, encapsulating data and functions together. This leads to a modular approach where related data and
behaviors are grouped together, making the codebase more scalable and manageable.

Structured Programming: In structured programming, code is organized into procedures or functions. This
approach promotes a top-down design model, starting with a broad problem and breaking it down into smaller and
more manageable functions. There’s less emphasis on how data is encapsulated or related to functions.

3. Data Handling

OOP: Data is almost always hidden from the outside world (private instance variables) and can only be accessed
or modified through methods (functions within classes), which is a part of encapsulation. This can prevent data
from being accidentally modified in unintended ways.

Setter methods in object-oriented programming let you change how values are set without affecting other parts
of your code that use the class. While using public variables directly might look more efficient, the gain in
performance is very small. The main benefit of setters is that they keep your code flexible and prevent issues
when changes are made internally.

Structured Programming: Data is generally more exposed and can be accessed directly from anywhere in the
program. This can lead to higher risk of bugs or data corruption due to unintended modifications.

Encapsulation with getter & setter - examples:

1. Control Over Data: This approach allows you to modify the implementation without affecting any existing code that
uses the getBalance() or setBalance(double) methods, demonstrating flexibility and future-proofing.

public class BankAccount {


private double balance;

// Initial simple getter


public double getBalance() {
return balance;
}

// Initial simple setter


public void setBalance(double balance) {
this.balance = balance;
}

// Later modification without changing the external interface


public void setBalance(double balance, String currency) {
// Additional logic to handle different currencies
if (currency.equals("USD")) {
this.balance = balance;
}
// log change for audit
logBalanceChanged(this.balance, balance);
this.balance = balance;
}
}

2. Encapsulation: Imagine a class UserProfile that should not allow the user's ID to be changed once it's set, but other
profile information can be updated. In this case, the absence of a setter for the userId protects its value from
external changes, ensuring the user ID remains consistent throughout the object's lifecycle.

public class UserProfile {


private String userId;
private String email;

// Constructor to set the user ID


public UserProfile(String userId) {
this.userId = userId;
}

// No setter for userId to prevent modification


public String getUserId() {
return userId;
}

// Setter for email allows updating


public void setEmail(String email) {
this.email = email;
}

public String getEmail() {


return email;
}
}

3. Additional Logic in Access/Mutation: Let's consider a Rectangle class where area needs to be computed based on
length and width, but it should not be stored directly. The getArea method computes the area dynamically from
length and width, ensuring that the area is always correct relative to its dimensions, without the need for storing it
directly.

public class Rectangle {


private double length;
private double width;

// Setters for length and width


public void setLength(double length) {
this.length = length;
}

public void setWidth(double width) {


this.width = width;
}

// Computed getter for area


public double getArea() {
return length * width;
}
}

4. Debugging and Logging: In this case, the setter logs each change to the temperature.

public class Thermostat {


private int temperature;

// Setter that logs changes


public void setTemperature(int temperature) {
System.out.println("Temperature changed from " + this.temperature + " to " + temperature);
this.temperature = temperature;
}

public int getTemperature() {


return temperature;
}
}

Inheritance and Reusability

OOP: OOP languages support inheritance, allowing new classes to derive attributes and behaviors from existing
ones, facilitating code reusability and the creation of a hierarchical organization of classes.

Structured Programming: Structured languages like C do not support inheritance. Code reuse is typically achieved
through functions and, sometimes, through copy-pasting code with modifications, which can be less efficient and
more error-prone.

Final Keyword
Final Variables: A final variable can only be initialized once. Once a final variable has been assigned, it cannot be
reassigned.

public class Example {


public static void main(String[] args) {
final int x = 10;
x = 20; // This will cause a compilation error
}
}

Final Methods: A final method cannot be overridden by subclasses. This ensures that the method's implementation
remains unchanged.

class Parent {
public final void display() {
System.out.println("This is a final method.");
}
}

class Child extends Parent {


// This will cause a compilation error
// @Override
// public void display() {
// System.out.println("Trying to override.");
// }
}

Final Classes: A final class cannot be subclassed. This is useful when you want to prevent inheritance of a class.

public final class FinalClass {


// Class implementation
}

// This will cause a compilation error


// public class SubClass extends FinalClass {
// // Subclass implementation
// }

Final Parameters: A final parameter cannot be changed within the method. It ensures that the parameter value remains
constant during method execution.

public class Example {


public void display(final int x) {
x = 20; // This will cause a compilation error
System.out.println(x);
}
}

Compiler and JVM


Runtime ArrayList and LinkedList
In Java, when an ArrayList needs to increase its capacity because it has run out of space to store new elements, it
typically grows by approximately 1.5 times its current size. (Initial default capacity: 10)
This resizing strategy is designed to optimize performance and minimize the number of resizes that need to occur
as the list grows, since each resize operation involves creating a new array and copying elements from the old
array to the new one, which can be costly in terms of performance.

Operation Arrays Linked Lists

Reading (Access) O(1) O(n)

Insertion - End
O(1) O(1) (if position is known)
(without resizing)

Insertion - End (with


O(n) worst-case, O(1) amortized O(n) (if position is not known)
resizing)

O(n) (if position is not known)


Insertion - Middle O(n)
+ O(1) (to insert)

O(n) (if position is not known)


Insertion - Beginning O(n)
+ O(1) (to insert)

O(n) (if position is not known)


Deletion - Middle O(n)
+ O(1) (to delete)

O(n) (if position is not known)


Deletion - Beginning O(n)
+ O(1) (to delete)

O(1) (unless resizing is involved, but typically no


Deletion - End O(1) (if position is known)
resizing is needed for deletion)
Initialization of variables
You must ensure that any local variables are initialized before you use them. If there's any code path that could lead to
the variable being used without prior initialization, the Java compiler will give an error. This helps prevent bugs
associated with uninitialized variables, which can lead to unpredictable program behavior.

For variables that are members of a class (fields), if you don’t explicitly initialize them, they are given a default
value (null for objects, 0 or 0.0 for numeric types, false for booleans) by the Java Virtual Machine (JVM). Thus, you
can use them even if you haven't assigned any initial value in your code.

Switch vs if-else
Switch is ideal for simple, direct comparisons, while if-else is necessary for more complex logical evaluations.

Usage: Use switch for straightforward equality checks against multiple constants or enums of a single variable. Use
if-else for more complex conditions involving ranges, multiple variables, or logical operations.

Readability: switch is cleaner and easier to manage with many cases. if-else is more flexible but can become
unwieldy with numerous conditions.

Performance: switch can be faster for many cases due to compiler optimizations like jump tables. if-else checks
conditions sequentially, which may be slower for large chains.

Immutable and Mutable Objects


Immutable Objects in Java

String: The String class in Java creates immutable objects. Any operation that seems to modify a String actually
creates a new String object.
Wrapper Classes: All the primitive wrapper classes (Integer, Long, Double, Float, Short, Byte, Character, Boolean)
are immutable.
BigDecimal and BigInteger: These classes are used for precise arithmetic operations, especially where standard
floating-point types like double and float cannot maintain precision. They are immutable.
LocalDate, LocalTime, and LocalDateTime: These classes from the Java 8 Date-Time API are immutable and
thread-safe, designed to replace the older mutable classes like java.util.Date and Calendar.
UUID: The universally unique identifier class is immutable.
Path: Represents a system-independent path, part of the NIO.2 upgrade to Java's file I/O capabilities, is immutable.

Mutable Objects in Java


StringBuilder and StringBuffer: These classes are like String but are designed to be mutable, allowing modification
of their contents without creating new objects.
Arrays: All types of arrays in Java (int[], Object[], etc.) are mutable. Their elements can be changed after they are
initialized.
ArrayList, HashMap, and other collections: Most of the Java Collections Framework implements mutable
collections. You can add, remove, and modify elements or pairs in these collections.
Date and Calendar: These older classes are mutable, meaning that their values can be changed after creation.
User-Defined Classes: Most user-defined classes are mutable unless specifically designed to be immutable. This
includes creating classes without setter methods, making all fields final and private, and ensuring no methods
modify the state.

String s = "initial";
s = s.concat(" more"); // 's' now points to a new string object, original string is unchanged.

StringBuilder sb = new StringBuilder("initial");


sb.append(" more"); // 'sb' is still the same object, but its internal state has changed.

Key Points:

Note:

Reference Assignment: When you assign one reference variable to another, both references point to the
same object.
Object State: Changes made through one reference to the object’s state are visible through the other
reference because they both point to the same object.
No Pointer Arithmetic: While you can change what object a reference points to, you cannot perform
arithmetic on the reference itself as you would with pointers in C/C++.
Like, can you refer to a Dog and then five minutes later refer to a Car? Of course not. Once I’m declared,
that’s it. If I’m a Dog remote control then I’ll never be able to refer to anything but a Dog. But I can be
referring to one Dog, and then five minutes later I can refer to some other Dog. As long as it’s a Dog, I can
be redirected. if I’m marked as final, then once I am assigned a Dog, I can never be reprogrammed to
anything else but that one and only Dog.

Primitives and Null Values


Primitives Cannot Be Null: In Java, primitive types (such as int, float, double, char, boolean, etc.) cannot be
assigned null. They always hold a value, and if not explicitly initialized, they take default values (e.g., 0 for numeric
types, false for boolean, etc.). This is different from reference types (objects), which can be null, indicating that they
do not currently reference any object.
Null and Garbage Collection for Objects: When an object in Java is no longer referenced by any variable, it
becomes eligible for garbage collection. The garbage collector automatically removes such objects from the heap
memory, freeing up resources.
Person p = new Person("John");
p = null; // The Person object previously referenced by 'p' is now eligible for garbage collection.

Primitives Cannot Be Null: Primitive types always have a value and cannot be assigned null.
Garbage Collection: Only applies to objects (reference types) on the heap. When an object becomes unreachable
(no references pointing to it), it becomes eligible for garbage collection.
What Happens When a Primitive is "Null"? Primitives are stored on the stack (if they are local variables) or within
the object's memory space on the heap (if they are instance variables). Since primitives cannot be null, the concept
of garbage collection does not apply to them directly. Local variables (including primitives) are automatically
managed by the stack frame. When a method call completes, its stack frame is popped from the stack, and all local
variables within that frame are discarded, effectively freeing that memory.

Stack vs ArrayDeque
Deque is like Stack & Queue combined Deque implements Queue interface

import java.util.ArrayDeque;
import java.util.Deque;

public class StackAndQueueExample {


public static void main(String[] args) {
Deque<Integer> deque = new ArrayDeque<>();

// Stack operations
deque.push(1); // Stack: 1
deque.push(2); // Stack: 2, 1

// Queue operation
deque.offer(3); // Queue/Deque: 2, 1, 3

// More Stack operations


System.out.println("Popped from stack: " + deque.pop()); // Outputs 2, remaining: 1, 3

// Queue operations
System.out.println("Polled from queue: " + deque.poll()); // Outputs 1, remaining: 3

// Final state of the deque


deque.push(4); // Stack/Deque: 4, 3
deque.offer(5); // Queue/Deque: 4, 3, 5

// Output all elements


System.out.println("Current deque: " + deque); // Should output: 4, 3, 5
}
}
Access Modifiers
Public: Visible to all classes everywhere, whether they are in the same package or have imported the package
containing the public class.

Protected: Visible to all classes in the same package and to subclasses even if they are in different packages.

Package-Private (Default): Visible only to classes within the same package. Not visible to classes outside the
package, regardless of subclassing.

Private: Visible only within the class itself.

package package1;

public class A {
int defaultMember = 1; // package-private
protected int protectedMember = 2;
private int privateMember = 3;
public int publicMember = 4;
}

package package2;
import package1.A;

public class B extends A {


void accessMembers() {
// System.out.println(defaultMember); // Error: defaultMember is not accessible outside its pa
System.out.println(protectedMember); // OK: protectedMember can be accessed because B is a sub
// System.out.println(privateMember); // Error: privateMember is not accessible outside its ow
System.out.println(publicMember); // OK: publicMember is accessible because it is public.
}
}

Pass-by-value / pass-by-copy / casting


Pass-by-value / pass-by-copy

The concept that Java is "pass by value" refers to the way Java handles method arguments. In Java, when you pass a
variable to a method, you are actually passing a copy of the variable's value, not the actual variable itself. This is true for
both primitive data types and objects. Let's break down what this means for each type

Passing Primitives: The actual value is copied, and modifications to this value do not affect the original
variable.
Passing Objects: The reference value is copied (not the object itself), and modifications through this copied
reference can affect the original object. However, reassigning the reference to a new object does not change
where the original reference points.

arguments: variable which is passed


parameters: function which expects arguments

Autoboxing

int i = 5;
Integer integerObject = i; // Autoboxing from int to Integer

// The compiler essentially treats the above assignment like this:


Integer integerObject = Integer.valueOf(i);

int a = i; //auto-unboxing -> from wrapper class Integer to primitive int

Explicit cast / type casting

In Java, explicit casting is necessary when you want to convert a value from a broader or higher precision type to a
narrower or lower precision type, or from a superclass to a subclass.

double myDouble = 9.78;


int myInt = (int) myDouble; // Casts double to int

Animal myAnimal = new Dog();


Dog myDog = (Dog) myAnimal; // Explicit casting from Animal to Dog

float f = 3.14f;
int x = (int) f; // x will equal 3

int calcArea(int height, int width) { return height * width; }

byte h = calcArea(4, 20); //This will not compile. The int value cannot be implicitly cast to byte wit

byte h = (byte) calcArea(4, 20); //This will compile. Since implicit conversion is provided for the re

Initialization of instance and local variables


Primitive data types have the following defaults:

byte, short, int, long: 0 or 0L


float, double: 0.0f or 0.0d
char: \u0000 (null character) (control character that signifies "null" in character data - distinct from null for
object references, which indicates the absence of an object.)
boolean: false
Object references (including arrays) are initialized to null.

Local variables, on the other hand, do not receive a default value. If you attempt to use a local variable without
initializing it, the Java compiler will issue a compilation error stating that the variable might not have been
initialized.

Method parameters are virtually the same as local variables—they’re declared inside the method. Parameters are
ALWAYS initialized, because the compiler guarantees that methods are always called with arguments that match
the parameters declared for the method, and the arguments are assigned (automatically) to the parameters. But
method parameters will never be uninitialized, so you’ll never get a compiler error telling you that a parameter
variable might not have been initialized.

public class VariableInitialization {


private int instanceVariable; // Instance variable
private Object referenceVariable; // Reference to an Object

public void showValues() {


int localVariable; // Local variable

// Print instance variables (automatically initialized)


System.out.println("Instance Variable: " + instanceVariable); // Outputs 0
System.out.println("Reference Variable: " + referenceVariable); // Outputs null

// Uncommenting the following line will cause a compilation error


// System.out.println("Local Variable: " + localVariable); // Error: variable localVariable mi
}

public static void main(String[] args) {


new VariableInitialization().showValues();
}
}

Pre and Post Increment/Decrement Operator


int x = 0;
int z = ++x; //produces: x is 1,z is 1 (x = x + 1)

int x = 0;
int z = x++; //produces: x is 1,z is 0
Useful library functions
ArrayList

add(obj element) / add(int index, obj element)


remove(obj element) / remove(int index)
get(int index)
size()
contains(obj element)
isEmpty()
indexOf(obj element)

Abstract classes & Interfaces


Abstract class = A class that can’t be instantiated.
Animal should not have instantiated objects (only it's subclasses)
Besides classes, you can mark methods abstract, too. An abstract class means the class must be extended;
an abstract method means the method must be overridden.
If you put even a single abstract method in a class, you have to make the class abstract. But you can mix both
abstract and non-abstract methods in the abstract class.
Abstract methods don’t have a body; they exist solely for polymorphism.
the first concrete class in the inheritance tree must implement all abstract methods.
Every class in Java extends class Object (Dog -> Animal -> Object)
A Java interface is like a 100% pure abstract class.
Deadly Diamond of Death: when multiple inheritance would be possible in Java: Because when not
implementing the methods, compiler would not know which method to call
Interface solves Diamond of Death problem: A Java interface is like a 100% pure abstract class -> make all the
methods abstract! That way, the subclass must implement(override) the methods.
interface methods are implicitly abstract, so typing in ‘public’ and 'abstract' is optional (no body).

// using interfaces to resolve ambiguity


interface A {
void display(); // Abstract method by default
}

interface B {
void display(); // Abstract method by default
}

class C implements A, B {
@Override
public void display() {
System.out.println("Display from C");
}
}
public class Main {
public static void main(String[] args) {
C obj = new C();
obj.display(); // No ambiguity here, calls display() from C
}
}

// Example with Conflicting Default Methods


interface A {
default void display() {
System.out.println("Display from A");
}
}

interface B {
default void display() {
System.out.println("Display from B");
}
}

class C implements A, B {
// The compiler requires this method to resolve the conflict
@Override
public void display() {
// Optionally, you can call a specific default method from an interface
A.super.display(); // or B.super.display();
// Or provide a completely new implementation
System.out.println("Display from C");
}
}

public class Main {


public static void main(String[] args) {
C obj = new C();
obj.display(); // No ambiguity here, calls display() from C
}
}

Local and instance variables


Instance variables are variables declared inside a class but outside any method.
Local variables are variables declared inside a method or method parameter.
All local variables live on the stack, in the frame corresponding to the method where the variables are
declared.
All objects live in the heap, regardless of whether the reference is a local or instance variable.
Object reference variables work just like primitive variables — if the reference is declared as a local variable, it
goes on the stack corresponding to the method.
Constructors
multiple constructors allowed
If you have more than one constructor in a class, it means you have overloaded constructors.
Java lets you declare a method with the same name as your class. That doesn’t make it a constructor, though.
The thing that separates a method from a constructor is the return type. Methods must have a return type,
but constructors cannot have a return type.
The argument list includes the order and types of the arguments. As long as they’re different, you can have
more than one constructor (same for methods).
While Java will automatically call the no-argument constructor of the parent class (same as default no arg
constructors). If no super call is present, using super explicitly is necessary when you need to call a parent
class constructor with arguments. It also improves readability, clarity, and maintainability.

public class Animal {


public Animal(String name) {
System.out.println("Making an Animal named " + name);
}
}

public class Hippo extends Animal {


public Hippo(String name) {
super(name); // Explicitly calling the parent class constructor with a String argument -> It's
// Compilation Error Without super(name): If you remove super(name) from the Hippo constructor
// Error: constructor Animal in class Animal cannot be applied to given types; public Hippo(St
System.out.println("Making a Hippo named " + name);
}
}

public class TestHippo {


public static void main(String[] args) {
System.out.println("Starting...");
Hippo h = new Hippo("Harry");
}
}

Object lifespan
A local variable lives only within the method that declared the variable.
An instance variable lives as long as the object does. If the object is still alive, so are its instance variables.
Life: A local variable is alive as long as its Stack frame is on the Stack. In other words, until the method
completes.
Scope: A local variable is in scope only within the method in which the variable was declared. When its own
method calls another, the variable is alive, but not in scope until its method resumes. You can use a variable
only when it is in scope.
public class Life {
int size;

public void read() {


int s = 42;
// 's' can be used only within this method.
// When this method ends, 's' disappears completely.
}

public void setSize(int s) {


size = s;
// 's' disappears at the end of this method,
// but 'size' can be used anywhere in the class
}
}

// Variable ‘s’ (this time a method parameter) is in scope only within the setSize() method.
// But instance variable size is scoped to the life of the object as opposed to the life of the method

public class StackRef {

public void foof() {


Duck d = barf(); // 'd' gets a reference to the Duck object created in barf()
// pass by value / pass by copy

System.out.println(d.equals(barf())); // Compare d with a new Duck object returned by another


// Since the equals method compares the id field, and bo
System.out.println(d == barf()); // Compare d with the new Duck object returned by another cal
}

public Duck barf() {


return new Duck(42); // Creates and returns a new Duck object with id 42
}
}

Static variables
instance variables: 1 per instance; static variables: 1 per class
Without an instance reference, static methods cannot directly access instance variables or instance methods
because those require an instance of the class to determine which object's variables or methods to use.
Duck.java:6: non-static variable size cannot be referenced from a static context
Duck.java:6: non-static method getSize() cannot be referenced from a static context
Instance Variables: Each instance (object) of the class has its own separate copy of these variables. They are
defined without the static keyword.
Class Variables (Static Variables): These are shared among all instances of the class. They are defined with
the static keyword.
In Java, if you have a local variable, method parameter, or field with the same name as a class-level variable
(static or instance), the local scope will shadow the class-level variable. This means that within the method, the
local variable or parameter takes precedence over the class-level variable.

class Example {
int instanceVariable = 10; // Instance variable
static int staticVariable = 20; // Static variable

// Instance method
void instanceMethod() {
System.out.println("Instance method");
}

// Static method
static void staticMethod() {
// Can access static variable
System.out.println("Static variable: " + staticVariable);

// Cannot access instance variable


// System.out.println("Instance variable: " + instanceVariable); // This would cause an error

// Cannot call instance method


// instanceMethod(); // This would cause an error
}
}

class Example {
int instanceVariable = 10; // Instance variable
static int staticVariable = 20; // Static variable

// Instance method
void instanceMethod() {
System.out.println("Instance method");
}

// Static method
static void staticMethod() {
// Create an instance of Example
Example example = new Example();

// Access instance variable through the instance


System.out.println("Instance variable: " + example.instanceVariable);

// Call instance method through the instance


example.instanceMethod();
}
}

public class Main {


public static void main(String[] args) {
Example.staticMethod(); // Calling static method
}
}

Key Differences: Static Initializer vs Constructor


Initialization Context:
Static Initializer: Initializes static variables or performs class-level setup.
Constructor: Initializes instance variables and sets up instance-specific state.
Execution Timing:
Static Initializer: Executed once when the class is loaded.
Constructor: Executed each time a new instance of the class is created.
Access Scope:
Static Initializer: Can only access static variables and methods.
Constructor: Can access both static and instance variables and methods.
Usage Context:
Static Initializer: Useful for complex static variable initialization or class-wide setup.
Constructor: Essential for setting up new objects and initializing their instance variables.

public class CombinedExample {


public static final double STATIC_CONSTANT;
private double instanceVariable;

// Static initializer block


static {
STATIC_CONSTANT = Math.random();
System.out.println("Static initializer block executed.");
}

// Constructor
public CombinedExample() {
this.instanceVariable = Math.random();
System.out.println("Constructor executed.");
}

public static void main(String[] args) {


System.out.println("STATIC_CONSTANT: " + STATIC_CONSTANT);

CombinedExample example1 = new CombinedExample();


System.out.println("Instance Variable of example1: " + example1.instanceVariable);

CombinedExample example2 = new CombinedExample();


System.out.println("Instance Variable of example2: " + example2.instanceVariable);
}
}

Exception
Throwable
├── Error
│ ├── LinkageError
│ ├── VirtualMachineError
│ │ ├── OutOfMemoryError
│ │ ├── StackOverflowError
│ ├── AssertionError
├── Exception
│ ├── RuntimeException
│ │ ├── ArithmeticException
│ │ │ ├── NullPointerException
│ │ │ ├── IndexOutOfBoundsException
│ │ │ ├── IllegalArgumentException
│ │ │ ├── IllegalStateException
│ │ │ ├── ClassCastException
│ │ │ ├── ArrayStoreException
│ │ │ ├── UnsupportedOperationException
│ ├── IOException
│ │ ├── FileNotFoundException
│ │ ├── EOFException
│ ├── SQLException
│ ├── ClassNotFoundException
│ ├── InterruptedException
│ ├── NoSuchMethodException
│ ├── IllegalAccessException
│ ├── InstantiationException

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Example {

// throws IOException is a must here: because FileReader constructor throws Exception.


public void readFile(String fileName) throws IOException {
FileReader fileReader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;

while ((line = bufferedReader.readLine()) != null) {


System.out.println(line); // Print the line (or process it in some other way)
}

bufferedReader.close();
}

// throws IOException not needed here (optional): because we catch Exception.


public void processFile(String fileName) {
try {
readFile(fileName); // if throws an error, stop here and run catch block
// doSomething()
} catch (IOException e) {
System.out.println("Caught IOException: " + e.getMessage());
// Handle exception
} finally {
System.out.println("Finished processing file: " + fileName);
// Cleanup or additional actions
}
}

public static void main(String[] args) {


Example example = new Example();
example.processFile("test.txt");
}
}

// A method with a try block and a catch block can optionally declare the exception.
// Theoretically yes, but practical makes no sense, because declaring an exception that is fully handl
// If a method handles an exception internally, it does not need to declare it in its signature; the t

SOLID
Single Responsibility Principle (SRP)

class Book {
private String title;
private String author;

// Constructor, getters, and setters


public Book(String title, String author) {
this.title = title;
this.author = author;
}

public String getTitle() {


return title;
}

public String getAuthor() {


return author;
}

// This method violates SRP as it adds printing responsibility to Book class


// A class should have only one reason to change, meaning that a class should have only one job or
public void print() {
System.out.println("Title: " + title);
System.out.println("Author: " + author);
}
}

Open/Closed Principle (OCP)

// the Shape class is open for extension (new shapes can be added by extending Shape) but closed for m
// Software entities (classes, modules, functions, etc.) should be open for extension but closed for m
abstract class Shape {
abstract void draw();
}

class Circle extends Shape {


@Override
void draw() {
System.out.println("Drawing Circle");
}
}

class Rectangle extends Shape {


@Override
void draw() {
System.out.println("Drawing Rectangle");
}
}

class ShapeDrawer {
public void drawShape(Shape shape) {
shape.draw();
}
}

// Violating OCP
class ShapeDrawer {
public void drawShape(Object shape) {
if (shape instanceof Circle) {
System.out.println("Drawing Circle");
} else if (shape instanceof Rectangle) {
System.out.println("Drawing Rectangle");
}
// Every time a new shape is added, this method needs to be modified.
}
}

Liskov Substitution Principle

// Objects of a superclass should be replaceable with objects of a subclass without affecting the func
// A better design would involve separating the flying behavior into its own interface.
class Bird {
public void fly() {
System.out.println("Flying");
}
}

class Ostrich extends Bird {


@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich can't fly");
}
// Ostrich cannot be used as a Bird without causing issues.
}

Interface Segregation Principle (ISP)

// A client should not be forced to implement interfaces it doesn't use.


// Instead of one fat interface, many small interfaces are preferred
// based on groups of methods with specific behaviors.

interface MultiFunctionDevice {
void print(Document document);
void scan(Document document);
void fax(Document document);
}
class SimplePrinter implements MultiFunctionDevice {
@Override
public void print(Document document) {
System.out.println("Printing document");
}

@Override
public void scan(Document document) {
// Not needed by SimplePrinter but must be implemented
}

@Override
public void fax(Document document) {
// Not needed by SimplePrinter but must be implemented
}
}

Dependency Inversion Principle (DIP)

// In this example, the Notification class depends on the MessageService abstraction


// rather than a concrete implementation, adhering to the Dependency Inversion Principle.
interface MessageService {
void sendMessage(String message, String receiver);
}

class EmailService implements MessageService {


@Override
public void sendMessage(String message, String receiver) {
System.out.println("Email sent to " + receiver + " with message: " + message);
}
}

class SMSService implements MessageService {


@Override
public void sendMessage(String message, String receiver) {
System.out.println("SMS sent to " + receiver + " with message: " + message);
}
}

class Notification {
private MessageService messageService;

// Constructor Injection
public Notification(MessageService messageService) {
this.messageService = messageService;
}

public void notifyUser(String message, String receiver) {


messageService.sendMessage(message, receiver);
}
}

public class Main {


public static void main(String[] args) {
MessageService emailService = new EmailService();
Notification notification = new Notification(emailService);
notification.notifyUser("Hello, World!", "[email protected]");
}
}

// In this violating example, the Notification class directly depends on the EmailService concrete cla
// making it hard to switch to a different messaging service without modifying the Notification class
class Notification {
private EmailService emailService = new EmailService(); // Direct dependency on concrete class

public void notifyUser(String message, String receiver) {


emailService.sendMessage(message, receiver);
}
}

class EmailService {
public void sendMessage(String message, String receiver) {
System.out.println("Email sent to " + receiver + " with message: " + message);
}
}

CRUD / ACID
CRUD is an acronym for the four basic types of operations you can perform on data stored in a database: Create,
Read, Update, and Delete.

ACID is an acronym for the set of properties that guarantee database transactions are processed reliably. ACID
stands for Atomicity, Consistency, Isolation, and Durability.

Atomicity: Ensures that each transaction is treated as a single "unit", which either completely succeeds or
completely fails. If any part of the transaction fails, the entire transaction is rolled back (When transferring
money between bank accounts, the operation must ensure that money is debited from one account and
credited to another, or neither action happens at all).
Consistency: Ensures that a transaction brings the database from one valid state to another, maintaining
database invariants (In a banking system, ensuring that the total amount of money remains constant after a
transaction).
Isolation: Ensures that concurrent transactions do not affect each other. Intermediate state of a transaction is
invisible to others until it's complete (Two transactions that update different rows in the same table will not
interfere with each other). By adhering to CRUD operations and ACID principles, database systems ensure
reliable, consistent, and safe handling of data.
Durability: Ensures that once a transaction is committed, it remains so, even in the case of a system failure
(Once a transaction to add money to a bank account is committed, the new balance will be stored
permanently).
By adhering to CRUD operations and ACID principles, database systems ensure reliable, consistent, and safe
handling of data.
Deadlock in a database occurs when two or more transactions are waiting for each other to release locks on
resources, creating a cycle of dependencies that prevents any of them from proceeding.

Transaction A locks Row1 in Table1.


Transaction B locks Row2 in Table2.
Transaction A attempts to lock Row2 in Table2 but has to wait because Row2 is already locked by Transaction
B.
Transaction B attempts to lock Row1 in Table1 but has to wait because Row1 is already locked by Transaction
A.
Now, both transactions are waiting for each other to release their respective locks, resulting in a deadlock.

SQL
SELECT department, SUM(salary) AS total_salary
FROM Employee
WHERE department <> 'Finance'
GROUP BY department
HAVING SUM(salary) > 10000
ORDER BY total_salary DESC, department ASC;

Coupling / Cohesion
Low coupling means that modules are largely independent of each other, which enhances modularity and makes
the system easier to maintain and extend.

// Example of Low Coupling


import java.util.ArrayList;
import java.util.List;

// Define the IProduct interface


interface IProduct {
String getName();
double getPrice();
String getCategory();
}

// Implement the Product class that implements the IProduct interface


class Product implements IProduct {
private String name;
private double price;
private String category;

public Product(String name, double price, String category) {


this.name = name;
this.price = price;
this.category = category;
}

@Override
public String getName() {
return name;
}

@Override
public double getPrice() {
return price;
}

@Override
public String getCategory() {
return category;
}
}

// Implement the Invoice class that uses the IProduct interface


class Invoice {
// The Invoice class now depends on the IProduct interface
// which reduces coupling with specific product implementations.
private List<IProduct> products;

public Invoice() {
products = new ArrayList<>();
}

public void addProduct(IProduct product) {


products.add(product);
}

public void printInvoice() {


for (IProduct product : products) {
System.out.println(product.getName() + ": $" + product.getPrice() + " (" + product.getCate
}
}

public void createAndAddProduct(String name, double price, String category) {


IProduct product = new Product(name, price, category); // Creating an instance of IProduct (Pr
addProduct(product);
}
}

// Main class to test the implementation


public class Main {
public static void main(String[] args) {
Invoice invoice = new Invoice();
invoice.createAndAddProduct("Laptop", 1200.00, "Electronics");
invoice.createAndAddProduct("Book", 25.50, "Books");
invoice.printInvoice();
}
}
// This design ensures low coupling by making the Invoice class depend on the IProduct interface
// rather than the concrete Product class, thus promoting flexibility and easier maintenance.

// Example of High Coupling


// The Product class represents a product with a name, price, and category.
public class Product {
private String name;
private double price;
private String category;

// Constructor to initialize the product with a name, price, and category.


public Product(String name, double price, String category) {
this.name = name;
this.price = price;
this.category = category;
}

// Getter method for the name.


public String getName() {
return name;
}

// Getter method for the price.


public double getPrice() {
return price;
}

// Getter method for the category.


public String getCategory() {
return category;
}
}

// The Invoice class is tightly coupled with the Product class.


public class Invoice {
// The Invoice class is tightly coupled to the Product class. If we add another product class (e.g
// we would need to change the Invoice class to accommodate it.
private List<Product> products;

// Constructor to initialize the product list.


public Invoice() {
products = new ArrayList<>();
}

// Method to add a product to the invoice.


public void addProduct(Product product) {
products.add(product);
}

// Method to print the invoice details.


public void printInvoice() {
for (Product product : products) {
System.out.println(product.getName() + ": $" + product.getPrice() + " (" + product.getCate
}
}

// Method to create and add a product to the invoice.


public void createAndAddProduct(String name, double price, String category) {
Product product = new Product(name, price, category); // Creating a new Product instance
// what when I want to create a new Product2; I need to change here!
addProduct(product); // Adding the new product to the invoice
}
}

Cohesion refers to how closely related the responsibilities and functionalities of a single module or class are. High
cohesion means that the methods and attributes of a class are highly related and focused on a single task or group
of related tasks. Low cohesion means that a class is trying to do too many unrelated things, making it harder to
understand and maintain.

// Example of High Cohesion


// In this example, the Invoice class has high cohesion because all its methods and attributes are dir
public class Invoice {
private List<Item> items;
private double totalAmount;

public Invoice() {
items = new ArrayList<>();
totalAmount = 0.0;
}

public void addItem(Item item) {


items.add(item);
totalAmount += item.getPrice();
}

public double getTotalAmount() {


return totalAmount;
}

public void printInvoice() {


for (Item item : items) {
System.out.println(item.getName() + ": $" + item.getPrice());
}
System.out.println("Total: $" + totalAmount);
}
}

// Example of Low Cohesion


// The Utility class in this example has low cohesion because it contains methods that perform unrelat
public class Utility {
public void printInvoice(Invoice invoice) {
for (Item item : invoice.getItems()) {
System.out.println(item.getName() + ": $" + item.getPrice());
}
System.out.println("Total: $" + invoice.getTotalAmount());
}

public void sendEmail(String email, String message) {


// Code to send email
}

public double calculateDiscount(double amount, double discountRate) {


return amount - (amount * discountRate);
}

public String convertToJson(Object object) {


// Code to convert object to JSON
return "{}";
}
}
MVC (Model-View-Controller)

You might also like