Java Class Attributes

A class attribute (also called a field or member variable) is a variable declared directly inside a class, outside of any method. Attributes define the state that every object of that class carries around — a Car might have a make, a model, and a year; a BankAccount might have a balance. Understanding how attributes are declared, initialized, and stored is the foundation of object-oriented programming in Java, because objects are essentially bundles of attributes plus the methods that operate on them.

Overview: How Class Attributes Work

When you write a class, you are writing a blueprint. The class itself does not hold any data — it is the objects created from that class (via new) that actually hold data in memory. Each attribute declared in the class becomes a slot of memory inside every object created from that class.

Java distinguishes two kinds of attributes:

  • Instance attributes — declared without the static keyword. Each object gets its own independent copy. Changing car1.year has no effect on car2.year.
  • Static attributes (also called class attributes in the strict sense) — declared with the static keyword. There is exactly one copy shared by the class itself, not by each object. Every instance sees the same value.

Internally, when the JVM creates an object with new, it allocates a block of memory on the heap large enough to hold all of that class’s instance attributes (plus some object header bookkeeping used by the JVM, like the reference to the object’s class). Static attributes are not stored inside each object’s heap block at all — they live once in the method area (part of metaspace in modern JVMs), associated with the Class object itself, and every instance simply reads/writes that single shared location.

Attributes also have default values if you don’t explicitly initialize them. This only applies to instance and static fields — local variables inside methods are never given defaults and must be assigned before use.

Type Default value
int, short, byte, long 0
double, float 0.0
boolean false
char '\u0000'
Any object reference (String, arrays, custom classes) null

Syntax

The general form of a field declaration is:

[access-modifier] [static] [final] type attributeName [= initialValue];
  • access-modifierprivate, protected, public, or omitted (package-private). Controls who can see the attribute directly.
  • static — optional. If present, the attribute belongs to the class, not to individual objects.
  • final — optional. If present, the attribute can only be assigned once (often used for constants).
  • type — any primitive type (int, double, boolean, etc.) or reference type (String, another class, an array).
  • attributeName — follows camelCase convention, e.g. accountBalance.
  • initialValue — optional. If omitted, the field gets its type’s default value.

Examples

Example 1: Instance attributes

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car();
        car1.make = "Toyota";
        car1.model = "Corolla";
        car1.year = 2022;

        Car car2 = new Car();
        car2.make = "Honda";
        car2.model = "Civic";
        car2.year = 2023;

        car1.display();
        car2.display();
    }
}

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

    void display() {
        System.out.println(year + " " + make + " " + model);
    }
}

Output:

2022 Toyota Corolla
2023 Honda Civic

Each Car object has its own independent copies of make, model, and year. Setting fields on car1 never touches car2‘s fields, because each object got its own memory block when created with new Car().

Example 2: Static attributes shared across objects

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

        System.out.println("c1 id: " + c1.id);
        System.out.println("c2 id: " + c2.id);
        System.out.println("c3 id: " + c3.id);
        System.out.println("Total counters created: " + Counter.count);
    }
}

class Counter {
    static int count = 0;
    int id;

    Counter() {
        count++;
        id = count;
    }
}

Output:

c1 id: 1
c2 id: 2
c3 id: 3
Total counters created: 3

Here, count is static, so all three Counter objects share the same underlying value. Every time the constructor runs, it increments the single shared count and copies that value into the object’s own instance field id. That’s why Counter.count is accessed through the class name rather than through an object — it emphasizes that it belongs to the class, not to any one instance.

Example 3: Encapsulation and default values

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        System.out.println("Default balance: " + account.getBalance());

        account.setOwner("Alice Johnson");
        account.deposit(500.0);
        account.deposit(250.5);

        System.out.println("Owner: " + account.getOwner());
        System.out.println("Balance: " + account.getBalance());
    }
}

class BankAccount {
    private String owner;
    private double balance;
    private boolean active = true;

    void setOwner(String name) {
        owner = name;
    }

    String getOwner() {
        return owner;
    }

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    double getBalance() {
        return balance;
    }
}

Output:

Default balance: 0.0
Owner: Alice Johnson
Balance: 750.5

Notice that balance was never explicitly initialized, yet it printed 0.0 instead of causing an error — that’s the double default value at work. The fields are all marked private, which is the core of encapsulation: outside code cannot read or write owner or balance directly, it must go through methods like deposit() that can enforce rules (here, rejecting non-positive deposits).

