Java Class Features

Java is an object-oriented programming language where classes form the backbone of its structure. Understanding the key features of a Java class is essential for writing clean, maintainable, and efficient code. This post explores the most important aspects of Java classes — from attributes and methods to constructors, access control, and more.

We’ll cover how fields (instance and static) store object state, how methods (instance and static) define behavior, and how variable scope governs visibility within the class. You’ll also learn how constructors initialize objects, how access modifiers protect data, and how initializer blocks assist in complex setups.

We dive into advanced topics such as interfaces for abstraction, packages for organizing code, inner and static nested classes for structural grouping, anonymous classes for quick customization, and singleton patterns for instance control. We wrap up with wrapper classes, enums, and best practices that will help you write robust and modern Java code.

Whether you’re a beginner aiming to master class structures or a developer brushing up on fundamentals, this guide will give you a comprehensive overview of everything that makes a Java class powerful.

Class Attributes

What they are: Class attributes (also called fields or instance variables) hold object state. There are two broad kinds:

  • Instance fields — belong to each object instance.
  • Static (class) fields — belong to the class itself, shared across all instances.

Key points:

  • Default values: numeric→0, boolean→false, reference→null.
  • Visibility controlled by access modifiers (private, protected, public, package-private).
  • Prefer encapsulation: private fields + public getters/setters.

Example:

public class Person {
  // instance fields
  private String name;
  private int age;

  // static field (class attribute)
  public static final String SPECIES = “Homo sapiens”;

  public Person(String name, int age)    {
    this.name = name;
    this.age = age;
  }

  // getters & setters
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }

  public int getAge() { return age; }
  public void setAge(int age) { this.age = age; }
}

Class Methods

What they are: Methods define behavior. Two main kinds:

  • Instance methods — operate on instance data, need an object to be called.
  • Static methods — belong to class, can be called without instance.

Key points:

  • Use static for utility behavior.
  • Overloading: same method name, different parameters.
  • Overriding: subclass provides specific implementation.

Example:

public class Calculator {
  // static method
  public static int add(int a, int b) {
    return a + b;
  }

  // instance method
  public int multiply(int a, int b) {
    return a * b;
  }
}

Usage:

int sum = Calculator.add(2,3); // static call
Calculator calc = new Calculator();
int prod = calc.multiply(2,3); // instance call

Variable Scopes

What they are: Scope determines where a variable is visible.

  • Local variables — declared in method/block; exist only within it.
  • Instance variables (fields) — visible across instance methods; lifetime = object.
  • Static variables — visible to all static and instance methods of the class.
  • Method parameters — local to method.

Key rules:

  • Local variables must be initialized before use.
  • Shadowing: local vars can hide fields (use this to reference field).

Example:

public class ScopeExample {
  private int x = 10; // instance

  public void exampleMethod(int x) {      // parameter shadows field
    int y = 5; // local
    System.out.println(“local x (param): ” + x);
    System.out.println(“field x: ” + this.x);
    System.out.println(“local y: ” + y);
  }
}

Constructors

What they are: Special methods that initialize new objects. Name matches class and has no return type.

Types:

  • Default constructor — provided if none declared.
  • No-arg constructor — explicitly declared with no parameters.
  • Parameterized constructor — takes parameters.
  • Constructor chaining — call another constructor using this(…).

Example:

public class Book {
  private String title;
  private String author;

  // no-arg constructor
  public Book() {
    this(“Unknown”, “Unknown”);
  }

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

Access Modifiers

Four levels:

  • public — visible everywhere.
  • protected — visible in same package and subclasses.
  • package-private (no modifier) — visible only within same package.
  • private — visible only within the class.

Example:

package com.example;

public class AccessDemo {
  public int pub = 1;
  protected int prot = 2;
  int pack = 3; // package-private
  private int priv = 4;
}

Tip: Favor private fields and public methods to expose behavior.

Initializer Block

What they are: Blocks that run when an object is created.

  • Instance initializer block: executed each time an instance is created (runs before constructor body).
  • Static initializer block: executed once when class is loaded.

Use-cases: Complex initialization shared across constructors, initializing static resources.

Example:

public class InitExample {
  static {
    System.out.println(“Static initializer: class loaded.”);
  }

  {
    System.out.println(“Instance initializer: before constructor.”);
  }

  public InitExample() {
    System.out.println(“Constructor body.”);
  }

  public static void main(String[] args) {
    new InitExample();
    new InitExample();
  }
}

Output:

Static initializer: class loaded.
Instance initializer: before constructor.
Constructor body.
Instance initializer: before constructor.
Constructor body.

Interfaces

What they are: Contract defining method signatures (and since Java 8, default and static methods; since Java 9, private methods). A class implements interfaces.

Key points:

  • Interfaces support multiple inheritance of types.
  • Methods are public abstract by default (unless default/static).
  • Fields in interfaces are public static final by default.

Example:

public interface Drivable {
  void accelerate(int increment); // abstract
  default void honk() {
    System.out.println(“Beep!”);
  }
  static void serviceCheck() {
    System.out.println(“Service check done.”);
  }
}

public class Car implements Drivable {
  private int speed = 0;

