OOP: Classes and Objects
A thorough guide to object-oriented programming in Java: defining classes, creating objects, instance variables and methods, constructors, the this keyword, static members, the final keyword, the Object class API, and object cloning.
What is Object-Oriented Programming?
Object-oriented programming (OOP) is a way of structuring code around objects rather than around procedures or functions. An object is a self-contained unit that combines data (called fields or attributes) and behaviour (called methods) into a single entity. You define the blueprint for objects using a class, then create as many instances of that class as you need. Java was designed from the ground up as an object-oriented language, and almost everything you write in Java will live inside a class.
The four pillars of OOP
OOP is built on four core principles. This tutorial covers the foundation; later tutorials cover each pillar in depth.
- Encapsulation:Bundling data and the methods that act on it inside a class, and controlling access from the outside. This protects your data from unintended modification.
- Inheritance:Allowing one class to acquire the fields and methods of another, promoting code reuse and establishing class hierarchies.
- Polymorphism:Allowing the same method name to behave differently depending on the object it is called on.
- Abstraction:Hiding implementation details and exposing only what is necessary through well-defined interfaces.
Class Definition
A class is declared with the class keyword followed by the class name and a pair of curly braces that contain its body. The class body holds field declarations, constructors, and methods. Java's naming convention is to start class names with an uppercase letter and use CamelCase for each subsequent word: for example, BankAccount or StudentRecord. Each public class must live in a file with the same name.
Anatomy of a class
A typical class has three types of members.
- Fields (instance variables):Variables declared directly inside the class body but outside any method. Each object gets its own copy of these.
- Constructors:Special methods used to initialise a new object. They have the same name as the class and no return type.
- Methods:Functions defined inside the class that describe what objects can do. They operate on the object's fields.
Class Definition
JavaA minimal class with fields and a method, showing the basic structure.
Object Creation with the new Keyword
You create an object using the new keyword followed by a constructor call. The new keyword allocates memory on the heap for the new object, calls the constructor to initialise it, and returns a reference to that object. The reference is then stored in a variable of the class type. You can create as many independent objects from the same class as you need: each one occupies its own memory and holds its own copies of the instance variables.
Creating Objects
JavaCreating multiple independent objects from the same class and showing they are distinct.
Instance Variables and Methods
Instance variables are the fields that belong to an object. Each object created from a class gets its own set of instance variables, stored separately on the heap. Instance methods are the functions that operate on those variables. You call them on a specific object using the dot operator, and they automatically have access to that object's variables.
Instance vs. class-level (static) members
The word 'instance' simply means 'belonging to a specific object', as opposed to belonging to the class as a whole.
- An instance variable like name in a Person class will have a different value for each Person object you create.
- An instance method like getName() always operates on the specific object you called it on, not on some global state.
- You cannot call an instance method without first having an object. Trying to call it on the class name directly is a compile error.
Instance Variables and Methods
JavaA BankAccount class demonstrating how each object maintains its own state.
The this Keyword
Inside any instance method or constructor, this is an implicit reference to the current object, the one on which the method was called. The most practical use of this is to disambiguate when a constructor or setter parameter has the same name as an instance variable. Without this, the local parameter would shadow the field, and you would be assigning the parameter to itself rather than to the field.
Three common uses of this
Each use solves a specific problem that arises in object-oriented code.
- Disambiguating fields from parameters:this.name = name; sets the instance variable name to the value of the parameter name. Without this, both refer to the parameter.
- Passing the current object to another method:someMethod(this) passes the object itself as an argument, useful in builder patterns and event listeners.
- Calling another constructor:this(...) as the first statement in a constructor calls a different constructor of the same class. Covered in the constructor chaining section below.
The this Keyword
JavaUsing this to resolve naming conflicts between parameters and instance variables.
Constructors
A constructor is a special block of code that runs automatically every time you create an object with new. Its job is to put the new object into a valid initial state. Constructors look like methods but have two distinguishing features: they have exactly the same name as the class, and they have no return type, not even void.
Default Constructor
Java provides a zero-argument constructor automatically under one specific condition.
- If you write no constructors at all in your class, the compiler silently inserts a public no-argument constructor that does nothing.
- The moment you define even one constructor yourself, the compiler no longer provides the default. If you want a no-argument constructor alongside a parameterized one, you must write both explicitly.
- The default constructor initialises all fields to their default values: 0 for numerics, false for boolean, and null for reference types.
Default vs. Parameterized Constructor
JavaShowing the compiler-provided default, then replacing it with explicit constructors.
Constructor Overloading
Like regular methods, constructors can be overloaded: you can define multiple constructors in the same class as long as each has a different parameter list. The compiler picks the right constructor based on the arguments you pass when creating the object. This gives callers flexibility to create objects with as much or as little initial information as they have available.
Constructor Overloading
JavaA Rectangle class offering three ways to construct an object depending on available data.
Constructor Chaining with this()
When you have multiple overloaded constructors and each one needs to do similar initialisation work, duplicating that code across all of them is error-prone. Constructor chaining solves this: calling this(...) as the very first statement in a constructor delegates to another constructor in the same class. All the shared logic then lives in one place (the most parameterized constructor, typically), and every other constructor routes through it.
Rules for this() calls
Constructor chaining has strict placement and ordering rules.
- The this(...) call must be the very first statement inside the constructor body. Nothing can precede it, not even a variable declaration.
- Constructors cannot chain to themselves (direct or indirect circular chains are a compile error).
- The arguments you pass to this(...) determine which overloaded constructor gets called, using the same overload-resolution rules as a regular method call.
Constructor Chaining with this()
JavaA Person class where all constructors delegate to a single canonical one.
Garbage Collection and finalize()
Java manages memory automatically. When an object is no longer reachable through any live reference, the garbage collector (GC) eventually reclaims its heap memory. You do not call free() or delete as you would in C or C++. The GC runs in the background and decides when to collect based on memory pressure and its own algorithms.
What you need to know about GC
You do not control when GC runs, which has practical implications for resource management.
- An object becomes eligible for garbage collection when no live variable holds a reference to it.
- Setting a reference variable to null removes that reference. If no other references point to the object, it becomes eligible for collection.
- The finalize() method was inherited from Object and called by the GC before reclaiming an object. It is deprecated since Java 9 and removed as a mechanism in Java 18+. Do not use it for cleanup.
- For cleanup of external resources like file handles or database connections, use the try-with-resources statement and implement the AutoCloseable interface instead. That is the correct modern Java pattern.
Garbage Collection Basics
JavaDemonstrating when objects become eligible for collection and why finalize() is unreliable.
The static Keyword
The static keyword marks a member as belonging to the class itself rather than to any particular object. Static members exist from the moment the class is loaded and persist until the program ends. You access them using the class name, not an object reference, though Java does allow access through an object reference (which is misleading and best avoided).
Static variables and methods
Static members solve the problem of shared state and utility functions that do not depend on any object.
- Static variable:One copy exists for the class as a whole, shared by every instance. A change made through one object is visible from all others. Typical uses: counters, configuration constants, shared caches.
- Static method:Can be called without creating an object. It has no implicit this reference and therefore cannot access instance variables or instance methods directly. Math.sqrt() and Arrays.sort() are static methods from the standard library.
- Accessing static members:Prefer ClassName.member over objectReference.member to make the static nature obvious to readers.
Static Variables and Methods
JavaUsing a static counter to track how many objects have been created, and a static utility method.
Static Blocks
A static block (also called a static initialiser) is a block of code prefixed with the static keyword and placed inside a class body. It runs exactly once: when the class is first loaded by the JVM. If a class has multiple static blocks, they execute in the order they appear in the source. Static blocks are useful when you need logic to initialise a static field that is too complex to fit in a single expression.
Static Blocks
JavaLoading configuration data into a static map once at class load time.
The final Keyword
The final keyword means different things depending on where you apply it: to a variable, a method, or a class. The common thread is the idea of immutability or preventing further modification or extension.
final applied in three places
Each use of final locks something down in a different way.
- final variable:The variable can be assigned exactly once. After that, reassigning it is a compile error. Use this for constants. By convention, constant names are ALL_CAPS_WITH_UNDERSCORES. Declaring a variable final does not make the object it points to immutable; it only prevents you from reassigning the variable itself.
- final method:The method cannot be overridden by any subclass. Use this when you want to guarantee that the implementation does not change in any subclass, which is important for security-sensitive or algorithm-critical code.
- final class:No class can extend (subclass) this class. The String class in the Java standard library is declared final, which is part of what makes it safe and immutable.
The final Keyword
JavaDemonstrating final variables, a final method, and a final class.
The Object Class: toString(), equals(), hashCode(), getClass()
Every class in Java implicitly extends java.lang.Object. This means every object you ever create already inherits a set of methods. Four of them come up constantly in real Java code and are almost always worth overriding in your own classes.
The four Object methods you should know
Each method has a specific purpose and a specific override contract.
- toString():Returns a String representation of the object. The default returns something like ClassName@hashCodeInHex, which is rarely meaningful. Override it in every class you write so that logging and debugging output is useful.
- equals(Object o):Returns true if this object is considered equal to o. The default checks reference equality (same as ==). Override it to define what "equal content" means for your class, for example: two Person objects are equal if they have the same ID.
- hashCode():Returns an integer hash code for the object. The contract with equals() is: if a.equals(b) is true then a.hashCode() must equal b.hashCode(). If you override equals(), you must override hashCode() too, otherwise your objects will misbehave in HashMaps and HashSets.
- getClass():Returns the runtime Class object representing the actual type of the object. Useful for reflection and for type-safe logging. It cannot be overridden.
Overriding toString(), equals(), and hashCode()
JavaA Product class with all three overridden correctly, including the equals/hashCode contract.
Object Cloning (Cloneable Interface)
Sometimes you need an independent copy of an object, not just a second reference to the same one. Java provides the clone() method inherited from Object for this purpose. To use it, your class must implement the Cloneable marker interface and override clone() to make it accessible. Without implementing Cloneable, calling clone() throws a CloneNotSupportedException.
Shallow copy vs. deep copy
The default Object.clone() performs a shallow copy, which is not always what you want.
- Shallow copy:All primitive fields are duplicated by value. Reference fields are copied by reference, meaning the clone and the original share the same nested objects. Mutating a nested object through one reference affects the other.
- Deep copy:All fields are duplicated recursively. If a field holds a reference to another object, that object is also cloned. You must implement this yourself inside your overridden clone() method.
- Modern alternatives:Copy constructors (a constructor that takes an instance of the same class and copies its fields) and factory methods are generally preferred over clone() in modern Java, because they are clearer, safer, and do not require the Cloneable ceremony.
Object Cloning
JavaShallow clone with Cloneable, then demonstrating the shared-reference problem and a copy constructor alternative.
Quiz - Test Your Knowledge
Ten questions covering OOP concepts, constructors, the this keyword, static members, the final keyword, Object class methods, and object cloning. Read each option carefully before selecting your answer.
Knowledge Check
1. What is the primary purpose of a constructor in Java?
2. Which statement about the default constructor is correct?
3. What does the this keyword refer to inside an instance method?
4. Which of the following correctly describes a static variable?
5. What is constructor chaining with this() used for?
6. What does the final keyword do when applied to a class?
7. What is the contract between equals() and hashCode() in Java?
8. When does a static block execute?
9. What must a class do to support cloning via the clone() method?
10. What is the default return value of toString() inherited from the Object class?