Java Class Attributes — Syntax, Types, Examples & Best Practices
Everything you need to know about Java Class Attributes — field declaration, instance vs static attributes, final fields, default values, access modifiers, naming conventions, encapsulation with getters/setters, and real-world production code examples.
Last Updated
March 2026
Read Time
20 min
Level
Beginner
Chapter
14 of 35
What are Class Attributes in Java?
In Java, class attributes (also called fields or instance variables) are variables declared directly inside the class body — outside any method, constructor, or block. They define the state of an object — the data each object holds and carries throughout its lifetime. Every object created from a class gets its own separate copy of the class's instance attributes, holding its own unique values.
Real-world analogy: Think of a class as a form template. The blank fields on the form — Name, Age, Address, Phone — are like class attributes. When a person fills out the form (creates an object), their specific answers become the attribute values for that object. Two people filling the same form get the same attribute names, but their own unique values.
Java supports multiple types of class attributes: instance attributes (unique per object), static attributes (shared across all objects), and final attributes (constant, assigned once). Together with methods, attributes form the complete definition of what a class represents. Properly designed attributes — private, well-named, validated — are the hallmark of professional Java code.
Attribute Declaration — Syntax & Structure
A class attribute declaration consists of an optional access modifier, optional non-access modifiers (static, final), a data type, and the attribute name. By convention, attribute names use camelCase (e.g., firstName, accountBalance). Attributes are declared at the class level — directly inside the class body, not inside any method or constructor.
[access_modifier] [static] [final] dataType attributeName [= initialValue]; Examples: private String name; private int age = 0; public static int totalCount = 0; private static final double TAX_RATE = 0.18; protected boolean isActive = true;
1. Declared inside class body, outside all methods. 2. Names use camelCase — firstName, accountBalance. 3. Constants (static final) use UPPER_SNAKE_CASE — MAX_SIZE, TAX_RATE. 4. Should be private for encapsulation (best practice). 5. Java assigns default values if not explicitly initialized. 6. Multiple attributes of same type can be declared in one line (not recommended for readability).
Attribute: declared in class body; has default value; accessible throughout all methods of the class; lives as long as the object lives. Local variable: declared inside a method/block; NO default value (must initialize before use); accessible only within that method/block; dies when method returns.
// ✅ Well-structured Java class showing all attribute types
public class Employee {
// ── Instance attributes (each object gets its own copy) ──
private String employeeId;
private String name;
private String department;
private double salary;
private int age;
private boolean isActive;
// ── Static attribute (ONE shared copy for ALL Employee objects) ──
private static int totalEmployees = 0;
// ── Static final attribute (constant — never changes) ──
public static final double MIN_WAGE = 15000.0;
public static final int RETIREMENT_AGE = 60;
public static final String COMPANY_NAME = "Tech Sustainify";
// ── Instance final attribute (set once in constructor, never changed) ──
private final String panNumber; // PAN card — immutable identity
// ── Constructor initializes instance attributes ──
public Employee(String employeeId, String name, String department,
double salary, int age, String panNumber) {
this.employeeId = employeeId;
this.name = name;
this.department = department;
this.salary = salary;
this.age = age;
this.isActive = true; // default active on creation
this.panNumber = panNumber; // final — set once here
totalEmployees++; // static — increments class-wide counter
}
// ── Display all attributes ──
public void display() {
System.out.println("ID : " + employeeId);
System.out.println("Name : " + name);
System.out.println("Dept : " + department);
System.out.printf( "Salary : ₹%.2f%n", salary);
System.out.println("Age : " + age);
System.out.println("Active : " + isActive);
System.out.println("PAN : " + panNumber);
System.out.println("Company : " + COMPANY_NAME);
}
public static int getTotalEmployees() { return totalEmployees; }
public static void main(String[] args) {
Employee e1 = new Employee("E001", "Aarav Shah", "Engineering", 75000, 28, "ABCDE1234F");
Employee e2 = new Employee("E002", "Priya Mehta", "Marketing", 55000, 32, "FGHIJ5678K");
e1.display();
System.out.println("---");
e2.display();
System.out.println("Total Employees: " + Employee.getTotalEmployees()); // 2
}
}Instance vs Static Attributes — Object-Level vs Class-Level
The most important distinction in class attributes is between instance attributes and static attributes. Instance attributes represent the unique state of each individual object — every object has its own copy. Static attributes belong to the class itself — there is only ONE copy shared by all objects. Confusing these two is a very common source of subtle bugs in Java programs.
public class BankAccount {
// ── Instance attributes — UNIQUE per object ──
private String accountNumber;
private String holderName;
private double balance;
// ── Static attribute — SHARED across ALL BankAccount objects ──
private static int totalAccounts = 0;
private static double totalDepositsAll = 0.0;
// ── Static constant ──
public static final double MIN_BALANCE = 500.0;
public BankAccount(String accountNumber, String holderName, double initialDeposit) {
this.accountNumber = accountNumber;
this.holderName = holderName;
this.balance = initialDeposit;
totalAccounts++; // shared counter goes up for every new account
totalDepositsAll += initialDeposit; // shared total increases
}
public void deposit(double amount) {
balance += amount; // only THIS object's balance changes
totalDepositsAll += amount; // shared class-wide total also changes
}
public void showBalance() {
System.out.printf("%s (%s) → Balance: ₹%.2f%n",
holderName, accountNumber, balance);
}
// ── Static method to access static attributes ──
public static void showBankSummary() {
System.out.println("Total Accounts : " + totalAccounts);
System.out.printf( "Total Deposits (All): ₹%.2f%n", totalDepositsAll);
}
public static void main(String[] args) {
BankAccount acc1 = new BankAccount("ACC001", "Aarav", 10000.0);
BankAccount acc2 = new BankAccount("ACC002", "Priya", 25000.0);
BankAccount acc3 = new BankAccount("ACC003", "Rohan", 5000.0);
acc1.deposit(3000.0);
acc2.deposit(7000.0);
acc1.showBalance(); // Aarav (ACC001) → Balance: ₹13000.00
acc2.showBalance(); // Priya (ACC002) → Balance: ₹32000.00
acc3.showBalance(); // Rohan (ACC003) → Balance: ₹5000.00
System.out.println("Min Balance Required: ₹" + BankAccount.MIN_BALANCE);
BankAccount.showBankSummary();
// Total Accounts : 3
// Total Deposits (All): ₹50000.00
}
}Final Attributes — Constants & Immutable Fields
The final keyword on an attribute means its value can be assigned only once — after that, any attempt to reassign it causes a compile error. Final attributes communicate a clear design intent: this value is fixed and should never change. Java uses two flavors: static final for class-level constants, and instance final for object-level immutable fields.
Declared as 'public static final dataType NAME = value;'. Belong to the class — one copy shared by all objects. By convention: UPPER_SNAKE_CASE names. Examples: MAX_SPEED, TAX_RATE, PI, DB_URL. Must be initialized at declaration. Accessible without creating any object: ClassName.CONSTANT_NAME. These replace magic numbers in code — 0.18 becomes TAX_RATE everywhere, making code readable and easy to update.
Declared as 'private final dataType fieldName;'. Each object has its own copy, but that copy cannot change after being set. Must be initialized either at declaration or inside the constructor — not in a method. Common for: ID fields, PAN numbers, timestamps, identity values that should never change after object creation. Makes objects more predictable and thread-safe.
When a final attribute is an object reference (e.g., final List<String> items), the REFERENCE is constant — you can't reassign 'items' to a different list. But the object it points to CAN still be mutated: items.add("new item") is allowed! To make the collection truly immutable, use Collections.unmodifiableList() or List.of() from Java 9+. Final only locks the reference, not the object state.
import java.time.LocalDate;
public class CreditCard {
// ── Static final constants (class-level, shared, UPPER_SNAKE_CASE) ──
public static final double MAX_CREDIT_LIMIT = 500000.0;
public static final double LATE_FEE_RATE = 0.02; // 2%
private static final int CVV_LENGTH = 3;
public static final String NETWORK = "VISA";
// ── Instance final attributes (per-object, immutable after construction) ──
private final String cardNumber; // set once — never changes
private final String holderName; // card holder name is permanent
private final LocalDate issueDate; // date of issue — fixed forever
private final LocalDate expiryDate;
// ── Regular instance attributes (mutable) ──
private double creditLimit;
private double outstandingBalance;
private boolean isBlocked;
public CreditCard(String cardNumber, String holderName, double creditLimit) {
if (creditLimit > MAX_CREDIT_LIMIT)
throw new IllegalArgumentException("Credit limit exceeds maximum allowed");
this.cardNumber = cardNumber; // ✅ final — assigned in constructor
this.holderName = holderName; // ✅ final — assigned in constructor
this.issueDate = LocalDate.now();
this.expiryDate = LocalDate.now().plusYears(5);
this.creditLimit = creditLimit;
this.outstandingBalance = 0.0;
this.isBlocked = false;
}
public void spend(double amount) {
if (isBlocked) { System.out.println("Card is blocked."); return; }
if (outstandingBalance + amount > creditLimit) {
System.out.println("Credit limit exceeded."); return;
}
outstandingBalance += amount;
System.out.printf("Spent ₹%.2f | Outstanding: ₹%.2f%n", amount, outstandingBalance);
}
public void display() {
System.out.println("Card : " + cardNumber);
System.out.println("Holder : " + holderName);
System.out.println("Network : " + NETWORK);
System.out.println("Expiry : " + expiryDate);
System.out.printf( "Limit : ₹%.2f%n", creditLimit);
System.out.printf( "Due : ₹%.2f%n", outstandingBalance);
}
public static void main(String[] args) {
CreditCard card = new CreditCard("4111-1111-1111-1111", "Aarav Shah", 100000.0);
card.display();
card.spend(15000.0);
card.spend(90000.0); // exceeds limit
// card.cardNumber = "NEW-NUMBER"; // ❌ COMPILE ERROR — final field
System.out.println("Max Limit Allowed: ₹" + CreditCard.MAX_CREDIT_LIMIT);
}
}Default Values of Class Attributes
Java automatically initializes class attributes (fields) to default values if you do not explicitly assign them. This is one key difference between class attributes and local variables — local variables inside methods have NO default values and MUST be initialized before use. Understanding default values prevents bugs where you assume a field is set when it has simply been left at its default.
public class DefaultValues {
// ── All attributes left uninitialized — Java applies defaults ──
byte byteVal;
short shortVal;
int intVal;
long longVal;
float floatVal;
double doubleVal;
char charVal;
boolean boolVal;
String stringVal; // Object reference — defaults to null
int[] arrayVal; // Array reference — defaults to null
public void printDefaults() {
System.out.println("byte : " + byteVal); // 0
System.out.println("short : " + shortVal); // 0
System.out.println("int : " + intVal); // 0
System.out.println("long : " + longVal); // 0
System.out.println("float : " + floatVal); // 0.0
System.out.println("double : " + doubleVal); // 0.0
System.out.println("char : [" + charVal + "]"); // [] — invisible null char
System.out.println("boolean : " + boolVal); // false
System.out.println("String : " + stringVal); // null
System.out.println("int[] : " + arrayVal); // null
}
public static void main(String[] args) {
DefaultValues obj = new DefaultValues();
obj.printDefaults();
// ⚠️ Accessing method on null reference — NullPointerException!
// System.out.println(obj.stringVal.length()); // ❌ NPE
// ✅ Always check before using object references
if (obj.stringVal != null) {
System.out.println(obj.stringVal.length());
}
// ── Local variable — NO default value ──
int localVar;
// System.out.println(localVar); // ❌ COMPILE ERROR: variable not initialized
int localVar2 = 0; // ✅ Must initialize explicitly
System.out.println(localVar2); // 0
}
}Access Modifiers for Attributes — Controlling Visibility
Access modifiers control which parts of the program can read or modify a class attribute. Choosing the correct access level for each attribute is one of the most important design decisions in Java. The golden rule: always use the most restrictive access level that still meets your needs. For class attributes, this almost always means private.
public class Student {
// ✅ private — most common for instance attributes
private String name;
private int age;
private double gpa;
// ✅ protected — accessible in subclasses (use carefully)
protected String enrollmentNumber;
// ✅ public static final — constants are fine as public
public static final int MAX_CREDITS = 180;
public static final double MAX_GPA = 10.0;
// ❌ public instance field — BAD PRACTICE (avoid this)
// public String address; — anyone can set student.address = null
public Student(String name, int age, double gpa, String enrollmentNumber) {
this.name = name;
this.age = age;
this.gpa = gpa;
this.enrollmentNumber = enrollmentNumber;
}
// ── Public getters — controlled read access ──
public String getName() { return name; }
public int getAge() { return age; }
public double getGpa() { return gpa; }
public String getEnrollmentNumber() { return enrollmentNumber; }
// ── Public setters with validation — controlled write access ──
public void setName(String name) {
if (name == null || name.isBlank())
throw new IllegalArgumentException("Name cannot be blank");
this.name = name.trim();
}
public void setAge(int age) {
if (age < 15 || age > 60)
throw new IllegalArgumentException("Invalid student age: " + age);
this.age = age;
}
public void setGpa(double gpa) {
if (gpa < 0.0 || gpa > MAX_GPA)
throw new IllegalArgumentException("GPA must be between 0 and " + MAX_GPA);
this.gpa = gpa;
}
@Override
public String toString() {
return String.format("Student{name='%s', age=%d, gpa=%.1f, enroll='%s'}",
name, age, gpa, enrollmentNumber);
}
}Naming Conventions & Best Practices for Attributes
Good attribute naming makes code self-documenting — a reader should understand what data an attribute holds just from its name, without any comments. Java has well-established naming conventions that all professional developers follow. Violating these conventions doesn't break compilation, but it makes your code look unprofessional and harder to maintain.
All instance and static (non-final) attributes use camelCase — start with lowercase, capitalize each new word. Good: firstName, accountBalance, isActive, totalScore, lastLoginDate. Bad: FirstName, first_name, FIRSTNAME, firstname. Boolean attributes often start with 'is', 'has', or 'can': isActive, hasPermission, canEdit.
All static final constants use UPPER_SNAKE_CASE — all caps with underscores between words. Good: MAX_SIZE, TAX_RATE, DEFAULT_TIMEOUT, PI, MIN_AGE. Bad: maxSize, Maxsize, maxsize. This makes constants instantly recognizable in code — you immediately know it's a fixed value.
Names should be descriptive and specific — not abbreviations or single letters (except in loops). Good: customerName, orderDate, monthlyInterestRate, maximumRetryCount. Bad: n, temp, x, dat, mIR, mRC. Exception: universally understood short names like 'id', 'url', 'pi' are acceptable. Aim for the longest name that is still clear and not redundant.
Never use Java reserved keywords: class, int, for, if, etc. Avoid type encoding: stringName, intAge (Hungarian notation — outdated). Avoid redundant class name: Student.studentName → just Student.name. Avoid confusing negatives: notActive, isNotBlocked — prefer positive forms: isActive, isBlocked. Avoid single-character names outside loops — 'a', 'x', 'b' are meaningless as field names.
// ❌ BAD naming — unprofessional, confusing
public class BadNaming {
public String N; // what is N?
private int A; // what is A?
String first_name; // snake_case — wrong for Java
protected Boolean IsActive; // PascalCase — wrong for field
public static final int maxsize = 100; // constant should be UPPER_SNAKE_CASE
private String stringName; // type prefix — unnecessary
private boolean notBlocked; // confusing negative boolean
}
// ✅ GOOD naming — clean, readable, self-documenting
public class ProductInventory {
// ── Constants: UPPER_SNAKE_CASE ──
public static final int MAX_STOCK_QUANTITY = 10000;
public static final double GST_RATE = 0.18;
private static final String DEFAULT_CATEGORY = "General";
// ── Static mutable: camelCase ──
private static int totalProductsListed = 0;
// ── Instance attributes: camelCase, descriptive ──
private String productId;
private String productName;
private String category;
private double unitPrice;
private int stockQuantity;
private boolean isAvailable; // boolean: starts with 'is'
private boolean hasFreeShipping; // boolean: starts with 'has'
private String manufacturerName;
// ── Final instance: camelCase ──
private final String sku; // Stock Keeping Unit — never changes
public ProductInventory(String productId, String productName,
double unitPrice, int stockQuantity, String sku) {
this.productId = productId;
this.productName = productName;
this.category = DEFAULT_CATEGORY;
this.unitPrice = unitPrice;
this.stockQuantity = stockQuantity;
this.isAvailable = stockQuantity > 0;
this.hasFreeShipping = unitPrice > 999.0;
this.manufacturerName = "Unknown";
this.sku = sku;
totalProductsListed++;
}
@Override
public String toString() {
return String.format("Product{id='%s', name='%s', price=₹%.2f, stock=%d}",
productId, productName, unitPrice, stockQuantity);
}
}Encapsulation — Getters, Setters & Protecting Attributes
Encapsulation of attributes means keeping fields private and providing controlled access through public getter and setter methods. Getters allow reading a field's value; setters allow updating it — with validation. This gives the class full control over its own data: invalid values can never be assigned, and internal representation can change without affecting external code.
public class PatientRecord {
// ── All attributes private — encapsulated ──
private String patientId;
private String fullName;
private int age;
private double weight; // in kg
private double height; // in cm
private String bloodGroup;
private boolean isDiabetic;
private static final double MIN_WEIGHT = 1.0;
private static final double MAX_WEIGHT = 500.0;
private static final double MIN_HEIGHT = 30.0;
private static final double MAX_HEIGHT = 300.0;
public PatientRecord(String patientId, String fullName, int age,
double weight, double height, String bloodGroup) {
this.patientId = patientId;
setFullName(fullName); // setter validates even in constructor
setAge(age);
setWeight(weight);
setHeight(height);
setBloodGroup(bloodGroup);
this.isDiabetic = false;
}
// ── Getters — read-only access ──
public String getPatientId() { return patientId; }
public String getFullName() { return fullName; }
public int getAge() { return age; }
public double getWeight() { return weight; }
public double getHeight() { return height; }
public String getBloodGroup() { return bloodGroup; }
public boolean isDiabetic() { return isDiabetic; } // boolean: isDiabetic()
// ── Setters with validation ──
public void setFullName(String fullName) {
if (fullName == null || fullName.isBlank())
throw new IllegalArgumentException("Patient name cannot be blank");
this.fullName = fullName.trim();
}
public void setAge(int age) {
if (age < 0 || age > 150)
throw new IllegalArgumentException("Invalid age: " + age);
this.age = age;
}
public void setWeight(double weight) {
if (weight < MIN_WEIGHT || weight > MAX_WEIGHT)
throw new IllegalArgumentException(
String.format("Weight must be between %.1f and %.1f kg", MIN_WEIGHT, MAX_WEIGHT));
this.weight = weight;
}
public void setHeight(double height) {
if (height < MIN_HEIGHT || height > MAX_HEIGHT)
throw new IllegalArgumentException(
String.format("Height must be between %.1f and %.1f cm", MIN_HEIGHT, MAX_HEIGHT));
this.height = height;
}
public void setBloodGroup(String bloodGroup) {
java.util.Set<String> valid = java.util.Set.of(
"A+","A-","B+","B-","AB+","AB-","O+","O-");
if (!valid.contains(bloodGroup))
throw new IllegalArgumentException("Invalid blood group: " + bloodGroup);
this.bloodGroup = bloodGroup;
}
public void setDiabetic(boolean isDiabetic) {
this.isDiabetic = isDiabetic;
}
// ── Computed attribute (derived from other fields) ──
public double getBmi() {
double heightInMetres = height / 100.0;
return weight / (heightInMetres * heightInMetres);
}
@Override
public String toString() {
return String.format(
"Patient{id='%s', name='%s', age=%d, blood='%s', BMI=%.1f}",
patientId, fullName, age, bloodGroup, getBmi());
}
public static void main(String[] args) {
PatientRecord p = new PatientRecord("P001", "Aarav Shah", 35, 72.5, 175.0, "B+");
System.out.println(p);
System.out.println("BMI: " + String.format("%.2f", p.getBmi()));
p.setWeight(75.0); // ✅ valid update
// p.setAge(-5); // ❌ IllegalArgumentException — invalid age
// p.setBloodGroup("XY+"); // ❌ IllegalArgumentException — invalid group
}
}Attribute Initialization Techniques
Java provides four ways to initialize class attributes. Choosing the right technique depends on whether the value is fixed, computed, or passed in at object creation time. Proper initialization prevents null surprises and ensures objects are always in a valid state from the moment they are created.
Assign a value directly where the field is declared: 'private int age = 0;' or 'private String status = "ACTIVE";'. Best for: fixed default values that are the same for every object. Runs before the constructor. Simple and readable. Cannot depend on constructor parameters.
Assign values inside the constructor body using 'this.field = value;'. Best for: values that differ per object (passed as constructor parameters) or computed from constructor arguments. Most common initialization technique. Allows validation before assignment.
A code block inside the class but outside any method — '{ this.createdAt = LocalDateTime.now(); }'. Runs before every constructor call. Useful when multiple constructors share common initialization logic. Less common — constructor chaining with this() is usually preferred.
A static { ... } block that runs once when the class is first loaded by JVM. Used for complex static field initialization that can't be done in a single expression: loading config files, building lookup maps, computing large constants. Example: static { TAX_TABLE = loadTaxConfig(); }
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
public class Order {
// ── Technique 1: Inline initialization ──
private String status = "PENDING"; // fixed default for every order
private int retryCount = 0;
private boolean isPriority = false;
// ── Technique 3: Instance initializer block ──
private LocalDateTime createdAt;
private String orderId;
{
// Runs before EVERY constructor
createdAt = LocalDateTime.now();
orderId = "ORD-" + System.currentTimeMillis();
}
// ── Technique 4: Static initializer block ──
private static final Map<String, Double> TAX_BY_CATEGORY;
static {
// Complex initialization — cannot be done in single expression
TAX_BY_CATEGORY = new HashMap<>();
TAX_BY_CATEGORY.put("Electronics", 0.18);
TAX_BY_CATEGORY.put("Clothing", 0.05);
TAX_BY_CATEGORY.put("Food", 0.00);
TAX_BY_CATEGORY.put("Furniture", 0.12);
System.out.println("Tax table loaded.");
}
// ── Technique 2: Constructor initialization ──
private String customerId;
private double totalAmount;
private String category;
public Order(String customerId, double totalAmount, String category) {
// orderId and createdAt already set by instance initializer above
this.customerId = customerId;
this.totalAmount = totalAmount;
this.category = category;
}
public double getTaxAmount() {
double rate = TAX_BY_CATEGORY.getOrDefault(category, 0.18);
return totalAmount * rate;
}
@Override
public String toString() {
return String.format(
"Order{id='%s', customer='%s', amount=₹%.2f, tax=₹%.2f, status='%s'}",
orderId, customerId, totalAmount, getTaxAmount(), status);
}
public static void main(String[] args) {
Order o1 = new Order("C001", 50000.0, "Electronics");
Order o2 = new Order("C002", 2000.0, "Clothing");
Order o3 = new Order("C003", 500.0, "Food");
System.out.println(o1);
System.out.println(o2);
System.out.println(o3);
}
}Common Mistakes & Pitfalls — Attribute Bugs That Fool Everyone
These attribute-related mistakes appear repeatedly in beginner Java code. Each one compiles successfully (or fails at runtime) in a way that surprises new developers. Study them carefully.
// ❌ MISTAKE 1: Shadowing — parameter hides field, 'this' forgotten
public class Box {
private int width;
private int height;
public Box(int width, int height) {
width = width; // ❌ Assigns parameter to itself — field stays 0
height = height; // ❌ Same bug
// Fix: this.width = width; this.height = height;
}
}
// ❌ MISTAKE 2: Accessing static attribute via object reference
public class Counter {
public static int count = 0;
public static void main(String[] args) {
Counter c = new Counter();
c.count = 10; // ❌ Misleading — looks like instance access
// Actually modifies the class-level static field
// Fix: Counter.count = 10; — use class name for static attributes
}
}
// ❌ MISTAKE 3: Assuming all variables get default values
public class DefaultMistake {
private int fieldVar; // ✅ gets default 0 — class attribute
public void method() {
int localVar; // ❌ NO default value — local variable
// System.out.println(localVar); // COMPILE ERROR: variable not initialized
System.out.println(fieldVar); // ✅ OK — prints 0
}
}
// ❌ MISTAKE 4: Reassigning a final attribute
public class FinalMistake {
private final String id = "ABC";
public void resetId() {
// id = "NEW"; // ❌ COMPILE ERROR: cannot assign value to final variable
}
}
// ❌ MISTAKE 5: NullPointerException from uninitialized object attribute
public class NpeDemo {
private String name; // default is null
public void printUpper() {
System.out.println(name.toUpperCase()); // ❌ NPE — name is null!
// Fix 1: private String name = "";
// Fix 2: if (name != null) { System.out.println(name.toUpperCase()); }
}
}
// ❌ MISTAKE 6: Modifying object via final reference
import java.util.ArrayList;
import java.util.List;
public class FinalRefMistake {
private final List<String> tags = new ArrayList<>();
public void addTag(String tag) {
tags.add(tag); // ✅ Allowed — modifying the object, not the reference
// tags = new ArrayList<>(); // ❌ COMPILE ERROR — cannot reassign final ref
}
}Bad Practices & Anti-Patterns — What Senior Developers Reject
These attribute anti-patterns are frequently flagged in code reviews in professional Java teams. Each violates fundamental OOP and clean code principles.
Never declare instance attributes as public: 'public double balance;' means any code anywhere can set 'account.balance = -1000000;'. This makes validation impossible and future refactoring very hard. Always declare instance fields private and provide getters/setters. The only acceptable public fields are public static final constants.
A class with 20+ attributes is a warning sign. It's likely doing too many things at once, violating the Single Responsibility Principle. Example: a User class that stores login info, personal info, billing info, address, and preferences all in one. Split into: UserCredentials, UserProfile, BillingInfo, UserAddress, UserPreferences — each focused and manageable.
Static mutable fields behave like global variables — any class can modify them, causing unpredictable behavior. They make unit testing a nightmare (tests pollute each other's state). Use static fields ONLY for constants (static final) or counters with very deliberate shared semantics. Prefer passing data through constructors or method parameters.
Creating objects that are only partially initialized is dangerous: 'User u = new User(); u.setName("Raj"); u.setEmail("...");' — between 'new User()' and the setters, the object is in an invalid state. Prefer constructors that require all mandatory fields, so objects are always fully valid from the moment of creation.
Returning a direct reference to an internal list: 'public List<String> getItems() { return items; }' allows outside code to modify the list directly, bypassing all class control: 'obj.getItems().clear()'. Instead return a defensive copy or unmodifiable view: 'return Collections.unmodifiableList(items);' or 'return new ArrayList<>(items);'
Instance attributes represent PERSISTENT object state — not temporary values needed only during one method call. Storing a temporary calculation result as an attribute (to avoid passing it between helper methods) pollutes the object's state and creates threading bugs. Use local variables for temporary computation; only promote to attribute if the value genuinely represents the object's persistent state.
// ❌ ANTI-PATTERN 1: Public instance fields
public class BadProduct {
public String name; // ❌ No validation possible
public double price; // ❌ Anyone can set price = -99
public int stockQuantity; // ❌ Anyone can set stock = -1000
}
// Usage: BadProduct p = new BadProduct(); p.price = -999; // legal but wrong!
// ✅ BETTER: Private fields with validation
public class GoodProduct {
private String name;
private double price;
private int stockQuantity;
public void setPrice(double price) {
if (price < 0) throw new IllegalArgumentException("Price cannot be negative");
this.price = price;
}
}
// ❌ ANTI-PATTERN 2: Exposing mutable internal list
public class BadOrder {
private List<String> items = new ArrayList<>();
public List<String> getItems() {
return items; // ❌ Caller can: getItems().clear() — bypasses class control
}
}
// ✅ BETTER: Return unmodifiable view
public class GoodOrder {
private List<String> items = new ArrayList<>();
public List<String> getItems() {
return Collections.unmodifiableList(items); // ✅ Read-only view
}
public void addItem(String item) {
if (item == null || item.isBlank()) throw new IllegalArgumentException();
items.add(item); // Only class can modify
}
}
// ❌ ANTI-PATTERN 3: Attribute used as temp variable
public class BadCalculator {
private double tempResult; // ❌ Not persistent state — just a temp variable
public double calculateTax(double amount) {
tempResult = amount * 0.18;
return tempResult;
}
}
// ✅ BETTER: Use local variable
public class GoodCalculator {
public double calculateTax(double amount) {
double taxAmount = amount * 0.18; // ✅ local variable — appropriate scope
return taxAmount;
}
}Real-World Production Code Examples — Attributes in Context
The following examples show class attribute design patterns from real enterprise Java codebases — a JPA entity with proper field design, and a Java Record used as an immutable DTO.
package com.techsustainify.catalog.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Entity
@Table(name = "products")
public class Product {
// ── Immutable identity (final — set once, never changes) ──
@Id
@Column(name = "product_id", nullable = false, updatable = false)
private final String productId;
@Column(name = "sku", unique = true, nullable = false, updatable = false)
private final String sku;
@Column(name = "created_at", nullable = false, updatable = false)
private final LocalDateTime createdAt;
// ── Mutable attributes ──
@Column(name = "product_name", nullable = false)
private String productName;
@Column(name = "description")
private String description;
@Column(name = "category", nullable = false)
private String category;
@Column(name = "unit_price", nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
@Column(name = "stock_quantity", nullable = false)
private int stockQuantity;
@Column(name = "is_active", nullable = false)
private boolean isActive;
@Column(name = "last_updated_at")
private LocalDateTime lastUpdatedAt;
// ── Static constants ──
public static final BigDecimal MAX_PRICE = new BigDecimal("999999.99");
private static final int MAX_STOCK = 100000;
public static final String DEFAULT_CATEGORY = "General";
// ── Constructor (required by JPA — no-arg; plus a working constructor) ──
protected Product() {
this.productId = null; this.sku = null; this.createdAt = null;
}
public Product(String productId, String sku, String productName,
String category, BigDecimal unitPrice, int stockQuantity) {
if (productId == null || productId.isBlank())
throw new IllegalArgumentException("Product ID required");
if (unitPrice == null || unitPrice.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Price must be non-negative");
if (stockQuantity < 0 || stockQuantity > MAX_STOCK)
throw new IllegalArgumentException("Invalid stock quantity");
this.productId = productId;
this.sku = sku;
this.productName = productName;
this.category = category != null ? category : DEFAULT_CATEGORY;
this.unitPrice = unitPrice;
this.stockQuantity = stockQuantity;
this.isActive = true;
this.createdAt = LocalDateTime.now();
this.lastUpdatedAt = this.createdAt;
}
// ── Getters ──
public String getProductId() { return productId; }
public String getSku() { return sku; }
public String getProductName() { return productName; }
public String getCategory() { return category; }
public BigDecimal getUnitPrice() { return unitPrice; }
public int getStockQuantity(){ return stockQuantity; }
public boolean isActive() { return isActive; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getLastUpdatedAt(){ return lastUpdatedAt; }
// ── Setters with validation ──
public void setProductName(String productName) {
if (productName == null || productName.isBlank())
throw new IllegalArgumentException("Product name cannot be blank");
this.productName = productName.trim();
this.lastUpdatedAt = LocalDateTime.now();
}
public void setUnitPrice(BigDecimal unitPrice) {
if (unitPrice == null || unitPrice.compareTo(BigDecimal.ZERO) < 0
|| unitPrice.compareTo(MAX_PRICE) > 0)
throw new IllegalArgumentException("Invalid price: " + unitPrice);
this.unitPrice = unitPrice;
this.lastUpdatedAt = LocalDateTime.now();
}
public void adjustStock(int delta) {
int newQty = stockQuantity + delta;
if (newQty < 0) throw new IllegalStateException("Stock cannot go below 0");
if (newQty > MAX_STOCK) throw new IllegalStateException("Stock exceeds maximum");
this.stockQuantity = newQty;
this.lastUpdatedAt = LocalDateTime.now();
}
public void deactivate() { this.isActive = false; this.lastUpdatedAt = LocalDateTime.now(); }
@Override
public String toString() {
return String.format(
"Product{id='%s', sku='%s', name='%s', price=%s, stock=%d, active=%b}",
productId, sku, productName, unitPrice, stockQuantity, isActive);
}
}package com.techsustainify.catalog.dto;
import java.math.BigDecimal;
// ✅ Java Record — perfect for immutable DTO attributes (no boilerplate)
public record ProductSummaryDTO(
String productId,
String sku,
String productName,
String category,
BigDecimal unitPrice,
int stockQuantity,
boolean isActive
) {
// ── Compact constructor — validate all attributes ──
ProductSummaryDTO {
if (productId == null || productId.isBlank())
throw new IllegalArgumentException("productId cannot be blank");
if (productName == null || productName.isBlank())
throw new IllegalArgumentException("productName cannot be blank");
if (unitPrice == null || unitPrice.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("unitPrice cannot be negative");
if (stockQuantity < 0)
throw new IllegalArgumentException("stockQuantity cannot be negative");
productName = productName.trim();
category = category != null ? category.trim() : "General";
}
// ── Derived/computed attributes (methods from field data) ──
public boolean isInStock() { return stockQuantity > 0 && isActive; }
public boolean isLowStock() { return stockQuantity > 0 && stockQuantity <= 5; }
public BigDecimal priceWithGst(double gstRate) {
return unitPrice.multiply(BigDecimal.valueOf(1 + gstRate));
}
public String stockLabel() {
if (!isActive) return "DISCONTINUED";
if (stockQuantity == 0) return "OUT OF STOCK";
if (isLowStock()) return "LOW STOCK (" + stockQuantity + ")";
return "IN STOCK";
}
}
class DTOUsage {
public static void main(String[] args) {
ProductSummaryDTO dto = new ProductSummaryDTO(
"P001", "LAP-001", "MacBook Air M3", "Electronics",
new BigDecimal("112000.00"), 4, true
);
// Record accessors — same name as the attribute (no 'get' prefix)
System.out.println(dto.productName()); // MacBook Air M3
System.out.println(dto.unitPrice()); // 112000.00
System.out.println(dto.stockLabel()); // LOW STOCK (4)
System.out.println(dto.priceWithGst(0.18)); // 132160.00
System.out.println(dto); // auto toString()
// Records are IMMUTABLE — all attributes are final
// dto.stockQuantity = 10; // ❌ COMPILE ERROR
}
}Attribute Types Flowchart — Choosing the Right Attribute
This flowchart helps you decide which type of class attribute to use for any given piece of data in your Java class design.
Code Execution Flow — from source to output
Java Class Attributes Interview Questions — Beginner to Advanced
These questions are consistently asked in Java fresher interviews, OCPJP certification exams, and campus placement tests related to class attributes and fields.
Practice Questions — Test Your Class Attributes Knowledge
Attempt each question independently before reading the answer — active recall is proven to be 2–3x more effective than passive reading.
1. What is the output? public class Demo { static int x = 5; int y = 10; public static void main(String[] args) { Demo d1 = new Demo(); Demo d2 = new Demo(); d1.y = 50; Demo.x = 99; System.out.println(d2.y); System.out.println(d1.x); } }
Easy2. Find the bug and fix it: public class Rectangle { private int width; private int height; public Rectangle(int width, int height) { width = width; height = height; } public int area() { return width * height; } }
Easy3. Design a 'Vehicle' class with: private instance attributes — brand (String), model (String), year (int), speed (double, default 0.0); a static attribute totalVehicles; a static final constant MAX_SPEED = 300.0; a constructor; and an accelerate(double amount) method that increases speed but does not exceed MAX_SPEED.
Easy4. What types of default values do these attributes have? public class Defaults { int a; double b; boolean c; String d; int[] e; char f; }
Easy5. Refactor this class to apply proper encapsulation: public class BankAccount { public String owner; public double balance; public String accountType; }
Medium6. What is wrong with this code? public class Config { public static final List<String> ALLOWED_ROLES = new ArrayList<>(); static { ALLOWED_ROLES.add("ADMIN"); ALLOWED_ROLES.add("USER"); ALLOWED_ROLES.add("VIEWER"); } public List<String> getAllowedRoles() { return ALLOWED_ROLES; } }
Medium7. Create a 'Temperature' class with a private final double attribute in Celsius. Provide getters for Celsius, Fahrenheit, and Kelvin (computed). Include a static factory method 'fromFahrenheit(double f)' and a static constant ABSOLUTE_ZERO_CELSIUS = -273.15. Validate that temperature cannot be below absolute zero.
Medium8. Convert this class to a Java Record and ensure all attributes are validated: public class Address { private final String street; private final String city; private final String state; private final String pinCode; // constructor, getters, equals, hashCode, toString... }
MediumConclusion — Class Attributes: The State of Every Java Object
Class attributes are the state of every Java object. Without attributes, a class is just a collection of stateless behaviors — like a calculator with no memory. Attributes give objects their identity, their data, and their ability to behave differently from one another. How you design, name, initialize, and protect attributes directly determines the quality, reliability, and maintainability of your Java code.
The difference between beginner and professional attribute design is clear: beginners use public fields, skip validation, mix up instance and static, and ignore default null values. Professional Java code uses private fields with validated getters/setters, meaningful camelCase names, static final constants with UPPER_SNAKE_CASE, final instance fields for immutable identity, and defensive methods for mutable collections.
Your next step: Java Class Methods — where you'll learn how to define the behavior of your classes, how methods work with your attributes, method overloading, return types, and how static and instance methods differ in accessing class state. ☕