top of page

Java Advanced Programming Tutorial with Examples

Updated: Mar 14, 2023



Object-oriented programming (OOP)


OOPs is a programming paradigm that focuses on the use of objects to design and build software systems. Java is a powerful object-oriented programming language that supports the principles of OOP. In this tutorial, we'll cover the basics of OOP in Java.


Classes and Objects


In Java, a class is a blueprint or template for creating objects. An object is an instance of a class. A class can contain fields (variables) and methods (functions) that define the behavior and properties of its objects.

Here's an example of a simple Java class:


public class Car {
    // Fields (instance variables)
private String make;
    private String model;
    private int year;

    // Constructor
public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    // Methodspublic String getMake() {
        return make;
    }

    public String getModel() {
        return model;
    }

    public int getYear() {
        return year;
    }

    public void drive() {
    System.out.println("The " + year + " " + make + " " + model + " is 
                        driving.");
    }
}

In this example, we've defined a Car class with three fields (make, model, and year) and four methods (Car, getMake, getModel, and getYear). The Car constructor initializes the make, model, and year fields, and the drive method prints a message to the console.

To create an object of the Car class, we can use the new keyword:


Car myCar = new Car("Honda", "Civic", 2020);

This creates a new Car object and assigns it to the myCar variable.

We can access the fields and methods of the Car object using the dot notation:


System.out.println(myCar.getMake()); 
// Output: Honda
myCar.drive(); 
// Output: The 2020 Honda Civic is driving.


Methods


Methods are used to encapsulate a block of code and perform a specific task. They can be called from other parts of the program, which makes them a powerful way to reuse code. Methods in Java can take zero or more parameters, and they can also return a value.

Here's an example of a method that takes two parameters and returns the sum of those parameters:


public int add(int num1, int num2) {
    int sum = num1 + num2;
    return sum;
}

Inheritance


Inheritance is a mechanism that allows a new class to be based on an existing class. The new class inherits the fields and methods of the existing class and can also add its own fields and methods.

In Java, inheritance is achieved using the extends keyword. Here's an example:


public class ElectricCar extends Car {
    private int batteryCapacity;

    public ElectricCar(String make, String model, int year, int batteryCapacity) {
        super(make, model, year);
        this.batteryCapacity = batteryCapacity;
    }

    public int getBatteryCapacity() {
        return batteryCapacity;
    }

    public void charge() {
        System.out.println("The " + getYear() + " " + getMake() + " " + getModel() + " is charging.");
    }
}

In this example, we've defined a ElectricCar class that extends the Car class. The ElectricCar class has an additional field (batteryCapacity) and two additional methods (ElectricCar and charge). The ElectricCar constructor calls the Car constructor using the super keyword to initialize the make, model, and year fields.

To create an object of the ElectricCar class, we can use the new keyword as before:


ElectricCar myElectricCar = new ElectricCar("Tesla", "Model S", 2022, 100);

We can access the fields and methods of the ElectricCar object using the dot notation.

, just like before:

javaCopy code
System.out.println(myElectricCar.getMake()); // Output: Tesla
myElectricCar.drive(); 
// Output: The 2022 Tesla Model S is driving.
myElectricCar.charge(); 
// Output: The 2022 Tesla Model S is charging.

Note that we can also use the methods of the Car class on the ElectricCar object because ElectricCar inherits from Car.


Encapsulation


Encapsulation is the practice of hiding the implementation details of a class from its users. In Java, encapsulation is achieved by making fields private and providing public getter and setter methods to access and modify them.

Here's an example of encapsulation in the Car class:


public class Car {
    private String make;
    private String model;
    private int year;

    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    public String getMake() {
        return make;
    }

    public void setMake(String make) {
        this.make = make;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public void drive() {
        System.out.println("The " + year + " " + make + " " + model + " is driving.");
    }
}

In this example, we've made the make, model, and year fields private and provided public getter and setter methods to access and modify them. This ensures that the implementation details of the Car class are hidden from its users.


Car myCar = new Car("Honda", "Civic", 2020);
System.out.println(myCar.getMake()); // Output: Honda
myCar.setMake("Toyota");
System.out.println(myCar.getMake()); // Output: Toyota


Polymorphism


Polymorphism is the ability of objects of different classes to be used interchangeably. In Java, polymorphism is achieved through method overriding and method overloading.

Method overriding occurs when a subclass provides its own implementation of a method that is already defined in its superclass. Here's an example:


public class Animal {
    public void makeSound() {
        System.out.println("The animal makes a sound.");
    }
}

public class Cat extends Animal {
    @Override
public void makeSound() {
        System.out.println("The cat meows.");
    }
}

In this example, we've defined an Animal class with a makeSound method and a Cat class that extends Animal and overrides the makeSound method. When we call the makeSound method on a Cat object, the overridden implementation in the Cat class is called:


Animal myAnimal = new Animal();
myAnimal.makeSound(); 
// Output: The animal makes a sound.
Cat myCat = new Cat();
myCat.makeSound(); 
// Output: The cat meows.
Animal anotherAnimal = new Cat();
anotherAnimal.makeSound();
// Output: The cat meows.

Method overloading occurs when a class has two or more methods with the same name but different parameters. Here's an example:


public class Calculator {
    public int add(int x, int y) {
        return x + y;
    }

