JavaRanch Home    
 
This page:         last edited 16 February 2014         What's Changed?         Edit

First Classes   

(Level: Beginner)

When you're starting out with Java, chances are you'll be writing classes pretty quickly. This page provides you with a way to do it that is easy to remember and repeat.

It is intended for beginners - especially ones who are writing their first projects away from the classroom - but students who are a bit further along may also find some useful tips.

It won't look like the classes you've seen in your books, so you might want to read the CAVEAT section at the end to find out why.

In particular, it shows you an approach to writing equals() that you probably won't have seen before. Don't worry, it's not wildly complicated; but it does take a bit of explaining

so make sure you have your thinking cap on when you read the section.

Once you've written a few classes, you'll be familiar with the pattern. You'll probably also know a bit more, so you won't need to keep to it slavishly.

It's quite long, so don't feel you need to read it all in one go. Just take a section at a time - especially the one on the rules of equals() - and make sure you understand what's going on before you continue to the next one.

.

NOTE: It is NOT intended as a substitute for reading, or following a proper tutorial or class.

.

Prerequisites

At the very least, you are assumed to have seen some class definitions before and know what constructors and methods look like, and also to have seen some basic assignments and method calls; otherwise the code will probably look like gobbledygook.

You should also familiarize yourself with the APIs for the Object, String and Integer classes before you go too far.



INTRODUCTION

For the purposes of this tutorial, the class we're creating is called Person, and will contain two fields (or 'members'):

name - a Java String.

age - a Java int indicating age in years.

.

Methods

There are three important methods to know about when you're writing a class: equals(Object), hashCode() and toString(), and these are the ones we're going to concentrate on.

Since they are all defined in the Object class, you will be overriding them; so they should all start with the "@Override" Annotation.

I'm also going to introduce you to a fourth method you probably haven't seen before: canEqual(Object).

You may have already heard that whenever you write an equals()  method, you should also write a hashCode() method - well, canEqual() is a third method in that list.

Basically, they work as a trio, so think of them as The Three Musketeers - "All for one, one for all":

If you write one, you MUST write all three; leave one out, leave all three out.

equals() is usually the "driving force" behind this decision; the other two methods simply make it work properly.

For now, take it as read that you should ALWAYS include a toString() method.

.

Constructors

These look like methods, and are used to create new objects (ie, instances of a class) when the new keyword is specified.

You can write many of these, but when you're starting out, you will probably be getting input in String form quite often (probably from a Scanner), so I'm going to concentrate on two in particular: Person(fields) and Person(strings).

Constructors always have the same name as their class.

.

Factory Methods  

These are an alternate way of creating objects; and in many ways are superior to constructors. Indeed, many experienced developers use them to hide constructors from clients.

In general, they are static and final, and convention says that they should usually be called valueOf(...) for a value class like Integer. For example:

Integer two = Integer.valueOf(2);

effectively does the same thing as:

Integer two = new Integer(2);

