In Java, a class is the fundamental building block of object-oriented programming. A class defines a blueprint for objects—each object created from a class can have its own data (fields) and behavior (methods).
Here's a simple class definition:
public class Person {
// Fields (also called instance variables)
String name;
int age;
// Method
void greet() {
System.out.println("Hello, my name is " + name);
}
}
public
is an access modifier that makes the class accessible from other packages.
class
is the keyword to declare a class.
Person
is the class name, following Java naming conventions (capitalize each word—PascalCase).
Inside the class, we define:
name
and age
, which store data for each object.greet()
, which define behavior the object can perform.Person.java
.BankAccount
, StudentRecord
).accountBalance
, calculateInterest
).Only one public class is allowed per .java
file, and it must match the filename. However, you can define other non-public (package-private) classes in the same file.
class Helper {
void help() {
System.out.println("Helping...");
}
}
But in practice, each class is usually placed in its own file for clarity and maintainability.
A class serves as a template—you don't use the class itself directly but create objects (instances) based on it. Defining a class with clear structure, good naming, and organized methods makes your code reusable, readable, and scalable as your program grows. Understanding how to properly define a class is a vital first step in mastering Java's object-oriented features.
In Java, objects are instances of classes. Once you've defined a class, you can create objects using the new
keyword, which calls the class's constructor and allocates memory for the new instance.
Here's how to create an object from a class and access its members:
public class Person {
String name;
int age;
void greet() {
System.out.println("Hi, I'm " + name + " and I'm " + age + " years old.");
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person(); // Object creation using 'new'
p1.name = "Alice";
p1.age = 30;
p1.greet(); // Output: Hi, I'm Alice and I'm 30 years old.
}
}
new Person()
creates a new instance of the Person
class.p1
is a reference variable that points to the object.name
and age
are accessed using dot notation (p1.name
).p1.greet()
.Constructors are special methods invoked during object creation. If you don't define one, Java provides a default constructor.
You can also define custom constructors to initialize values:
Person(String name, int age) {
this.name = name;
this.age = age;
}
Then call it like:
Person p2 = new Person("Bob", 25);
new
If you forget to use new
, like:
Person p;
p.name = "Error!";
You'll get a NullPointerException because p
has not been initialized. Always instantiate with new
before using the object.
public class Person {
String name;
int age;
// Custom constructor
Person(String name, int age) {
this.name = name;
this.age = age;
}
// Default constructor (optional to declare, Java provides it if no other constructor exists)
Person() {
this.name = "Unknown";
this.age = 0;
}
void printInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
public static void main(String[] args) {
// Using custom constructor
Person p1 = new Person("Alice", 30);
p1.printInfo();
// Using default constructor
Person p2 = new Person();
p2.printInfo();
// Uncommenting the following will cause NullPointerException at runtime
/*
Person p3; // Declared but not initialized
p3.name = "Error!"; // NullPointerException
*/
}
}
Creating and using objects is the heart of Java programming. Constructors simplify initialization, and using object references promotes clean and reusable code. Mastering object creation helps you structure your programs around real-world entities.
In Java, fields are variables declared within a class. These can be either instance fields (each object has its own copy) or static fields (shared across all objects of the class). Understanding the difference is essential for writing efficient and clear code.
Instance fields are tied to individual objects. Each object created from a class gets its own unique copy of these fields.
public class Car {
String model; // instance field
int year;
}
public class Main {
public static void main(String[] args) {
Car car1 = new Car();
car1.model = "Toyota";
car1.year = 2020;
Car car2 = new Car();
car2.model = "Honda";
car2.year = 2022;
System.out.println(car1.model); // Toyota
System.out.println(car2.model); // Honda
}
}
Here, car1
and car2
have their own separate model
and year
fields.
Static fields belong to the class itself and are shared among all instances.
public class Car {
String model;
int year;
static int totalCars = 0; // static field
Car() {
totalCars++;
}
}
public class Main {
public static void main(String[] args) {
new Car();
new Car();
System.out.println(Car.totalCars); // 2
}
}
Notice that totalCars
is accessed via the class name Car.totalCars
, and not through an instance. Every time a new Car
is created, the static field is updated globally.
Use instance fields for data that varies between objects (like name
, age
, or model
). Use static fields for:
public static final double PI = 3.14159;
)totalCars
)Using static fields carefully helps conserve memory and avoids redundancy—but overusing them can reduce flexibility and break encapsulation. Always consider whether a field should represent object-specific state or shared class-level data.
In Java, methods are blocks of code that define behavior. Like fields, methods can be either instance (belonging to an object) or static (belonging to the class). Understanding how to use each appropriately is key to writing well-structured Java code.
An instance method is associated with an object. You must create an instance of the class to call the method.
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
int result = calc.add(5, 3); // instance method call
System.out.println(result); // 8
}
}
Here, add()
is called on the calc
object, and each object can have its own internal state that instance methods can work with.
A static method belongs to the class, not any instance. You can call it without creating an object.
public class MathUtils {
public static int square(int x) {
return x * x;
}
}
public class Main {
public static void main(String[] args) {
int result = MathUtils.square(4); // static method call
System.out.println(result); // 16
}
}
Static methods are often used for utility or helper functions.
public class Example {
static void staticMethod() {
// Cannot access instanceMethod() directly
}
void instanceMethod() {
staticMethod(); // Valid
}
}
Choosing the right method type improves encapsulation, performance, and clarity.
public
, private
, protected
In Java, access modifiers control the visibility of classes, methods, and variables. They define how accessible a member is from other classes and packages. Java provides four main levels of access control:
public
A public
member is accessible from anywhere in the program.
public class Example {
public int number = 42;
}
If another class imports or refers to Example
, it can access number
directly.
private
A private
member is only accessible within the same class.
public class Example {
private int secret = 123;
public int getSecret() {
return secret;
}
}
Other classes cannot directly access secret
. Instead, controlled access is typically provided via public methods like getSecret()
. This is a core part of encapsulation.
protected
A protected
member is accessible:
package animals;
public class Animal {
protected void speak() {
System.out.println("Animal speaks");
}
}
package zoo;
import animals.Animal;
public class Dog extends Animal {
public void bark() {
speak(); // Legal: subclass can access protected method
}
}
If no modifier is used, the member is package-private, meaning it is accessible only within the same package.
class Example {
int data = 100; // default/package-private
}
Modifier | Same Class | Same Package | Subclass (Other Package) | Other Packages |
---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
protected |
✅ | ✅ | ✅ | ❌ |
(default) | ✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
Choosing the correct access modifier is critical for encapsulation—a principle where internal details are hidden to protect and simplify object usage. It's best to start with private
and only increase visibility as needed, following the principle of least privilege. This keeps your classes well-encapsulated, secure, and easier to maintain.
In Java, you can perform complex setup tasks using initializer blocks, which are special code blocks that run automatically when a class or object is loaded or created. These blocks come in two types:
A static block runs once, when the class is first loaded into memory. It's typically used to initialize static variables.
Syntax:
public class Example {
static int staticValue;
static {
staticValue = 100;
System.out.println("Static block executed");
}
}
Execution Order:
An instance block runs every time an object is created, before the constructor.
Syntax:
public class Example {
int instanceValue;
{
instanceValue = 42;
System.out.println("Instance initializer executed");
}
public Example() {
System.out.println("Constructor executed");
}
}
Output:
Instance initializer executed
Constructor executed
Execution Order:
When an object is created:
public class Example {
static int staticValue;
// Static block runs once when the class is loaded
static {
staticValue = 100;
System.out.println("Static block executed");
}
int instanceValue;
// Instance initializer block runs every time an object is created, before constructor
{
instanceValue = 42;
System.out.println("Instance initializer executed");
}
public Example() {
System.out.println("Constructor executed");
}
public static void main(String[] args) {
System.out.println("Main started");
// Creating first object
Example ex1 = new Example();
// Creating second object
Example ex2 = new Example();
}
}
Use initializer blocks for logic that is shared across constructors or for complex static initialization that cannot be expressed with a simple assignment. However, use them sparingly—for most cases, static variables should be initialized where declared, and constructor logic should remain in constructors for clarity.
Initializer blocks can help reduce code duplication and organize setup logic, but overusing them can make your class harder to read. When in doubt, prefer clear constructors and static factory methods.
this
KeywordIn Java, the this
keyword is a special reference to the current object—the instance whose method or constructor is being executed. It helps differentiate between instance variables and parameters or local variables, and supports constructor chaining and passing object references.
this
When a constructor or method parameter shares a name with an instance variable, this
is used to clarify intent:
public class Book {
private String title;
public Book(String title) {
this.title = title; // Assigns parameter to instance variable
}
}
Without this
, title = title;
would refer to the parameter on both sides, leaving the field unchanged.
this()
Java allows one constructor to call another in the same class using this()
. This avoids repeating initialization logic:
public class Rectangle {
int width, height;
public Rectangle() {
this(10, 20); // Calls the parameterized constructor
}
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
Note: this()
must be the first statement in a constructor.
You can pass this
as an argument to methods that expect an instance of the current class:
public class Printer {
void printInfo(Book b) {
System.out.println("Book title: " + b.title);
}
void start() {
printInfo(this); // Passes current object
}
}
public class Main {
public static void main(String[] args) {
// Testing Book with this
Book myBook = new Book("Effective Java");
System.out.println("Book title: " + myBook.getTitle());
// Testing Rectangle constructor chaining
Rectangle rect1 = new Rectangle();
System.out.println("Rectangle default: " + rect1.width + "x" + rect1.height);
Rectangle rect2 = new Rectangle(5, 8);
System.out.println("Rectangle custom: " + rect2.width + "x" + rect2.height);
// Testing passing this
Printer printer = new Printer(myBook);
printer.start();
}
}
class Book {
private String title;
public Book(String title) {
this.title = title; // disambiguate instance variable and parameter
}
public String getTitle() {
return title;
}
}
class Rectangle {
int width, height;
public Rectangle() {
this(10, 20); // constructor chaining: calls the parameterized constructor
}
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
class Printer {
private Book book;
public Printer(Book book) {
this.book = book;
}
void printInfo(Book b) {
System.out.println("Printing book info: " + b.getTitle());
}
void start() {
printInfo(this.book); // pass current object state (the book)
}
}
Using this
enhances clarity and reduces ambiguity, especially when naming conventions overlap. It's essential in constructor chaining and useful when an object needs to refer to itself. However, overusing it can clutter code unnecessarily. Use it where it improves readability or is required by syntax, and let naming clarity handle the rest.