    public double add(double x, double y) {
        return x + y;
    }
}

In this example, we've defined a Calculator class with two add methods that take different parameter types. When we call the add method on a Calculator object, the appropriate method is called based on the parameter types:


Calculator myCalculator = new Calculator();
System.out.println(myCalculator.add(1, 2));
 // Output: 3
System.out.println(myCalculator.add(1.5, 2.5)); 
 // Output: 4.0


Abstraction


Abstraction is the process of reducing complexity by hiding unnecessary details. In Java, abstraction is achieved through abstract classes and interfaces.

An abstract class is a class that cannot be instantiated and is meant to be extended by subclasses. It may contain abstract methods, which are declared but not implemented, as well as concrete methods, which are implemented. Here's an example:


public abstract class Shape {
    public abstract double area();
}

public class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
public double area() {
        return width * height;
    }
}

In this example, we've defined an abstract Shape class with an abstract area method, which is meant to be implemented by its subclasses. We've also defined a Rectangle class that extends Shape and overrides the area method.


Shape myShape = new Rectangle(5, 10);
System.out.println(myShape.area()); 
// Output: 50.0

An interface is a collection of abstract methods and constants. It is similar to an abstract class, but it cannot contain any implemented methods. Here's an example:


public interface Drawable {
    void draw();
}

public class Circle implements Drawable {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
public void draw() {
        System.out.println("Drawing a circle with radius " + radius);
    }
}

In this example, we've defined a Drawable interface with a draw method, and a Circle class that implements Drawable and overrides the draw method.


Drawable myDrawable = new Circle(5);
myDrawable.draw(); 
// Output: Drawing a circle with radius 5.0

Object-oriented programming is a powerful paradigm that allows us to create complex programs by modeling real-world entities as objects. In this tutorial, we've covered the basics of object-oriented programming in Java, including classes, objects, inheritance, encapsulation, polymorphism, and abstraction. With this knowledge, you should be well on your way to becoming a proficient Java programmer.


Interface in JAVA


In Java, an interface is a collection of abstract methods and constants that can be implemented by any class. An interface defines a contract that a class must adhere to in order to be considered of that interface type. Interfaces provide a way to achieve abstraction and polymorphism in Java.


Defining an interface

To define an interface in Java, you use the interface keyword followed by the name of the interface. Here's an example:


public interface Drawable {
    void draw();
    void erase();
}

In this example, we've defined an interface called Drawable that contains two abstract methods, draw and erase.


Implementing an interface

To implement an interface in Java, you use the implements keyword followed by the name of the interface. Here's an example:


public class Circle implements Drawable {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a circle with radius " + radius);
    }

    @Override
    public void erase() {
        System.out.println("Erasing a circle with radius " + radius);
    }
}

In this example, we've defined a class called Circle that implements the Drawable interface. The Circle class must provide implementations for the draw and erase methods defined in the Drawable interface.


Default methods in interfaces

Starting from Java 8, interfaces can also have default methods. A default method is a method that has a default implementation in the interface itself, and can be overridden by implementing classes. Here's an example:


public interface Drawable {
    void draw();
    void erase();

    default void print() {
        System.out.println("Printing a drawable");
    }
}

In this example, we've added a default method called print to the Drawable interface. This method has a default implementation that can be used by any class that implements the Drawable interface.


Static methods in interfaces

Starting from Java 8, interfaces can also have static methods. A static method is a method that belongs to the interface itself and can be called without creating an instance of the interface. Here's an example:


public interface Drawable {
    void draw();
    void erase();

    static void printStatic() {
        System.out.println("Printing a static drawable");
    }
}

In this example, we've added a static method called printStatic to the Drawable interface. This method belongs to the interface itself and can be called without creating an instance of the interface.


Multiple inheritance with interfaces

In Java, a class can implement multiple interfaces. This allows a class to inherit behavior from multiple sources. Here's an example:


public interface Movable {
    void move();
}

public interface Drawable {
    void draw();
}

public class Circle implements Movable, Drawable {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a circle with radius " + radius);
    }

    @Override
    public void move() {
        System.out.println("Moving a circle");
    }
}

In this example, we've defined two interfaces, Movable and Drawable, and a class called Circle that implements both interfaces. The Circle class must provide implementations for the methods defined in both interfaces.


EXCEPTIONS in Java


In Java, an exception is an event that occurs during the execution of a program and disrupts the normal flow of the program. When an exception occurs, the program stops executing and jumps to the exception handler that can handle the exception.


Types of exceptions


In Java, there are two types of exceptions: checked and unchecked.


Checked exceptions

A checked exception is an exception that must be declared in the method signature or handled by a try-catch block. Examples of checked exceptions include IOException, SQLException, and ClassNotFoundException.


Unchecked exceptions

