Chapter ONE
Utilizing Java Object-Oriented Approach - Part 1


Exam Objectives

Declare and instantiate Java objects including nested class objects, and explain the object life-cycle including creation, reassigning references, and garbage collection.
Create classes and records, and define and use instance and static fields and methods, constructors, and instance and static initializers.
Implement overloading, including var-arg methods.

Chapter Content


Introduction to Object-Oriented Programming

As the name implies, object-oriented programming (OOP) is a programming paradigm centered around the concept of objects. Rather than structure programs around procedures and functions (like procedural programming), OOP organizes code into objects, which represent real-world entities containing data (attributes) and behaviors (methods). This approach offers several advantages:

Java is an OOP language, so its basic building blocks are objects and classes.

Objects and Classes

Objects are distinct instances in code that contain data and behaviors. Classes, on the other hand, are blueprints or templates that define the data and behaviors common to all objects of that class.

To better understand these concepts, think of cookies made from a cookie cutter. The cookie cutter defines the shape and size of the cookies, just as classes define what attributes and methods the object instances will have. Each cookie can be unique, with different chocolate chip placements, just as objects contain distinct data values.

For example, we can define a Cookie class that specifies the attributes of cookies, such as flavor, shape, topping, etc. You can also define methods, which are functions that operate on the data. Methods allow objects to perform actions. Our Cookie objects could have an eat() method:

public class Cookie {
    // Attributes
    String flavor; 
    int size;
                     
    // Behavior (Method)
    public void eat() {
        System.out.println("That was yummy!");
    }
}

And we can instantiate cookie objects from the Cookie class:

Cookie chocoChip = new Cookie();
chocoChip.flavor = "Chocolate Chip";
chocoChip.size = 2;

Cookie oatmealRaisin = new Cookie(); 
oatmealRaisin.flavor = "Oatmeal Raisin";
oatmealRaisin.size = 1;

The objects chocoChip and oatmealRaisin are both cookies with the same methods defined by the Cookie class. However, they contain different data values for attributes like flavor and size.

A common misconception is that objects and classes are the same. However, while objects and classes are related, they serve distinct purposes:

The class acts as the mold, while objects are the cookies produced.

Higher-Level OOP Principles

Once you understand objects and classes, grasping the higher-level principles of OOP, like inheritance, encapsulation, and polymorphism, becomes easier:

Bringing this full circle, we can model real-world cookie hierarchies through:

Together, these core OOP concepts enable flexible, modular cookie class design. We’ll review these concepts in more detail in the next chapter. First, let’s talk about the life-cycle of an object.

Object Life-Cycle in Java

Understanding the different stages of an object’s life-cycle is essential in Java’s object-oriented programming. This includes the creation of objects, how reference variables access them, and how unused objects are managed by Java’s garbage collector.

Here’s a diagram that illustrates the typical life-cycle of a Java object, from creation to garbage collection:

┌────────────────────┐
│   Object Creation  │
│    (new keyword)   │
└────────┬───────────┘
         │
         ▼
┌────────────────────┐
│   Initialization   │
│   (Constructor)    │
└────────┬───────────┘
         │
         ▼
┌───────────────────┐
│     Object Use    │
│ (Active Lifetime) │
└────────┬──────────┘
         │
         ▼
┌────────────────────┐
│     Unreachable    │
│(No more references)│
└────────┬───────────┘
         │
         ▼
┌────────────────────┐
│   Garbage Collect  │
│     (finalize)     │
└────────────────────┘

But to illustrate the life stages of a Java object, let’s use the analogy of a library book. When a new book arrives at the library, it is similar to constructing a new object using the new keyword. For example:

Book javaBook = new Book("The Java Book");

Let’s break down what happens in that single line step-by-step:

  1. Declaring the Reference Variable:
     Book javaBook;
    

    This declares a variable called javaBook of type Book. At this point, no Book object exists yet; we have just created a reference variable that can point to a Book object.

  2. Instantiating the Object:
     = new Book("The Java Book");
    

    The new keyword instantiates or constructs a new Book object. This allocates memory on the heap for the object, passes the string argument to the Book constructor to initialize its state, and returns a reference to the newly created object.

  3. Assigning the Reference: The = operator assigns the reference of the new Book object to the javaBook variable.

So, javaBook now contains a reference pointing to the new Book instance in memory:

javaBook --> [New Book object]

Here, javaBook is the reference variable pointing to the newly created Book instance on the Java heap.

Reference Reassignment

Like library books being checked out by different people, object references in Java can be reassigned. For example:

Book refBook = javaBook; // Assign second reference
javaBook = null; // Remove original reference

Let’s review this step by step:

  1. Creating a Second Reference:
     Book refBook = javaBook;
    

    This creates a new reference variable refBook and assigns it the value of javaBook. Both javaBook and refBook now point to the same Book object.

     javaBook --> [Book object]
     refBook --> [Book object]
    
  2. Nullifying the Original Reference:
     javaBook = null;
    

    This sets javaBook to null, meaning it no longer refers to any object.

     javaBook --> null
     refBook --> [Book object]
    

Only refBook now points to the Book object. The object does not qualify for garbage collection because refBook still references it.

Garbage Collection

Books no longer borrowed are eventually removed from a library’s catalog. Similarly, in Java, objects with no references are cleaned up by the garbage collector:

refBook = null; // Unreferenced object eligible for garbage collection

When all references to an object are gone, it becomes eligible for garbage collection.

The garbage collection process can be summarized as follows:

  1. Identifying Unused Objects: The garbage collector (GC) periodically scans the heap to find objects no longer referenced by any part of the application.

  2. Reclaiming Memory: Unreferenced objects, which cannot be accessed anymore, are considered garbage. The GC frees the memory occupied by these objects, returning it to the pool of available memory on the heap.

  3. Automatic Management: Garbage collection happens automatically in the background, without explicit program triggering, ensuring that memory management is handled efficiently.

In languages like C, memory must be managed manually by allocating and freeing memory. Java automates this process with garbage collection, increasing programmer productivity and reducing the risk of memory leaks and other related issues.

Now, let’s discuss some concepts we’ll use to declare a class and other elements.

Keywords

In Java, a keyword is a reserved word that has a predefined meaning in the language. Keywords define the structure and syntax of Java programs. They cannot be used as identifiers (names for variables, methods, classes, etc.) because they are reserved for specific purposes.

Java includes a set of keywords fundamental to the language. Some commonly used keywords include:

Always keep in mind that each keyword has a specific purpose and is used to define the structure and behavior of Java programs.

Also, it’s important to note that keywords are case-sensitive in Java. For example, class is a keyword, but Class is not. Additionally, you cannot use keywords as identifiers, such as variable or method names, because they are reserved by the language.

Here’s an example demonstrating the usage of some keywords:

public class MyClass {
    private static int myVariable;
    
    public static void myMethod() {
        if (myVariable > 0) {
            System.out.println("Positive");
        } else {
            System.out.println("Negative");
        }
    }
}

In this example, public, class, private, static, int, void, if, and else are all keywords used to define the structure and behavior of the MyClass class.

We’ll review these and other keywords in the upcoming sections and chapters.

Comments

Comments are annotations in the code that are ignored by the compiler. They can be used to:

Java supports three types of comments:

  1. Single-line comments
  2. Multi-line comments
  3. Documentation (javadoc) comments

