Encapsulation does have a purpose, but it can also be misused or abused.
Consider something like the Android API which has classes with dozens (if not hundreds) of fields. Exposing those fields the consumer of the API makes it harder to navigate and use, also it gives the user the false notion that he can do whatever he wants with those fields which may conflict with how they are supposed to be used. So encapsulation is great in that sense for maintainability, usability, readability and avoiding crazy bugs.
On the other hand, POD or plain old data types, like a struct from C/C++ in which all the fields are public can be useful as well. Having useless getters/setters like the ones generated by the @data annotation in Lombok is just a way to keep the "Encapsulation pattern". One of the few reason we do "useless" getters/setters in Java is that the methods provide a contract.
In Java, you can not have fields in an interface, therefore you use getters and setters to specify a common property that all implementers of that interface have. In more recent languages like Kotlin or C# we see the concept of properties as fields for which you can declare a setter and getter. In the end, useless getters/setters are more of a legacy that Java has to live with, unless Oracle adds properties to it. Kotlin, for example, which is another JVM language developed by JetBrains, has data classes which basically do what the @data annotation does in Lombok.
Also here are a few examples:
class DataClass
{
private int data;
public int getData() { return data; }
public void setData(int data) { this.data = data; }
}
This is a bad case of encapsulation. The getter and setter are effectively useless. Encapsulation is mostly used because this is the standard in languages like Java. Does not actually help, besides maintaining consistency across the code base.
class DataClass implements IDataInterface
{
private int data;
@Override public int getData() { return data; }
@Override public void setData(int data) { this.data = data; }
}
This is a good example of encapsulation. Encapsulation is used to enforce a contract, in this case IDataInterface. The purpose of encapsulation in this example is to make the consumer of this class use the methods that are provided by the interface. Even though the getter and setter don't do anything fancy, we have now defined a common trait between DataClass and other implementers of IDataInterface. Thus I can have a method like this:
void doSomethingWithData(IDataInterface data) { data.setData(...); }
Now, when talking about encapsulation I think its important to also adress the syntax problem. I often see people complain about the syntax that is necessary to enforce encapsulation rather than encapsulation itself. One example that comes to mind is from Casey Muratori (you can see his rant here).
Suppose you have a player class that uses encapsulation and want to move his position by 1 unit. The code would look like this:
player.setPosX(player.getPosX() + 1);
Without encapsulation it would look like this:
player.posX++;
Here he argues that encapsulations leads to a lot more typing with no added benefits and this can in many cases be true, but notice something. The argument is against the syntax, not encapsulation itself. Even in languages like C that lack the concept of encapsulation you will often see variables in structs prexifed or sufixed with '_' or 'my' or whatever to signify that they should not be used by the consumer of the API, as if they were private.
The fact of the matter is encapsulation can help make code much more maintainable and easy to use. Consider this class:
class VerticalList implements ...
{
private int posX;
private int posY;
... //other members
public void setPosition(int posX, int posY)
{
//change position and move all the objects in the list as well
}
}
If the variables were public in this example then a consumer of this API would be confused as to when to use posX and posY and when to use setPosition(). By hiding those details you help the consumer better use your API in an intuitive way.
The syntax is a limitation in many languages though. However newer languages offer properties which gives us the nice syntax of publice members and the benefits of encapsulation. You will find properties in C#, Kotlin, even in C++ if you use MSVC. here is an example in Kotlin.
class VerticalList : ...
{
var posX: Int
set(x) { field = x; ... }
var posY: Int
set(y) { field = y; ... }
}
Here we achieved the same thing as in the Java example, but we can use posX and posY as if they were public variables. When I try to change their value though, the body of the settter set() will be executed.
In Kotlin for example, this would be the equivalent of a Java Bean with getters, setters, hashcode, equals and toString implemented:
data class DataClass(var data: Int)
Notice how this syntax allows us to do a Java Bean in one line. You correctly noticed the problem that a language like Java has in implementing encapsulation, but that is the fault of Java not of encapsulation itself.
You said that you use Lombok's @Data to generate getters and setters. Notice the name, @Data. It's mostly meant to be used on data classes that only store data and are meant to be serialized and deserialized. Think of something like a save file from a game. But in other scenarios, like with a UI element, you most definetely want setters because just changing the value of a variable may not be enough to get the expected behaviour.
"It will create getters, setters and setting constructors for all private fields."
- The way you describe this tool, it sounds like it is maintaining encapsulation. (At least in a loose, automated, somewhat anemic-model sense.) So what exactly is the problem?