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).
Classes define common properties and behaviors for the objects they represent. For example, the
Animal
class would have the following properties:
name
energy
and the following behaviors
move
(if an animal moves, it consumes energy, the energy
property decreases)
eat
(if an animal eats, it recovers energy, the energy
property increases)
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.
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.
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
{ClassModifier}
is one of: public
, protected
, private
,
abstract
, static
, final
,
sealed
,
non-sealed
, strictfp
.
class
is the Java keyword used to specify we're declaring a class.TypeIdentifier
is the name we want to give the class. In our previous example we used Animal
.
[TypeParameters]
is a collection of types we can define within angle brackets (<>
). For instance,
<T>
. See
generics
and JLS
for more info about this.
[ClassExtends]
is the portion of the class declaration where we can specify, after the keyword extends
, the class
(if any) from which our class is a direct child (descendant, inheritor). For instance,
extends LivingBeing
.
[ClassImplements]
is the portion of the class declaration where we can list, after the keyword implements
, the
interfaces
(if any) whose behavior our class implements. For instance, implements MovingBeing, Eater
.
[ClassPermits]
is used when the
sealed
modifier is specified, and it indicates, after the keyword
permits
,
the other classes (if any) that are allowed to extend our class. For example,
permits Tiger, Ostrich, Bear
.
ClassBody
,
is the body of our class, including the curly braces ({}). See next section.
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.
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 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 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
).
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 aboutClass.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");
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:
.java
file (Animal.java
and
BodyPart.java
)
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...
}
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 ).
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.
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...
}
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...
}
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.
Let's see some examples where we apply all the knowledge we've learned.
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");
}
}
public class Animal {
protected static class BodyPart {
}
String name;
static double perceivedTemperature;
{
name = "Leo";
}
static {
perceivedTemperature = 32.4d;
}
}
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();
}
}
sealed class Animal permits Tiger {
}
final class Tiger extends Animal {
}
sealed class Animal permits Tiger {
}
non-sealed class Tiger extends Animal {
}
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.
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.