tl;dr
Either:
- Make a
record
like this, in Java 16 and later:
public record Planet( String name , LocalDate discovered ) {}
- Or, before Java 16, make a class where you:
- Mark all member fields
final
and private
.
- Make getter methods as needed, but no setter methods.
Record
Just use the new records feature in Java 16 (previewed in Java 15).
Define your class as a record
when its main job is to transparently and immutably carry data. The compiler implicitly creates a constructor, the getters, hashCode
& equals
, and toString
.
Notice that the getter methods implicitly defined in a record do not begin with the JavaBeans-style get…
wording. The getter method is simply the name of member field as defined in the parentheses following the class name.
Of course, if your getter methods provide access to an object that is itself mutable, being contained in a record does nothing to stop the calling programmer from mutating the contained object. Notice in the example class next that both String
and LocalDate
classes are themselves immutable by design. So the mutability of a contained object is a non-issue here.
package org.example;
import java.time.LocalDate;
public record Planet( String name , LocalDate discovered )
{
}
Using that record.
Planet Earth = new Planet( "Earth" , LocalDate.of( 2020 , 1 , 16 ) );
System.out.println( "Earth" );
System.out.println( "------------------------------------" );
System.out.println( "Earth.name: " + Earth.name() );
System.out.println( "Earth.discovered: " + Earth.discovered() );
When run.
Earth
------------------------------------
Earth.name: Earth
Earth.discovered: 2020-01-16
Class
Without the records feature, to make sure a class is immutable you should:
- Mark the member fields
final
. This means the field cannot be assigned a different object after the constructor has finished.
- Mark the member fields
private
. This means objects of other classes will not have direct access to read or change those fields.
- Provide getter methods, if needed, but no setter methods. By convention, the JavaBeans-style
get…
or is…
naming is used.
You should also provide appropriate override implementations of hashCode
, equals
, and toString
. Your IDE will help generate the source code for those.
package org.example;
import java.time.LocalDate;
import java.util.Objects;
public class Planète
{
// Member fields
final String name;
final LocalDate discovered;
// Constructors
public Planète ( String name , LocalDate discovered )
{
Objects.requireNonNull( name );
Objects.requireNonNull( discovered );
this.name = name;
this.discovered = discovered;
}
// Getters (read-only immutable class, no setters)
public String getName ( ) { return this.name; }
public LocalDate getDiscovered ( ) { return this.discovered; }
// Object class overrides
@Override
public boolean equals ( Object o )
{
if ( this == o ) return true;
if ( o == null || getClass() != o.getClass() ) return false;
Planète planète = ( Planète ) o;
return getName().equals( planète.getName() ) && getDiscovered().equals( planète.getDiscovered() );
}
@Override
public int hashCode ( )
{
return Objects.hash( getName() , getDiscovered() );
}
@Override
public String toString ( )
{
return "Planète{ " +
"name='" + name + '\'' +
" | discovered=" + discovered +
" }";
}
}
Using that class.
Planète Earth = new Planète( "Earth" , LocalDate.of( 2020 , 1 , 16 ) );
System.out.println( "Earth" );
System.out.println( "------------------------------------" );
System.out.println( "Earth.getName: " + Earth.getName() );
System.out.println( "Earth.getDiscoveryDate: " + Earth.getDiscovered() );
Side issues
Do not start a decimal integer literal with 0
. The leading zero makes the number octal rather decimal. So your code passing 2020,01,16
should be 2020,1,16
.
Never use the Date
class, nor Calendar
or SimpleDateFormat
. These terrible classes are now legacy, supplanted years ago by the modern java.time classes defined in JSR 310. In code above, we used java.time.LocalDate
to represent a date-only value, without a time-of-day and without a time zone.