An In-Depth Guide to Types of Classes in Java

As an experienced Java developer, one of the most fundamental concepts you need to master is the different types of classes that Java provides. Classes form the basic building blocks for Java applications by encapsulating code into reusable templates.

This comprehensive tutorial will discuss the major types of classes you need to know in Java such as:

  • Concrete Classes
  • Abstract Classes
  • Final Classes
  • Static Classes
  • Inner Classes
  • POJO Classes

We will also cover special classes in Java including:

  • Immutable Classes
  • Singleton Classes
  • Wrapper Classes

By understanding these important class types and their significance, you can greatly improve your Java programming skills. Let‘s get started!

Why Different Class Types Matter

As your Java projects and applications grow in complexity, using appropriate class types becomes critical for managing code and dependencies efficiently. Instead of bundling all logic into generic classes, leveraging specific class types based on your need allows much cleaner code organization and improves performance.

For example, making classes immutable where state should not change prevents bugs. Using singleton classes lets multiple parts of code access the same object instance easily. Building reusable code with abstract classes reduces duplication. Appropriate use of Java‘s class types leads to higher quality applications!

Concrete Classes

A concrete class in Java provides a complete implementation for the methods it defines. It is the most common and basic class type you will work with as a Java developer for building applications.

Here is a simple example concrete class:

public class Rectangle {

    private int length;
    private int width;

    public Rectangle(int length, int width) {
        this.length = length;
        this.width = width;
    }

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

}

The Rectangle class demonstrates some key characteristics that define a concrete class:

✅ Complete implementation for all methods
✅ No abstract methods
✅ Can instantiate and create objects

Concrete classes allow modeling real-world entities really well, like users, payments, products etc. Any concepts can be translated into concrete classes that encapsulate associated data and behaviors logically.

Out of all the types of classes in Java, concrete classes are the ones you will work most extensively with. They represent the core components in domain modeling and building applications.

Now that you understand the basics of concrete classes, let‘s move on to…

Abstract Classes

Unlike concrete classes, abstract classes cannot be instantiated directly and are meant to be inherited from. They contain abstract methods that only have method signature but no implementation.

The concrete subclass inheriting the abstract class must provide implementations for all the abstract methods before objects can be created.

Here is a simple abstract class:

public abstract class BankAccount {

    private String number;

    public abstract void deposit(int amount);

    public void showNumber() {
        System.out.println("Account number: " + number); 
    }

}

Notice how the abstract deposit() method is declared without any method body or implementation inside BankAccount abstract class.

Let‘s make SavingsAccount concrete subclass extend BankAccount to implement deposit method:

public class SavingsAccount extends BankAccount {

    public void deposit(int amount) {
        //deposit logic
        System.out.println("Deposited " + amount);
    }

}

Now we can create instances of SavingsAccount since the abstract method is implemented.

Abstract classes exhibit following characteristics:

✅ Can contain abstract methods without implementation
✅ Cannot instantiate directly
✅ Must be extended by concrete subclass

Where are abstract classes useful?

  • Define skeletal class templates with partial logic
  • Inherit skeletal templates to reuse code and reduce duplication
  • Develop frameworks and libraries like collections
  • Runtime polymorphic use after extending subclasses

Abstract classes are extremely useful for providing boilerplate code and generic logic to subclasses. Java‘s collections use abstract classes significantly for this purpose.

Moving on let‘s understand another type called…

Final Classes

Final classes in Java impose restrictions that once a class is declared as final, it cannot be inherited or subclassed. For all practical purposes that class becomes immutable.

For example:

public final class AdhaarCard {

    private String name;

    public String getName() {
        return name; 
    }

}

public class PANCard extends AdhaarCard {
    // Compile time error since AdhaarCard is final 
}

Properties of final classes:

✅ Cannot be subclassed or inherited from
✅ Useful for creating immutable classes
✅ Can allow method overrides using final

Final classes are immutable and prevent unexpected behavior changes through inheritance. Java‘s String class and Wrapper classes are declared final.

Now let‘s discuss a unique type – static classes.

Static Classes

While normal or non-static classes allow instantiation with new keyword, static classes cannot be instantiated at all.

They can only contain static members such as:

✅ Static variables
✅ Static methods

For example:

public static class MyMathUtils {

    public static final double PI = 3.14;

    public static int add(int a, int b) {
        return a + b;  
    }

} 

Here MyMathUtils is a static class containing the static variable PI and static method add().

To access the static members, we use the class name directly rather than creating an object.

int sum = MyMathUtils.add(3, 4);
System.out.println(MyMathUtils.PI);