but actually does it slightly better (I'll leave you to find out how ). Note the use of the class name in the first call, because the method is static.

For more "informational" classes - like ours - there is no real 'set' name, although you will often see instanceOf(...) or newInstance(...) used if there is nothing better, or if the class only has one factory method. My advice: make it as descriptive as you can.

We shall be creating three; however, since they don't really change the basic structure of our class, I have put them in a separate page called FirstFactoryMethods.



KICKOFF

Right, we're ready to start writing our new class. In general, you'll want it to be visible to everyone, which means that it should be public. This, in turn, means that it must be put in a file called Person.java (note the spelling; Java is case-sensitive).

So let's start with the definition:

public final class Person { ...

Fairly straightforward: The "class" keyword says that we're defining a class, and the "final" keyword says that, at least for the moment, we don't intend to subclass (or "extend") it.

And that "at least for the moment" is very important:

The great thing about Java classes is that you can change them. In fact, it's one of the reasons they were invented. But some things - specifically: visibility (public/private etc.), extendability, and immutability (see below) - can only be changed in ONE direction.

Also: extendability is a decision you don't want to take lightly (although it might not seem so from your lessons so far). As Joshua Bloch says in his wonderful book (Item 17):

"Design and document for inheritance, or else prohibit it".

final prohibits it ... at least for the moment.

So:

Tip #1 (and you won't find this in too many books):  

Unless you're defining an abstract class, make it "final" by default.

Obviously, if you know you're going to extend it, you can leave it off; but very often you won't - and in those cases, put it in.

You can always remove it later if you discover that you do want to subclass; but you can't ADD it.

Bloch freely admits that two of the classes he wrote - BigInteger and BigDecimal - should have been final. Unfortunately, it's too late to change now without possibly breaking existing code.

.

Another thing: Looking at that definition, you might think that your Person class exists in a vacuum.

Well, it doesn't - it extends Object - ALL Java classes do. So:

Tip #2:  

Unless you're defining a subclass (in which case you'll be extending something else), include the fact that you're extending Object in your definition, viz:

public final class Person
  extends Object
{ ...

Indeed, I know many experienced programmers who follow this practice religiously.



FIELDS  

OK, let's add in those fields:

public final class Person
  extends Object
{
  private final String name;
  private final int age;

.

Tip #3:  

ALWAYS, ALWAYS, ALWAYS make your fields private.

It's a primary rule of Object-Orientation:

Never expose internal fields (or logic) of your class to anyone.

.

The final is optional, but I always put it in - even after 12 years of writing Java - because I don't want anyone changing my fields unless I say they can.

And that's what final does on a field - it says: "this field can only be set ONCE".

Basically, it makes your class immutable (look it up ), which is actually no bad thing. So:

Tip #4:  

Get into the habit of putting "final" on your fields by default.

This is basically the same tip as for classes. If you know you're going to need to change a field, by all means leave it off; but if you don't - and you should think very carefully before you DO allow fields to be changed - put it in.

As above, you can always remove it later on if you find it too restrictive; but you can't ADD it.



Person(fields) constructor  

This is a constructor you will almost always want - a way to create an object given initial values for all its fields. In our case we have two, so it will look something like this:

public Person(String name, int age) {
  super();  // don't worry about this just for the moment
  this.name = name;
  this.age = age;
}

The "this" is important, because it allows the compiler to distinguish between the class field called name, and the parameter field called name. If you leave it out, it would simply assign the parameter field to itself (because it's the closest definition), which effectively does nothing at all.

In fact, I would advise putting "this." in front of all your class field references.

However, there's something missing from the above code. Can you see what it is?

.

Based on what we've written, all of the following constructions would be perfectly valid:

Person person1 = new Person(null, 0);  
Person person2 = new Person("", -35);
Person person3 = new Person("    ", 1043);

But do we really want a Person with no name? Or one who is 1043 years old?

Tip #5:  

Validate ALL information coming into constructors and public methods; and do it as quickly as possible.

and that last part is a basic design maxim that is well worth remembering:

Fail fast, and fail LOUD.

Simply put: if something goes wrong in your program, you want to know about it straight away; and in a way you can't ignore.

.

The first of those examples is probably the most important. In Java, Strings are objects, and ANY object can have a value of null.

Nulls are usually something you want to avoid, because they have an annoying habit of generating NullPointerException's (NPEs) down the line; and NPEs can be one of the nastiest errors to try and track down after the fact.

So, back to our rule: "Fail FAST" - and it's particularly important when it comes to nulls.

Tip #6:  

Eliminate nulls from your program as soon as you find them.

.

As you saw, a String can also be blank (ie, all spaces) or have no characters in it at all (""), so we should eliminate any "names" that look like that too. Also, age can't be negative; and we should probably put an upper limit on it - say 125 (since nobody has yet been known to reach that age).

Fortunately, String has a nice little method called trim() which saves you a lot of hassle when dealing with "blanks". In fact, if you haven't already, I suggest you take a good look at the String API before you continue much further.

.

So, let's re-write this constructor properly:

public Person(String name, int age) {
  super();

  if (name == null)
    throw new IllegalArgumentException(
      "name cannot be null");

  // Remove leading/trailing blanks from supplied name
  name = name.trim(); // NOTE: no "this."

  if (name.length() == 0)
    throw new IllegalArgumentException(
      "name cannot be blank");

  if (age < 0 || age > 125)
    throw new IllegalArgumentException(
      "invalid age");

  this.name = name;
  this.age = age;
}

For the moment, we're simply saying "if the data is bad, make the program fail" and IllegalArgumentException is the normal way to indicate a parameter (or "argument") error.

I don't propose to go through the code line by line. If you haven't done it by now, after all my urging, familiarise yourself with the String API NOW.

.

Modularity

The only problem with what we've done is that those checks are likely to be needed whenever we have to deal with a name or an age; and if we leave them where they are, we'll have to copy them wherever they're needed, and copying code is BAD.

So, lets set up a couple of little checker methods. We'll make them private - at least for the moment - since only our class is likely to need them; and they can also be static since the validation doesn't depend on any particular instance of our class, viz:

private static final String validName(String name) {
  if (name == null)
    throw new IllegalArgumentException(
      "name cannot be null");

  name = name.trim();

  if (name.length() == 0)
    throw new IllegalArgumentException(
      "name cannot be blank");

  // NOTE: this returns the trimmed name
  // (which is probably no bad thing)
  return name;
}

private static final int validAge(int age) {
  if (age < 0 || age > 125)
    throw new IllegalArgumentException(
      "invalid age");
  return age;
}

and plugging those into our constructor, we get:

public Person(String name, int value) {
  super();
  this.name = validName(name);
  this.age = validAge(age);
}

which can also be written as:

public Person(String name, int value) {
  super();
  this.name = Person.validName(name);
  this.age = Person.validAge(age);
}

I strongly urge you to use this form, because it's a great reminder that the methods you're calling are static.

Make sure you follow how all this works before you continue, because you will probably want a "validation" method for each field in your class; and they should all follow the same pattern:

  • The signature should be:
private static final <type> validXXX(<type> field) { ...
You can choose a prefix other than "valid" if you want, but be consistent about it.
  • Validate the supplied field.
  • Return the supplied field if it passes all the tests.

And that last point is a good one to remember:

Methods should usually return values.

Obviously, there are exceptions, such as main() and "setter" methods, but in general, avoid writing methods with a return type of void.

.

And why the "final"?

Tip #7:  

Make your methods "final" by default.

Many will tell you that it's redundant for instance (non-static) methods; especially when your class is final or the method is private. I disagree:

  1. It stops anyone from accidentally overriding your methods.
  2. If you ever remove the final from the class definition and/or change the visibility, your methods remain final until YOU decide otherwise.
  3. The compiler may be able to optimize your code better. On its own, it's a bad reason to do it; but in this case it doesn't do any harm, and it may be an added bonus.
  4. It's just a good habit to get into. If you don't start now, you may forget to do it when it really DOES make a difference.

And, as with all the other cases we've seen, you can always remove it later on if you find it too restrictive; but you can't ADD it.

Think of it this way - You (the developer) are the gaoler: classes, fields and methods are your prisoners; and "final" is the key. If you think one of your lags is worthy, set them free; but get it wrong, and repent at leisure, because once they're out, you can't get 'em back.

Having said all that, the four methods that we're going to write probably shouldn't be final (Sod's Law ) because they're meant to be overridden. There's absolutely nothing wrong with putting it on them as well, but you may well find that you have to remove it later. Our examples will leave it out.

.

A look at the complete class listing near the end may help to put all the stuff in this section into context.

.

One final thing (you probably thought I'd forgotten about it ):

What's that "super()" statement all about?

What it says is: "call our superclass constructor" - and our superclass is Object, remember?

The reason that you may not have seen it is that if you don't put it in the compiler will add "super()" (and specifically that) for you silently.

Well, don't let it.

For one thing, it may be the wrong constructor. Object has a constructor that takes no arguments (indeed, it's its only constructor), but not all classes do - ours, for one.

So:

Tip #8:  

Always include a specific call to super(...) in your constructors.

And it must be the first statement.



Person(strings)

This is the constructor you will need to convert "String-based" input (eg, from a Scanner or a text file) to a Person object. I'm surprised how many books leave it out, since you will almost always need it for the first few months that you're writing Java classes (and probably a lot longer than that).

.

Like Person(fields), it is going to need input for each field in our class. The only difference being that ALL of them will be Strings; so in our case, it will look like this:

public Person(String name, String age) {
  // convert "input"
}

Obviously, if all the fields in your class are already Strings, you don't have to write it; however, the chances of that happening are pretty slim. Or they darn well should be. If they are all Strings, look at them carefully, because there's a good chance that they're defined badly.

Strings should NOT be used to store:

  • Numbers
  • Dates
  • Postcodes
  • Colours
  • Telephone numbers

or indeed, anything that has a predefined format, or a "meaning" beyond the simple characters themselves.

Indeed, it's arguable that even "name" isn't a simple String, because it has a specific format - in the Western world: first name + " " + family name, and possibly some initials - but the way we've defined it, someone could just supply "XXX" and we'd never know it was bad.

However, that's a lesson for another day . For our purposes, a String will do just fine. Check out the StringsAreBad page for more details if you're interested.

.

OK, let's write it:

public Person(String name, String age) {
  this( name, Integer.parseInt( age.trim() ) );
}

.

Blimey. That was quick.

Why? Because we've already done most of the work.

Our Person(fields) constructor already knows how to convert and validate a name. It also knows how to validate an age value, so all we need to do is to call it with the right types.

And that's what the this(...) statement does: it's very similar to the super(...) statement you saw earlier, except that it calls one of OUR constructors, rather than a superclass one.

It's called "constructor chaining" and, as you can see, it's very useful. The only thing to remember is that, like super(...), it must be the first statement in the constructor.

.

And just to complete the explanation: Integer.parseInt(...) converts a String to an int, and age.trim(), as you saw above, removes spaces from the start and end of the supplied age String - just in case they were supplied by accident.

It's a bit of a mouthful, but unfortunately necessary, because this(...) MUST be the first statement, so something like:

public Person(String name, String age) {
  int salaryValue = Integer.parseInt( age.trim() );
  this(name, salaryValue);
}

won't work, even though it's arguably a lot clearer.

Hopefully, it also shows you why it's good for methods to return values: parseInt(...) uses the result of trim(), and in turn returns a result itself, which is used by this(...). This sort of "stacking" is very common, but it only works if methods return values.

.

Again, I strongly urge you to get familiar with the APIs of String and Integer before you go any further.



toString()  

This is probably the 2nd most important method to know about when writing a Java class, because it allows you to print out the contents of objects to the console (very useful when you're debugging).

If you don't write your own, you will get the one that Object provides for you, which will produce something like:

Person@0af739b2

ie, not very useful. So:

Tip #9:  

ALWAYS write a toString() method for your classes.

.

OK, so what do we want our "Person string" to look like?

A lot of beginners get obsessed with producing the most descriptive output they can, because they're thinking about exactly how it's going to look in some application they're writing.

My advice: DON'T.

The method is for your class, not some application; and furthermore, it's primary use is as a debugging aid, not as a vehicle for producing "fancy output". So keep it simple.

That said, a good toString() method should:

  • Output ALL of the fields in your class in text form.
  • Use a format that can be easily added to. A simple one is comma-delimited text, eg:

Joe Bloggs,47

although it may not be suitable for all types of data.

.

"But what if one of my fields is a JPEG?" I hear you cry.

If it is, it will certainly be an object, so you can call its toString() method. And if the designer of your JPEG object didn't bother to write one? You'll probably get:

CrapJPEGClass@0af739b2

and you have my full permission to roast their reproductive organs over an open fire.

Seriously though, most designers of objects that aren't meant for text output will usually have the foresight to provide something that makes sense, eg:

image/jpeg: some URL

where "some URL" is the URL that contains the image.

For those that are interested,  the bit before the ":" is the MIME type for a JPEG according to RFC 2046. And that illustrates another point:

Tip #10:  

UseExistingStandards.

The link contains a few that you may find useful.

.

Going back to our format just for a second; I have to admit a slight preference for semicolon-delimited text; simply because commas often occur naturally in Strings - certainly more often than semicolons. For example, if our name field used a slightly different - but commonly-used - format:

Bloggs, Joe

the semicolon still allows us to separate "fields" visually:

Bloggs, Joe;47

.

Tip #11 (a common rookie mistake):  

Don't include 'bookends' like "[...]" or "{...}" in your strings.

Why? Because it makes them harder to add to, which, as you'll see later, is quite important.

.

So, getting back on track with our method, we'll use semicolon-delimited text output, viz:

@Override
public final String toString() {
  return name + ";" + age;
}

Notice the "@Override"? toString() is actually defined in Object, so we're overriding it.

Tip #12:  

Remember to include "@Override" on ALL methods you override.

.

Otherwise: pretty simple, eh?

.

Not so fast, kemosabe. We're not quite finished yet. Check out the Subclasses section for the rest of the story.



canEqual(Object)  

This is that "new" method I told you about earlier; and you will need it whenever you write an equals() method - the Three Musketeers, remember?

Luckily, it's dead simple:

protected boolean canEqual(Object other) {
  return other instanceof Person;
}

.

That "Object" is important because it will be called by our equals() method.

DON'T write:

protected boolean canEqual(Person other) { ...

Note also:

  • We don't use "@Override", because it's not overriding anything (more's the pity).
  • It's "protected", NOT "public".

It's there for our class and its subclasses; NOT for general use.

.

The other thing to note is the use of "instanceof",  which is a type check. Specifically:

  • If "other" is a Person, it returns true.
  • If "other" is a subclass of Person, it also returns true.

otherwise, it returns false - and that includes two very important cases:

  • "other" is a superclass of Person.
  • "other" is null.

Remember this well, because it's important: The comparison works downwards.

.

The only other thing to remember is that whenever you write this method, the class after "instanceof" must be the current one. More formally:

For any class C that includes it, canEqual() MUST look precisely as follows:

protected boolean canEqual(Object other) {
  return other instanceof C;
}

It's also an exception to our Tip 7 above: It should not be final because it's meant to be overridden.

.

For now, that's it. canEqual() checks if the supplied object "other" is the same class as ours or a subclass, but it's designed to be overridden. More on that later.



equals(Object)  

Finally, we come to the most important method you will ever learn about, because it allows you to compare your object with others to see if they are "equal", which is something you're going to need to do a LOT.

The actual meaning of "equal" will depend very much on the class you're writing, but usually it means that all of the fields in the object we're trying to compare with have the same values as ours.

Unfortunately, it's not quite as simple as that. In fact:

Writing a proper equals() method is NOT simple.

According to this article by Martin Odersky et al:

"...after studying a large body of Java code, the authors of a 2007 paper concluded that almost all implementations of equals() are faulty"

and I can well believe it.

.

However, we're fearless, right?

Let's have a first squint:

@Override
public boolean equals(Object other) {
  // see if this object is "equal" to 'other'
}

.

The first thing to notice is what it looks like:

public boolean equals(Object other) { ...

That "Object" is VERY important. One of the most common mistakes beginners make is to write:

public boolean equals(Person other) { ...

which is WRONG, because then you aren't actually overriding Object's equals() method, which is the whole point of the exercise.

The "@Override" annotation makes sure that you can't accidentally make that mistake, because if you do the compiler will complain. So don't forget Tip #12.

If you don't put it in, you won't get any errors because the 2nd signature is a perfectly valid method - just not the one we want - and you'll probably only realise your mistake when your class starts behaving oddly.

.

Getting back to what "equals" means just for a second: it's worth noting that there are two basic definitions:

  1. The two objects being compared are identical - ie, they are the same object.
  2. The two objects being compared contain the same information.

For the first, you can use '==', so the only one that concerns us when we're writing equals() is the second one and, as you'll see, it's much more important.

If you don't write it, you will get Object's definition, which is basically the same as '==' - ie, not very useful.

.

OK, now we're ready to write our method:  

@Override
public final boolean equals(Object other) {
  if (this == other) // Step 1  
    return true;

  if ( ! this.canEqual(other) ) // Step 2  
    return false;

  Person that = (Person) other; // Step 3  

  if ( ! that.canEqual(this) )  // Step 4  
     return false;

  // Step 5 - includes deliberate mistake!!!  
  if (this.name == that.name  
     && this.age == that.age)
     return true;

  return false;
}

Remember this pattern (apart from the deliberate mistake of course ) because, until you get much more advanced, every equals() method you ever write should look similar.

.

Let's have a look at those steps in detail:  

  1. Check if other is the same object as this one - If it is, they MUST be equal, so we might as well return true straight away.
  2. Check if other is the same type as this one - If it isn't, they CAN'T be equal, so we might as well return false straight away.
  3. Cast other to our type - We need to do this because other is an Object, not a Person; and the check in Step 2 ensures that we now know that it is, in fact, a real Person object (or a subclass, in which case the cast is still fine).
  4. Check if this object is the same type as other. Now that probably sounds like the same check as Step 2, but it isn't - instanceof is directional, remember?
  5. Compare fields for equality - Did you spot the deliberate mistake? The check in Step 1 might give you a clue.

.

Basically, you shouldn't use '==' with objects because, as already stated, it will only return true if the two objects being compared are the same object.

name is a String, which is an object, so:

this.name == that.name

will only return true if this.name is the same String object as that.name.

So what do we want instead?

this.name.equals(that.name)

Remember this, because it's VERY important:

Never use '==' when comparing objects.

Well, almost never . Our Step 1 check is one of the very rare exceptions to that rule, because we specifically want a "same object" check.

If you're interested, have a look at the AvoidTheEqualityOperator page for more details.

.

OK, back to the code. Correcting our mistake, we get:

if (this.name.equals(that.name)
  && this.age == that.age)
   return true;
return false;

which can be shortened to:

return this.name.equals(that.name)
  && this.age == that.age;

(at my age, I like to save typing).

.

So, putting all that together, we get:

@Override
public final boolean equals(Object other) {
  if (this == other)
    return true;

  if ( ! this.canEqual(other) )
    return false;

  Person that = (Person) other;

  if ( ! that.canEqual(this) )
    return false;

  return this.name.equals(that.name)
    && this.age == that.age;
}

.

So, why are we doing that same check (canEqual()) in both directions?

Unfortunately, the explanation is quite involved; so, before we go into that, let's have a look at the final method we're going to need...



hashCode()

This is probably the hardest method for beginners to get their heads around, but it's absolutely essential.

Whenever you write an equals() method, you MUST write a compatible hashCode() method - The Three Musketeers, remember?

Right: So, what is a "hashcode"?

For Java purposes, it's a number that indicates when two objects of the same class are different; and since this is the flip-side to equals(), it's important that the two methods are compatible.

The rules are detailed in Object.hashCode(), but basically there's only one you need to remember:

  • If x.equals(y) returns true, then both x.hashCode() and y.hashCode() MUST return the same value.

And it causes no end of confusion, because most beginners assume that the above implies that if two objects are different, they must return different hashcodes.

NOT TRUE ... I'll say that a second time: NOT TRUE.

The following is a perfectly legal (and correct) hashCode() method:

@Override
public int hashCode() {
  return 42;
}

it's just not very useful.

The fact is that you can't ensure that different objects will always produce different hashcodes, but the idea is that a good hashCode() method will do so as much as possible.

And this is where things get tricky. There's a lot to know about writing good hashCode() methods, and a lot of very clever people have worked on the problem.

Luckily, you don't have to worry about it.

If you're on version 7 or above (and if you're not; why not?), there is a great new class called Objects that contains a lovely hash() method, which takes all the effort out of generating good hashes. Just call it with every field you use for comparison in your equals() method. So in our case this would be:

@Override
public int hashCode() {
  return Objects.hash(name, age);
}

If you're not yet on version 7, but on version 5 or later you can do something very similar by calling Arrays.hashCode(), viz:

@Override
public int hashCode() {
  Object[] fields = new Object[] {name, age};
  return Arrays.hashCode(fields);
}

which can be shortened to:

@Override
public int hashCode() {
  return Arrays.hashCode(
    new Object[] {name, age} );
}

(Arrays is another class well worth getting to know)

If you're not on version 5, you'll have to do it the hard way - and it serves you right.

For the complete class, I'm going to assume that you're running version 7.

.

And just in case you missed it in passing:

Your hashCode() calculation must include ALL fields used in your equals() comparison, and ONLY those fields.

It's all part of ensuring that the two methods are in sync.

.

As with the previous methods, we're not quite finished yet. Check out the Subclasses section for more information.



THE RULES OF equals()  

OK. Before we finish, we're going to re-visit equals() once again.

Remember we did a canEqual() check in both directions? Well, the reasons for that have to do with the rules for an equals() method, of which there are 5:

  1. It must be reflexive.  
  2. It must be symmetric.  
  3. It must be transitive.  
  4. It must be consistent.  
  5. x.equals(null) must return false (assuming it doesn't throw an NPE).  

These are explained in detail in Object.equals() and, as you can see, they're quite involved.

And so is this explanation - so put on your thinking cap on before you continue.

.

The two main ones to know about are numbers 2 and 3 (#1 was dealt with by our Step 1 check). Lets have a look at #2:

  • It must be symmetric: basically what this means is that:

x.equals(y) == y.equals(x)

.

Now that may seem obvious, but it's remarkably easy to get wrong, because we're calling two different methods: x.equals(y) calls x's method, and y.equals(x) calls y's.

If the two objects are unrelated, then our Step 2 check is sufficient. For example, if x is a Person and y is a String, then x.equals(y) will call our method and fail on Step 2, because "other" (y) is not a Person.

For y.equals(x), we have to assume that String's equals() method contains a similar check to make sure that what is passed to it is a String. If it doesn't, the designer didn't do his job properly, and there's very little we can do about that (it does, BTW ).

.

But what if y is a subclass of x? Let's just suppose for a moment that Person isn't final, and that someone has written a RichGuy subclass that redefines equals() - and so has also done the same thing for canEqual() - The Three Musketeers, remember?

x.equals(y) will call our method, but this time Step 2 will pass, since "other" (y) is a RichGuy, and RichGuy is a subclass of Person.

So, we continue on to Step 4: that.canEqual(this).

Now we'll be calling RichGuy's canEqual() method, which should look like this:

@Override
protected boolean canEqual(Object other) {
  return other instanceof RichGuy;
}

and, since in this case "other" (x) is merely a lowly Person, that check will fail.

Remember: until the revolution comes, equals() is class-conscious .

And we'll deal with the y.equals(x) part of this in the Subclasses section below.

.

"So", I hear you ask, "does that mean that two classes in the same hierarchy CAN'T be the same if one of them redefines equals()?"

YES

"But why can't we just compare them on the basis of their superclass? Surely a Person with a the same name and age as a RichGuy could be considered equal, whichever way we compare them?"

A very good question.

.

Which brings us onto that rule #3:

  • It must be transitive:  and what this means is that, for any three objects, it must be circularly consistent - ie:
x.equals(y) && y.equals(z) == x.equals(z)

.

So, let's consider three objects: a RichGuy (x), a Person (y), and another RichGuy (z), whose names and ages are ALL the same (Joe Bloggs, age: 47).

The trouble is, RichGuy's are status-conscious (which is probably why we had to write the stupid equals() method for them to begin with):

A millionaire is NOT the same thing as a billionaire - at least to them - "different order of breed don't you know".

So RichGuy's equals() method will contain some comparison of net worth.

Now let's further say that our first RichGuy (x) really is a billionaire, but our second (z) is merely a millionaire.

Do you see where this is going?

.

If we compare based on superclass stuff (name and age) when two objects are different, we can't suddenly abandon it when they're the same; because then:

  • x (our billionaire RichGuy) would equal y (our Person) - same name and age.
  • y would equal z (our millionaire RichGuy) - same name and age.

BUT:

  • x would NOT equal z - same name and age, but different net worths.

So transitivity is the reason; and let's just say it one more time:

Objects of two different classes in the same hierarchy CANNOT be equal if one of those classes redefines equals().

.

Actually,  as with almost everything in programming, that's not quite true . There are ways of doing mixed-type comparisons while still observing the "transitive" rule, but they get very involved - much too involved for a tutorial like this.

The methodology you've been given is enough to serve you for a while; and far superior to some others you may see.

.

PHEW.

Do you see now what I was talking about earlier? equals() is NOT simple - especially when class hierarchies get involved.

.

And before we put this to bed completely, it's probably worth mentioning one other thing:

Unless you know how a superclass does it's equals() check - and sometimes even then - you CANNOT add a subclass to an existing hierarchy AND redefine its equals() method.

It's just a fact of life. Basically, you're at the mercy of the writer of the superclass.

The methodology in this tutorial does allow you to add classes and redefine equals() - with the restrictions already mentioned - but only for hierarchies written the same way.

And I'm afraid that's all you can reasonably hope for.

.

There is a LOT to know about writing equals() methods - believe me, we've only scratched the surface here - but for now, remember the pattern you saw earlier, because pretty much every one you write should look similar.

You should also check the Subclasses section, because we're not quite finished yet...



The Complete Class  

Quite a lot to take in, eh? Here's our final class without all the woffle and, as you'll see, it's nowhere near as big as you probably thought.

I've swapped the methods around a bit according to the way I like to see them - constructors first, private methods next, then protected, then public; with methods arranged in alphabetical sequence - but you're free to choose any order that makes sense to you. Just be consistent about it:

.

public final class Person
  extends Object
{
  private final String name;
  private final int age;

  public Person(String name, int age) {
    super();
    this.name = Person.validName(name);
    this.age  = Person.validAge(age);
  }

  public Person(String name, String age) {
    this( name, Integer.parseInt(age.trim()) );
  }

  private static final int validAge(int age) {
    if (age < 0 || age > 125)
     throw new IllegalArgumentException(
      "invalid age");
    return age;
  }

  private static final String validName(String name) {
    if (name == null)
     throw new IllegalArgumentException(
      "name cannot be null");

    name = name.trim();

    if (name.length() == 0)
     throw new IllegalArgumentException(
      "name cannot be blank");

    return name;
  }

  protected boolean canEqual(Object other) {
    return other instanceof Person;
  }

  @Override
  public boolean equals(Object other) {
    if (this == other)
      return true;

    if ( ! this.canEqual(other) )
      return false;

    Person that = (Person) other;

    if ( ! that.canEqual(this) )
      return false;

    return this.name.equals(that.name)
      && this.age == that.age;
  }

  @Override
  public int hashCode() {
    return Objects.hash(name, age);
  }

  @Override
  public String toString() {
    return name + ";" + age;
  }
}

.

You're now ready to add anything else that takes your fancy. And if it seems like a lot of code right now, believe me, it's peanuts compared to what you will write.

We're not quite done yet, though...



SUBCLASSES  

Up to now, we've assumed that our Person class is a top-level class (ie, it extends Object); but what if the one that you're defining isn't?

.

Let's suppose that we've decided that we do want to allow Person to be subclassed, and so we've removed the final keyword from its class definition.

We now want to define an Employee class that extends Person, and adds a salary field. The definition will initially look like this:

public final class Employee
  extends Person
{
  private final int salary;

Note that we don't need to include Person's fields, because they're implicit in the extends Person statement. Now that may seem obvious, but it implies something else too:

We now have an active superclass and, unless we want to repeat all of its code, we need some way to re-use it.

And to do that, you'll need to become familiar with a new keyword: super.

You already saw it in the Person(fields) constructor, but now we're going to use it as a reference.

"super" is a bit like "this", but refers specifically to the "superclass part" of our object; so when we write something like "super.equals(...)", what we're actually saying is: "call the equals() method defined for our superclass; NOT ours".

.

Just to be consistent, let's add a validation method for our salary field:

private static final int validSalary(int salary) {
  // We don't want pay them too little ... or too much :-)
  if (salary < 1000 || salary > 100000)
    throw new IllegalArgumentException(
      "invalid salary");
  return salary;
}

.

Now, let's deal with each case in turn:

.

Employee(fields)

This is pretty straightforward. The main thing to remember is that since Employee is a subclass of Person, we also need to include its fields, viz:

public Employee(String name, int age, int salary) {
  super(name, age);
  this.salary = Employee.validSalary(salary);
}

The only thing worthy of note here is that super(...) call:

Employee is a subclass, so it will be calling the Person(fields) constructor. Do you see why I told you to always include that call now? If we'd left it up to the compiler, it would have inserted:

  super();

which doesn't exist (Person doesn't have a Person() constructor).

So you'd get a compiler error for code you didn't write.

.

Employee(strings)

Our "string-conversion" constructor. Again, this is pretty straightforward:

public Employee(String name, String age, String salary) {
  super(name, age);

  salary = salary.trim(); // just to be safe   

  this.salary = Employee.validSalary(
    Integer.parseInt(salary) );
}

The only difference here is that the super(name, age) call is now calling the Person(strings) constructor, because both name and age are Strings.

Hopefully, the rest is familiar by now.

.

canEqual(Object)

As you saw earlier, overriding this method is incredibly simple. In our case:

@Override
protected boolean canEqual(Object other) {
  return other instanceof Employee;
}

Remember the "@Override" annotation, because now we are overriding; and remember also that it takes an Object, not an Employee, and that it's "protected", NOT "public". Unfortunately, the compiler can't warn you if you get that last bit wrong, so you just need to remember it.

Tip #13:  

Don't make methods public unless they need to be.

.

equals()

If your class is part of a hierarchy, you'll have to include a new Step, viz:

    if ( ! super.equals(other) )
      return false;

Why? Because hopefully, the person who wrote the superclass to yours (in this case, us) has also read this tutorial, and has hidden its contents even from you.

Therefore, the only way to know if the superclass portion of your object is "equal" is to call its equals() method. If it returns false, then obviously the two objects can't be "equal", so we may as well return false straight away.

.

Moreover, we can replace Steps 2 AND 4 with our new Step, viz:

  @Override
  public boolean equals(Object other) {
    if (this == other)
      return true;

    if ( ! super.equals(other) )
      return false;

    Employee that = (Employee) other;

    return this.salary == that.salary;
  }

Hunh? How does that work? We haven't written any this.canEqual(other) check, so how can we suddenly cast other to an Employee?

.

The answer is that super.equals(other) calls Person's equals() method, and Step 2 in that method calls OUR canEqual() method - that's how overriding works - so it can't possibly return true unless other is an Employee (or a subclass).

And it doesn't matter how many classes there are between us and Person because - providing they are ALL written the same way - super.equals() must eventually call Person's method.

Furthermore, we also know that other is NOT a subclass of Employee that redefines equals(), since it also has to pass Step 4; so we can be sure that our equals() method will also follow the 'transitivity' rule.

So in fact, equals() for a subclass is actually simpler than for a top-level one.

Nice, eh?

.

The main thing to remember is that this pattern relies on two things:

  1. ALL subclass equals() methods follow it.
  2. The Three Musketeers rule - Specifically: if you override equals(), you MUST override canEqual(); if you don't, leave it out.

.

Note that we can't do this for a top-level class like Person, because Object's equals() method only returns true if the this and other are the same object (ie, it behaves like '==').

.

hashCode()

The only thing to remember here is that, for a subclass, you must include the superclass's hashcode in the calculation of yours. Just think of it as one of your class's "fields".

So, if you're on version 7 this would be:

@Override
public int hashCode() {
  return Objects.hash(super.hashCode(), salary);
}

Otherwise, on version 5 or later:

@Override
public int hashCode() {
  return Arrays.hashCode(
    new Object[] {super.hashCode(), salary} );
}

Note that, as with equals(), we can't do this for a top-level class like Person, because Object's hashCode() method returns a different value for each object, regardless of content.

.

toString()

As you can probably imagine, this is similar to the previous two. Maybe you can see why we chose semicolon-delimited output now - because it's easy to add to, viz:

@Override
public String toString() {
  return super.toString() + ";" + salary;
}

which will return a String like:

Joe Bloggs;47;25000

and, as before, we can't do this for a top-level class like Person, because we'd get:

Person@0af739b2;Joe Bloggs;47



The complete Employee Class

So here's what a subclass looks like without all the woffle:

.

public final class Employee
  extends Person
{
  private final int salary;

  public Employee(String name, int age, int salary) {
    super(name, age);
    this.salary = Employee.validSalary(salary);
  }

  public Employee(String name, String age, String salary) {
    super(name, age);

    salary = salary.trim();

    this.salary = Employee.validSalary(
      Integer.parseInt(salary) );
  }

  private static final int validSalary(int salary) {
    if (salary < 1000 || salary > 100000)
     throw new IllegalArgumentException(
      "invalid salary");
    return salary;
  }

  @Override
  protected boolean canEqual(Object other) {
    return other instanceof Employee;
  }

  @Override
  public boolean equals(Object other) {
    if (this == other)
      return true;

    if ( ! super.equals(other) )
      return false;

    Employee that = (Employee) other;

    return this.salary == that.salary;
  }

  @Override
  public int hashCode() {
    return Objects.hash(super.hashCode(), salary);
  }

  @Override
  public String toString() {
    return super.toString() + ";" + salary;
  }
}

.

As you can see, quite similar to our Person class. You just need to remember those "super" calls.



OTHER STUFF

In describing the methodology, we've glossed over some stuff that needs to be mentioned; and, as you can imagine, most of it has to do with our old friend: equals()

Type checking

There is an alternative way to write Step 2 of our method.  Instead of:

if ( ! this.canEqual(other) )
  return false;

you can use:

if ( other == null 
  || other.getClass() != this.getClass() )
  return false;

It all has to do with your definition of a "type". The version I gave you allows you to compare a Person with a subclass of a Person - providing it doesn't redefine equals() of course - the alternative above doesn't.

Obviously, I prefer the version I gave you; but you may well see the alternative used in some books. The arguments for each style are probably too advanced to go into at the moment, but a few in favour of the alternative are:

  1. It's simple.
  2. It doesn't require a canEqual() method.
  3. It's logically SAFE (remember all those rules?)

If other is not specifically a Person - even if it's a subclass - the method returns false.

This safety comes at the price of flexibility (and some might say "objectivity") however; and I'm a great believer in the old quote of Einstein's (paraphrased):

"Everything should be as simple as possible, but no simpler."

.

Consistency

Remember that Rule #4 - "it must be consistent"?

What that means is that x.equals(y) must consistently return the same value every time it's called, unless a field used in the equals() comparison has changed. And hashCode() has a very similar rule.

Unfortunately, it manifests itself with hashed collections like HashSet and HashMap.

.

Let's assume, just for a moment, that we allowed clients to change the salary of an Employee. We might end up with a case like this:

Employee joe = new Employee("Joe Bloggs", 47, 25000);
HashSet<Employee> empSet = new HashSet<Employee>();
empSet.add(joe);
empSet.contains(joe); // returns 'true'

everything fine so far. Now we give Joe a raise:

joe.setSalary(30000);

and check him again:

empSet.contains(joe); // returns 'false'

Hunh? What just happened? We didn't remove Joe from the Set, so what's going on?

.

ALL collections in the Java Collections Framework (well, except for one ) use equals() to determine whether an object exists or not.

The trouble is, hashed collections ALSO use hashCode() to place objects in internal areas called "buckets" - it's one of the things that makes them so darn fast. The idea is that when you do a search, a HashSet only has to check the bucket where it put the object to see if there is one in there that is "equal".

Unfortunately, now that we've changed Joe's salary, his hashCode() probably won't be the same as it was when we added him, which means that we'll now be looking for him in the wrong bucket. Even worse: on rare occasions, we might just be "lucky" with our new hashcode, and our HashSet will find Joe; so we can't even rely on the search failing. And believe me, when you're trying to debug, inconsistency is far worse than failure.

Do you see now why I suggested that you make ALL your fields final?

Hashed collections and mutability just don't mix. And there are other cases where changing the contents of an object can make equals() and/or hashCode() behave oddly too, so think very carefully before you allow it.

.

Documentation

You may have noticed that one thing conspicuously absent from our class listing is any sort of documentation. That's because we've been concentrating on the content, but it's definitely NOT something I'd advise in general.

One of the most important aspects of writing good classes is good documentation. Furthermore, Java has a wonderful tool called javadoc, which takes most of the effort out of producing it, and is also where you get all those nice API pages from.

Just to give you a quick taster, here's how our Person.equals() method might look with a few javadoc comments:

  /**
   * Indicates whether some other object is "equal to"
   * this Person. 
   * <p/>
   * The supplied object '<tt>other</tt>' is considered
   * "equal" if, and ONLY if, it is:
   * <ul>
   * <li>A <tt>Person</tt> (or subclass).</li>
   * <li>Has the same name and age.</li>
   * </ul>
   *
   * @param other The object to be compared to <tt>this</tt>.
   *
   * @return <tt>true</tt> if <tt>other</tt> is "equal"
   *	 to <tt>this</tt>; otherwise <tt>false</tt>.
   **/
  @Override
  public boolean equals(Object other) {
    ...

which will produce output similar (but not identical) to this.

And, other than writing the comments themselves, the only thing you have to do is run the javadoc command. Notice how you can also mix in HTML to get lists, paragraphs and different fonts if you want.

A detailed review of the tool is beyond the scope of this tutorial, but I strongly urge you to read its howto manual before you write too many classes; and start using it as a matter of course.



LESSONS

This is just a re-hash of some of the things already mentioned:

  1. Always, always, always make your fields private.
  2. Fail fast, and fail LOUD.
  3. Check ALL parameter values supplied to methods and constructors - especially nulls.
  4. Always put an explicit "super(...)" or "this(...)" call in line 1 of your constructors.
  5. Put "this." in front of all class field references.
  6. Write all your equals() methods the same way (ie, use the steps defined in this tutorial).
  7. Write hashCode() and canEqual() methods whenever you override equals() - remember: The Three Musketeers.
  8. Use equals(), NOT '==', whenever you're comparing objects.
  9. Always write a toString() method, and keep it simple.
  10. Make all your classes, fields and methods final by default. You can always remove it later on if you find it too restrictive, but you can't ADD it.



CAVEAT  

The methodology detailed in this page creates immutable, final classes, which might not look like the ones you've seen in your books or tutorials. This is because they're trying to teach you about aspects of Object-Orientation like inheritance and polymorphism and overriding; and so aren't too worried about "real-world" issues like making your classes safe.

The fact is that big class hierarchies have rather gone out of fashion, and composition is often the preferred way of "extending" behaviour; so chances are that a lot of classes you write will be final (but not all ).

What I've tried to do is show you a bulletproof approach for writing a real class that is easy to follow, but that you can depart from if you need to (probably by taking out final when you KNOW it isn't applicable). And, as I said before, that's the great thing about Java classes: you can change them if you need to - but only if you start out the right way.

I've also deliberately left out all sorts of things, like "getter" and "setter" methods, that you can find out about elsewhere. What this tutorial provides you with is a foundation to build on.

Basically, we've only scratched the surface. Now it's time for you to learn some more.



FURTHER READING

How to write an Equality method in Java - Martin Odersky et al, 2009.

Effective Java, version 2 - Joshua Bloch, Addison-Wesley 2008.

The Java Tutorials - Sun Microsystems/Oracle Corporation (Full index).

How to Write Doc Comments for the Javadoc Tool - Sun Microsystems/Oracle Corporation.

Secrets of equals() - Part 1 - Angelika Langer & Klaus Kreft, 2002.



CategoryWinston

JavaRanchContact us — Copyright © 1998-2014 Paul Wheaton