  @Override
  public void accelerate(int increment) {
    speed += increment;
    System.out.println(“Speed: ” + speed);
  }
}

Packages

What they are: Namespaces for organizing classes and controlling access. Package statement must be first line (except comments).

Benefits:

  • Avoid name clashes.
  • Access control (package-private).
  • Easier project structure.

Example:

// file: src/com/example/util/Utils.java
package com.example.util;

public class Utils {
  public static String greet(String name) { return “Hello, ” + name; }
}

Usage from another package:

import com.example.util.Utils;
System.out.println(Utils.greet(“Alice”));

Inner Classes

Kinds:

  1. Non-static nested (inner) class — associated with an instance, can access outer instance members.
  2. Static nested class — like a top-level class but nested; cannot access non-static members of outer without an instance.
  3. Local class — defined inside a method, constructor, or even a static initializer block.
  4. Anonymous class — expression to create an instance of an unnamed class (covered later).

Non-static nested  (inner) class

A non-static nested class, also known as an inner class, is a class defined within another class but without the static modifier. It has access to all the members (including private ones) of its enclosing outer class, which makes it useful for logically grouping classes that are only used in one place. Inner classes are often used to represent an object that exists only in the context of its outer class. To create an instance of an inner class, you must first create an instance of the outer class.

Example:

class Outer {
  private String message = “Hello from Outer Class!”;

  class Inner {
    void display() {
      System.out.println(message);
    }
  }

  public static void main(String[] args) {
    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();
    inner.display();
  }
}

Static Classes

A static nested class is a nested class declared with the static modifier inside another class. Unlike non-static inner classes, a static nested class does not have access to the instance variables or methods of its enclosing outer class; it can only access the outer class’s static members directly. This makes it behave much like a top-level class but helps in logically grouping classes that are used only by their outer class. Static nested classes are often used to represent helper or utility components that don’t rely on an outer class instance. To create an instance of a static nested class, you don’t need an object of the outer class.

Example:

class Outer {
  static String message = “Hello from Outer class!”;

  static class Nested {
    void display() {
      System.out.println(message); // Accessing static member of Outer class
    }
  }
}

public class Main {
  public static void main(String[] args) {
    Outer.Nested nested = new Outer.Nested();
    nested.display();
  }
}

In this example, the Nested class directly accesses the static variable message of the Outer class. Static nested classes improve code organization and can reduce memory usage since they don’t maintain a reference to the outer class instance.

Local Class

A local class in Java is a type of nested class that is defined within a block, typically inside a method, constructor, or even a static initializer block. It is created when you need a small helper class that is used only within that specific block, keeping the code more encapsulated and readable. Local classes can access all members of their enclosing class, including private ones, as well as final or effectively final variables from the enclosing block. They are not accessible outside the block in which they are defined.

Example:

class Outer {
  void showMessage() {
    String localVar = “Hello from Local Class!”;

    class Local {
      void display() {
        System.out.println(localVar);
      }
    }

    Local local = new Local();
    local.display();
  }

  public static void main(String[] args) {
    new Outer().showMessage();
  }
}

In this example, the Local class is defined inside the showMessage() method and can access the local variable localVar because it is effectively final. Local classes are useful for temporary or one-off tasks where defining a separate top-level or member class would be unnecessary.

Anonymous Classes

An anonymous class in Java is a local inner class without a name, used to create a one-time subclass or implementation of an interface directly at the point of use. It provides a concise way to define and instantiate a class simultaneously, making it ideal for short-lived tasks like event handling, callbacks, or creating custom behavior for a single use. Anonymous classes can access final or effectively final variables from their enclosing scope and can override methods of their superclass or interface. Since they have no explicit name, they cannot be reused elsewhere in the program.

Example:

abstract class Greeting {
  abstract void sayHello();
}

public class Main {
  public static void main(String[] args) {
    Greeting greet = new Greeting() {
      void sayHello() {
        System.out.println(“Hello from Anonymous Class!”);
      }
    };
    greet.sayHello();
  }
}

In this example, the anonymous class provides an implementation for the sayHello() method of the Greeting abstract class without creating a separate named subclass. Anonymous classes make code more concise and readable when you need a quick, one-time implementation.

Singleton Class

What it is: A class that allows only one instance in the JVM.

Common implementations:

  • Eager initialization
  • Lazy initialization with synchronized
  • Initialization-on-demand holder idiom (recommended)
  • Enum-based singleton (most robust)

Example — Initialization-on-demand holder

public class Singleton {
  private Singleton() { }

  private static class Holder {
    private static final Singleton INSTANCE = new Singleton();
  }

