1. Properties: getters and setters

When a large project is being developed by dozens of programmers at the same time, problems often crop up if they handle the data stored in class fields differently.

Maybe people fail to study the class documentation in detail, or perhaps it doesn't describe every case. As a result, there are frequent situations when an object's internal data can become "corrupted", making the object invalid.

To avoid these situations, it is customary to make all class fields private in Java. Only the class's methods can modify the variables of the class. No methods from other classes can directly access the variables.

If you want other classes to be able to get or change the data inside objects of your class, you need to add two methods to your class — a get method and a set method. Example:

Code Note
class Person
{
   private String name;

   public Person(String name)
   {
      this.name = name;
   }

   public String getName()
   {
      return name;
   }

   public void setName(String name)
   {
      this.name = name;
   }
}


private name field



Initialization of the field via the constructor


getName() — This method returns the value of the name field




setName() — This method changes the value of the name field

No other class can directly change the value of the name field. If someone needs to get the value of the name field, they will have to call the getName() method on a Person object. If some code wants to change the value of the name field, it will need to call the setName() method on a Person object.

The getName() method is also called the "getter for the name field", and the setName() method is called the "setter for the name field".

This is a very common approach. In 80-90% of all Java code, you will never see public variables in a class. Instead, they will be declared private (or protected), and each variable will have public getters and setters.

This approach makes the code longer, but more reliable.

Accessing a class variable directly is like turning your car through double yellow lines: it's easier and faster, but if everyone does it, then things get worse for everyone.

Let's say you want to create a class that describes a point (x, y). Here's how a novice programmer would do it:

class Point
{
   public int x;
   public int y;
}

Here's how an experienced Java programmer would do it:

Code
class Point {
   private int x;
   private int y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }

   public int getX() {
      return x;
   }

   public void setX(int x) {
      this.x = x;
   }

   public int getY() {
      return y;
   }

   public void setY(int y) {
      this.y = y;
   }
}

Is the code longer? Undoubtedly.

But you can add parameter validation to getters and setters. For example, you can make sure that x and y are always greater than zero (or not less than zero). Example:

Code Note
class Point {
   private int x;
   private int y;

   public Point(int x, int y) {
      this.x = x < 0 ? 0 : x;
      this.y = y < 0 ? 0 : y;
   }

   public int getX() {
      return x;
   }

   public void setX(int x) {
      this.x = x < 0 ?  0 : x;
   }

   public int getY() {
      return y;
   }

   public void setY(int y) {
      this.y = y < 0 ? 0 : y;
   }
}


2. Object lifetime

You already know that objects are created using the new operator, but how are objects deleted? They don't exist forever. There isn't enough memory for that.

In many programming languages, such as C++, there is a special delete operator for deleting objects. But how does this work in Java?

In Java, everything is arranged a little differently. Java has no delete operator. Does this mean that objects are not deleted in Java? No, they are deleted, of course. Otherwise, Java applications would quickly run out of memory, and there wouldn't be any talk of programs running without interruption for months.

In Java, the deletion of objects is completely automated. The Java machine itself handles the deletion of objects. This process is called garbage collection, and the mechanism that collects garbage is called the garbage collector (GC).

So how does the Java machine know when to delete an object?

The garbage collector divides all objects into "reachable" and "unreachable". If there is at least one reference to an object, it is considered reachable. If there is no variable that refers to an object, the object is considered unreachable and is declared garbage, which means that it can be deleted.

In Java, you can't create a reference to an existing object — you can only assign references that you already have. If we erase all references to an object, then it is lost forever.

Circular references

That logic sounds great until we come upon a simple counterexample: suppose we have two objects that reference each other (store references to each other). No other objects store references to these objects.

These objects cannot be accessed from the code, but they are still referenced.

This is why the garbage collector divides objects into reachable and unreachable rather than "referenced" and "unreferenced".

Reachable objects

First, objects that are 100% alive are added to the reachable list. For example, the current thread (Thread.current()) or the console InputStream (System.in).

Then the list of reachable objects expands to include objects that are referenced by the initial set of reachable objects. Then it is expanded again to include objects that are referenced by this enlarged set, and so on.

That means that if there are some objects that only refer to each other, but there is no way to reach them from reachable objects, then those objects will be considered garbage and will be deleted.


3. Garbage collection

Memory fragmentation

Another important point related to object deletion is memory fragmentation. If you constantly create and delete objects, soon memory will be heavily fragmented: areas of occupied memory will be interspersed with areas of unoccupied memory.

As a result, we can easily get into a situation where we can't create a large object (for example, an array with a million elements), because there is not a large chunk of free memory. In other words, there may be free memory, even a lot of it, but there may not be a large contiguous block of free memory

Memory optimization (defragmentation)

The Java machine solves this problem in a specific way. It looks something like this:

Memory is divided into two parts. All objects are created (and deleted) in just one half of memory. When it comes time to clean up the holes in memory, all objects in the first half are copied to the second half. But they are copied right next to each other so that there are no holes.

The process looks roughly like this:

Step 1: After creating objects

Garbage collection in Java

Step 2: Appearance of "holes"

Garbage collection in Java 2

Step 3: Elimination of "holes"

Garbage collection in Java 3

And that's why you don't need to delete objects. The Java machine simply copies all reachable objects to a new location, and frees the entire area of memory where the objects used to be stored.