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
statickeyword. Each object gets its own independent copy. Changingcar1.yearhas no effect oncar2.year. - Static attributes (also called class attributes in the strict sense) — declared with the
statickeyword. 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-modifier —
private,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
- 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.
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.- Every instance attribute in that block is immediately zeroed out to its default value (
0,false, ornull) — this happens before any field initializer or constructor code runs. - Field initializers (like
= trueon a field declaration) and constructor body statements then run, in the order they appear, overwriting those defaults as needed. - The reference returned by
newis 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
privateby default and expose them only through getter/setter methods (encapsulation), so you can validate input and change internal representation later without breaking callers. - Use
staticonly for data that truly belongs to the class as a whole (shared counters, constants, configuration), never for per-object state. - Combine
staticwithfinalfor constants, e.g.static final double TAX_RATE = 0.07;, and name them inUPPER_SNAKE_CASE. - Always disambiguate with
this.fieldNameinside 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, notaborAccountBalance). - 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
- Create a
Bookclass with instance attributestitle,author, andpages. Create twoBookobjects with different values and print both. - Create a
Libraryclass with astatic int totalBooksattribute that increments every time a newBookis added via a method. Print the total after adding three books. - Create an
Employeeclass with a privatedouble salaryattribute, agiveRaise(double amount)method that only applies positive raises, and agetSalary()getter. Verify that callinggiveRaise(-500)has no effect.
Summary
- Class attributes (fields) declared without
staticare instance attributes — each object gets its own copy stored on the heap. - Attributes declared with
staticare 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.0for numbers,falsefor booleans,nullfor 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.fieldNameto reference the field explicitly. - Encapsulating fields as
privateand exposing controlled access through getters/setters keeps object state valid and is a cornerstone of good OOP design.
