In the previous lesson we built the Person utility class.
Immutable classes and the Object class
Immutable classes are those which don't enable their state to change after they have been constructed. The Object class provides a core set of functionalities inherited by every Java class, including those you define yourself.
In this section you will learn:
- How to develop an immutable class
- The recommended overrides from the Object class
Developing an immutable class
The ZooKeeper and Visitor classes each currently define an email attribute of type String to store an email address. Because emails need to be in a prescribed format, and are applicable to many applications, it would make sense to define another utility class to model this rather than a using plain String object directly. You will therefore write a class called Email in com.example.util with the following features:
- The constructor will accept a
Stringargument representing the email address. This will be validated to ensure that it is in an acceptable format: - There must be no spaces in any part of the entire email address
- There must be exactly one @ symbol in the entire email address
- It should consist of a local-part before the @ symbol and a domain-part after it
- The local-part must contain at least one character
- The domain-part must contain at least one dot, either side of which is at least one character
- There will be getter methods to return the entire email address, the local-part only, and the domain-part only
- The
toString()method will return the entire email address - The class will have a natural ordering of alphabetical by the entire email address
This is not intended to be a full implementation of a class to model an email. In particular, the validation described above does not detect all possible incorrect email formats.
You may have noticed that no setter methods were specified: only getters. This is known as an immutable class – once the state of an object has been set through its constructor then it cannot be changed. Immutable classes offer several advantages over mutable ones, and if you see an opportunity to make a class immutable then you should seriously consider doing so. As mentioned previously, the ubiquitous String class is immutable, as are several other Java supplied classes.
The natural question to ask, therefore, is how to handle the inevitable time when a person's email address changes? The answer is straightforward: simply create a brand-new Email object and discard the old one. This applies even to simple changes such as correcting a spelling mistake and is a reasonable approach since modifications to email address are relatively uncommon.
In com.example.util define a new class called Email:
package com.example.util;
public class Email {
private String email;
public Email(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
}
You need a method to return the local-part (i.e., the part before the @ symbol):
public String getLocalPart() {
int atIndex = email.indexOf('@');
return email.substring(0, atIndex);
}
The indexOf() method of the String class returns the index of the first occurrence of the char or String specified in the argument. In this case it looks for @ in the email attribute. If it is not found, then -1 will be returned;
- For example, if email contains then
atIndexwill become 4, since the @ character is in the fifth position. Remember, strings are indexed starting from zero.
The substring() method of the String class returns a String consisting of the characters which lie between the first argument index position (which here is 0) and the second argument index position less one (which here is atIndex).
- For example, if email contains then the characters from index 0 to 3 will be returned as a
String, which will be "fred"
You need a similar method to return the domain-part:
public String getDomainPart() {
int atIndex = email.indexOf('@');
return email.substring(atIndex + 1);
}
You can see above that the substring() method is overloaded to accept a single argument. In this case, it returns the characters from the argument index until the end of the string
- For example, if email contains then characters from index position 5 onwards will be returned, in this case "example.com"
You can override the toString() method (inherited from Object) to return the full email address:
@Override
public String toString() {
return email;
}
For the natural ordering you need to implement the Comparable interface:
public class Email implements Comparable<Email> {
And provide the code for the interface's compareTo() method:
@Override
public int compareTo(Email otherEmail) {
return email.compareTo(otherEmail.email);
}
- The method delegates to the
compareTo()method of theStringattributeemail
You now need to write a helper method to validate the email address. If it is invalid an exception will be thrown. Define a private validate() method, initially only validating that the email contains no spaces anywhere within:
private void validate() {
// Ensure there are no spaces
if (email.indexOf(' ') >= 0) {
throw new IllegalArgumentException
("Email must not contain a space character");
}
// OTHER VALIDATIONS WILL GO HERE ...
}
- The above code uses the
indexOf()method ofStringwhich you saw previously, this time to search for a space character. If its returned value is zero or more then a space was found, in which case anIllegalArgumentExceptionis thrown with some explanatory text
Now include another section inside the method above to ensure that there is exactly one @ character:
// Ensure there is exactly one @ character
int countAts = 0;
for (char c : email.toCharArray()) {
if (c == '@') countAts++;
}
if (countAts != 1) {
throw new IllegalArgumentException
("Email must contain exactly one @ character");
}
- The
toCharArray()method ofStringreturns an array ofcharprimitives, where each element of the array is a single character. This is then looped over, and each time the @ character is found the variablecountAtsis incremented - When the loop has completed
countAtsis tested to ensure it is exactly 1
The next section needs to ensure the local-part is not empty:
// Ensure local-part is not empty
if (getLocalPart().isEmpty()) {
throw new IllegalArgumentException
("Email local-part must contain at least one character");
}
- The
isEmpty()method ofStringreturnstrueif the string is empty andfalseotherwise. Note that "empty" is not the same asnull; an empty string is one which exists but has no characters in it
The domain-part needs to contain at least one dot:
// Ensure domain-part contains at least one dot
int countDots = 0;
for (char c : getDomainPart().toCharArray()) {
if (c == '.') countDots++;
}
if (countDots < 1) {
throw new IllegalArgumentException
("Email domain-part must contain at least one dot");
}
Finally, each String either side of any dot character must not be empty:
// Ensure each part of domain-part is not empty
int fromIndex = 0;
for (int i = 0; i < countDots; i++) {
int dotIndex = getDomainPart().indexOf('.', fromIndex);
String part = getDomainPart().substring(fromIndex, dotIndex);
if (part.isEmpty()) {
throw new IllegalArgumentException
("Email domain-part is not valid");
}
fromIndex = dotIndex + 1;
}
// check the part after the last dot
String part = getDomainPart().substring(fromIndex);
if (part.isEmpty()) {
throw new IllegalArgumentException
("Email domain-part is not valid");
}
- The above is the most complex of the validation sections, although it uses techniques you have already learned about. Essentially, it loops through the domain-part for each
Stringbefore a dot character, and ensures it is not empty. After the loop completes, theStringafter the final (or only) dot character is tested to ensure it is not empty. Note the use of a second argument passed to theindexOf()method which tells it the starting index to search from
Some of the code in the validate() method could be simplified through the use of the String method called split(), which returns an array of String objects either side of the argument to split(). However, the argument needs to be a regular expression which is beyond the scope of this course.
With the validation method completed you need to call it from inside the constructor:
public Email(String email) {
this.email = email;
validate();
}
In the next lesson we will see how to prevent immutable classes from being compromised.
Next lesson: 9.2 Preventing the compromisation of immutable classes
Comments