  public static Singleton getInstance() {
    return Holder.INSTANCE;
  }
}

Enum singleton example

public enum EnumSingleton {
  INSTANCE;
  public void doSomething() { System.out.println(“Doing…”); }
}

Enum approach handles serialization and reflection safely.

Singleton Class – Explanation

Let’s break down and explain both examples — the Initialization-on-Demand Holder Singleton and the Enum Singleton — so you can clearly understand how and why they work.

1. Singleton with Initialization-on-Demand Holder Idiom

Explanation:

This is a thread-safe, lazy-loaded singleton pattern without synchronization overhead.

Key Concepts:

FeatureDescription
Private ConstructorPrevents external instantiation — only the class itself can create the instance.
Static Inner Class (Holder)Holds the singleton instance. It is not loaded until the outer class calls getInstance().
Lazy InitializationThe instance is created only when getInstance() is first called.
Thread SafetyGuaranteed by Java’s class loading mechanism: the JVM loads the Holder class only once and safely, even in multithreaded contexts.

Flow:

  • Singleton.getInstance() is called.
  • JVM loads the inner Holder class.
  • The static field INSTANCE is initialized once, thread-safely.
    Returns the Singleton instance.

Why Use This?

  • No explicit synchronization needed.
  • Efficient and elegant.
  • Recommended in most modern Java applications.
 

2. Enum Singleton

Explanation:

This is the simplest and most robust singleton pattern in Java.

Key Concepts:

FeatureDescription
Enum TypeJava guarantees that each enum constant is instantiated only once.
Thread SafetyEnum instances are inherently thread-safe.
Serialization SafetyEnums are safe from serialization attacks that can create new instances.
Reflection Safety

Enums cannot be instantiated using reflection, unlike regular classes.

Usage:
EnumSingleton.INSTANCE.doSomething();

Why Use This?

  • Short, simple, and built-in protection against common pitfalls (e.g., serialization, reflection).
  • Best choice if the singleton doesn’t need lazy initialization or complex logic.
 

Summary Comparison

FeatureInitialization-on-Demand HolderEnum Singleton
Thread-safe✅ Yes✅ Yes
Lazy-loaded✅ Yes❌ No (eager by default)
Simple to implement🟡 Medium✅ Very simple
Safe from reflection hacks❌ No✅ Yes
Serialization safe❌ No (needs readResolve)✅ Yes
Best for complex logic?✅ Yes❌ No (less flexible)

Final Recommendation:

Use Enum Singleton when you want a simple and safe singleton (no lazy init required).

Use Initialization-on-Demand Holder when you need lazy initialization and more control (e.g., extending interfaces, adding complex setup).

Wrapper Classes

What they are: Object representations of primitive types (Integer, Double, Boolean, etc.).

Why use them:

  • Collections (generics) work with objects only.
  • Utility methods (e.g., Integer.parseInt).

Autoboxing/unboxing: Java auto-converts between primitives and wrappers since Java 5.

Example

Integer a = 10; // autoboxing
int b = a + 5; // unboxing
List<Integer> list = new ArrayList<>();
list.add(20); // works because of wrapper

Enum Class

What it is: A type-safe set of constants. Enums are full-featured classes: can have fields, methods, and constructors.

Example

public enum Day {
  MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

Advanced enum with fields and methods

public enum TrafficLight {
  RED(30), YELLOW(5), GREEN(25);

  private int durationSeconds;

  TrafficLight(int durationSeconds) {
    this.durationSeconds = durationSeconds;
  }

  public int getDuration() { return durationSeconds; }
}

Enum Constructor

Rules:

  • Enum constructors are private (or package-private) by design — you cannot use public.
  • Instantiation occurs automatically for each constant.

Example (continuation from TrafficLight above):

TrafficLight t = TrafficLight.RED;
System.out.println(“Red lasts ” + t.getDuration() + ” seconds.”);

Enum String

Meaning/usage: Converting enums to Strings and back.

  • enumInstance.name() returns the identifier as declared.
  • enumInstance.toString() returns a string; can be overridden.
  • Enum.valueOf(EnumType.class, “CONSTANT”) to convert String to enum (throws IllegalArgumentException if mismatch).
  • Safer conversion methods: use try-catch or map by custom string field.

Example

public enum Level {
  LOW(“Low priority”),
  MEDIUM(“Medium priority”),
  HIGH(“High priority”);

  private String label;
  Level(String label) { this.label = label; }
  @Override public String toString() { return label; }
  public String nameAsDeclared() { return name(); }
}

// convert back
Level l = Level.valueOf(“LOW”); // by declared name
System.out.println(Level.HIGH); // prints “High priority” (toString)

Robust parsing from arbitrary string

public static Level fromLabel(String label) {
  for (Level lv : Level.values())
  if (lv.label.equalsIgnoreCase(label)) return lv;
  throw new IllegalArgumentException(“Unknown label: ” + label);
}

Quick tips & best practices (summary)

  • Use private fields + public getters/setters for encapsulation.
  • Prefer composition over inheritance.
  • Use interfaces for contracts and multiple type inheritance.
  • Use enums (or enum singletons) for fixed sets of constants and singletons where safe instantiation matters.
  • Prefer initialization-on-demand holder for singletons or the enum approach for safety.
  • Keep nested static classes when they don’t need an outer instance.
  • Avoid large anonymous classes; use lambdas where applicable.
Scroll to Top