Java Classes
A class defines a new type. It describes the data that objects of that type hold (fields) and the operations they support (methods), and it specifies how new objects are initialized (constructors). Once a class exists, you can create as many objects from it as you need, and the compiler checks that every use of those objects matches what the class declares.
Declaring a Class
public class Book {
// fields: the data each Book object holds
String title;
String author;
int pages;
// method: behavior every Book object has
void describe() {
System.out.println(title + " by " + author + ", " + pages + " pages");
}
}
| Part | Meaning |
|---|---|
public | The class can be used from any package |
class | Declares a class |
Book | The class name, in PascalCase; also the name of the new type |
{ ... } | The class body containing fields, methods, constructors, and nested types |
A public class must be in a file of the same name: Book.java.
Declaring the class creates the type but no objects. Objects are created with new:
Book b = new Book();
b.title = "Clean Code";
b.author = "Robert C. Martin";
b.pages = 464;
b.describe(); // Clean Code by Robert C. Martin, 464 pages
This works but leaves the fields open to any code, and an object can exist with missing data. The rest of this lesson builds the class into a well-designed form.
Members of a Class
A class body can contain:
| Member | Purpose | Example |
|---|---|---|
| Fields | Store an object's state | private double balance; |
| Methods | Define behavior | public void deposit(double amount) |
| Constructors | Initialize new objects | public Book(String title) |
| Static fields | Data shared by the whole class | static int count; |
| Static methods | Behavior not tied to one object | static Book fromCsv(String line) |
| Initializer blocks | Code run during class or object initialization | static { ... } |
| Nested types | Classes, interfaces, enums, records declared inside | enum Format { HARDCOVER, EBOOK } |
Fields
Fields are variables declared directly in the class body. Each object gets its own copy of every non-static field:
public class Book {
private String title;
private String author;
private int pages;
private boolean available = true; // field with an initial value
}
Fields that are not given a value start at their default: 0 for numbers, false for booleans, null for references. This differs from local variables, which have no default and must be assigned before use.
Fields are usually private: only the class's own code can read or write them directly. This is the first step of encapsulation.
Constructors
A constructor runs when an object is created and sets its initial state. It has the same name as the class and no return type:
public class Book {
private final String title;
private final String author;
private final int pages;
private boolean available = true;
public Book(String title, String author, int pages) {
if (pages <= 0) {
throw new IllegalArgumentException("pages must be positive");
}
this.title = title;
this.author = author;
this.pages = pages;
}
}
Book b = new Book("Clean Code", "Robert C. Martin", 464);
Now every Book is guaranteed to have a title, an author, and a valid page count from the moment it exists. this.title refers to the field; title alone refers to the parameter. Fields marked final must be assigned exactly once, in the constructor or at declaration, and can never change afterward.
If a class declares no constructor at all, the compiler adds a default constructor with no parameters. Declaring any constructor removes it. Constructors have their own lesson.
Methods
Methods give objects behavior. Instance methods can read and modify the object's own fields:
public class Book {
private final String title;
private final String author;
private final int pages;
private boolean available = true;
public Book(String title, String author, int pages) {
this.title = title;
this.author = author;
this.pages = pages;
}
public boolean checkOut() {
if (!available) {
return false;
}
available = false;
return true;
}
public void returnBook() {
available = true;
}
public boolean isAvailable() { return available; }
public String getTitle() { return title; }
public int getPages() { return pages; }
public int readingMinutes(int pagesPerHour) {
return pages * 60 / pagesPerHour;
}
}
Book b = new Book("Clean Code", "Robert C. Martin", 464);
System.out.println(b.checkOut()); // true
System.out.println(b.checkOut()); // false: already out
b.returnBook();
System.out.println(b.isAvailable()); // true
System.out.println(b.readingMinutes(40)); // 696
The available field is private, so other code cannot set it directly; it must go through checkOut and returnBook, which apply the library's rules. Methods like getTitle (called getters or accessors) provide read access to private fields.
Overriding toString
Every class inherits toString() from Object. Its default output is the class name and a hash code, which is rarely useful:
System.out.println(b); // Book@6d06d69c
Override it to describe the object meaningfully:
@Override
public String toString() {
return "Book[" + title + ", " + author + ", " + pages + "p, "
+ (available ? "available" : "checked out") + "]";
}
System.out.println(b); // Book[Clean Code, Robert C. Martin, 464p, available]
println, string concatenation, debuggers, and logging all call toString automatically. @Override asks the compiler to confirm that the method really overrides a parent method; a typo such as tostring then becomes a compile error instead of a silently unused method.
Static Members
A static field or method belongs to the class itself rather than to any object:
public class Book {
private static int created = 0; // one shared counter
private final String title;
public Book(String title) {
this.title = title;
created++;
}
public static int getCreatedCount() { // called on the class
return created;
}
}
new Book("A");
new Book("B");
System.out.println(Book.getCreatedCount()); // 2
Use static members for data or behavior that does not depend on a particular object: counters, constants (static final), and factory or utility methods. The static keyword has its own lesson.
Classes as Types
A class name can be used anywhere a type is needed: variable declarations, parameters, return types, fields, and generic type arguments.
public class Library {
private final List<Book> books = new ArrayList<>();
public void add(Book book) {
books.add(book);
}
public Book findByTitle(String title) {
for (Book b : books) {
if (b.getTitle().equalsIgnoreCase(title)) {
return b;
}
}
return null;
}
public int totalPages() {
int sum = 0;
for (Book b : books) {
sum += b.getPages();
}
return sum;
}
}
Library uses Book as a field's element type, a parameter type, and a return type. The compiler checks every use: passing a String to add, or calling a method Book does not have, is a compile error. Classes built from other classes this way (composition) are how real applications are structured.
Class Design Guidelines
Model one concept per class. Book represents a book; Library manages a collection of books. A class that does unrelated things is hard to name, a sign it should be split.
Make fields private. Expose behavior through methods. This lets the class enforce its rules and change its internal representation later without breaking other code.
Establish a valid state in the constructor. Require the data an object needs to be meaningful, and reject invalid values immediately.
Prefer immutability where possible. Fields that never change should be final. An object whose state cannot change after construction is simpler to reason about and safe to share between threads.
Avoid setters by reflex. Generating a setX for every field gives up the control encapsulation provides. Add a method only when there is a legitimate operation, and name it after that operation (checkOut, rename, applyDiscount).
Kinds of Classes
As you progress, you will meet several specialized forms, each with its own lesson:
| Form | Declared with | Purpose |
|---|---|---|
| Regular class | class | General-purpose type with state and behavior |
| Abstract class | abstract class | Partial implementation meant to be extended; cannot be instantiated |
| Interface | interface | A contract of methods that classes implement |
| Enum | enum | A fixed set of named constants, which can have fields and methods |
| Record | record | A concise, immutable data carrier |
| Nested / inner class | class inside another class | Helper types tied to an outer class |
| Anonymous class | new Type() { ... } | One-off implementation defined inline |
For pure data with no behavior beyond accessing it, a record is often the best choice:
public record BookInfo(String title, String author, int pages) {}
This single line provides a constructor, private final fields, accessor methods (title(), author(), pages()), and correct equals, hashCode, and toString.
Practical Example: Product Inventory
public class Product {
private final String sku;
private final String name;
private double price;
private int stock;
public Product(String sku, String name, double price, int stock) {
if (price < 0 || stock < 0) {
throw new IllegalArgumentException("price and stock must be non-negative");
}
this.sku = sku;
this.name = name;
this.price = price;
this.stock = stock;
}
public void restock(int quantity) {
if (quantity <= 0) throw new IllegalArgumentException("quantity must be positive");
stock += quantity;
}
public boolean sell(int quantity) {
if (quantity <= 0 || quantity > stock) return false;
stock -= quantity;
return true;
}
public void applyDiscount(double percent) {
price = price * (100 - percent) / 100;
}
public double inventoryValue() {
return price * stock;
}
public boolean isLowStock() {
return stock < 5;
}
@Override
public String toString() {
return String.format("%s %-10s %8.2f x %3d", sku, name, price, stock);
}
public static void main(String[] args) {
Product kb = new Product("KB-01", "Keyboard", 2499, 10);
Product ms = new Product("MS-02", "Mouse", 799, 3);
kb.sell(7);
ms.restock(20);
kb.applyDiscount(10);
System.out.println(kb);
System.out.println(ms);
System.out.printf("Total value: %.2f%n", kb.inventoryValue() + ms.inventoryValue());
System.out.println("Keyboard low stock? " + kb.isLowStock());
}
}
Output:
KB-01 Keyboard 2249.10 x 3
MS-02 Mouse 799.00 x 23
Total value: 25124.30
Keyboard low stock? true
The constructor rejects invalid starting values. sku and name are final because they identify the product and should never change; price and stock change only through methods that express real business operations. sell returns false instead of allowing negative stock. inventoryValue and isLowStock compute answers from the object's own state, so callers never need to know how that state is stored. Each Product object maintains its own state independently: selling keyboards does not affect mice.