An unchecked exception is an exception that does not need to be declared in the method signature or handled by a try-catch block. Examples of unchecked exceptions include NullPointerException, ArrayIndexOutOfBoundsException, and ArithmeticException.


Handling exceptions

In Java, exceptions are handled using try-catch blocks. Here's the basic syntax:


try {
    // code that may throw an exception
} catch (Exception e) {
    // code to handle the exception
}

In this example, we've enclosed the code that may throw an exception in a try block. If an exception is thrown, the catch block will handle it. The catch block takes an Exception object as a parameter, which can be used to obtain information about the exception.


Throwing exceptions

In Java, exceptions can also be thrown manually using the throw keyword. Here's an example:


public int divide(int a, int b) throws ArithmeticException {
    if (b == 0) {
        throw new ArithmeticException("Cannot divide by zero");
    }
    return a / b;
}

In this example, we've defined a method called divide that throws an ArithmeticException if the second parameter is zero. We've used the throw keyword to manually throw the exception.


Creating custom exceptions

In Java, you can also create your own exceptions by extending the Exception class or one of its subclasses. Here's an example:


public class MyException extends Exception {
    public MyException(String message) {
        super(message);
    }
}

In this example, we've defined a custom exception called MyException that extends the Exception class. We've also provided a constructor that takes a message as a parameter and passes it to the Exception constructor using the super keyword.


Finally block

In Java, you can also use a finally block to execute code regardless of whether an exception is thrown or not. Here's an example:


try {
    // code that may throw an exception
} catch (Exception e) {
    // code to handle the exception
} finally {
    // code that will always execute
}

In this example, the finally block will always execute, regardless of whether an exception is thrown or not.


Catching multiple exceptions

In Java, you can catch multiple exceptions in the same try block using multiple catch blocks. Here's an example:


try {
    // code that may throw an exception
} catch (IOException e) {
    // code to handle an IOException
} catch (SQLException e) {
    // code to handle an SQLException
}

In this example, we've used two catch blocks to handle two different types of exceptions - IOException and SQLException. If an IOException is thrown, the first catch block will handle it; if an SQLException is thrown, the second catch block will handle it.


Propagating exceptions

In Java, exceptions can be propagated up the call stack by throwing them from a method and not catching them. This allows higher-level methods to handle the exceptions, or to propagate them further up the call stack. Here's an example:


public void method1() throws IOException {
    // code that may throw an IOException
}

public void method2() throws IOException {
    method1();
}

public void method3() {
    try {
        method2();
    } catch (IOException e) {
        // code to handle the IOException
    }
}

In this example, method1 throws an IOException, which is propagated up the call stack to method2, which also declares that it can throw an IOException. method3 calls method2 and handles the IOException using a catch block. If method3 did not handle the IOException, it would be propagated further up the call stack to the caller of method3.


Exceptions are an important part of Java programming, as they allow you to handle errors and unexpected events in your code. In this tutorial, we've covered the basics of handling exceptions in Java programs, including the different types of exceptions, how to handle them using try-catch blocks, and how to throw your own exceptions.


THREADS

A thread is a lightweight subprocess in Java that can execute concurrently with other threads. Threads are used to achieve concurrency and improve performance in a Java program. In this tutorial, we'll cover the basics of threads in Java and provide examples to illustrate their use.


Creating a thread

In Java, you can create a thread by implementing the Runnable interface and passing an instance of your implementation to a Thread constructor. Here's an example:


public class MyRunnable implements Runnable {
    public void run() {
        // code to be executed in this thread
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

In this example, we've created a MyRunnable class that implements the Runnable interface. The run method of MyRunnable contains the code that will be executed in the new thread. In the main method of our Main class, we create an instance of MyRunnable and pass it to a new Thread object. We then call the start method on the Thread object, which creates and starts a new thread.


Thread states

A thread can be in one of several states during its lifetime:

  • New: The thread has been created, but has not yet started.

  • Runnable: The thread is running or ready to run, but may be paused by the scheduler to allow another thread to run.

  • Blocked: The thread is waiting for a lock or other resource to become available.

  • Waiting: The thread is waiting for a specific event to occur, such as a notification from another thread.

  • Timed waiting: The thread is waiting for a specific event to occur, but with a timeout specified.

  • Terminated: The thread has finished executing.

You can query the state of a thread using the getState method.


Multithreading in Java


Thread synchronization

In a multithreaded program, it's often necessary to synchronize access to shared resources to prevent race conditions and other concurrency issues. Java provides several mechanisms for thread synchronization, including the synchronized keyword, locks, and semaphores.

Here's an example of using the synchronized keyword to synchronize access to a shared resource:


public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + counter.getCount());
    }
}

In this example, we've created a Counter class that has two synchronized methods - increment and getCount. These methods use the synchronized keyword to ensure that only one thread can access the count variable at a time. In our Main class, we create two threads that increment the count variable in the Counter object.


Thanks for reading, and happy coding!


Master Android Development in Java Series part-4 -> Android Basics: Everything You Need to Know to Get Started

bottom of page