January 10, 2024

lealceldeiro

What are Java classes and objects?

To put it simple, a Java class is a "form of classification". It is a concept from Object-Oriented Programming (OOP) that allows us to "classify" (or group together) a type of object(s) —another concept from OOP— An object represents a specific instance of a class. With classes and objects we can represent real-world concepts and entities in our programs as well as their interactions.

For instance, in a program called ZooManager we could have the following classes: Animal, Cage, Food, and ZooKeeper. Likewise, the specific animal, cage, food and zookeeper instances that are present in the Zoo we're managing would be the objects instantiated during the execution of the program. In this case, examples of instances would be teo (a tiger), cage1 (a cage), meat (food), and john (a zookeeper).

How do we decide which classes to define in our program?

Classes define common properties and behaviors for the objects they represent. For example, the Animal class would have the following properties:

and the following behaviors

So, when we identify a collection of objects in our business domain that share the same properties and behaviors, we are usually in the presence of a class to be defined in our program. Sometimes it can be obvious, like knowing we'll need a class called Animal in our ZooManager program, but sometimes it's more difficult to identify when we would benefit from the definition of a class. As we gain experience throughout the years, as developers/software engineers, it becomes easier to identify each case.

Syntax

The simplest form of a class definition in Java you could find is this (Animal could have been any other name we define):


class Animal {
}

But that class isn't very useful, is it? So, let's see everything we have in our power to declare and define our own classes.

Structure

Let's use the chapter about classes from the Java Language Specification (JLS) for Java 21 to understand the anatomy of a class in Java. Notice you may want to check the JLS specific to your Java version.

Formally, this is the structure of a Java class:


{ClassModifier} class TypeIdentifier [TypeParameters] [ClassExtends] [ClassImplements] [ClassPermits] ClassBody

Where

Class body

In short, this is the structure of the class body ( {ClassBodyDeclaration} ):


{
    ClassMemberDeclaration
    InstanceInitializer
    StaticInitializer
    ConstructorDeclaration
}

Let's see what these elements are, along with some examples.

Members

With members ( ClassMemberDeclaration ), we refer to fields ( FieldDeclaration ), methods ( MethodDeclaration ), nested classes ( ClassDeclaration ), and nested interfaces ( InterfaceDeclaration ). The first two are the respective properties and behaviors that we were referring to initially. The other two are covered in next sections, along with InstanceInitializer and StaticInitializer.

Let's see fields and methods.

Fields

Fields are properties, attributes that characterize our classes, they hold the state of the objects created from our classes and are useful for storing the objects information during runtime.

We saw already some examples of properties for our mentioned Animal class. Now, let's see how we can actually write that in our code. Let's enhance our Animal class with some fields:


class Animal {
    String name;
    int energy;
}
Methods

Methods define the behavior of the objects, they specify what "actions" the objects can do during the execution of our program, they define the API through which we (technically speaking, other objects) can interact with them. They can do two things: return information about the object, and/or modify it, that's it, modify the internal state of the object.

Let's see how we can define the actions our Animal objects can do:


class Animal {
    String name;
    int energy;

    void move() {
        energy--;
        System.out.println(name + " moved");
    }

    void eat() {
        energy++;
        System.out.println(name + " ate");
    }
}

In this example, the method move decreases the energy value and prints the name of the animal that moved (i.e.: teo moved).

The method eat does the opposite: it increases the energy value, and also prints the name of the animal that ate (i.e.: teo ate).

Constructors

So far, we've talked a lot about Java classes, but not so much about the objects.

Just to make sure we understand the concept of object, if we were to make an analogy, we could say that Woman is a class, while Ana is an object. Similarly, Man is a class, and John is an object.

Now, let's see how we can create objects in our programs. Java provides us with something called constructors ( ConstructorDeclaration ). They are a "special" piece of code inside the body of our class that allow us to instantiate, create objects from a class definition.

This is the structure of the constructor. Look closely; it looks similar to a method, but it lacks a return type:


    {ConstructorModifier} ConstructorDeclarator [Throws] ConstructorBody

But, it can be better understood with an example, so let's see it in action in our Animal class.


class Animal {
    String name;
    int energy;

    Animal(String name) {   // <-- Constructor added
        this.name = name;
        energy = 100;
    }

    // rest of the code omitted for brevity...
}

Here we've created a constructor that accepts a name argument and assigns it to the name field. It also initializes the energy field with a value of 100.

Actually, there are other ways to create objects, but, to keep it simple, they're not covered here. If you feel curious about it, you can read more about Class.forName(String className) , Object.clone() , and Deserialization .

Up to here, we've defined the means to create an object of type Animal, but, how do we actually instantiate one?

Simple, we use the new keyword.


    Animal teo = new Animal("Teo");
Nested classes

We've mentioned earlier that we can have nested classes as a members of another class. But how does that actually look like? Well, it's quite simple once we have understood fields and methods because their declaration follows the same principle: it's a code structure that "belongs" to the class where they're declared. More, formally: a member class is a class whose declaration is directly enclosed in the body of another class or interface declaration ( JLS 8.5 ). Let's see an example.

Let's suppose we want to represent each part of the body of an animal with a class. And then each animal would be formed by its parts —granted, this is a contrived example, only for demonstration purposes—

