Structs have their place, even in Java. You should only use them if the following two things are true:
- You just need to aggregate data that doesn't have any behavior, e.g. to pass as a parameter
- It doesn't matter one bit what sort of values that aggregate data has
If this is the case, then you should make the fields public and skip the getters/setters. Getters and setters are clunky anyway, and Java is silly for not having properties like a useful language. Since your struct-like object shouldn't have any methods anyway, public fields make the most sense.
However, if either one of those do not apply, you're dealing with a real class. That means all fields should be private. (If you absolutely need a field at a more accessible scope, use a getter/setter.)
To check if your supposed-struct has behavior, look at when the fields are used. If it seems to violate tell, don't ask, then you need to move that behavior into your class.
If some of your data shouldn't change, then you need to make all those fields final. You might consider making your class immutable. If you need to validate your data, then provide validation in the setters and constructors. (A useful trick is to define a private setter and modify your field within your class using only that setter.)
Your Bottle example would most likely fail both tests. You could have (contrived) code that looks like this:
public double calculateVolumeAsCylinder(Bottle bottle) {
return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}
Instead it should be
double volume = bottle.calculateVolumeAsCylinder();
If you changed the height and diameter, would it be the same bottle? Probably not. Those should be final. Is a negative value ok for the diameter? Must your Bottle be taller than it is wide? Can the Cap be null? No? How are you validating this? Assume the client is either stupid or evil. (It's impossible to tell the difference.) You need to check these values.
This is what your new Bottle class might look like:
public class Bottle {
private final int height, diameter;
private Cap capType;
public Bottle(final int height, final int diameter, final Cap capType) {
if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");
setCapType(capType);
this.height = height;
this.diameter = diameter;
}
public double getVolumeAsCylinder() {
return height * (diameter / 2.0) * Math.PI;
}
public void setCapType(final Cap capType) {
if (capType == null) throw new NullPointerException("capType cannot be null");
this.capType = capType;
}
// potentially more methods...
}