Static classes are essentially a grouping of utilities that do not require instantiation with state. Their methods operate solely based on input parameters.

Java‘s Math class is a popular example of static utility class with constants and methods.

Up next are…

Inner Classes

We know classes can have methods, fields, constructors etc. But did you know, classes can also nest other classes within themselves as inner classes!

For example:

public class Outer {

    private int value = 5;

    class Inner {
        public int getValue() {
            return Outer.this.value;
        }
    } 

}

Here Inner is a non-static nested class declared inside the Outer class.

An inner class has these fundamental characteristics:

✅ Declared directly inside outer class
✅ Has access to private members of outer class including private fields and methods
✅ Requires instance of outer class to instantiate

Because inner classes allow access to outer class private members, they help group related classes together while maintaining encapsulation.

Constructing inner class instances is slightly tricky compared to normal classes:

Outer outer = new Outer(); 
Outer.Inner inner = outer.new Inner();

First Outer instance needs to be created before we can instantiate Inner by qualifying with outer reference.

With this background on inner classes, let‘s now move on to…

POJO Classes

POJO stands for Plain Old Java Object. By convention, a POJO class in Java follows following rules:

  • Only private instance fields
  • Public getter and setter methods
  • Does not extend special pre-defined classes
  • Does not implement pre-defined interfaces
  • No predefined annotations

Essentially, POJO represents a simple business domain object without any special hierarchy restrictions.

Here is an example POJO modeling a product:

public class Product {

    private String name; 
    private double price;

    public String getName() {
        return name;
    }

    public void setName(String name) {
         this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price; 
    }

}

Product class follows POJO conventions:

✅ Private fields
✅ Public getter setters
✅ No special hierarchy

POJO classes promote writing clean business domain models without inheritance complexity. Their simplicity also aids testing and persistence mapping like ORM.

That covers most of the common types you will encounter. Now let‘s discuss two special types…

Singleton Classes

The Singleton class is unique as Java ensures only one instance of Singleton class can exist throughout the lifespan of an application.

No matter where and how many ever times Singleton instance is requested, the same exact object will be returned from a global access point.

Implementing Singletons involves:

✅ Making constructor private so no one can instantiate with new
✅ Defining a static method that returns static instance

For example:

public class Logger {

    private static Logger instance; 

    private Logger() {}

    public static Logger getInstance() {
        if(instance == null) {
            instance = new Logger(); 
        }
        return instance;
    }

}

To get the singleton instance:

Logger logger = Logger.getInstance(); 

No matter how many times getInstance() is called, same logger instance will be returned from the Logger class.

This singleton behavior is extremely useful:

Use cases

  • Managing database connections
  • Accessing cached data
  • Logging across application
  • Thread pools

With this background on singleton classes, let‘s move on to…

Immutable Classes

While objects normally have mutable state, cases exist where immutable objects are desirable to prevent unexpected side effects when data changes unexpectedly.

Some ways to define immutable classes in Java are:

✅ Declare the class final
✅ Declare all fields final
✅ Prevent method overrides in subclasses using final
✅ Don‘t provide setter methods
✅ Make defensive copies in methods

For example:

public final class SSN {

    private final int firstPart;

    private final int secondPart;  

    public SSN(int first, int second) {
        this.firstPart = first; 
        this.secondPart = second;
    }

    public final int getFirst() {
        return firstPart; 
    }

    public final int getSecond() {
        return secondPart;  
    }

}

Here SSN class is:

✅ Declared final
✅ Contains final fields only
✅ Provides no setters
✅ Declares getters final

Therefore, it cannot be inherited and the object cannot be modified after creation achieving immutability.

String and wrapper classes like Integer exhibit similar immutability.

Wrapper Classes

We are finally down to the last type!

Java Wrapper classes encapsulate primitive Java data types into Immutable Object representations.

Each primitive type like int, boolean etc. has a dedicated Wrapper class.

Primitive Wrapper Class
int Integer
long Long
double Double
boolean Boolean

For example:

int primitive = 10; 
Integer wrapper = primitive; //Autoboxing  

Here the primitive int is auto-boxed an Integer object.

Wrapper classes allow primitives to be:

✅ Used as Objects
✅ Enable primitives in Collections
✅ Safely pass primitives across networks

Specialized methods in wrappers also provide utility functions for primitives.

Well there you have it! In this comprehensive guide, I have covered all major types of classes in Java and when you may want to use them appropriately.

Feel free to bookmark this writeup for a quick reference on Java class types! Let me know if you have any other questions.

Read More Topics