Single-line comments start with two forward slashes (//). Anything following // on the same line is ignored by the Java compiler:

// This is a single-line comment
int variable = 1; // This is another single-line comment

Multi-line comments, also known as block comments, start with /* and end with */. Everything between /* and */ is considered a comment, regardless of how many lines it spans:

/* This is a multi-line comment
   and it can span multiple lines. */
int variable = 1;

Documentation comments, or javadoc comments, are designed to document the Java code. They start with /** and end with */. These comments can be extracted to a HTML document using the Javadoc tool. Documentation comments are mostly used before definitions of classes, interfaces, methods, and fields:

/**
 * This is a documentation comment.
 * It can be used to describe classes, interfaces, methods, and fields.
 */
public class MyClass {
    /**
     * This method adds up two int values.
     *
     * @param a First value
     * @param b Second value
     * @return The sum of a and b
     */
    public int add(int a, int b) {
        return a + b;
    }
}

Organizing Classes into Packages

A package organizes related classes, interfaces, and sub-packages into a single unit.

For example, imagine you own a grocery store that sells many types of products. To keep things organized and easy to find, you decide to group similar products together in different sections or aisles of the store.

In this analogy:

Just like how you group related products together in the same section of the store, you group related classes and interfaces together in the same package in Java.

For example, in your grocery store, you might have:

Similarly, in your Java project, you can have:

Here’s a visual representation of these packages and classes:

┌─────────────────────────────────────────────────────────────┐
│                      com.example                            │
│  ┌─────────────────────────┐  ┌─────────────────────────┐   │
│  │       products          │  │         orders          │   │
│  │  ┌─────────────────┐    │  │  ┌─────────────────┐    │   │
│  │  │  Product.java   │    │  │  │  Order.java     │    │   │
│  │  └─────────────────┘    │  │  └─────────────────┘    │   │
│  │  ┌─────────────────┐    │  │  ┌─────────────────┐    │   │
│  │  │ Inventory.java  │    │  │  │ShoppingCart.java│    │   │
│  │  └─────────────────┘    │  │  └─────────────────┘    │   │
│  │  ┌─────────────────┐    │  │  ┌─────────────────┐    │   │
│  │  │ Category.java   │    │  │  │  Payment.java   │    │   │
│  │  └─────────────────┘    │  │  └─────────────────┘    │   │
│  └─────────────────────────┘  └─────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────┐                                │
│  │         auth            │                                │
│  │  ┌─────────────────┐    │                                │
│  │  │   User.java     │    │                                │
│  │  └─────────────────┘    │                                │
│  │  ┌─────────────────┐    │                                │
│  │  │   Login.java    │    │                                │
│  │  └─────────────────┘    │                                │
│  │  ┌─────────────────┐    │                                │
│  │  │ Permission.java │    │                                │
│  │  └─────────────────┘    │                                │
│  └─────────────────────────┘                                │
└─────────────────────────────────────────────────────────────┘

By organizing your classes into packages, you create a logical structure that makes it easier to locate and manage related code elements, just like how organizing products into sections makes it easier for customers to find what they need in the grocery store.

Creating a Package

To create a package, use the package keyword followed by the package name at the top of your Java source file. For example:

package com.example.mypackage;

The package name should be in lowercase and follow the reverse domain name convention to ensure uniqueness.

The package declaration must be the first statement in the source file, before any import statements or class declarations. The following will not compile:

import java.util.ArrayList; // Import statement before the package declaration

package mypackage; // Package declaration not at the beginning

public class MyClass {
    public static void main(String[] args) {
        System.out.println("This will not compile.");
    }
}

Using Import Statements

import statements are used to bring classes or interfaces from other packages into the current namespace. Instead of using the fully qualified name each time you refer to a class from another package, you can use an import statement to refer to the class by its name. For example:

import java.util.ArrayList;
// ...
ArrayList list = new ArrayList();

If you choose not to use an import statement for a class from another package, you would have to use the class’s fully qualified name every time you reference it in your code. Remember, the fully qualified name includes both the package name and the class name.

For example, if you do not import the ArrayList class from the java.util package, you would have to use java.util.ArrayList every time you want to create or use an ArrayList object in your code:

// No import statement for java.util.ArrayList
// ...
java.util.ArrayList list = new java.util.ArrayList();

Special Cases and Best Practices

There are a couple of exceptions or special cases to the rule regarding the use of fully qualified names and import statements:

  1. Classes in the java.lang Package: Classes and interfaces in the java.lang package do not need to be imported explicitly, as they are automatically available. For example, you don’t need to import classes like String, Math, System, or wrapper classes like Integer, Double, etc.

  2. Same Package: Classes and interfaces that are in the same package as the class you’re writing do not require an import statement. Java automatically looks in the current package for other classes and interfaces if it doesn’t find the referenced class or interface in the imported packages.

  3. Fully Qualified Name Collision: When two classes have the same name but are in different packages, and you need to use both in the same file, you cannot import both directly because of the name collision. In such cases, at least one (and possibly both) must be referred to by their fully qualified names to avoid ambiguity.

Here’s an example to illustrate this last point:

import java.sql.Date;

public class Example {
    public static void main(String[] args) {
        Date sqlDate = new Date(System.currentTimeMillis());
        java.util.Date utilDate = new java.util.Date();
    }
}

In this example, Date from java.sql is imported, so it can be referred to by its simple name. However, since we also want to use Date from java.util, we must refer to it by its fully qualified name to distinguish it from java.sql.Date.

You can also use a wildcard (*`) to import all the classes from a package. For example:

import java.util.*;

However, it’s generally recommended to import specific classes rather than using wildcards because they can make the code less readable, lead to naming conflicts if multiple packages have classes with the same name, and add redundancy, such as including a class twice.

Redundant Imports

Although the compiler allows redundant imports, they can clutter your code and reduce readability.

For example, assuming we have two classes, MyClass and HelperClass, in the same package, mypackage:

// File: HelperClass.java
package mypackage;

public class HelperClass {
    public static void doSomething() {
        System.out.println("Doing something...");
    }
}

The following class illustrates redundant imports:

package mypackage;

import mypackage.HelperClass; // Redundant import because HelperClass is in the same package
import java.util.List; // Redundant import because it's not used in the class

public class MyClass {
    public static void main(String[] args) {
        HelperClass.doSomething();
    }
}

In this example:

Removing these redundant imports would make the code cleaner without affecting its functionality.

Access Control

Packages provide a level of access control, similar to how certain sections of the store might be restricted to authorized personnel only. You can use access modifiers (public, protected, default, private) to control the visibility and accessibility of classes and members within and across packages.

For example, let’s say you have a package named com.example.internals that contains classes and methods intended for internal use only within that package:

package com.example.internals;
class InternalClass {
    void internalMethod() {
        // Internal implementation
    }
}

Now, consider another package com.example.api:

package com.example.api;
import com.example.internals.InternalClass;
public class APIClass {
    public void someMethod() {
        InternalClass obj = new InternalClass(); // Not accessible
        obj.internalMethod(); // Not accessible
    }
}

In this example, the InternalClass and its methods have default (package-private) access. They are accessible within the com.example.internals package but not from other packages. The APIClass in the com.example.api package cannot access the InternalClass or its methods directly.

Let’s review in more detail the available access modifiers.

Access Modifiers

Access modifiers are keywords used in classes, methods, or variable declarations to control the visibility of that member from other parts of the program. There are four main types of access modifiers in Java:

  1. public: The public access modifier specifies that the member is accessible from any other class in the Java application, regardless of the package it belongs to. Using the public modifier means there are no restrictions on accessing the member.

  2. protected: The protected access modifier allows the member to be accessed within its own package and also by subclasses of its class in other packages. This is less restrictive than package-private but more restrictive than public.

  3. default (also known as package-private): If no access modifier is specified, the member has package-private access by default. This means the member is accessible only within its own package and is not visible to classes outside the package. It’s important to note that there is no explicit default keyword in Java; you simply omit the access modifier.

  4. private: The private access modifier specifies that the member is accessible only within the class it is declared in. It is the most restrictive access level and is used to ensure that the member cannot be accessed from outside its own class, not even by subclasses.

Each of these access modifiers serves a specific purpose in the context of object-oriented design and encapsulation. They allow you to structure your code in a way that protects sensitive data and implementation details while exposing necessary functionality to other parts of your application.

Here’s a diagram to understand the scope of each access modifier more easily:

┌─────────────────────────────────────────────────────────────┐
│                         public                              │
│  ┌─────────────────────────────────────────────────┐        │
│  │               protected                         │        │
│  │  ┌─────────────────────────────────────┐        │        │
│  │  │    default (package-private)        │        │        │
│  │  │  ┌─────────────────────────┐        │        │        │
│  │  │  │      private            │        │        │        │
│  │  │  └─────────────────────────┘        │        │        │
│  │  └─────────────────────────────────────┘        │        │
│  └─────────────────────────────────────────────────┘        │
└─────────────────────────────────────────────────────────────┘

Access Levels (from most restrictive to least restrictive):
private   : Same class only
default   : Same package
protected : Same package + subclasses in other packages
public    : Accessible from anywhere

In the next sections, we’ll explain access modifiers in the context of classes, fields, and methods. But first, let’s review how to properly declare a class.

Declaring Classes

A class in Java acts as a blueprint for objects, encapsulating both data and behavior.

The syntax to declare a class follows this format:

[accessModifier] class ClassName [extends Superclass] [implements Interface1, Interface2, ...] {
    // class body
}

For example, a class declaration might look like this:

public class MyClass extends MySuperClass implements MyInterface {
    private int myField;

    public MyClass() {
        // Constructor body
    }

    public void myMethod() {
        // Method body
    }
}

First, you can optionally specify an access modifier to determine the visibility and accessibility of the class to other parts of a Java application:

Following the optional access modifier, you have to use the class keyword, followed by the name of the class.

A class name or class identifier should follow the following rules:

  1. Unicode characters: Java allows the use of Unicode characters in identifiers, which means you can use letters from non-Latin alphabets as well. However, this is not commonly used and can lead to code that is difficult to read and maintain.

  2. Alphabetic characters, digits, underscores (_), and dollar signs ($): These are the most common characters used in identifiers. Any combination of these characters is allowed, but class names must not begin with a digit.

  3. No special characters: Other than underscores and dollar signs, special characters such as @, %, !, ?, #, &, *, ^, ~, _, -, +, =, {, }, [, ], |, ,, ;, <, >, /, \, or ', are not allowed in class identifiers.

  4. Class names should not contain spaces. This would make the code invalid and lead to compilation errors.

  5. Cannot be a Java reserved word: Identifiers cannot use any of Java’s reserved words (like int, if, for, etc.). Reserved words have specific meanings in Java and cannot be used for class names, variable names, or any other identifiers.

  6. Case Sensitivity: Java is case-sensitive, meaning identifiers like MyClass, myclass, and MYCLASS will be considered different.

  7. Length: There is no length limit for class names in Java.

These rules ensure that class name are syntactically correct and avoid conflicts with Java’s built-in language features. It’s also good practice to follow Java naming conventions on top of these rules, like starting class names with a capital letter and using camel case for multi-word names (like using MyClass instead of myclass or MY_CLASS). But again, this is just a convention, not a rule.

After the class name, you can optionally extend a superclass using the extends keyword, followed by the name of the superclass. Java supports single inheritance, meaning a class can only extend one superclass.

However, you can implement one or more interfaces using the implements keyword, followed by a comma-separated list of interface names:

public class MyClass implements MyInterface1, MyInterface2, MyInterface3 {
    // ...
}

Finally, you define the class body within a pair of curly braces {}. The class body contains the members of the class, including fields, methods, constructors, and nested classes.

This way, in the next example:

public class MyClass extends MySuperClass implements MyInterface {
    /* Class body begins */
    // Fields
    private int myField;

    // Constructor
    public MyClass() {
        // Constructor body
    }

    // Methods
    public void myMethod() {
        // Method body
    }
    /* Class body ends */
}

Now, before reviewing how to declare fields and methods in more detail, let’s talk about static and instance members.

Static and Instance Members

Classes can have two types of members: static members and instance members. Let’s use the analogy of a TV model to better understand these types of members.

Imagine different TV sets of the same model in different homes. Each TV set represents an instance (object) of the Television class. The TV model itself represents the class.

Instance members, such as instance variables and instance methods, belong to each individual TV set (object):

Static members, such as static variables and static methods, belong to the TV model (class) itself:

Here’s the Television class:

public class Television {
    // Instance fields
    private int currentChannel;
    private int volume;
    private boolean isOn;
    
    // Static field
    private static String manufacturerLogo = "MyBrand";
    
    // Instance method
    public void changeChannel(int channel) {
        this.currentChannel = channel;
        System.out.println("Channel changed to: " + channel);
    }
    
    // Static method
    public static void getManufacturerInfo() {
        System.out.println("All TVs by: " + manufacturerLogo);
    }
}

In this example:

To access instance members, you need to create an instance of the class:

Television tv1 = new Television();
tv1.changeChannel(5); // Changes channel of tv1

But to access static members, you can use the class name directly:

Television.getManufacturerInfo();

Static members are useful for representing class-level data and behavior that is shared among all instances of the class. They can be accessed without creating an instance of the class, making them memory-efficient. However, static members cannot access instance members directly, as they are not associated with any specific instance.

It is important to note that Java allows static members (fields and methods) to be accessed through instances of a class. For example, the static method getManufacturerInfo() can be used this way too:

tv1.getManufacturerInfo();

However, this is not recommended practice, as it does not clearly convey that the member is static and belongs to the class rather than the instance.

Instance members, on the other hand, are associated with each individual instance of the class. They hold data specific to each object and can access both static and instance members.

Now, you might be thinking: Why can static members be accessed without creating an instance of the class? Does this not go against the idea of object-oriented programming??

Well, this doesn’t necessarily go against the principles of object-oriented programming (OOP), but rather complements them by providing a mechanism for defining class-level behavior and state.

Static methods can be used to implement utility or helper functions that do not depend on the state of an object instance. This is common in utility classes, such as the Math class, where all methods are static because they do not require access to instance-level data.

Also, static members allow for global access. Granted, there’s some controversy about this due to the potential for increased coupling and harder-to-test code, however, it can be appropriate for global constants that need to be accessed from various points in an application.

This diagram illustrates several key points about static and instance members in Java:

┌─────────────────────────────────────────────────────────────┐
│                         Class                               │
│  ┌─────────────────────────┐ ┌─────────────────────────┐    │
│  │    Static Members       │ │    Instance Members     │    │
│  │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │    │
│  │ │   Static Fields     │ │ │ │  Instance Fields    │ │    │
│  │ └─────────────────────┘ │ │ └─────────────────────┘ │    │
│  │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │    │
│  │ │   Static Methods    │ │ │ │  Instance Methods   │ │    │
│  │ └─────────────────────┘ │ │ └─────────────────────┘ │    │
│  └─────────────────────────┘ └─────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────┐ ┌─────────────────────────┐    │
│  │      Object 1           │ │      Object 2           │    │
│  │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │    │
│  │ │  Instance Fields    │ │ │ │  Instance Fields    │ │    │
│  │ └─────────────────────┘ │ │ └─────────────────────┘ │    │
│  └─────────────────────────┘ └─────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

- The class contains both static and instance members.
- Static members (fields and methods) are associated with the class itself.
- Instance members (fields and methods) are associated with objects of the class.
- Multiple objects of the class each have their own instance members.
- All objects share the same static members.

Now, let’s review in more detail how to declare fields.

Declaring Fields

A field is a variable that is declared at the class level. Fields, which are also referred to as attributes or instance variables, are used to hold the state of an object.

To declare a field, you use the following syntax:

[accessModifier] [specifiers] type fieldName [= initialValue];

Here are some examples:

public class MyClass {
    public static final int MAX_VALUE = 100;
    private String name;
    protected double salary;
    boolean active = true;
    
    // ...
}

The access modifier is optional and can be public, private, protected or default (package-private) access if none is specified. Notice that unlike classes, fields can use all four types of access modifiers. Depending on the access modifiers used, fields can be accessed from inside the class, subclasses, classes in the same package or any other class. More on this later.

The specifiers part is also optional and can include keywords like static, final, transient, and volatile. You can specify zero or more specifiers (like in the first field declaration), but the final keyword can only be applied once:

The type of the field follows the specifiers. It can be a primitive type like int, boolean, etc. or a reference type like String, LocalDate, ArrayList, etc.

The field name follows standard Java identifier naming rules. Here are the main rules you need to remember for field identifiers:

  1. Unicode Characters: Java allows Unicode characters in identifiers, which means you can use characters from non-Latin character sets. However, this is not commonly used for field names, as it can make the code harder to read and maintain.

  2. Characters Allowed: Field identifiers can only include alphanumeric characters (A-Z, a-z, 0-9), underscore (_), and dollar sign ($). The identifier must begin with a letter (A-Z or a-z), underscore (_), or dollar sign ($). It cannot start with a digit.

  3. No Reserved Words: Identifiers cannot be Java reserved words. Reserved words include keywords like int, if, class, and so on. These are part of the Java language syntax and have specific meanings to the compiler.

  4. Case Sensitivity: Java is case-sensitive, meaning identifiers like myField, MyField, and MYFIELD would be considered distinct.

  5. Unlimited Length: Technically, there is no limit to the length of an identifier, but it’s essential to keep it reasonable for readability and maintainability.

It’s important to differentiate between rules and conventions. Rules must be followed for the Java code to compile, while conventions, such as starting field names with a lowercase letter or using camelCase for multiple words, are best practices designed to make the code more readable and maintainable but are not enforced by the compiler.

Finally, providing an initial value is optional. If none is provided, fields will be initialized with their default values (0, false or null depending on the type). However, the initial value must be a compile-time constant for static final fields.

Once a field is declared, you can access it to read its value or modify it by assigning a new value. The way you access a field depends on whether it’s an instance field or a static field and what access modifier it uses.

Accessing and Modifying Fields

To access an instance field, you first need an instance of the class. Then you can read the field’s value using the dot (.) operator like this:

instanceVariable.fieldName

For example:

String name = person.firstName;
int age = employee.age;  

To modify an instance field, you use the assignment operator (=) like this:

person.firstName = "John";
employee.age = 45;

Accessing static fields is a bit different. Since they belong to the class itself, you don’t need an instance. You can access a static field using the class name and the dot operator:

ClassName.fieldName

For example:

double pi = Math.PI;
int max = Integer.MAX_VALUE;

Inside the same class that declares a field, you can access it directly by its name, without any prefix, regardless of the access modifier used. The only exception is accessing a static field, it’s recommended to use the class name even within the same class for readability.

The access modifiers public, private, protected and default(package) control the visibility of a field and determine whether it can be accessed directly from outside the class.

Let’s look at some examples to illustrate the different access levels.

public class Person {
    public String name;
    private int age;
    protected String email;
    double height;
}

The name field is public, so it can be accessed from any other class:

Person p = new Person();
p.name = "Alice";

The age field is private. It can only be accessed within the Person class. Trying to access it directly from outside the class will result in a compile error:

// This will not compile
p.age = 30; 

The email field is protected. It can be accessed within the same class, any subclass, and other classes in the same package:

// This is okay
String email = p.email;

// This is also valid in a subclass, even in a different package
class Employee extends Person {
    public void setEmail(String e) {
         email = e;
    }
}

The height field has default (package) access since no modifier is specified. It can be accessed by other classes within the same package:

// This is okay if Person and Student are in same package 
class Student {
    public void printHeight(Person p) {
        System.out.println(p.height);
    }
}

It’s common to declare fields as private and access them through getter and setter methods. public and protected fields are used less frequently. Default (package-private) access is useful for related classes within the same package.

Declaring Methods

A method is a block of code that performs a specific task and optionally returns a value. Methods are used to define the behavior of an object. They provide a way to encapsulate complex logic, break down a program into manageable parts, and enable code reuse.

To declare a method, use the following syntax:

[accessModifier] [specifiers] returnType methodName([parameters]) [throws ExceptionType1, ExceptionType2, ...] {
    // method body
}

For example:

public static String addParenthesis(String s) {
    return "(" + s + ")";
}

private int sum(int a, int b) {
    return a + b;
}

protected void setName(String name) throws IllegalArgumentException {
    if (name == null || name.isEmpty()) {
        throw new IllegalArgumentException("Name cannot be null or empty");
    }
    this.name = name;
}

The access modifier is optional and controls the visibility of the method. It can be public, private, protected, or default (package) access if none is specified. The same rules apply as for fields, which we discussed earlier.

The specifiers are also optional and can include keywords like static, final, abstract, and synchronized. These keywords modify the behavior of the method:

The return type specifies the type of value the method returns. It can be a primitive type, a reference type, or void if the method doesn’t return anything. Every method declaration must have a return type.

The method name follows the same naming conventions as classes and fields, typically using camelCase. Choose meaningful names that describe the purpose of the method.

The parameters are specified within parentheses after the method name. There can be zero or more parameters. Multiple parameters are separated by commas. Parameters are variables that receive the values passed to the method when it is called. Each parameter consists of two, optionally three, parts:

[parameterModifier] parameterType parameterName

The parameter modifier is optional and can be final. If a parameter is declared as final, it means that the value of the parameter cannot be changed inside the method body. Here’s an example:

public void printMessage(final String message) {
    // message = "Hello"; // This would cause a compile error
    System.out.println(message);
}

The parameter type is required and specifies the data type of the parameter. It can be a primitive type (like int, double, boolean) or a reference type (like String, ArrayList, or custom classes).

The parameter name is also mandatory and follows the same naming conventions as class, fields and method identifiers, typically using camelCase. The parameter name is used to refer to the passed value within the method body.

Here are a few examples of parameter definitions:

// A single parameter of type int
public void printNumber(int number) {
    System.out.println("The number is: " + number);
}

// Multiple parameters of different types
public void printPersonDetails(String name, int age, boolean isStudent) {
    System.out.println("Name: " + name);
    System.out.println("Age: " + age);
    System.out.println("Is a student? " + isStudent);
}

// A parameter with a modifier
public void calculateDiscount(final double price, double discountPercentage) {
    double discountAmount = price * (discountPercentage / 100);
    double finalPrice = price - discountAmount;
    System.out.println("Discounted price: " + finalPrice);
}

Back to the parts of a method declaration, the throws clause is optional and specifies any checked exceptions that the method might throw. Multiple exceptions are separated by commas.

The method body is enclosed in curly braces {} and contains the code that implements the method’s functionality. It can include variable declarations, loops, conditionals, method calls, and other statements.

If the method has a return type other than void, it must include a return statement that specifies the value to be returned. The return value must be compatible with the declared return type:

// A simple method that returns a string
public String getName() {
    return "Mark";
}

Method Signatures

A method signature uniquely identifies a method within a class. It consists of the method’s name and the ordered list of parameter types. The access modifiers (sucha as public or private), return types (such as void or int), and parameter names are not part of the method signature:

methodName(parameterType1, parameterType2, ...)

For example, consider the following method declarations:

public void printMessage(String message) {
    System.out.println(message);
}

public int calculateSum(int a, int b) {
    return a + b;
}

private void updateUser(String username, int age, boolean isActive) {
    // method body
}

The method signatures for these methods are:

Calling a Method

When calling a method, you pass arguments that match the types and order of the parameters declared in the method signature. The arguments are the actual values that are passed to the method.

This way, to call a method, you need to use the method name followed by parentheses and provide any required arguments. The syntax is:

[ObjectReference.]methodName([arguments]);

If the method is an instance method (non-static), you need to have an object of the class that contains the method. You can then call the method using the object reference followed by the dot operator and the method name.

If the method is a static method, you can call it directly using the class name followed by the dot operator and the method name. You don’t need an object instance to call a static method.

Here are a few examples of calling methods:

// Calling an instance method
Person person = new Person();
person.setName("John");
String name = person.getName();

// Calling a static method
int max = Math.max(10, 20);
double random = Math.random();

// Calling a method with arguments
Calculator calculator = new Calculator();
int sum = calculator.add(5, 3);
double result = calculator.multiply(2.5, 4.0);

Make sure to provide the correct number and type of arguments as defined in the method signature. If there is a mismatch, the compiler will throw an error.

Using Access Modifiers with Methods

Just like with fields, access modifiers control the visibility and accessibility of methods. The same four access modifiers can be used: public, private, protected, and default (package-private).

Consider this class:

package com.my.package;

public class MathUtils {
    public static int add(int a, int b) {
        return a + b;
    }

    private static int subtract(int a, int b) {
        return a - b;
    }

    protected static int multiply(int a, int b) {
        return a * b;
    }

    static int divide(int a, int b) {
        return a / b;
    }
}

The add method is declared as public, so it can be called from any other class:

int sum = MathUtils.add(1, 2);

The subtract method is declared as private. It can only be called from within the MathUtils class itself. Trying to call it from another class will result in a compile error:

// This will not compile
int difference = MathUtils.subtract(10, 7);

The multiply method is declared as protected. It can be called from within the same class, any subclass (even in a different package), and other classes in the same package:

package com.my.other.package;

// Calling from a subclass in a different package
public class AdvancedMathUtils extends MathUtils {
    public static int square(int a) {
        return multiply(a, a);
    }
}

The divide method has default (package-private) access since no explicit modifier is specified. Remember, this means that the method is accessible only to classes within the same package:

package com.my.package;

// Calling from another class in the same package
public class ArithmeticOperations {
    public static int performDivision(int a, int b) {
        return MathUtils.divide(a, b);
    }
}

Passing Arguments Among Methods

In Java, when you pass arguments to a method, they are always passed by value. This means that a copy of the value is passed to the method, rather than a reference to the original variable. However, the behavior of pass-by-value differs depending on whether you are passing a primitive type (like an int) or a reference type (like an object such as String).

When you pass a primitive type to a method, the method receives a copy of the value. Any changes made to the parameter inside the method do not affect the original variable outside the method.

Here’s an example:

public void testPrimitive() {
    int num = 10;
    modifyPrimitive(num);
    System.out.println(num); // Output: 10
}

public void modifyPrimitive(int value) {
    value = 20;
}

In this example, the modifyPrimitive method receives a copy of the value of num. Modifying the value parameter inside the method does not change the original num variable in the testPrimitive method.

When you pass a reference type to a method, the method receives a copy of the reference to the object. While the reference itself is passed by value, the method can still modify the state of the object that the reference points to.

Here’s an example:

public void test() {
    Person person = new Person("John", 25);
    modifyPerson(person);
    System.out.println(person.getName()); // Output: Alice
    System.out.println(person.getAge()); // Output: 25
}

public void modifyPerson(Person p) {
    p.setName("Alice"); // Sets a new name
    p = new Person("Bob", 30); // Reassigns p to a new person
}

In this example, the modifyPerson method receives a copy of the reference to the Person object. Inside the method, the setName() method is called on the object referenced by p, which modifies the name of the original object. However, when p is reassigned to a new Person object, it does not affect the original person reference in the main method.

Let’s explore a few more examples to clarify the difference between reassigning a reference and modifying the object itself.

First, consider this one about reassigning a reference:

public void test() {
    StringBuilder sb = new StringBuilder("Hello");
    modifyStringBuilder(sb);
    System.out.println(sb.toString()); // Output: Hello
}

public void modifyStringBuilder(StringBuilder builder) {
    builder = new StringBuilder("World");
}

In this example, the modifyStringBuilder method receives a copy of the reference to the StringBuilder object. Inside the method, the builder reference is reassigned to a new StringBuilder object, but this does not affect the original sb reference in the main method.

Contrast the previous example with the following, which demonstrates how modifying the state of an object differs from simply reassigning a reference:

public void test() {
    StringBuilder sb = new StringBuilder("Hello");
    appendToStringBuilder(sb);
    System.out.println(sb.toString()); // Output: Hello, World!
}

public void appendToStringBuilder(StringBuilder builder) {
    builder.append(", World!");
}

In this example, the appendToStringBuilder method receives a copy of the reference to the StringBuilder object. Inside the method, the append() method is called on the object referenced by builder, which modifies the state of the original object. The changes made to the object are visible outside the method.

Understanding the behavior of pass-by-value and the difference between reassigning a reference and modifying the object itself is important for writing correct and predictable code. Always consider whether you intend to modify the object or simply reassign the reference when passing reference types to methods.

Method Overloading

In Java, it is possible to define two or more methods within the same class that share the same name, as long as their parameter declarations are different. This is called method overloading. Consider the methods of the following class:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }
}

When the add method is called, the Java compiler determines which version of the overloaded method to call based on the type of the arguments passed to it.

This is similar to ordering coffee at a coffee shop. The barista can prepare different variations of coffee based on your specifications, black coffee, coffee with milk, or coffee with milk and sugar. Each variation is ordered using the same word (coffee), but the ingredients you specify determine the exact type of coffee you’ll receive. In the same way, when you call an overloaded method in Java, the arguments you pass determine which version of the method will be executed.

For example, if we call the add method with different arguments:

Calculator calc = new Calculator();

int result1 = calc.add(5, 10);
System.out.println(result1);  // Output: 15

double result2 = calc.add(5.5, 10.2);
System.out.println(result2);  // Output: 15.7

double result3 = calc.add(5, 10.2);
System.out.println(result3);  // Output: 15.2

This is what happens:

  1. When calc.add(5, 10) is called, both arguments are of type int. The Java compiler matches this call with the add method that takes two int parameters, and the result is an int value of 15.

  2. When calc.add(5.5, 10.2) is called, both arguments are of type double. The Java compiler matches this call with the add method that takes two double parameters, and the result is a double value of 15.7.

  3. When calc.add(5, 10.2) is called, one argument is an int, and the other is a double. In this case, the Java compiler performs a conversion of the int argument to a double to match the add method that takes two double parameters. The result is a double value of 15.2.

This example demonstrates how the Java compiler uses the type of the arguments to determine which overloaded method to call. It matches the arguments with the most specific method signature available.

Java can only pick an overloaded method if it can find an exact match for the arguments or if it can find a version that is more specific through widening conversions.

Widening conversions are when you go from a smaller data type to a larger data type, for instance, from an int to a long, or, like in the above example, from an int to a double.

However, Java cannot apply narrowing conversions (going from a larger data type to a smaller one) automatically. If it doesn’t find an exact match or a match through widening, it will give a compile error.

It’s important to note that method overloading is not the same as method overriding. We’ll talk more about overriding in the next chapter, but when overriding, you provide a different implementation for an inherited method. The overridden method must have the same name, return type, and parameters as the inherited method. On the other hand, overloaded methods must have the same name but different parameters.

So always keep this in mind, changing just the return type is not enough for method overloading. The parameter list must be different.

Also, a common misconception is that Java always choose the overloaded method with the most parameters. It doesn’t. Java selects the method based on the most specific match to the argument types, not necessarily the method with the most parameters.

Consider this class that has multiple overloaded methods named display:

public class DisplayOverload {
    
    // Method with a single String argument
    public void display(String str) {
        System.out.println("Displaying a String: " + str);
    }
    
    // Overloaded method with a single int argument
    public void display(int num) {
        System.out.println("Displaying an integer: " + num);
    }
    
    // Overloaded method with two int arguments
    public void display(int num1, int num2) {
        System.out.println("Displaying two integers: " + num1 + " and " + num2);
    }
}

// ...

DisplayOverload obj = new DisplayOverload();
        
obj.display("Hello, World!"); // Calls the method with a String argument
obj.display(5); // Calls the method with a single int argument
obj.display(10, 20); // Calls the method with two int arguments

In this example:

One last thing to note is that you cannot overload methods that differ only by a varargs parameter. For example, this will not compile:

public void sum(int[] numbers) { }
public void sum(int... numbers) { } // Compile-time error

The reason is that both int[] numbers and int... numbers are essentially the same from Java’s perspective because int... is just syntactic sugar for an array of integers (int[]). When you try to overload a method with these two parameter types, Java sees them as identical signatures. But let’s talk more about varargs.

Varargs

Varargs, short for variable-length arguments, are a feature that allow methods to accept an unspecified number of arguments of a specific type. Think of varargs like an all-you-can-eat buffet. At a buffet, you’re not limited to a fixed number of dishes; you can choose to have as many different dishes as you want, and you can even go back for more. Similarly, with varargs, a method can be called with a varying number of arguments; you’re not fixed to a specific number. This makes your methods more flexible and easier to use when the exact number of inputs may vary.

To define a method with varargs, you use an ellipsis (...) after the data type of the last parameter. Here’s how it works:

public void display(String... words) {
    for (String word : words) {
        System.out.println(word);
    }
}

In this example, display can be called with any number of String arguments, including none at all. It’s as if you’re telling the method, “Here’s what I have, take it all.” This flexibility makes varargs extremely useful for creating methods that need to handle an unknown number of objects, like a list of names, numbers, or even complex objects.

Now, there are specific rules you must follow to use varargs effectively and correctly.

First, a varargs parameter must be the last parameter in a method’s parameter list. This rule ensures that the method can accept a variable number of arguments without ambiguity regarding which arguments belong to the varargs parameter and which do not. For instance, consider the following method:

void printStrings(String title, String... strings) {
    System.out.println(title + ":");
    for (String str : strings) {
        System.out.println(str);
    }
}

In this example, String... strings is a varargs parameter that can accept any number of String arguments. Being the last parameter allows you to call printStrings with any number of strings, or even no strings at all.

Second, only one varargs parameter is allowed in a method’s parameter list. This restriction prevents confusion over which arguments belong to which varargs parameter if more than one were allowed. For example, if you wanted to create a method that sums numbers, you might do the following:

double multiplyAndSum(double multiplier, int... numbers) {
    double sum = 0;
    for (int num : numbers) {
        sum += num;
    }
    return sum * multiplier;
}

This method correctly includes only one varargs parameter (int... numbers), ensuring clarity in how it should be called and how it operates on the passed arguments.

Third, a method with a varargs parameter can be overloaded, but you have to make sure to avoid ambiguity. This requires ensuring that each method signature is distinct enough to prevent compile-time errors. For example, you could have:

void display(String s, int... numbers) {
    System.out.println(s);
    for (int num : numbers) {
        System.out.print(num + " ");
    }
    System.out.println();
}

void display(String first, String second) {
    System.out.println(first + ", " + second);
}

Here, display is overloaded with one version accepting a string and a varargs integer parameter, and another accepting two strings. This overloading is valid because the method signatures are distinct, ensuring that the compiler can determine which method to call based on the arguments provided.

Inside the method, accessing the elements of a varargs parameter can be done in several ways, each suitable for different scenarios.

The simplest way to access elements in a varargs parameter is by treating it as an array and accessing its elements directly using an index. This method is useful when you know the exact number of arguments or need to access specific elements. For example, consider a method that prints the first, second, and last elements of a varargs parameter:

void printSelectedNumbers(int... numbers) {
    if (numbers.length >= 3) {
        System.out.println("First: " + numbers[0]);
        System.out.println("Second: " + numbers[1]);
        System.out.println("Last: " + numbers[numbers.length - 1]);
    } else {
        System.out.println("Insufficient arguments.");
    }
}

This method directly accesses elements by their indices, similar to an array access, making it straightforward to retrieve specific values.

For iterating over each element in a varargs parameter, the enhanced for loop provides a clean and concise way to process each argument. This approach is most beneficial when you need to perform operations on every element or when the number of arguments is variable. Here’s an example that sums all numbers passed to the method:

int sumAll(int... numbers) {
    int sum = 0;
    for (int num : numbers) {
        sum += num;
    }
    return sum;
}

The enhanced for loop automatically iterates over each element in numbers, allowing for easy aggregation or processing.

Although similar to using an enhanced for loop, you might sometimes need to manually iterate over a varargs parameter using its length property for more complex logic, such as when you need to access the current index. Here’s how you might print each element with its index:

void printWithIndices(String... strings) {
    for (int i = 0; i < strings.length; i++) {
        System.out.println("Element " + i + ": " + strings[i]);
    }
}

This method leverages the length property of the varargs parameter to manually control the iteration, offering flexibility for index-based operations.

For more complex operations, including filtering, mapping, or aggregating elements, Java’s Stream API can work directly with varargs. This method is particularly powerful for processing elements in functional programming style. We’ll cover streams in a later chapter, but, for instance, you can filter and sum only the even numbers as follows:

int sumEvenNumbers(int... numbers) {
    return Arrays.stream(numbers) // Convert varargs to a stream
                 .filter(n -> n % 2 == 0) // Filter even numbers
                 .sum(); // Sum them
}

Once you have defined a method that takes a vararg parameter, you can call it by passing individual arguments, by passing an array, or by calling it without any arguments.

The most straightforward way to call a method with varargs is by passing individual arguments to it. This approach is identical to calling a method with a fixed number of parameters, but with the added flexibility of specifying any number of arguments. Here’s an example using a method that prints out each argument:

void printArgs(String... args) {
    for (String arg : args) {
        System.out.println(arg);
    }
}

// Calling the method
printArgs("Hello", "World", "Varargs", "are", "flexible");

In this example, the printArgs method is called with five string arguments, demonstrating the ease with which you can pass any number of arguments.

Alternatively, you can call a varargs method by passing an array of the specified type. This approach is useful when the arguments are already stored in an array, or when you wish to dynamically construct the list of arguments. Consider a method that sums an arbitrary number of integers:

int sumNumbers(int... numbers) {
    return Arrays.stream(numbers).sum();
}

// Calling the method with an array
int[] numberArray = {1, 2, 3, 4, 5};
int sum = sumNumbers(numberArray);
System.out.println("Sum is: " + sum);

Here, sumNumbers is called with an integer array, showcasing how an array matches the varargs signature, providing a compact way to pass multiple arguments.

Finally, a varargs method can also be called without passing any arguments. This feature is particularly useful when an operation is optional or when there is a valid default behavior in the absence of inputs. Here’s a method that concatenates any number of strings, with a demonstration of calling it without arguments:

String concatenateStrings(String... strings) {
    return Stream.of(strings).collect(Collectors.joining(", "));
}

// Calling the method without arguments
String result = concatenateStrings();
System.out.println("Result: " + result);

The above example illustrates that calling concatenateStrings without any arguments is perfectly valid and that varargs provide a flexible method signature that accommodates a wide range of use cases.

The main Method

The main method is a special method in Java that serves as the entry point of a Java application. When you run a Java program, the JVM looks for this method and starts executing the code inside it. Every Java application must have a main method in at least one of its classes.

Here’s the syntax for declaring a main method:

public static void main(String[] args) {
    // ...
}

Let’s break down each part:

Here’s an example of a simple main method:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

In this example, the main method simply prints Hello, World! to the console.

The arguments of the program are passed as a String array, where each element represents a separate argument:

public class CommandLineArguments {
    public static void main(String[] args) {
        if (args.length > 0) {
            System.out.println("Arguments:");
            for (String arg : args) {
                System.out.println(arg);
            }
        } else {
            System.out.println("No arguments provided.");
        }
    }
}

In this example, the main method checks if any arguments were passed, using args.length. If there are arguments, it iterates over the args array and prints each argument. If no arguments were provided, it prints a message indicating that.

You can run this program from the command line and pass arguments like this:

java CommandLineArguments arg1 arg2 arg3

This will be the output:

Arguments:
arg1
arg2
arg3

If you run the program without any arguments:

java CommandLineArguments

This will be the output:

No arguments provided.

It’s possible to have multiple methods named main in a class, as long as they have different parameter lists. However, only the method defined as public static void main(String[] args) will be recognized as the entry point of the application:

public class MainOverloading {
    public static void main(String[] args) {
        System.out.println("Main method with String[] args");
        main(42);
    }

    public static void main(int num) {
        System.out.println("Main method with int parameter: " + num);
    }
}

In this example, the class has two main methods: one with the standard signature and another with an int parameter, however, the main(String[] args) method is the entry point, and it calls the main(int num) method.

This is the output:

Main method with String[] args
Main method with int parameter: 42

Constructors and Initializers

Constructors

In Java, a constructor is a special method used to initialize objects. It is called when an instance of a class is created.

Imagine a constructor as a recipe for baking a specific cake. Just as the recipe contains the instructions and ingredients to make the cake, a constructor has the code to set up the initial state of an object.

The syntax to define a constructor is straightforward. It has the same name as the class and no return type, not even void.

Here’s an example:

class Cake {
    String flavor;
    double price;
    Cake() {
        flavor = "Vanilla";
        price = 9.99;
    }
}

To create an object, we use the new keyword followed by a call to the constructor:

Cake myCake = new Cake();

The above line will create a new Cake object with the default Vanilla flavor and a price of 9.99.

But what if you want the default flavor but a different price? Or customize both in certain cases?

Well, just as you can bake different varieties of cakes by tweaking the recipe, you can create objects with different initial states by providing multiple constructors.

For example, let’s add another constructor to our Cake class:

Cake(String flavor, double price) {
    this.flavor = flavor; 
    this.price = price;
}

With this constructor, we can create a cake with any flavor and price we want:

Cake specialCake = new Cake("Chocolate", 12.99);

Having multiple constructors gives flexibility in object creation. We can provide different ways to initialize an object based on the data available at the time of creation.

The constructor with no parameters is called the default constructor. If you don’t define any constructors in your class, the compiler will automatically provide a default constructor with an empty body.

However, if you define any constructor (like our parameterized one), the compiler will not provide a default constructor. In this case, if you still want the option to create an object without specifying parameters, you need to explicitly define the default constructor.

So in our Cake class, we could have both constructors:

class Cake {
    String flavor;
    double price;

    Cake() {
        flavor = "Vanilla";
        price = 9.99;
    }

    Cake(String flavor, double price) {
        this.flavor = flavor;
        this.price = price;
    }
}

Now we can create a default vanilla cake with new Cake() or a customized cake with new Cake("Chocolate", 10.99).

Instance Initializers

Instance initializers are blocks of code that are executed when an object is created, just like constructors. However, while constructors are methods with a specific name and potentially parameters, instance initializers are just code blocks within a class.

Let’s use an analogy to understand instance initializers.

Imagine moving into a new house. We all have our unique rituals to make a house feel like a home. Some might hang family photos, others might paint the walls in their favorite color. These rituals are specific to each person, just as instance initializers are specific to each object.

Here’s the syntax for an instance initializer:

class House {
    String color;
    // instance initializer
    {
        color = "White";
        System.out.println("Performing move-in ritual");
    }
}

Whenever a new House object is created, the code inside the instance initializer block will run. It will set the color to White and print "Performing move-in ritual".

So, how do instance initializers compare to constructors, and when might you use them?

Well, imagine you have a class with multiple constructors. Each constructor needs to perform some common initialization tasks. Instead of duplicating the code in each constructor, you can put it in an instance initializer. The initializer code will run regardless of which constructor is used.

class House {
    String color;
    int numberOfRooms;

    // instance initializer    
    {
        color = "White";
        System.out.println("Performing move-in ritual");
    }

    House(int numberOfRooms) {
        this.numberOfRooms = numberOfRooms;
    }

    House(String color, int numberOfRooms) {
        this.color = color;
        this.numberOfRooms = numberOfRooms;
    }
}

In this case, regardless of which constructor is used to create a House object, the instance initializer will run, setting the default color to White and printing the move-in message.

However, it’s important to note that in most cases, you can achieve the same result by simply moving the common initialization code into a separate method and calling that method from each constructor.

In fact, some argue that instance initializers are redundant since anything you can do with an instance initializer, you can also do with a constructor. The main difference is that constructors can take parameters, while instance initializers cannot.

That said, there are some scenarios where instance initializers can be useful. For example, if you’re using anonymous classes (which we’ll cover later), you can’t define a constructor, so an instance initializer is your only option for initialization code.

Static Initializers

Static initializers are blocks of code that are executed when a class is loaded into memory, before any instances of the class are created. They are used to initialize static variables or perform actions that are common to all instances of the class.

Imagine a town hall meeting that happens once when a town is established. In this meeting, the town’s leaders set up rules and guidelines that apply to everyone in the town. This one-time setup is similar to what a static initializer does for a class.

Here’s the syntax for a static initializer:

class TownHall {
    static String townName;
    static int population;

    // static initializer
    static {
        townName = "JavaVille";
        population = 1000;
        System.out.println("Town established: " + townName);
    }
}

The static keyword before the opening brace denotes that this is a static initializer block. It will run once when the TownHall class is loaded, setting the townName to JavaVille, the initial population to 1000, and printing Town established: JavaVille.

Now, you might think that static initializers are just another way to initialize static variables, and you could achieve the same result by directly initializing the variables at their declaration, like this:

static String townName = "JavaVille";
static int population = 1000;

And you’d be partially correct. For simple initializations, direct assignment is often clearer and more concise.

However, static initializers provide more flexibility. They allow you to write more complex initialization logic, such as:

Here’s an example that demonstrates this:

static List<String> residents = new ArrayList<>();

static {
    Path path = Paths.get("residents.txt");
    try (Stream<String> lines = Files.lines(path)) {
        lines.forEach(residents::add);
    } catch (IOException e) {
        System.out.println("Residents file not found.");
    }
}

In this case, we’re using the static initializer to read a list of residents from a file and populate the residents list. This kind of complex initialization would not be possible with a simple direct assignment.

Another key difference is that a class can have multiple static initializers, and they will be executed in the order they appear in the class. This can be useful for organizing complex initialization logic into readable chunks.

It’s important to note that static initializers are executed before any instance of the class is created, and even before the main method is called. They are part of the class loading process.

In contrast, instance initializers and constructors are run every time a new instance of the class is created. They are part of the object creation process.

Initialization Order

We have reviewed constructors, instance initializers, and static initializers. However, if a class includes all three, which one executes first? What is the order of initialization?

When a class is loaded, the first things to be initialized are the static variables and static initializers, in the order they appear in the class. This happens once per class loading, before any instances are created.

After that, whenever a new instance of the class is created, the instance variables are initialized, and the instance initializers and constructors are run.

The order is as follows:

  1. Instance variables are initialized to their default values (0, false, or null). This step ensures that all instance variables have a predictable starting state before any further initialization code is executed.
  2. Instance initializers are run in the order they appear in the class.
  3. The constructor is executed.

Here’s an example that demonstrates this order:

class InitializationOrder {
    static int staticVar = 1;
    int instanceVar = 1;

    static {
        System.out.println("Static Initializer: staticVar = " + staticVar);
        staticVar = 2;
    }

    {
        System.out.println("Instance Initializer: instanceVar = " + instanceVar);
        instanceVar = 2;
    }

    InitializationOrder() {
        System.out.println("Constructor: instanceVar = " + instanceVar);
        instanceVar = 3;
    }

    public static void main(String[] args) {
        System.out.println("Creating new instance");
        InitializationOrder obj = new InitializationOrder();
        System.out.println("Created instance: instanceVar = " + obj.instanceVar);
    }
}

If you run this code, the output will be:

Static Initializer: staticVar = 1
Creating new instance
Instance Initializer: instanceVar = 1
Constructor: instanceVar = 2
Created instance: instanceVar = 3

Let’s break this down:

  1. When the InitializationOrder class is loaded, the static variable staticVar is initialized to 1, and then the static initializer is run, which prints the current value of staticVar (1) and then sets it to 2.
  2. In the main method, we print Creating new instance to mark the start of instance creation.
  3. A new InitializationOrder object is created. First, the instance variable instanceVar is initialized to its default value of 1.
  4. The instance initializer is run, which prints the current value of instanceVar (1) and then sets it to 2.
  5. The constructor is executed, which prints the current value of instanceVar (2) and then sets it to 3.
  6. Finally, back in the main method, we print the final value of instanceVar (3).

It’s important to keep this order in mind, especially if your initializers and constructors depend on each other. Incorrect assumptions about initialization order can lead to subtle bugs.

Also, note that if a class has multiple static initializers, they will run in the order they appear in the class. The same is true for instance initializers.

Let’s extend our previous example to demonstrate this:

class MultipleInitializers {
    static int staticVar1;
    static int staticVar2;
    int instanceVar1;
    int instanceVar2;

    static {
        System.out.println(
          "Static Initializer 1: staticVar1 = " + staticVar1
        );
        staticVar1 = 1;
    }

    static {
        System.out.println(
          "Static Initializer 2: staticVar2 = " + staticVar2
        );
        staticVar2 = 2;
    }

    {
        System.out.println(
          "Instance Initializer 1: instanceVar1 = " + instanceVar1
        );
        instanceVar1 = 1;
    }

    {
        System.out.println(
          "Instance Initializer 2: instanceVar2 = " + instanceVar2
        );
        instanceVar2 = 2;
    }

    MultipleInitializers() {
        System.out.println("Constructor");
    }

    public static void main(String[] args) {
        System.out.println("Creating new instance");
        MultipleInitializers obj = new MultipleInitializers();
        System.out.println(
          "Created instance: instanceVar1 = " 
              + obj.instanceVar1 
              + ", instanceVar2 = " 
              + obj.instanceVar2
        );
    }
}

When we run this code, the output will be:

Static Initializer 1: staticVar1 = 0
Static Initializer 2: staticVar2 = 0
Creating new instance
Instance Initializer 1: instanceVar1 = 0
Instance Initializer 2: instanceVar2 = 0
Constructor
Created instance: instanceVar1 = 1, instanceVar2 = 2

Here’s what’s happening:

  1. When the MultipleInitializers class is loaded, the static variables staticVar1 and staticVar2 are initialized to their default value of 0.

  2. The first static initializer is run, which prints the current value of staticVar1 (0) and then sets it to 1.

  3. The second static initializer is run, which prints the current value of staticVar2 (0) and then sets it to 2.

  4. In the main method, we print Creating new instance to mark the start of instance creation.

  5. A new MultipleInitializers object is created. First, the instance variables instanceVar1 and instanceVar2 are initialized to their default value of 0.

  6. The first instance initializer is run, which prints the current value of instanceVar1 (0) and then sets it to 1.

  7. The second instance initializer is run, which prints the current value of instanceVar2 (0) and then sets it to 2.

  8. The constructor is executed, which simply prints Constructor.

  9. Finally, back in the main method, we print the final values of instanceVar1 (1) and instanceVar2 (2).

Remember, all static initializers will run before any instance initializers, and all initializers will run before the constructor. But within each category (static or instance), the initializers will run in the order they are defined in the class.

Extending from java.lang.Object

In Java, every class is implicitly a subclass of the java.lang.Object class, which is the root of the class hierarchy. Even if you don’t explicitly extend any class, your class will automatically inherit from Object.

The Object class provides a set of fundamental methods that are common to all objects. When you create a new class, you automatically inherit these methods. Some of the commonly used methods inherited from Object include:

  1. toString(): Returns a string representation of the object. By default, it returns a string consisting of the object’s class name, an @ symbol, and the object’s hash code in hexadecimal format. You can override this method to provide a custom string representation of your object.

  2. equals(Object obj): Compares the object with another object for equality. By default, it compares the object references using the == operator. You can override this method to define custom equality logic based on the object’s state.

  3. hashCode(): Returns a hash code value for the object. The hash code is used in hash-based data structures such as HashSet and HashMap. By default, it returns a unique integer value for each object. If you override the equals() method, you should also override the hashCode() method to ensure that equal objects have the same hash code.

  4. getClass(): Returns the runtime class of the object. It is a final method, which means it cannot be overridden.

  5. clone(): Creates and returns a copy of the object. By default, it performs a shallow copy of the object. To use this method, your class must implement the Cloneable interface.

Here’s an example that demonstrates some of the methods inherited from Object:

class MyClass {
    private int value;
    
    public MyClass(int value) {
        this.value = value;
    }
    
    @Override
    public String toString() {
        return "MyClass[value=" + value + "]";
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        MyClass other = (MyClass) obj;
        return value == other.value;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

In this example, the MyClass overrides the toString(), equals(), and hashCode() methods inherited from Object. The toString() method provides a custom string representation of the object, the equals() method defines equality based on the value field, and the hashCode() method generates a hash code based on the value field.

By leveraging these methods, you can provide meaningful string representations, define equality comparisons, and ensure proper behavior in hash-based data structures.

Nested Classes

In Java, it’s possible to define a class within another class. Such classes are called nested classes. Similar to how a box can contain several smaller boxes inside it, a class (the outer or enclosing class) can have other classes (nested classes) defined within it.

There are four types of nested classes in Java:

  1. Static nested class

  2. Inner class (also known as a non-static nested class)

  3. Local class

  4. Anonymous class

Each type of nested class has its own characteristics and use cases:

When deciding between a static nested class and an inner class, consider the relationship between the nested class and the enclosing class. If the nested class doesn’t need access to the non-static members of the enclosing class, use a static nested class. This makes the class more independent and reusable. If the nested class requires access to the non-static members of the enclosing class or needs to be tied to an instance of the enclosing class, use an inner class.

It’s important to note that while nested classes can help organize code better, they do impact how the code works. Each type of nested class has its own specific behavior and use cases. For example, an inner class has an implicit reference to an instance of the enclosing class, which can have implications for memory usage and serialization.

A common misconception is that static nested classes and inner classes are essentially the same since they are both defined within another class. However, this is not true. Static nested classes are semantically similar to any other top-level class and do not have an implicit reference to an instance of the enclosing class. Inner classes, on the other hand, are intimately tied to an instance of the enclosing class and cannot exist independently.

In terms of access modifiers, nested classes can be declared as public, package-private (default), protected, or private, unlike top-level classes that can’t be declared as protected or private.

The accessibility of static and non-static nested classes depends on its access modifier and the accessibility of the enclosing class. For example, if the enclosing class is public and the nested class is private, the nested class can only be accessed within the enclosing class. If the nested class is public, it can be accessed from anywhere, provided the enclosing class is also accessible.

Here’s a bit more detail on each type:

Local classes are defined in a block, typically within a method body. The visibility of a local class is restricted to the block in which it is defined. Thus, while you cannot apply traditional access modifiers (public, protected, private) to the class itself because it is not visible outside the block, you can control the access to instances of this class from within the block.

And since anonymous classes are used within an expression, they don’t allow access modifiers for the class itself. The context in which they are declared dictates their accessibility. However, the methods and fields within an anonymous class can have access modifiers, subject to normal scoping rules.

Here’s a summary table of the allowed access modifiers for each type of nested class:

Nested Class Type public protected default private
Static Nested Class Yes Yes Yes Yes
Inner Class Yes Yes Yes Yes
Local Class No No Yes* No
Anonymous Class No No Yes* No

Now let’s go over each type in more detail.

Static Nested Classes

A static nested class is a class defined within another class and marked with the static keyword:

class OuterClass {
    static class StaticNestedClass {
        // members of the static nested class
    }
}

Static nested classes can be declared with any of the four access modifiers: public, protected, package-private (default), or private. The accessibility of the static nested class depends on the access modifier used and the accessibility of the enclosing class. Here’s an example:

public class OuterClass {
    private static class PrivateNestedClass {
        // ...
    }

    protected static class ProtectedNestedClass {
        // ...
    }

    static class PackagePrivateNestedClass {
        // ...
    }

    public static class PublicNestedClass {
        // ...
    }
}

In this example, PrivateNestedClass is accessible only within OuterClass, ProtectedNestedClass is accessible within OuterClass and its subclasses, PackagePrivateNestedClass is accessible within the same package as OuterClass, and PublicNestedClass is accessible from anywhere.

Static nested classes can extend another class and implement interfaces, just like any other top-level class:

class BaseClass {
    // ...
}

interface MyInterface {
    // ...
}

class OuterClass {
    static class NestedClass extends BaseClass implements MyInterface {
        // ...
    }
}

Here, NestedClass extends BaseClass and implements MyInterface, demonstrating that a static nested class can extend another class and implement interfaces.

They can access the static members of the enclosing class directly, using the name of the enclosing class followed by the dot notation. However, to access non-static members of the enclosing class, a static nested class requires an instance of the enclosing class. This is because static nested classes do not inherently have access to the instance variables of the enclosing class.

Here’s an example of a nested static class:

class OuterClass {
    private static int staticField = 10;
    private int instanceField = 20;

    static class NestedClass {
        void accessOuterMembers() {
            System.out.println(staticField); // Accessible directly
            System.out.println(instanceField); // Compilation error: cannot access non-static field
            System.out.println(new OuterClass().instanceField); // Accessible via an instance of OuterClass
        }
    }
}

In this example, NestedClass can directly access the staticField of OuterClass, but it cannot directly access the instanceField. To access instanceField, it needs an instance of OuterClass.

To create an instance of a static nested class, you don’t need an instance of the enclosing class. You can instantiate it using the name of the enclosing class followed by the dot notation and the name of the static nested class.

OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();

When referencing static members of a static nested class from outside the enclosing class, use the enclosing class’s name, followed by a dot, the static nested class’s name, another dot, and then the member’s name. This syntax highlights the nested structure while providing clear paths to access static members:

OuterClass.StaticNestedClass.staticField;
OuterClass.StaticNestedClass.staticMethod();
OuterClass.StaticNestedClass.StaticNestedNestedClass nestedNestedObject 
                  = new OuterClass.StaticNestedClass.StaticNestedNestedClass();

From within the enclosing class, you can directly access the members of the static nested class without the name of the enclosing class.

class OuterClass {
    static class StaticNestedClass {
        static void staticMethod() {
            // ...
        }
    }

    void outerMethod() {
        StaticNestedClass.staticMethod();
    }
}

As you can see, static nested classes are similar to regular, top-level classes in many ways:

  1. They can have all types of access modifiers (public, private, protected, and package).

  2. They can extend other classes and implement interfaces.

  3. They can have static and non-static members.

  4. They can be instantiated independently (without an instance of the enclosing class).

However, there are a few key differences:

  1. Static nested classes are defined within another class, whereas top-level classes are defined independently.

  2. Static nested classes have access to the static members of the enclosing class directly, while top-level classes need to use the enclosing class name to access its static members.

  3. Static nested classes can be private, allowing for better encapsulation, whereas top-level classes can only be public or package-private.

In summary, static nested classes are essentially like regular top-level classes that have been nested within another class for organizational purposes. They do not have an implicit reference to an instance of the enclosing class and can be instantiated independently. This makes them useful for grouping related classes together and providing a level of encapsulation.

Non-static Nested Classes

Non-static nested classes, also known as inner classes, are classes that are defined within another class without the static keyword:

class OuterClass {
    class InnerClass {
        // members of the inner class
    }
}

An inner class can be declared with any of the four access modifiers: public, protected, private, or the default access level. The accessibility of the inner class depends on the access modifier used and the accessibility of the enclosing class. If the outer class is public and the inner class is private, the inner class can only be accessed within the outer class. Here’s an example:

public class OuterClass {
    private class PrivateInnerClass {
        // ...
    }

    protected class ProtectedInnerClass {
        // ...
    }

    class PackagePrivateInnerClass {
        // ...
    }

    public class PublicInnerClass {
        // ...
    }
}

In this example, PrivateInnerClass is accessible only within OuterClass, ProtectedInnerClass is accessible within OuterClass and its subclasses, PackagePrivateInnerClass is accessible within the same package as OuterClass, and PublicInnerClass is accessible from anywhere, provided OuterClass is accessible.

An inner class can extend another class and implement interfaces, just like any other class. This allows inner classes to inherit behavior and conform to contracts defined by other classes and interfaces:

class BaseClass {
    // ...
}

interface MyInterface {
    // ...
}

class OuterClass {
    class InnerClass extends BaseClass implements MyInterface {
        // ...
    }
}

Here, InnerClass extends BaseClass and implements MyInterface, demonstrating that an inner class can inherit from another class and conform to an interface.

An inner class has access to all members (fields, methods, and nested classes) of the enclosing class, including private members. This is because an inner class is associated with an instance of the outer class and shares a special relationship with it. The inner class can directly access and manipulate the state of the outer class instance:

class OuterClass {
    private int privateField = 10;
    protected int protectedField = 20;
    int packagePrivateField = 30;
    public int publicField = 40;

    class InnerClass {
        void accessOuterMembers() {
            System.out.println(privateField);
            System.out.println(protectedField);
            System.out.println(packagePrivateField);
            System.out.println(publicField);
        }
    }
}

In this example, InnerClass has direct access to all members of OuterClass, including the private field privateField. The inner class can freely access and manipulate the state of the outer class instance.

To create an instance of an inner class, you typically need an instance of the outer class. The most common way to instantiate an inner class is from within a non-static method of the outer class:

class OuterClass {
    class InnerClass {
        // ...
    }

    void outerMethod() {
        InnerClass innerObject = new InnerClass();
    }
}

From outside the outer class, you can instantiate an inner class using the following syntax:

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

To reference members (fields, methods, nested classes) of an inner class from outside the outer class, you first need an instance of the outer class, then use the dot notation to access the inner class, followed by another dot and the member name:

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
innerObject.innerField;
innerObject.innerMethod();

From within the outer class, you can directly access the members of the inner class using an instance of the inner class:

class OuterClass {
    class InnerClass {
        void innerMethod() {
            // ...
        }
    }

    void outerMethod() {
        InnerClass innerObject = new InnerClass();
        innerObject.innerMethod();
    }
}

Inner classes differ from regular, top-level classes in several ways:

  1. Inner classes are defined within another class, whereas top-level classes are defined outside of other classes.

  2. Inner classes have access to all members of the enclosing class, including private members, while top-level classes can only access public and protected members of other classes.

  3. Inner classes are associated with an instance of the outer class and cannot exist independently, while top-level classes can be instantiated independently.

  4. Inner classes can be private, allowing for better encapsulation, whereas top-level classes can only be public or package-private.

Inner classes are useful when a class is closely tied to another class and needs access to its internals. They provide a way to organize related classes and maintain a tight coupling between them. Inner classes are commonly used for implementing event listeners, iterators, or other functionality that is specific to the enclosing class.

Local Classes

Local classes are defined within a block of code, typically within a method or a constructor. They have limited scope and are only accessible within the block where they are defined:

void someMethod() {
    class LocalClass {
        // members of the local class
    }
}

A local class cannot have any access modifiers. They cannot be accessed from outside the block or method in which they are defined. This is because local classes are not members of the enclosing class, but rather defined within a method or block.

However, they can extend another class and implement interfaces, just like any other class.

Example:

void someMethod() {
    class LocalClass extends BaseClass implements MyInterface {
        // ...
    }
}

Also, a local class has access to all members (fields, methods, and nested classes) of the enclosing class, including private members. Additionally, a local class can access final or effectively final local variables and parameters of the enclosing method:

class OuterClass {
    private int privateField = 10;

    void someMethod(final int parameter) {
        final int localVariable = 20;

        class LocalClass {
            void accessOuterMembers() {
                System.out.println(privateField);
                System.out.println(parameter);
                System.out.println(localVariable);
            }
        }

        LocalClass localObject = new LocalClass();
        localObject.accessOuterMembers();
    }
}

In this example, LocalClass has access to the private field privateField of OuterClass, as well as the final parameter parameter and the final local variable localVariable of the someMethod().

To create an instance of a local class, you can instantiate it within the method or block where it is defined, using the new keyword:

void someMethod() {
    class LocalClass {
        // ...
    }

    LocalClass localObject = new LocalClass();
}

To reference members (fields, methods, nested classes) of a local class, you can directly access them using an instance of the local class within the method or block where it is defined:

void someMethod() {
    class LocalClass {
        int localField = 10;

        void localMethod() {
            System.out.println("Local method");
        }
    }

    LocalClass localObject = new LocalClass();
    System.out.println(localObject.localField);
    localObject.localMethod();
}

Local classes differ from regular, top-level classes in several ways:

  1. Local classes are defined within a method or block, whereas top-level classes are defined independently.

  2. Local classes have limited scope and are only accessible within the block where they are defined, while top-level classes have a broader scope.

  3. Local classes cannot have access modifiers, while top-level classes can be public or package-private.

  4. Local classes can access final or effectively final local variables and parameters of the enclosing method, while top-level classes cannot directly access local variables or parameters.

Local classes are useful when you need to define a class that is only used within a specific method or block and does not need to be accessed from other parts of the code. They provide a way to encapsulate behavior and state within a limited scope.

Anonymous Classes

Anonymous classes are a way to define and instantiate a class at the same time, without giving it a name. They are used for creating one-time implementations of interfaces or abstract classes.

To declare an anonymous class, you use the new keyword followed by the name of an interface or an abstract class, and then provide the class body in curly braces.

interface MyInterface {
    void myMethod();
}

MyInterface myObject = new MyInterface() {
    @Override
    public void myMethod() {
        // Implementation of myMethod()
    }
};

Since anonymous classes are not explicitly named and are defined at the point of use, they cannot have any explicit access modifiers. Their accessibility is determined by the context in which they are used. Specifically, the scope in which an anonymous class is defined determines its accessibility. For instance, if an anonymous class is defined within a method, it is accessible only within that method. If it is defined within a class, it follows the accessibility rules of that class.

An anonymous class can extend a class or implement an interface, however, it cannot do both at the same time:

class BaseClass {
    void baseMethod() {
        System.out.println("Base method");
    }
}

interface MyInterface {
    void myMethod();
}

BaseClass anonymousObject1 = new BaseClass() {
    @Override
    void baseMethod() {
        System.out.println("New implementation of base method");
    }
};

MyInterface anonymousObject2 = new MyInterface() {
    @Override
    public void myMethod() {
        System.out.println("Implementation of myMethod()");
    }
};

An anonymous class has access to all members (fields, methods, and nested classes) of the enclosing class, including private members. Additionally, an anonymous class can access final or effectively final local variables and parameters of the enclosing method:

class OuterClass {
    private int privateField = 10;

    void someMethod(final int parameter) {
        final int localVariable = 20;

        MyInterface anonymousObject = new MyInterface() {
            @Override
            public void myMethod() {
                System.out.println(privateField);
                System.out.println(parameter);
                System.out.println(localVariable);
            }
        };

        anonymousObject.myMethod();
    }
}

An anonymous class does not have a name, so you cannot directly reference its members from outside the class body. However, you can reference the members of the interface or abstract class that the anonymous class implements or extends:

interface MyInterface {
    void myMethod();
    int myField = 10;
}

MyInterface anonymousObject = new MyInterface() {
    @Override
    public void myMethod() {
        System.out.println("Implementation of myMethod()");
    }
};

anonymousObject.myMethod();
System.out.println(MyInterface.myField);

Anonymous classes differ from regular, top-level classes in several ways:

  1. Anonymous classes are defined and instantiated at the same time, without an explicit name, whereas top-level classes are defined separately and instantiated using the new keyword.

  2. Anonymous classes are defined at the point of use, typically as an argument to a method or as an initializer, while top-level classes are defined independently.

  3. Anonymous classes cannot have explicit access modifiers, constructors, or static members, while top-level classes can have all of these.

  4. Anonymous classes are used for creating one-time implementations or instances, while top-level classes are used for creating reusable and named classes.

In summary, anonymous classes are useful when you need to create a one-time implementation of an interface or abstract class without the need for a named class. They provide a concise way to define and instantiate a class in a single expression.

Finally, to wrap up this section, here’s a table that summarizes many properties of each type of nested class:

Property Static Nested Class Inner Class Local Class Anonymous Class
Association with Outer Class Loosely associated (can exist without an instance of the outer class) Tightly coupled (cannot exist without an instance of the outer class) Tightly coupled (associated with an instance of the enclosing block) Tightly coupled (instantiated within an expression and associated with an instance of the enclosing block)
Can Declare Static Members Yes (including static methods and fields) No (except final static fields) No (cannot declare static members, only final variables) No (cannot declare static members, only final variables)
Access to Members of the Outer Class Only static members Both static and instance members Both static and instance members Both static and instance members
Requires Reference to Outer Class Instance No Yes Yes (implicitly final or effectively final variables from the enclosing scope) Yes (implicitly final or effectively final variables from the enclosing scope)
Typical Use Cases Grouping classes that are used in only one place, enhancing encapsulation Handling events, accessing private members of the outer class, providing more readable and maintainable code Encapsulating complex code within a method without making it visible outside Simplifying the instantiation of objects that are meant to be used once or where the class definition is unnecessary

Classes and Source Files

It’s important to note that you can have one or more class definitions in one Java source file. However, you should follow these rules:

Public Class Rule

If a Java class is declared as public, the name of the file must exactly match the name of the public class, including case sensitivity, with the addition of the .java extension. For example, if you have a public class named MyClass, then the source file must be named MyClass.java:

// File name: MyClass.java
public class MyClass {
    // class body
}

Single Public Class Per File

A Java source file can contain multiple classes, but it can only have one public class. If there are multiple classes in a file and one of them is declared public, the file name must match the name of the public class. For example, if PublicClass is the public class, the file must be named PublicClass.java, and it can also contain AnotherClass which is not public:

// File name: PublicClass.java
public class PublicClass {
    // class body
}

class AnotherClass {
    // class body
}

No Public Class

If there’s no public class in the file, any name can be used. For example, the following file, ManyClasses.java contains multiple classes, none of which are public:

// File name: ManyClasses.java
class FirstClass {
    // class body
}

class SecondClass {
    // class body
}

Non-Public Classes

If multiple non-public classes exist in a single file, then the file name does not need to match the name of any of the classes. For example, you can have a file named UtilityClasses.java containing multiple non-public classes that don’t match this name:

// File name: UtilityClasses.java
class HelperClass {
    // class body
}

class AnotherHelperClass {
    // class body
}

Case Sensitivity

Java is case-sensitive. If your class is named CaseSensitiveClass, the file name must match exactly (CaseSensitiveClass.java):

// File name: CaseSensitiveClass.java
public class CaseSensitiveClass {
    // class body
}

So the main restriction in Java is that a source file cannot contain more than one public class. This helps in organizing code and making it easier to manage. Each public class must be in its own source file, and the file name must match the class name (including case sensitivity) with the .java extension.

However, a single Java source file can contain any number of non-public classes. These classes are by default package-private, and the file can also contain protected or private nested classes within public or package-private classes. This flexibility allows for logically related classes to be grouped together within the same file if they are not intended for public use, aiding in encapsulation and modular design.

Key Points

Practice Questions

1. Consider the following code snippet:

public class Main {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("Java");
        StringBuilder sb2 = new StringBuilder("Python");
        sb1 = sb2;
        // More code here
    }
}

After the execution of the above code, which of the following statements is true regarding garbage collection?

A) Both sb1 and sb2 are eligible for garbage collection.
B) Only the StringBuilder object initially referenced by sb1 is eligible for garbage collection.
C) Only the StringBuilder object initially referenced by sb2 is eligible for garbage collection.
D) Neither of the StringBuilder objects are eligible for garbage collection.

2. Which of the following are reserved keywords in Java? (Choose all that apply.)

A) implement
B) array
C) volatile
D) extends

3. Consider the following code snippet:

1. // calculates the sum of numbers
2. public class Calculator {
3.     /* Adds two numbers
4.      * @param a the first number
5.      * @param b the second number
6.      * @return the sum of a and b
7.      */
8.     public int add(int a, int b) {
9.         // return the sum
10.        return a + b;
11.    }
12.    //TODO: Implement subtract method
13.}

Which of the following statements are true about the comments in the above code? (Choose all that apply.)

A) Line 1 is an example of a single-line comment.
B) Lines 3-7 demonstrate the use of a javadoc comment.
C) Line 9 uses a javadoc comment to explain the add method.
D) Line 12 uses a special TODO comment, different from a single-line comment.
E) Lines 3-7 is a block comment that is used as if it were a javadoc comment.

4. Consider you have the following two Java files located in the same directory:

// File 1: Calculator.java
package math;

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

// File 2: Application.java
package app;

import math.Calculator;

public class Application {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(5, 3));
    }
}

Which of the following statements is true regarding the package and import statements in Java?

A) The import statement in Application.java is unnecessary because both classes are in the same directory.
B) The import statement in Application.java is necessary for using the Calculator class because they belong to different packages.
C) The Calculator class will not be accessible in Application.java due to being in a different directory.
D) Removing the package statement from both files will allow Application.java to use Calculator without an import statement, regardless of directory structure.

5. Consider the default access levels provided by Java’s four access modifiers: public, protected, default (no modifier), and private. Which of the following statements correctly describe the access levels granted by these modifiers? (Choose all that apply.)

A) A public class or member can be accessed by any other class in the same package or in any other package.
B) A protected member can be accessed by any class in its own package, but from outside the package, only by classes that extend the class containing the protected member.
C) A member with default (no modifier) access can be accessed by any class in the same package but not from a class in a different package.
D) A private member can be accessed only by methods that are members of the same class or within the same file.
E) A protected member can be accessed by any class in the Java program, regardless of package.

6. Which of the following class declarations correctly demonstrates the use of access modifiers, class keyword, and class naming conventions in Java?

A) class public Vehicle { }
B) public class vehicle { }
C) Public class Vehicle { }
D) public class Vehicle { }
E) classVehicle public { }

7. Consider the following code snippet:

public class Counter {
    public static int COUNT = 0;
    
    public Counter() {
        COUNT++;
    }
    
    public static void resetCount() {
        COUNT = 0;
    }
    
    public int getCount() {
        return COUNT;
    }
}

Which of the following statements are true about static and instance members within the Counter class? (Choose all that apply.)

A) The COUNT variable can be accessed directly using the class name without creating an instance of Counter.
B) The getCount() method is an example of a static method because it returns the value of a static variable.
C) Every time a new instance of Counter is created, the COUNT variable is incremented.
D) The resetCount() method resets the COUNT variable to 0 for all instances of Counter.

8. Which of the following are valid field name identifiers in Java? (Choose all that apply.)

A) int _age;
B) double 2ndValue;
C) boolean is_valid;
D) String $name;
E) char #char;

9. Consider the syntax used to declare methods in a class. Which of the following method declarations is correct according to Java syntax rules?

A) int public static final computeSum(int num1, int num2)
B) private void updateRecord(int id) throws IOException
C) synchronized boolean checkStatus [int status]
D) float calculateArea() {}

10. Given the method declarations below, which of them have the same method signature?

A) public void update(int id, String value)
B) private void update(int identifier, String data)
C) public boolean update(String value, int id)
D) void update(String value, int id)
E) protected void update(int id, int value) throws IOException

11. Given this class:

public class AccountManager {
    private void resetAccountPassword(String accountId) {
        // Implementation code here
    }
    
    void auditTrail(String accountId) {
        // Implementation code here
    }
    
    protected void notifyAccountChanges(String accountId) {
        // Implementation code here
    }
    
    public void updateAccountInformation(String accountId) {
        // Implementation code here
    }
}

Which of the following statements correctly describe the accessibility of the methods within the AccountManager class from a class in the same package and from a class in a different package?

A) The resetAccountPassword method can be accessed from any class within the same package but not from a class in a different package.
B) The auditTrail method can be accessed from any class within the same package and from subclasses in different packages.
C) The notifyAccountChanges method can be accessed from any class within the same package and from subclasses in different packages.
D) The updateAccountInformation method can be accessed from any class, regardless of its package.

12. What will be the output of this program?

public class TestPassByValue {
    public static void main(String[] args) {
        int originalValue = 10;
        TestPassByValue test = new TestPassByValue();
        System.out.println("Before calling changeValue: " + originalValue);
        test.changeValue(originalValue);
        System.out.println("After calling changeValue: " + originalValue);
    }

    public void changeValue(int value) {
        value = 20;
    }
}

A)

Before calling changeValue: 10  
After calling changeValue: 20  

B)

Before calling changeValue: 10  
After calling changeValue: 10  

C)

Before calling changeValue: 20  
After calling changeValue: 20  

D)

Before calling changeValue: 20  
After calling changeValue: 10  

13. What will be the output of the following program?

public class Test {
    public static void main(String[] args) {
        print(null);
    }

    public static void print(Object o) {
        System.out.println("Object");
    }

    public static void print(String s) {
        System.out.println("String");
    }
}

A) Object
B) String
C) Compilation fails
D) A runtime exception is thrown

14. Which of the following method declarations correctly uses varargs? Choose all that apply.

A) public void print(String... messages, int count)
B) public void print(int count, String... messages)
C) public void print(String messages...)
D) public void print(String[]... messages)
E) public void print(String... messages, String lastMessage)

15. Given the class Vehicle:

public class Vehicle {
    private String type;
    private int maxSpeed;

    public Vehicle(String type) {
        this.type = type;
    }

    public Vehicle(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }

    // Additional methods here
}

Which of the following statements is true regarding its constructors?

A) The class Vehicle demonstrates constructor overloading by having multiple constructors with different parameter lists.
B) The class Vehicle will compile with an error because it does not provide a default constructor.
C) It is possible to create an instance of Vehicle with both type and maxSpeed initialized.
D) Calling either constructor will initialize both type and maxSpeed fields of the Vehicle class.

16. Consider the following class with an instance initializer block:

public class Library {
    private int bookCount;
    private List<String> books;

    {
        books = new ArrayList<>();
        books.add("Book 1");
        books.add("Book 2");
        // Instance initializer block
    }

    public Library(int bookCount) {
        this.bookCount = bookCount + books.size();
    }

    public int getBookCount() {
        return bookCount;
    }

    // Additional methods here
}

Given the Library class above, which of the following statements accurately describe the role and effect of the instance initializer block?

A) The instance initializer block is executed before the constructor, initializing the books list and adding two books to it.
B) The instance initializer block replaces the need for a constructor in the Library class.
C) Instance initializer blocks cannot initialize instance variables like books.
D) If multiple instances of Library are created, the instance initializer block will execute each time before the constructor, ensuring the books list is initialized and populated for each object.

17. Consider the following Java class with a static initializer block:

public class Configuration {
    private static Map<String, String> settings;
    
    static {
        settings = new HashMap<>();
        settings.put("url", "https://eherrera.net");
        settings.put("timeout", "30");
        // Static initializer block
    }

    public static String getSetting(String key) {
        return settings.get(key);
    }

    // Additional methods here
}

Given the Configuration class above, which of the following statements accurately describe the role and effect of the static initializer block?

A) The static initializer block is executed only once when the class is first loaded into memory, initializing the settings map with default values.
B) The static initializer block allows instance methods to modify the settings map without creating an instance of the Configuration class.
C) static initializer blocks are executed each time a new instance of the Configuration class is created.
D) The static initializer block is executed before any instance initializer blocks or constructors, when an instance of the class is created.

18. Consider the following class definition:

public class InitializationOrder {
    static {
        System.out.println("1. Static initializer");
    }

    private static int staticValue = initializeStaticValue();

    private int instanceValue = initializeInstanceValue();

    {
        System.out.println("3. Instance initializer");
    }

    public InitializationOrder() {
        System.out.println("4. Constructor");
    }

    private static int initializeStaticValue() {
        System.out.println("2. Static value initializer");
        return 0;
    }

    private int initializeInstanceValue() {
        System.out.println("3. Instance value initializer");
        return 0;
    }

    public static void main(String[] args) {
        new InitializationOrder();
    }
}

When the main method of the InitializationOrder class is executed, what is the correct order of execution for the initialization blocks, method calls, and constructor?

A)

   1. Static initializer
   2. Static value initializer
   3. Instance initializer
   3. Instance value initializer
   4. Constructor 

B)

   1. Static initializer
   2. Static value initializer
   3. Instance value initializer
   3. Instance initializer
   4. Constructor 

C)

   1. Static initializer
   3. Instance initializer
   2. Static value initializer
   3. Instance value initializer
   4. Constructor

D)

   2. Static value initializer
   1. Static initializer
   3. Instance value initializer
   3. Instance initializer
   4. Constructor 

19. Consider a class CustomObject that does not explicitly override any methods from java.lang.Object:

public class CustomObject {
    // Class implementation goes here
}

Which of the following statements correctly reflect the outcomes when methods from java.lang.Object are used with instances of CustomObject? (Choose all that apply.)

A) Invoking toString() on an instance of CustomObject will return a String that includes the class name followed by the @ symbol and the object’s hashcode.
B) Calling equals(Object obj) on two different instances of CustomObject that have identical content will return true because they are instances of the same class.
C) Using hashCode() on any instance of CustomObject will generate a unique integer that remains consistent across multiple invocations within the same execution of a program.
D) The clone() method can be used to create a shallow copy of an instance of CustomObject without the need for CustomObject to implement the Cloneable interface.

20. Consider the code snippet below that demonstrates the use of a static nested class:

public class OuterClass {
    private static String message = "Hello, World!";

    static class NestedClass {
        void printMessage() {
            // Note: A static nested class can access the static members of its outer class.
            System.out.println(message);
        }
    }

    public static void main(String[] args) {
        OuterClass.NestedClass nested = new OuterClass.NestedClass();
        nested.printMessage();
    }
}

Which of the following statements is true regarding static nested classes in Java?

A) A static nested class can access both static and non-static members of its enclosing class directly.
B) Instances of a static nested class can exist without an instance of its enclosing class.
C) A static nested class can only be instantiated within the static method of its enclosing class.
D) Static nested classes are not considered members of their enclosing class and cannot access any members of the enclosing class.

21. Consider the following code snippet that demonstrates the use of a non-static nested (inner) class:

public class OuterClass {
    private String message = "Hello, World!";

    class InnerClass {
        void printMessage() {
            System.out.println(message);
        }
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        OuterClass.InnerClass inner = outer.new InnerClass();
        inner.printMessage();
    }
}

Which of the following statements is true regarding non-static nested (inner) classes in Java?

A) A non-static nested class can directly access both static and non-static members of its enclosing class.
B) Instances of a non-static nested class can exist independently of an instance of its enclosing class.
C) A non-static nested class cannot access the non-static members of its enclosing class directly.
D) Non-static nested classes must be declared static to access the static members of their enclosing class.

22. Consider the following code snippet demonstrating the use of a local class within a method:

public class LocalClassExample {
    public void printEvenNumbers(int[] numbers, int max) {
        class EvenNumberPrinter {
            public void print() {
                for (int number : numbers) {
                    if (number % 2 == 0 && number <= max) {
                        System.out.println(number);
                    }
                }
            }
        }
        EvenNumberPrinter printer = new EvenNumberPrinter();
        printer.print();
    }

    public static void main(String[] args) {
        LocalClassExample example = new LocalClassExample();
        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        example.printEvenNumbers(numbers, 6);
    }
}

Which of the following statements correctly describe local classes in Java, based on the example provided?

A) Local classes can be declared within any block that precedes a statement.
B) Instances of a local class can be created and used outside of the block where the local class is defined.
C) Local classes are a type of static nested class and can access both static and non-static members of the enclosing class directly.
D) Local classes can access local variables and parameters of the enclosing block only if they are declared final or effectively final.

23. Consider the following Java code snippet demonstrating the use of an anonymous class:

public class HelloWorld {
    interface HelloWorldInterface {
        void greet();
    }

    public void sayHello() {
        HelloWorldInterface myGreeting = new HelloWorldInterface() {
            @Override
            public void greet() {
                System.out.println("Hello, world!");
            }
        };
        myGreeting.greet();
    }

    public static void main(String[] args) {
        new HelloWorld().sayHello();
    }
}

Which of the following statements is true about anonymous classes in Java?

A) Anonymous classes can implement interfaces and extend classes without the need to declare a named class.
B) An anonymous class must override all methods in the superclass or interface it declares it is implementing or extending.
C) Anonymous classes can have constructors as named classes do.
D) Instances of anonymous classes cannot be passed as arguments to methods.

24. Which of the following statements accurately reflects a valid rule regarding how classes and source files are organized?

A) A source file can contain multiple public classes.
B) Private classes can be declared at the top level in a source file.
C) A public class must be declared in a source file that has the same name as the class.
D) If a source file contains more than one class, none of the classes can be public.

Do you like what you read? Would you consider?


Do you have a problem or something to say?

Report an issue with the book

Contact me