In this article, we’ll talk about Java Classes & Objects, one of the most vital components of the Java programming language.
1. Java Classes
In this section, we will dig deep into Java Classes and learn everything about them.
1.1 What is a Class in Java
You can think of a Java class as a blueprint or a template, of how any instance created using this class will be formed. Inside a class, you can define all the information that can be held and all the functionalities that instances of this class should be able to perform.
To define a class, you must use the class keyword as shown below:
access_modifier class className { }
Moreover, classes can extend(inherit) only one class but can implement multiple interfaces.
To extend a class, you just use the extends keyword as shown below:
access_modifier class className extends parentClassName { }
While if you want to implement interfaces, you must use the implements keyword
access_modifier class className implements interfaceName1, interfaceName2, ..., interfaceNameN { }
Last but not least, you can add keywords such as final
, abstract
, static
, sealed
, etc., to restrict how classes should work. Of course, we will dig deeper in the following sections where we will learn about the types of classes that exist.
1.2 Java Class Structure
In this section, we will learn about all the components that a class might have.
1.2.1 Java Class Fields
The fields of a class are its characteristics. For example, for an employee, we might want to be able to hold information such as name, surname, and employee id, so when requested, all info can be returned as a single unit, inside an object.
An Employee class with the aforementioned characteristics could look like this:
public class Employee { private String name; private String surname; private String employeeID; }
Of course, with this implementation, you will never be able to access or modify any Employee instance outside of this class, but we will look in the following sections at how this could be done.
1.2.2 Java Class Constructors
Now that we defined the blueprint of an Employee, we need to be able to create it. To create an Employee
instance you must use a constructor, and unless you define your own constructors, the only one you can use, is the default with new
keyword:
Employee employee = new Employee();
Of course, since we haven’t declared any method that could modify the instance, with the current implementation, we can only create an employee with no data. However, if we declare a constructor like this:
public Employee(String name, String surname, String employeeID) { this.name = name; this.surname = surname; this.employeeID = employeeID; }
We will be able to create a new employee as shown below:
Employee employee = new Employee("Georgios", "Pal", "HGI-87954");
Last but not least, note that we still can’t access or modify any information of any employee object outside Employee
class.
To dig deeper into constructors, read the Java Constructor article.
1.2.3 Java Class Methods
The methods of a class define the behavior of the object. When defining an entity like Employee
, we should use getters(public methods that allow you to access fields of an object) and setters(public methods that allow you to modify fields of an object).
For instance, in order to be able to access and modify Employee
objects, you should change the Employee class as shown below:
public class Employee { private String name; private String surname; private String employeeID; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public String getEmployeeID() { return employeeID; } public void setEmployeeID(String employeeID) { this.employeeID = employeeID; } }
As a result, you will be able to do the following:
Employee employee = new Employee(); // Modify object employee.setName("Georgios"); employee.setName("Pal"); employee.setEmployeeID("HGI-87954"); //access object String name = employee.getName(); String surname = employee.getSurname(); String employeeID = employee.getEmployeeID();
Note that there might be cases when you do not want to create setters and getters, in order to encapsulate information.
Now, besides getters and setters, you might want to define other methods inside a class, for example, in a class named Circle, you could define one method that calculates the area and one that calculates the perimeter of the circle:
public class Circle { private double radius; public Circle(double radius) { this.radius = radius; } public double getArea() { return Math.PI * Math.pow(radius, 2); } public double getPerimeter() { return 2 * Math.PI * radius; } }
Then, we will be able to create new Circle objects and call these methods and get the results:
Circle circle = new Circle(5); System.out.println(circle.getArea()); System.out.println(circle.getPerimeter());
Finally, there are some other methods that are usually overridden:
- hashCode – Returns a hash code value for the object
- equals – the default implementation just checks for reference equality and may produce unwanted results – should be overridden
- toString – this method is called implicitly when we print an object, unless we override it, it will print the class name along with the hexadecimal representation of the hashcode value.
1.2.4 Nested Classes, Enums, Records, and Interfaces
We can also declare classes enums, records, or interfaces, usually when they have meaning only inside a specific class and won’t be used by other classes.
Consider the following example:
public class Car { private String brand; private String model; private Engine engine; private Oldness oldness; private StartEndProductionYear startEndProductionYear; public Car(String brand, String model, Engine engine, Oldness oldness, StartEndProductionYear startEndProductionYear) { this.brand = brand; this.model = model; this.engine = engine; this.oldness = oldness; this.startEndProductionYear = startEndProductionYear; } public static class Engine { private int cc; private int horsePower; public Engine(int cc, int horsePower) { this.cc = cc; this.horsePower = horsePower; } } public enum Oldness { NEW, USED } public record StartEndProductionYear(int start, int end){}; }
Then we can create a Car object as shown below:
Car car = new Car( "Opel", "Astra", new Car.Engine(1600, 105), Car.Oldness.NEW, new Car.StartEndProductionYear(2004, 2006) );
Moreover, you could add a nested interface and have some other classes implement it.
1.3 How to Import a Class in Java
To import a class, you should type the word import, along with the full path of the location of the class.
For example, if you want to import the ArrayList class, you should write:
import java.util.ArrayList
When it comes to user-defined classes, if two classes live in the same package, you won’t have to import them at all, for example, in the following structure:
If you want to use AccountType
class inside Account
class, you do not need to import it. However, if you want to use e.g. Account
inside AccountRepository
, you would have to write:
import com.youlearncode.springBootJPA.model.Account;
1.4 Best Practices for Writing Java Classes
1.4.1 Naming
The name of every class should follow the camel case pattern, with the first letter capitalized. For example, you could name a class “Car
“, “CarController
“, “CarDAO
“.
1.4.2 Fields
Visibility:
The fields of a class should be private so that they only be accessed and modified through setters and getters. However, there are cases where you might want to use the protected access modifier so they are accessed directly by a child class.
Immutability:
In case the application requires that the fields do not change their value, you have three options:
- define the fields as final and initialize them only on object construction
- use a record since all of its fields are final
Of course, you could just not create any setters and at the same time, have an all-args constructor.
Constants:
If we want to define a constant, we must declare it as static final
, in order to save memory(since it is a constant, we do only need one common version for all instances)
Inheritance:
You should try to make the fields reusable and move the common attributes to a higher level.
For example, if we want to have three classes: Person, Employee, and Contractor, we could do it like this:
public class Person { private String name; private String surname; private String SSN; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public String getSSN() { return SSN; } public void setSSN(String SSN) { this.SSN = SSN; } }
public class Employee extends Person { private double monthlySalary; private int daysOff; public double getMonthlySalary() { return monthlySalary; } public void setMonthlySalary(double monthlySalary) { this.monthlySalary = monthlySalary; } public int getDaysOff() { return daysOff; } public void setDaysOff(int daysOff) { this.daysOff = daysOff; } }
public class Contractor extends Person { private double hourlyPayment; private int durationOfContractInMonths; public double getHourlyPayment() { return hourlyPayment; } public void setHourlyPayment(double hourlyPayment) { this.hourlyPayment = hourlyPayment; } public int getDurationOfContractInMonths() { return durationOfContractInMonths; } public void setDurationOfContractInMonths(int durationOfContractInMonths) { this.durationOfContractInMonths = durationOfContractInMonths; } }
Hadn’t we used this pattern, we would have had to write the common attributes on both Employee
and Contractor
.
1.4.3 Constructors
When it comes to constructors, we should have proper naming for the arguments, ideally, the name of each attribute, like this:
public Person(String name, String surname, String SSN) { this.name = name; this.surname = surname; this.SSN = SSN; }
Furthermore, we should try to reuse the constructors that we have already created, by using the this
keyword:
public Person(String name, String surname, String SSN) { this(name, surname); this.SSN = SSN; } public Person(String name, String surname) { this.name = name; this.surname = surname; }
1.4.4 Methods
As for methods, you should take advantage of method overloading and overriding, in order to enhance the readability and maintainability of code.
Method overloading is having methods with the same name but different number and/or type of arguments. Whereas, Method overriding is when a child class or class that implements an interface, “ignores” the previous implementation and defines a new one.
Moreover, we should always move methods that are common for the children classes, inside the parent class.
Last but not least, you should define methods with the most appropriate keywords:
public
if it needs to be accessed from anywhereprotected
if it needs to be accessed only by subclasses or classes in the same package- default(no keyword) if it needs to be accessed by classes in the same package
private
if only methods inside the class should use itfinal
, if a method should never be overriddenstatic
, when:- these methods belong in a Utility class
- implementing the Singleton Design Pattern
- it makes sense to be able to call this method without having created any instances
synchronized
, when in need of synchronization at method-level
1.5 Java Class Types
There are many types of classes that basically are a more restricted version of a normal class. In this section, we will take a brief look at each of them.
1.5.1 Abstract Classes
Abstract classes are meant to provide partial abstraction, as they can have both abstract and non-abstract methods. Moreover, abstract classes may have constructors but you will never be able to instantiate one.
Should you like to learn everything about them, you should read the Java Abstract Class article.
1.5.2 Final Classes
Final classes cannot have any descendants. In other words, by adding the final keyword when declaring a class, you will never be able to extend it.
Learn more about the final keyword in Final Keyword in Java.
1.5.3 Sealed Classes
In JDK 17, sealed classes were added, and they provide a way to restrict which classes can extend other classes.
You can get a deep understanding of how they work by reading Java Sealed Class & Interface.
1.5.4 Nested Static Classes & Nested Non-Static(Inner) Classes
As we saw in the previous sections, we can have classes inside classes. These classes can be categorized into two: static and non-static.
If you create a nested static class, you won’t have to have already created an object of the outer class:
public class OuterClass { public static class NestedClass{} }
Then, in another class, you could do the following:
OuterClass.NestedClass nestedClass = new OuterClass.NestedClass();
On the other hand, if you declare the nested class as non-static, in order to create an instance of the nested class, you would have to first create an object of the OuterClass and then use it to create an object of the NestedClass
:
OuterClass outerClass = new OuterClass(); OuterClass.NestedClass = outerClass.new NestedClass();
1.5.5 Enums
Enum is a special type of class and in order to create one, you type the enum
keyword instead of the class
and it is used mainly when the instance of a class will have specific values.
For example, an enum could be the day of the week, where there are only 7 possible values:
public enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY; }
By using the following commands:
javac Day.java javap Day.class
We can see that under the hood, it is a final class that extends the Enum class:
$ javap Day.class Compiled from "Day.java" public final class Day extends java.lang.Enum<Day> { public static final Day MONDAY; public static final Day TUESDAY; public static final Day WEDNESDAY; public static final Day THURSDAY; public static final Day FRIDAY; public static final Day SATURDAY; public static final Day SUNDAY; public static Day[] values(); public static Day valueOf(java.lang.String); static {}; }
Finally, to master Enums, read the Java Enum article.
1.5.6 Records
Java Record is another special type of class, that is final, all of its members are final. Additionally, unlike any other class that you might create, getters, equals, hashCode, and toString are implemented without the need to do it yourself.
Delve more into Records by reading Java Record.
1.6 Inheritance in Java Classes
As Java is an OOP language, it supports inheritance, and more specifically:
- Inheritance between classes using the
extends
keyword - Inheritance between classes and interfaces using the
implements
keyword
Furthermore, as we said in section 1.1, a class can only extend one class, but it can implement one or more interfaces.
To delve more into this matter, you should read Inheritance in Java.
2. Java Objects
In this section, we will take a deeper look at Java Objects
2.1 What is an Object in Java
An object is nothing more than an instance of a class. As we saw in the previous section, the class is just the blueprint and the object is the actual implementation of the blueprint.
For example, let’s say that we have a class Student:
public class Student { private String name; private String surname; private double averageGrade; public Student(String name, String surname, double averageGrade) { this.name = name; this.surname = surname; this.averageGrade = averageGrade; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public double getAverageGrade() { return averageGrade; } public void setAverageGrade(double averageGrade) { this.averageGrade = averageGrade; } public void study() { System.out.println("Studying..."); } public void skipClass() { System.out.println("Class is boring, skipping..."); } }
Now we can use this blueprint, to create actual objects with values by using the new
keyword:
// Create two students using constructors Student george = new Student("Geo", "Pal", 88); Student nick = new Student("Nick", "Pal", 80); // Create another using empty constructors + setters Student mau = new Student(); mau.setName("Mau"); mau.setSurname("Ave"); mau.setAverageGrade(90); george.skipClass(); nick.study(); mau.study();
The output of the above would be:
Class is boring for Geo, skipping... Nick is Studying... Mau is Studying...
2.2 How to Create an Object in Java
There are many ways to create an object in Java, with the most prominent being the new keyword, that we described in the previous section.
Nevertheless, there are other ways to create one.
Creating an Object using Object.clone():
Consider the following class:
public class Food implements Cloneable { private String name; private Amount amount; private boolean glutenFree; public Food() { } public Food(String name, Amount amount, boolean glutenFree) { this.name = name; this.amount = amount; this.glutenFree = glutenFree; } public Amount getAmount() { return amount; } private static class Amount { private double number; private String metric; public Amount(double number, String metric) { this.number = number; this.metric = metric; } } }
In order to be able to use clone()
on an object, it must implement the Cloneable
interface. Moreover, clone()
does a shallow copy, therefore a member that has custom-type will point to the same memory address and any change to the original object will affect the cloned.
Considering the above, if we do the following:
Food food = new Food("Souvlaki", new Amount(300, "gr"), false ); Food clonedFood = (Food) food.clone(); System.out.println(clonedFood.getAmount() == food.getAmount());
We will see that it will print true
.
Creating an Object using className.class.newInstance():
This way of creating an object has been marked as deprecated since Java 9, so we will just show a brief example:
// no-args constructor must exist since it is used by newInstance() // otherwise an exception will thrown Food newInstanceFood = Food.class.newInstance(); // the object will have name = null, amount = null, glutenFree = 0
Creating an Object using className.class.getConstructor():
You can also create an object by using the following approach:
Constructor<Food> allArgsFoodConstructor = Food.class.getConstructor( String.class, Amount.class, boolean.class ); Food food = allArgsFoodConstructor.newInstance( "Souvlaki", new Amount(300, "gr"), false );
Firstly, we retrieved the specific constructor, by providing the types of parameters and then, by using the newInstance()
method, we created a new object.
Creating an Object using Serialization and Deserialization:
Another way that we can use to create an object is by writing an existing object to a file and then reading from the file. In order to do that, we must first make Food
implement Serializable
and then do the following:
// Creating the souvlaki Food souvlaki = new Food("Souvlaki", new Amount(300, "gr"), false); // Write the souvlaki to file ObjectOutputStream objectOut = new ObjectOutputStream(new FileOutputStream("souvlaki.txt")); objectOut.writeObject(souvlaki); objectOut.close(); // Read from the file ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("souvlaki.txt")); // Create a new object from the file Food souvlakiFromFile = (Food) objectInputStream.readObject(); objectInputStream.close();
2.3 Comparing Objects in Java
When comparing objects of type String, or any wrapper class, the .equals method of the Object class is sufficient to produce the expected results.
However, when dealing with user-defined objects, such as the Student or Food class we examined before, the results might not be expected.
Consider that we have the Food class that we used in the previous section, if we do the following:
Food souvlaki = new Food("Souvlaki", new Amount(300, "gr"), false); Food souvlakiCopy = new Food("Souvlaki", new Amount(300, "gr"), false); System.out.println(souvlaki.equals(souvlakiCopy ));
It will print false
, even though the two foods are exactly the same. This happens because the equals()
just compares the memory addresses of the two objects, and since we have used the new keyword, they must point to a different one.
Should you like to remedy this problem, you must override both equals and hashCode(otherwise we would violate the contract for hashCode
method, that equal objects must have equal hashCodes)
Now if we just override the equals as follows:
public class Food { private String name; private Amount amount; private boolean glutenFree; public Food() { } public Food(String name, Amount amount, boolean glutenFree) { this.name = name; this.amount = amount; this.glutenFree = glutenFree; } public Amount getAmount() { return amount; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Food)) return false; Food food = (Food) o; return glutenFree == food.glutenFree && Objects.equals(name, food.name) && Objects.equals(amount, food.amount); } // // @Override // public int hashCode() { // return Objects.hash(name, amount, glutenFree); // } private static class Amount { private double number; private String metric; public Amount(double number, String metric) { this.number = number; this.metric = metric; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Amount)) return false; Amount amount = (Amount) o; return Double.compare(amount.number, number) == 0 && Objects.equals(metric, amount.metric); } // // @Override // public int hashCode() { // return Objects.hash(number, metric); // } } }
We would get true
, if we rerun the same snippet.
Nevertheless, the following:
System.out.println(souvlaki.hashCode() == souvlakiCopy.hashCode());
Would print false, which is not the expected output, and break the contract for hashCode. Of course, if we uncomment the two hashCode implementations, it would print true, as expected.
3. Conclusion
After reading this article, you should know what a class is, its components, best practices, and how to make a class restricted. Moreover, you learned about what an object is, all the ways to create one, and how to compare two objects properly.
4. Sources
[1]: Javaâ„¢ Tutorials – Classes and Objects – Oracle