At this point, we have two options:

  1. Create two independent classes, each in its own .java file (Animal.java and BodyPart.java)
  2. Create one top level class (Animal), and one inner class (BodyPart), both in one .java file called Animal.java

How do we decide which option is better? Obviously "better" always depends on your specific use case, but for this example, let's say a BodyPart doesn't make any sense without an Animal, so it's better to model the solution as if it's a member of Animal. In this case we declare another class "inside" Animal, with all the same components we've discussed so far.


class Animal {
    class BodyPart {      // <-- inner class added
        String partName;
        double weight;
    }

    /**
     * Holds the information for each body part of this animal.
     */
    Collection<BodyPart> bodyParts;

    // rest of the code omitted for brevity...
}
Nested interfaces

It's difficult to explain what a nested interface is without knowing what's a Java interface . To keep it simple, and focus on classes, which is the topic of the article, I'll oversimplify it: it's basically a group of related methods with empty bodies, and its declaration is quite similar to that of a class. For example, this is an interface called MovingBeing:


interface MovingBeing {
    void move();
}

Now, if we were to declare this interface as a member of the Animal class, we just need to follow the same logic we used to declare the inner class BodyPart:


class Animal {
    interface MovingBeing {     // <-- inner interface added, not very useful now
        void move();
    }

    // rest of the code omitted for brevity...
}

More, formally: a member interface is an interface whose declaration is directly enclosed in the body of another class or interface declaration ( JLS 8.5 ).

Instance and static initializer

There are two elements of the body of the class we haven't covered yet: instance initializers ( InstanceInitializer ) and static initializers ( StaticInitializer ).

They both are a block of code where some initialization happens. The only difference between them is that the instance initializer is tied to the life-cycle of the object being created; hence, the code block is executed once for each object created. The static initializer, on the other hand, is executed only when the class is loaded (usually only once during the program execution). This is more related to the concept of static members (see JLS).

Let's see them in an example.

Instance initializers

Let's suppose we want to move the initialization of the field energy out of the constructor. We can do it by placing the expression energy = 100; inside an instance initializer:


class Animal {
    String name;
    int energy;

    {
        energy = 100;       // <-- initialization moved to this instance initializer
    }

    Animal(String name) {
        this.name = name;
    }

    // rest of the code omitted for brevity...
}
Static initializers

Now, let's suppose that we want to have a field inside the Animal class called perceivedTemperature, which indicates the environment temperature perceived by the animals. In this case we don't want to have a separate value for each animal, because we can say this temperature is the same for every animal —again, this is a contrived example, for demonstration purposes—

To store such field we should not do it with an instance field, but with a static one. This way, when the field is modified, we get the same value, regardless of the Animal instance the value is being accessed from. It would look like this:


class Animal {
    static double perceivedTemperature;

    // rest of the code omitted for brevity...
}

Now, we would like to initialize it in an instance initializer block, how do we do it? Simple:


class Animal {
    static double perceivedTemperature;

    static {
        perceivedTemperature = 32.4d;
    }

    // rest of the code omitted for brevity...
}

The .java file

Where do we write our classes? Inside .java files.

Most of the time, each class is defined in its own .java file. Such class is called a top level class (type).

But it's not mandatory to have only one top level type in a .java file. We can define several of them, as long as at most one of them is public. Put in another way: we can not declare more than one public top level type in the same .java file.

Putting it all together

Let's see some examples where we apply all the knowledge we've learned.

Examples

A public class with two fields, two methods, and one constructor

We saw this one already, but for the sake of completeness, let's review it:


public class Animal {
    String name;
    int energy;

    Animal(String name) {
        this.name = name;
        energy = 100;
    }

    void move() {
        energy--;
        System.out.println(name + " moved");
    }

    void eat() {
        energy++;
        System.out.println(name + " ate");
    }
}
A public class with one protected static inner class, two fields, one instance initializer, and one static initializer

public class Animal {
    protected static class BodyPart {
    }

    String name;
    static double perceivedTemperature;

    {
        name = "Leo";
    }
    static {
        perceivedTemperature = 32.4d;
    }
}
A package-private class with one private static inner class, and one public static inner interface
Side note: interfaces are static by default, but I'm putting it here for demonstration purposes only.

class Animal {
    private static class BodyPart {
        String partName;
    }
    public static interface MovingBeing {
        void move();
    }
}
A sealed class that permits another final class to extend it

sealed class Animal permits Tiger {
}
final class Tiger extends Animal {
}
A sealed class that permits another non-sealed class to extend it

sealed class Animal permits Tiger {
}
non-sealed class Tiger extends Animal {
}

Could we implement our programs entirely without classes?

Certainly. Technically speaking, we don't need to create our own classes to implement a business logic. But, by doing so, we can structure better our code and re-use it more effectively, we can achieve more robust and secure software and more adaptable to future changes. The enormous benefits of designing our code around OOP concepts such as classes and objects pay off immensely in the long term.

Final thoughts

There are much more details to learn about Java classes and objects; we barely scratched the surface here, but hopefully, the content and links I've shared are a good start for you to continue learning on your own and apply that knowledge in building high quality software.