EN VI

Java Getter and Setters for simple properties best practice?

2024-03-10 04:00:07
How to Java Getter and Setters for simple properties best practice

I know this is a common question and I've read several answers but they're all very subjective and I'm trying to get an answer on best common practice (i.e what to use a majority of the time).

My background is in C# .net and I'm re-writing an application in Java (ver. 21). With C# a public property with a getter and setter is functionally the same thing as a private field with a public getter and setter (C# example below). The get and set are implicitly used when referencing the property. So in practice 99% of the time I use a public property with getter and setter because it gives me the added benefit of encapsulation and doesn't add any verbosity to the code.

Now with Java I'm struggling when to use getters and setters. If I where to code like I do with C#, most of my java object properties would be private fields with public getter and setter methods. That makes the code really verbose (example1 below) and I'm not sure if it's necessary/common for simple objects like json responses (mapped by jackson) or entities.

So with that all being said, in practice what is the most standard way of handling simple properties that just get and set a field in Java (without data manipulation). Like if I where to see an enterprise application what would be the most standard way of handling example1 and example2 below. Based on my research thus far I'm leaning towards example1 (keeps encapsulation), but I don't want to use getters and setters everywhere if it's actually more common practice to just use public fields with simple properties. Let me know your thoughts.

C# get/set:

namespace SpeedRunAppImport.Model.Entity
{
    public class GameEntity
    {
        public string Name { get; set; }
        
        // Same thing as above
        private string _Name;
        public string Name
        { 
            get
            {
                return _Name;
            }
            set
            {
                _Name = value;
            } 
        }
    }
}

Game Entity:

@Entity
@Table(name = "tbl_game")
public class Game {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)    
    private Integer id;
    private String name;
    private String code;
    private String abbreviation;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getCode() {
         return code;
    }

    public void setCode(String code) {
         this.code = code;
    }

    public String getAbbreviation() {
         return abbreviation;
    }

    public void setAbbreviation(String abbreviation) {
         this.abbreviation = abbreviation;
    }
}

GameResponse (JSON response mapped by Jackson):

public class GameResponse
{
    private String id;
    private String name;
    private String abbreviation;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
         return name;
    }

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

    public String getAbbreviation() {
         return abbreviation;
    }

    public void setAbbreviation(String abbreviation) {
         this.abbreviation = abbreviation;
    }
}

Example1 (using getters/setters):

public void SaveGames(List<GameResponse> games)
{
    _logger.info("Started SaveGames: {@count}", games.size());
    
    var existingGamesVWs = _gameRepo.GetGamesByCode(games.stream().map(x -> x.getId()).toList());            

    var gameEntities = games.stream().map(i -> { var game = new Game();
                              var existingGame = existingGamesVWs.stream().filter(x -> x.code == i.getId()).findFirst().orElse(null);
                              game.setId(existingGame != null ? existingGame.getId() : null);
                              game.setName(i.getName());
                              game.setCode(i.getId());
                              game.setAbbr(i.getAbbreviation());

                              return game;
                            }).toArray(Game[]::new);


    _logger.info(Integer.toString(existingGamesVWs.size()));
}

Example2 using fields (assume public fields on objects above):

public void SaveGames(List<GameResponse> games)
{
    _logger.info("Started SaveGames: {@count}", games.size());
    
    var existingGamesVWs = _gameRepo.GetGamesByCode(games.stream().map(x -> x.id).toList());             

    var gameEntities = games.stream().map(i -> { var game = new Game();
                              var existingGame = existingGamesVWs.stream().filter(x -> x.code == i.id).findFirst().orElse(null);
                              game.id = existingGame != null ? existingGame.id : null;
                              game.name = i.name;
                              game.code = i.id;
                              game.abbr = i.abbreviation;

                              return game;
                            }).toArray(Game[]::new);
}

Solution:

The most common approach or convention with Java is to use Getters and Setters to respect the encapsulation principle and make classes easier to mock/test, as in any other language. Some languages for a long time made this easier and less verbose, C# and Kotlin being good examples of that.

However, Java generally favors stability over syntactic sugar, not to mention that not too many years ago the release cycle used to be a lot longer, and these syntax improvements would take more time to be available.

A couple of ways to achieve it:

1. Explicitly declare getters and setters
public class Example {
   private final String value;

   public Example(String value) {
       this.value = value;
   }

   public String getValue() {
       return this.value;
   }

   public void setValue(String value) {
       this.value = value;
   }
}
2. Use Lombok's annotation processing, which generates the boilerplate at compile time, and can be used at the class level or the property level.
@Getter
@Setter 
// or @Data, which adds more functionality (e.g. equals, hashCode, toString)
@AllArgsConstructor
public class Example {
   private final String value;
}
3. Use Records (Since Java 17), the best equivalent to a data class in other languages.
record Example(String value) {}

At the end of the day, the option depends on preference, but the principle of encapsulation and the testability quality attribute should ideally be respected.

Answer

Login


Forgot Your Password?

Create Account


Lost your password? Please enter your email address. You will receive a link to create a new password.

Reset Password

Back to login