(Updated September 1, 2024)
Table of contents
-
Overview
Classes
Constructors
Variables and Instantiation
Constructors and Methods of the
Person
ClassCopy Constructor
The
toString
MethodStatic Members of a Class
The
this
ReferenceMethod Chaining
Inner Classes and File Scope
Records
Exercises
Overview
Up to this point, we have been solving problems using objects. Recall that an object is an instance of a class. Classes enable us to combine data and methods to operate on that data as a single, manageable type. This combination of data and methods is known as encapsulation. For every class, the data and methods of the class are known as members.
A class can be defined simply as:
public class MyClass
{
public int x;
}
The modifiers public
and private
we have seen throughout and these modifiers will be used extensively in this section. Another modifier, protected
, will be discussed in a separate chapter. All three of these are reserved words in the Java language.
MyClass
has a single variable named x
which is of type int
and is a public
declaration. This means we can access the variable outside of the class. Being that x
is a member of MyClass
, it is also known as a field. Data members of classes are also called fields.
Remember that MyClass
does not actually allocate any memory. This is simply a definition or template for objects that will be created later. Only when we instantiate an object is memory actually allocated and our reference variables contain the addresses of our instantiated objects.
Classes
Using the class definition from above, we know that anything declared in the class as public
can be accessed from outside the class. We know this to be true already because we can use pow()
from the Math
class and nextInt()
from the Scanner
class. If these were not declared as public
, in other words if they were declared as private
, we could not use them in their respective classes. Members declared as private
can only be accessed from within the class itself. This means that only other methods within the class could make use of the private
methods.
When declaring members in a class as named constants or variables, these are declared just as we have always done. This is also true when defining methods that are members of the class. In addition, when defining your methods in a class, you have direct access to the class’s fields (data members). It does not matter if the fields are declared as public
or private
– the methods of the class have direct access.
To solidify the class concepts described thus far and to introduce new concepts, let us define a class called Person. This class will have a given name, surname (last name), and age. Since not everyone has a middle name and the concept of a middle initial is not always straightforward (a person may have more than one middle name), we will forgo the entire middle name concept for this example. The tradition of some Asian cultures where the family name is first and the given name last is also not addressed in this example.
The beginning of the class could look like the following:
public class Person
{
private String givenName;
private String surname;
private int age;
...
Now that we have the basic data types let us consider how we will access and modify the contents of this class. Remember that we will eventually be instantiating objects of this type, and since the fields are private, we cannot directly access these values outside the class.
The fields that are variable declarations and that do not have the static
modifier are known as instance variables. We call these fields instance variables because each object that we instantiate, that is, each instance of the class, will have its own set of these variables.
Consider for a moment that we made everything in the class public
. Why then create a separate user-defined class if everything is public
? We could just create variables in the main()
method and arrange to have them passed as parameters to a group of methods that we later define. Well, that is part of the point. Besides the use of encapsulation as a means of tying the data together with the operations (methods) that maintain the data is the detail of not having to pass these data items to the methods. Using encapsulation, we have tied the data to the operations and simplified the mechanism by which we make the data available to the methods. Recall that we were concerned about the number of parameters and their respective types with user-defined methods. Since the data and the methods operating on the data are in the class, we eliminate the need to pass that data as parameters. Remember that the methods of a class already have direct access to those data fields.
As a final note on the use of private
fields, we force the use of this class to follow strict guidelines. No program may directly manipulate the fields – the program must use the operations provided to access this data.
Let us now define some of the operations we need to perform on the private fields of the Person
class. These operations are itemized below:
1. Set the given name. 2. Return the given name. 3. Set the surname. 4. Return the surname. 5. Set the age. 6. Return the age. 7. Increment the age. 8. Compare two persons for equality. 9. Copy a person. 10. Return a copy of a person. 11. Print a person.
For each of these 11 operations, we will construct a method. It is often difficult to determine if the methods of a class should be public
or private
. The methods for the 11 operations will be public
since they all need to be accessed from outside the class. We will discover later that operations 1, 3, 5, 7 and 9 will be mutator methods and 2, 4 and 6 are accessor methods.
The methods for these operations are shown in Table 1.
Methods for the Person class |
public void setGivenName(String s) Set the given name. |
public String getGivenName() Return the given name. |
public void setSurname(String s) Set the surname. |
public String getSurname() Return the surname. |
public void setAge(int y) Set the age in years. |
public int getAge() Return the age in years. |
public void incAge() Increment the age by one year. |
public boolean equals(Person otherPerson) Compare this person to another person. Return true if all fields are identical. |
public void makeCopy(Person otherPerson) Copy the contents of otherPerson into this object. |
public Person getCopy() Returns a new Person object with a copy of the current object contents. |
@Override public String toString() Print a person. |
Table 1: Methods that represent the operations allowed in the Person class.
Constructors
Before we delve into the details of the methods in Table 1, let us fist consider instantiation. Recall that we create or instantiate objects with the new
reserved word and a constructor is called when the object is created.
Constructors are unique methods of classes. These constructors, which have the same name as the class, are intended to help initialize an object’s instance variables. Every class has at least one constructor, even if you do not define one.
Like methods, the constructors you create may have a list of parameters. The constructor with no parameters is the default constructor. If you do not define any constructor(s), the default constructor will always be created for you because at least one constructor must exist in every class.
Note: If you define a custom constructor and do not provide a default constructor, Java will not create the default constructor.
Since constructors are similar to methods and based on what has been mentioned thus far, they may be overloaded. An overloaded constructor must differ from the other constructors in the same way we overload methods.
Suppose you do not elect to set values for the instance variables via the class constructor. In that case, each is initialized with a default value of zero for the respective data type and null
for reference variables.
We will define two constructors for the Person
class. One will be the default constructor, and the other will have three parameters representing the instance variables. These constructors are shown in Table 2.
Constructors for the Person class |
public Person() Default constructor initializing givenName and surname to empty strings and age to zero. |
public Person(String g, String s, int a) Constructor to initialize givenName to g , surname to s and age to a . |
Table 2: The list of Constructors for the Person class.
Here is a list of properties concerning constructors:
- The name of the constructor and the name of the class are the same.
- A class can have many constructors. All constructors have the same name and are therefore overloaded.
- A constructor is neither a
void
method nor a value-returning method. - Overloaded constructors must differ in the number of parameters, or at least one parameter must be a different type when the number of parameters is the same.
Variables and Instantiation
With the framework of the class in place, we will now work with a couple of reference variables. We will start with a refresher. Recall that:
Person firstPerson;
Person secondPerson;
firstPerson = new Person();
secondPerson = new Person("Bob", "Smith", 36);
Can also be combined into:
Person firstPerson = new Person();
Person secondPerson = new Person("Bob", "Smith", 36);
Figure 1 shows the reference variables and their respective objects. As you can see, each was initialized based on the rules defined above. The default constructor would initialize the strings to the empty string and the age to zero. The alternate constructor would use the three parameters provided to initialize the object.
Figure 1: Reference variables and their objects.
Recall that the dot (.) is the member access operator. This operator is used to access named constants, methods and instance variables within an object. However, the instance variables listed for this class are private
. Therefore, the following statements are illegal with respect to the Person
class:
firstPerson.givenName = "Steve"; // illegal
firstPerson.surname = "Jones"; // illegal
firstPerson.age = 0; // illegal
We are only allowed to access public
and public static
members of the class from outside the class. Recall that public
members are accessed on an instance of the class (object) while public static
members can be accessed on the class itself.
Since the previous set of statements have been proven invalid, we are forced to use the methods provided in the first place. Remember that this is by design. The following statements are completely legal:
firstPerson.setGivenName("Steve");
firstPerson.setSurname("Jones");
firstPerson.setAge(0);
We now have the layout depicted in Figure 2.
Figure 2: Changes made to firstPerson
using methods.
Be careful with the use of the assignment operator with reference variables. Consider the following statement:
firstPerson = secondPerson;
On the surface you might expect to have the contents of the secondPerson
object copied in the firstPerson
object. Remember that with reference variables, the use of the assignment operator assigns addresses, not content. The result of the above statement is shown in Figure 3.
Figure 3: Effects of shallow copying.
This particular form of copying is known as shallow copying. Shallow copying is used when you intend to have more than one reference variable point to the same object.
If you intend to copy the contents, you need to perform a deep copy. A deep copy results in each reference variable having the same data but pointing to different objects.
Figure 4 shows the result of a deep copy.
Figure 4: Results of a deep copy.
We can achieve this deep copy by one of two methods:
- We can either copy the contents of an object into an existing object. (The
makeCopy()
method provided later will do this.) - Create a new object from an existing object copying the contents of the original object in the new object. (The
getCopy()
method provided later will do this.)
To copy the contents of an object into an existing object, we could use the following statement:
firstPerson.makeCopy(secondPerson);
The other method of creating a new object from an existing object is done with the following statement:
firstPerson = secondPerson.getCopy();
Let us discuss one final note about the depiction of firstPerson
and secondPerson
and their respective instance variables. These instance variables givenName
and surname
are also reference variables and point to objects. Technically, Figure 4 should look more like Figure 5.
Figure 5: More accurate depiction of Person and String objects.
In Figure 5, we see that although we’ve duplicated or copied the objects, the String objects are ultimately shared between the objects. This is because Java only uses one copy of the string among many references since they are immutable objects.
Constructors and Methods of the Person Class
Now that we have spent some time describing the constructors and methods of the Person
class and how they can be used, let us define all of the constructors and methods in Java.
public Person() {
givenName = "";
surname = "";
age = 0;
}
public Person(String g, String s, int a) {
givenName = g;
surname = s;
age = a;
}
Example 1: Constructors for the Person class.
Let us begin with the constructors. Example 1 shows the current set of constructors. As discussed previously, the default constructor will initialize givenName
and surname
to the empty string and age
to zero.
Now is a good time to discuss the differences between accessor and mutator methods of a class. An accessor method retrieves or accesses the values of instance variables whereas a mutator method modifies them. The methods getGivenName()
, getSurname()
and getAge()
are accessor methods while the methods setGivenName()
, setSurname()
, setAge()
and incAge()
are mutator methods.
Remember that the point of this level of encapsulation is to manage this object and other objects individually using the same set of methods and constructors. The accessor methods are shown in Example 2.
public String getGivenName() {
return givenName;
}
public String getSurname() {
return surname;
}
public int getAge() {
return age;
}
Example 2: Accessor methods of the Person
class.
The mutator methods are shown in Example 3.
public void setGivenName(String s) {
givenName = s;
}
public void setSurname(String s) {
surname = s;
}
public void setAge(int y) {
age = y;
}
public void incAge() {
++age;
}
Example 3: Mutator methods of the Person
class.
The remainder of the methods are shown in Example 4.
public boolean equals(Person otherPerson) {
return ( ( age == otherPerson.age) &&
( givenName.compareToIgnoreCase(otherPerson.givenName) == 0 ) &&
( surname.compareToIgnoreCase(otherPerson.surname) == 0 ) );
}
public void makeCopy(Person otherPerson) {
givenName = otherPerson.givenName;
surname = otherPerson.surname;
age = otherPerson.age;
}
public Person getCopy() {
Person temp = new Person(givenName, surname, age);
return temp;
}
public String toString() {
return givenName + " " + surname + ". Age " + age + ".";
}
Example 4: Remaining methods of the Person class.
The equals()
method is called on an instance of the Person
class to compare with another instance of Person
passed as a parameter. This means we would use a statement like:
if ( firstPerson.equals(secondPerson) ) {
...
}
The equals()
method returns boolean
so we can use it easily in a conditional test as demonstrated above.
Copy Constructor
Recall the getCopy()
method of Person
that created a new object and duplicated the values of the instance variables. Since getCopy()
creates a new object, we could take the step to provide a copy constructor for the class that performs the same work, but at the time of instantiation rather than having a separate method to create and perform the copy. Consider this example:
public Person(Person otherPerson) {
givenName = otherPerson.givenName;
surname = otherPerson.surname;
age = otherPerson.age;
}
Like the other constructors, the copy constructor becomes the object’s initializer and uses the otherPerson
object to populate the new object. Consider the following:
Person firstPerson = new Person("Pat", "Connor", 56);
Person secondPerson = firstPerson.getCopy();
With the copy constructor this can be rewritten as:
Person firstPerson = new Person("Pat", "Connor", 56);
Person secondPerson = new Person(firstPerson);
This helps in the understanding of the code since we cannot immediately perceive the intent of getCopy()
in the first version. In the second version, the use of the copy constructor and the new operator clearly indicates that an object is being instantiated, that firstPerson
will be used to initialize the object assigned to secondPerson
and we eliminate the need for a getCopy()
method.
The toString
Method
Java will provide a method called toString()
. This is actually inherited from the Object
class. This is because if a new class is not declared to be a child of another class (extends
), then it becomes a child of Object
. The toString()
method is one of a handlful of methods defined in Object
that every class ultimately inherits, even if you do not use extends
.
[The use of extends
indicates we are creating a subclass by inheriting the properties of what we are extending. Refer back to Chapter 7 regarding GUI interfaces and also Chapter 11 for the details of inheritance.]
The toString()
method returns a String
value when called. The purpose of this method is to allow objects to be printed. While that sounds a bit strange, there are plenty of reasons to display an object. For example, when you run the following:
int x = 10;
System.out.println("The value of x is " + x + ".");
The variable x
is boxed to the Integer
class which has a toString()
method to return a String
object. Only then would we have three String
objects to concatenate together for the println()
method. Of course, this level of detail has been hidden behind the autoboxing and auto-unboxing nature of Java.
Consider the following statement:
System.out.println(firstPerson);
Now if we had not overridden the toString()
method as shown earlier, the default value printed for a class by the Java provided toString()
method would be:
Person@16a8b92
This is the name of the class followed by an at sign followed by the hash code for the object. The hash code is used by Java and does not represent an actual memory location. Since this particular form of output is rather useless to most, the toString(
) method provided earlier allows us to produce output similar to:
Pat Connor. Age 56.
We say that the new toString()
method overrides the one that was inherited. This concept and several others are expained in greater detail in Chapter 11.
public class Person {
private String givenName;
private String surname;
private int age;
public Person() {
givenName = "";
surname = "";
age = 0;
}
public Person(String g, String s, int a) {
givenName = g;
surname = s;
age = a;
}
public String getGivenName() {
return givenName;
}
public String getSurname() {
return surname;
}
public int getAge() {
return age;
}
public void setGivenName(String s) {
givenName = s;
}
public void setSurname(String s) {
surname = s;
}
public void setAge(int y) {
age = y;
}
public void incAge() {
++age;
}
public boolean equals(Person otherPerson) {
return ( ( age == otherPerson.age) &&
( givenName.compareToIgnoreCase(otherPerson.givenName) == 0 ) &&
( surname.compareToIgnoreCase(otherPerson.surname) == 0 ) );
}
public void makeCopy(Person otherPerson) {
givenName = otherPerson.givenName;
surname = otherPerson.surname;
age = otherPerson.age;
}
public Person getCopy() {
Person temp = new Person(givenName, surname, age);
return temp;
}
@Override
public String toString() {
return givenName + " " + surname + ". Age " + age + ".";
}
}
Example 5: Fully completed Person
Class
The next piece of code will exercise the Person
class. This is not an extensive test of the class, but the constructors, mutators, some accessors, copy methods and toString()
are demonstrated.
public class ExercisePerson {
public static void main (String[] args) {
Person firstPerson;
Person secondPerson;
Person thirdPerson;
firstPerson = new Person();
// second person is initialized in the constructor
secondPerson = new Person("Bob", "Smith", 36);
// set values for firstperson
firstPerson.setGivenName("Karen");
firstPerson.setSurname("Parson");
firstPerson.setAge(43);
System.out.println("secondPerson.getGivenName() = " + secondPerson.getGivenName());
System.out.println("firstPerson.getSurname() = " + firstPerson.getSurname());
// test toString
System.out.println("\nfirstPerson -> " + firstPerson);
System.out.println("secondPerson -> " + secondPerson);
// make new object using firstPerson contents
thirdPerson = firstPerson.getCopy();
System.out.println("\nthirdPerson -> " + thirdPerson);
// replace contents of existing object with secondPerson contents.
firstPerson.makeCopy(secondPerson);
System.out.println("firstPerson -> " + firstPerson);
}
}
Example 6: Exerciser for the Person
Class.
The output looks like the following:
secondPerson.getGivenName = Bob firstPerson.getSurname = Smith firstPerson -> Karen Parson. Age 43. secondPerson -> Bob Smith. Age 36. thirdPerson -> Karen Parson. Age 43. firstPerson -> Bob Smith. Age 36.
Static Members of a Class
Recall that static
methods of a class can be called on the class as there is no need to create an object. For example:
double f;
...
f = Math.pow(5,3);
No Math
object was created. We simply called pow()
on the class and this method works on any version of Java’s JDK. Starting back in JDK 5.0, you could also import statically using an altered import
statement and simplifying the code like:
import static java.lang.Math.*;
...
double f;
...
f = pow(5,3);
Of course, you will need static import statements for the classes you wish to use this simplified style.
You may also create static instances of variables within a class. These, too, can be accessed on the class without needing to create an object, providing they are also declared as public
. Therefore, any public static
members may be accessed on the class without needing to create an object.
With that said, variables of a class that are static
exist without class instances. You can access their values prior to instantiating any objects. When you instantiate an object, only the non-static variables are created in the object. The static variables are shared between all of the objects. Consider the following:
public class SharedData {
private int x, y;
private static int shared;
public SharedData(int xval, int yval)
...
}
The memory already exists for the static variable shared
before we instantiate any objects. In addition, shared
is initialized to the standard default, which in this case is zero as an int
. Variables that are static
are useful when objects need to share data whereas maintaining this information between objects would be far too involved.
Figure 6: View of memory for objects and shared static variable.
Figure 6 depicts the memory of the following code:
SharedData obj1 = new SharedData(3,5);
SharedData obj2 = new SharedData(6,9);
The this
Reference
Whenever Java executes methods for a class, it needs to know which object it is executing methods on and which instance variables to access. It does this by implicitly using the reserved word this
. Every object has access to a reference of itself. This is necessary to eliminate ambiguity. Consider the following completed SharedData
constructor:
public SharedData(int xval, int yval) {
x = xval;
y = yval;
}
This particular piece of code actually has an implied use of the this
reference and can be rewritten like the following:
public SharedData(int xval, int yval) {
this.x = xval;
this.y = yval;
}
We can also use it explicitly to resolve conflicts in naming if the SharedData
constructor had parameter names that matches the instance variable names as in the following example:
public SharedData(int x, int y) {
this.x = x;
this.y = y;
}
This is known as a name space collision. As you can see, we have removed the ambiguity over the use of x
and y
. How else would the compiler know that we are talking about the parameter x
versus the instance variable x
simply by using x
?
Method chaining
Method chaining alters the convention of void
methods to having them return a reference to themselves. This allows us to chain calls together. Consider the Person
class from earlier. One of the details of classes is that we know that constructors can have most or all of the information upfront. Sometimes we have no upfront knowledge, and it is discovered later, at which point we can update the instance variables with mutator method calls.
public static void main (String[] args) {
Person p = new Person();
// ... some time later ...
p.setGivenName("Bob");
p.setSurname("Smith");
p.setAge(36);
}
The above is not new; this was seen earlier when exercising the Person
class. Now, we could not chain these methods like we could with something like we have with Scanner
:
ch = kb.next().charAt(0);
The reason we can call charAt()
is because next()
returns a String
. So we can call charAt()
directly after the call to next()
. Perfect!
However, the way that Person
is built, we cannot chain anything. All the methods must be individual calls on the object. This is why the above seems so tedious.
Where is the flare?
Where is the simplicity?
We can introduce some changes to a few mutators and try to make things more apt to be chained. Why the mutators? Well, the accessor methods tend to return a value already, so they cannot be used in chaining. On the other hand, Mutators tend to be void
methods. We simply change them to return the reference to themself. Consider the changes below:
public Person setGivenName(String s) {
givenName = s;
return this;
}
public Person setSurname(String s) {
surname = s;
return this;
}
public Person setAge(int y) {
age = y;
return this;
}
Now we can rewrite the code like so:
public static void main (String[] args) {
Person p;
// ... some time later ...
p = new Person().setGivenName("Bob").setSurname("Smith").setAge(36);
}
You may also see it frequently written like the following:
public static void main (String[] args) {
Person p;
// ... some time later ...
p = new Person()
.setGivenName("Bob")
.setSurname("Smith")
.setAge(36);
}
Why the second version? Well, it stems from a practice of putting the punctuation at the beginning of the line to make future editing easier. (This is also seen in SQL queries and other languages.)
How is editing easier? Let us say you want to add another method call to the chain. Simply open a line somewhere in the chain a, put “dot” and the new method call. Want to take one call away? Delete the line with that call—no need to try and find the one call buried in a long line of chained method calls.
The Person2
class is a reworked to provide method chaining.
public class Person2 {
private String givenName;
private String surname;
private int age;
public Person2() {
givenName = "";
surname = "";
age = 0;
}
public Person2(String g, String s, int a) {
givenName = g;
surname = s;
age = a;
}
public String getGivenName() {
return givenName;
}
public String getSurname() {
return surname;
}
public int getAge() {
return age;
}
public Person2 setGivenName(String s) {
givenName = s;
return this;
}
public Person2 setSurname(String s) {
surname = s;
return this;
}
public Person2 setAge(int y) {
age = y;
return this;
}
public void incAge() {
++age;
}
public boolean equals(Person2 otherPerson) {
return ( ( age == otherPerson.age) &&
( givenName.compareToIgnoreCase(otherPerson.givenName) == 0 ) &&
( surname.compareToIgnoreCase(otherPerson.surname) == 0 ) );
}
public void makeCopy(Person2 otherPerson) {
givenName = otherPerson.givenName;
surname = otherPerson.surname;
age = otherPerson.age;
}
public Person2 getCopy() {
Person2 temp = new Person2(givenName, surname, age);
return temp;
}
@Override
public String toString() {
return givenName + " " + surname + ". Age " + age + ".";
}
}
Example 7: The Person2
class supporting method chaining.
public class ExercisePerson2 {
public static void main (String[] args) {
Person2 firstPerson;
Person2 secondPerson;
Person2 thirdPerson;
// second person is initialized in the constructor
secondPerson = new Person2("Bob", "Smith", 36);
// set values for firstperson
firstPerson = new Person2()
.setGivenName("Karen")
.setSurname("Parson")
.setAge(43);
System.out.println("secondPerson.getGivenName = " + secondPerson.getGivenName());
System.out.println("firstPerson.getSurname = " + firstPerson.getSurname());
// test toString
System.out.println("\nfirstPerson -> " + firstPerson);
System.out.println("secondPerson -> " + secondPerson);
// make new object using firstPerson contents
thirdPerson = firstPerson.getCopy();
System.out.println("\nthirdPerson -> " + thirdPerson);
// replacce contents of existing object with secondPerson contents.
firstPerson.makeCopy(secondPerson);
System.out.println("firstPerson -> " + firstPerson);
}
}
Example 8: Code to exercise Person2
.
Inner Classes and File Scope
The classes we’ve defined thus far are designed with file scope. This means that the class itself is a file. It starts as a Java source file, then is compiled into a class file but cannot be executed on its own since it contains no main()
method, nor should it.
Once that file is compiled, the class file often lives in the same directory as the programs that reference that class unless the class file is made part of a package that any Java program could then import.
The class we created during the GUI discussion [Graphical User Interface (an Introduction)] for handling the button events was created inside of an existing class. This is called an inner class and is typically used for precisely this purpose.
Records
In Java 14, there was a preview of a new feature. It offered the details of a user-defined class but with the simplicity of knowing that the data it contained was immutable. In Java 16, this became a permanent feature of the language. The Record
class is extended when creating a record
.
The Person class we devised above could be implemented as follows:
public record Person (String givenName, String surname, int age) {}
That’s it. This is syntactic sugar to make the development of final
classes easier to create and manage. There is one constructor that initializes the instance variables using the arguments passed. It also defines accessor methods that are named after the record members.
public class PersonRecord1 {
public record Person (String givenName, String surname, int age) { }
public static void main(String[] args) {
String gn, sn;
int a;
Person first = new Person("Bob", "Stevens", 54);
gn = first.givenName();
sn = first.surname();
a = first.age();
System.out.println(first + "\n");
System.out.println("givenName = " + gn + "\nsurname = " + sn + "\nage = " + a);
}
}
Example 9a: Making Person
into a record.
The sample output of Example 9a is shown below.
Person[givenName=Bob, surname=Stevens, age=54] givenName = Bob surname = Stevens age = 54
The toString()
that is invoked when printing the object is exacting in its results. We can override the default and provide our own, as shown in Example 9b.
public class PersonRecord2 {
public record Person (String givenName, String surname, int age) {
@Override
public String toString() {
return givenName + " " + surname + ". Age " + age + ".";
}
}
public static void main(String[] args) {
String gn, sn;
int a;
Person first = new Person("Bob", "Stevens", 54);
gn = first.givenName();
sn = first.surname();
a = first.age();
System.out.println(first + "\n");
System.out.println("givenName = " + gn + "\nsurname = " + sn + "\nage = " + a);
}
}
Example 9b: Overriding toString()
.
Bob Stevens. Age 54. givenName = Bob surname = Stevens age = 54
If you are truly curious about how this all looks, you can use the javap
command to get more information about the structure of the Person
record, as shown below.
user@host:~$ javap Person
Compiled from "Person.java"
public final class Person extends java.lang.Record {
public Person(java.lang.String, java.lang.String, int);
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public java.lang.String givenName();
public java.lang.String surName();
public int age();
}
user@host:~$
Specific details to know about records:
- You can override the default
toString()
method. - You can override the accessors.
- You can declare
static
fields. - You can override the constructor insofar as you can add more code beyond the initialization of the instance variables (which are still
final
). - The
equals()
andhashCode()
methods specify that two records are equal if they are of the same type and contain equal component values. See Example 9c.
public class PersonRecord3 {
public record Person (String givenName, String surname, int age) { }
public static void main(String[] args) {
Person first, second;
first = new Person("Bob", "Stevens", 54);
second = new Person("Bob", "Stevens", 54);
System.out.println("first.equals(second) = " + first.equals(second));
System.out.println("first.hashCode() = " + first.hashCode());
System.out.println("second.hashCode() = " + second.hashCode());
}
}
Example 9c: Using equals()
and hashCode()
.
Sample output from Example 9c is shown below.
first.equals(second) = true first.hashCode() = 1563572947 second.hashCode() = 1563572947