The answer is Encapsulation
Consider this.
public class Point
{
int x, y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
//no getters or setters needed, I can just modify or read x and y directly.
}
public class FinalPoint
{
public final Point point;
public FinalPoint(int x, int y)
{
this.point = new Point(x, y);
}
//no getters needed, I'll just read point since it is public
}
Right now, this FinalPoint has a final Point as an instance field. On the one hand, it means that that instance field cannot be reassigned. However, the fields of that instance field can definitely be reassigned.
For example.
FinalPoint p = new FinalPoint(1, 2);
p.point.x = 4; //I have now modified state! final did not protect us here
The final keyword is powerful, but it does not mean that your data is unchangeable. It only means that that surface level reference is unchangeable - it will force a reference to always point to the same object. That does not stop the object its pointing to from changing it's internal state as much as it wants. The only guarantee final makes is that you always be pointing at the same object.
Which brings us to encapsulation. Encapsulation was created to solve this exact problem and more.
Consider this.
public class EncapsulatedPoint
{
private Point point;
public EncapsulatedPoint(int x, int y)
{
this.point = new Point(x, y);
}
public int getX() { return this.point.x; }
public int getY() { return this.point.y; }
}
Now, because I have encapsulated Point and exposed only the data I can safely expose, I have protected myself from modification. It is impossible to change this object (without using reflection or other hacks the designers are actively removing). It truly is Immutable.
Of course, Encapsulation is not superior to using final. There is a time and a place for both. Knowing when and where allows you to make your software secure.