Under the Hood: What Happens When an Object Is Created

  1. The JVM looks up the class’s metadata (loaded once, the first time the class is used) to determine how many bytes are needed for all instance attributes combined.
  2. new Car() asks the heap for a block of that size, plus a small header the JVM uses for garbage collection bookkeeping and to identify which class the object belongs to.
  3. Every instance attribute in that block is immediately zeroed out to its default value (0, false, or null) — this happens before any field initializer or constructor code runs.
  4. Field initializers (like = true on a field declaration) and constructor body statements then run, in the order they appear, overwriting those defaults as needed.
  5. The reference returned by new is a pointer to that heap block, which is what gets stored in your variable (e.g., account).

Static attributes skip step 1–3 entirely for each object; they are allocated exactly once, when the class is first loaded by the JVM, and every object of that class reads and writes that same shared location for the lifetime of the program.

Common Mistakes

Mistake 1: Parameter shadowing hides the field

A very common bug is naming a constructor or setter parameter the same as the field, then forgetting to disambiguate with this:

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.setName("Bob");
        System.out.println("Name: " + p.getName());
    }
}

class Person {
    String name;

    void setName(String name) {
        name = name;
    }

    String getName() {
        return name;
    }
}

Output:

Name: null

This compiles perfectly fine — there is no syntax error — but it does nothing useful. Inside setName, the parameter name shadows the field name, so name = name; just assigns the parameter to itself. The field is never touched, so it keeps its default value, null. The fix is to use this.name to explicitly refer to the field:

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.setName("Bob");
        System.out.println("Name: " + p.getName());
    }
}

class Person {
    String name;

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

    String getName() {
        return name;
    }
}

Output:

Name: Bob

Mistake 2: Making an attribute static by accident

Adding static to a field that was meant to be per-object state silently makes every instance share it:

public class Main {
    public static void main(String[] args) {
        Player p1 = new Player();
        Player p2 = new Player();

        p1.score = 10;
        p2.score = 20;

        System.out.println("p1 score: " + p1.score);
        System.out.println("p2 score: " + p2.score);
    }
}

class Player {
    static int score;
}

Output:

p1 score: 20
p2 score: 20

Setting p2.score overwrote p1.score too, because there is really only one score variable shared by the whole class. Removing static gives each Player its own independent score, which is almost always what you want for per-object data like a player’s score.

Best Practices

  • Make attributes private by default and expose them only through getter/setter methods (encapsulation), so you can validate input and change internal representation later without breaking callers.
  • Use static only for data that truly belongs to the class as a whole (shared counters, constants, configuration), never for per-object state.
  • Combine static with final for constants, e.g. static final double TAX_RATE = 0.07;, and name them in UPPER_SNAKE_CASE.
  • Always disambiguate with this.fieldName inside constructors and setters when a parameter shares the field’s name.
  • Initialize fields explicitly when the default value (0, false, null) isn’t a meaningful starting state, rather than relying on readers to know the defaults.
  • Keep attribute names descriptive and camelCase (accountBalance, not ab or AccountBalance).
  • Avoid public mutable fields on classes meant to represent controlled state (like a bank account) — they let any code corrupt invariants without validation.

Practice Exercises

  1. Create a Book class with instance attributes title, author, and pages. Create two Book objects with different values and print both.
  2. Create a Library class with a static int totalBooks attribute that increments every time a new Book is added via a method. Print the total after adding three books.
  3. Create an Employee class with a private double salary attribute, a giveRaise(double amount) method that only applies positive raises, and a getSalary() getter. Verify that calling giveRaise(-500) has no effect.

Summary

  • Class attributes (fields) declared without static are instance attributes — each object gets its own copy stored on the heap.
  • Attributes declared with static are class attributes in the strict sense — one shared copy exists per class, stored once regardless of how many objects are created.
  • Uninitialized fields automatically get default values: 0/0.0 for numbers, false for booleans, null for references — unlike local variables, which have no default and must be assigned before use.
  • Parameter names that match field names shadow the field; use this.fieldName to reference the field explicitly.
  • Encapsulating fields as private and exposing controlled access through getters/setters keeps object state valid and is a cornerstone of good OOP design.