thinkapjava 5.1.2 documentation

Object-oriented programming

«  Objects of Arrays   ::   Contents   ::   GridWorld: Part 3  »

Object-oriented programming

Programming languages and styles

There are many programming languages and almost as many programming styles (sometimes called paradigms). The programs we have written so far are procedural, because the emphasis has been on specifying computational procedures.

Most Java programs are object-oriented, which means that the focus is on objects and their interactions. Here are some of the characteristics of object-oriented programming:

  • Objects often represent entities in the real world. In the previous chapter, creating the Deck class was a step toward object-oriented programming.
  • The majority of methods are object methods (like the methods you invoke on Strings) rather than class methods (like the Math methods). The methods we have written so far have been class methods. In this chapter we write some object methods.
  • Objects are isolated from each other by limiting the ways they interact, especially by preventing them from accessing instance variables without invoking methods.
  • Classes are organized in family trees where new classes extend existing classes, adding new methods and replacing others.

In this chapter I translate the Card program from the previous chapter from procedural to object-oriented style. You can download the code from this chapter from Card3.java.

Object methods and class methods

There are two types of methods in Java, called class methods and object methods. Class methods are identified by the keyword static in the first line. Any method that does not have the keyword static is an object method.

Although we have not written object methods, we have invoked some. Whenever you invoke a method “on” an object, it’s an object method. For example, charAt and the other methods we invoked on String objects are all object methods.

Anything that can be written as a class method can also be written as an object method, and vice versa. But sometimes it is more natural to use one or the other.

For example, here is printCard as a class method:

public static void printCard(Card c) {
    System.out.println(ranks[c.rank] + " of " + suits[c.suit]);
}

Here it is re-written as an object method:

public void print() {
    System.out.println(ranks[rank] + " of " + suits[suit]);
}

Here are the changes:

  1. I removed static.
  2. I changed the name of the method to be more idiomatic.
  3. I removed the parameter.
  4. Inside an object method you can refer to instance variables as if they were local variables, so I changed c.rank to rank, and likewise for suit.

Here’s how this method is invoked:

Card card = new Card(1, 1);
card.print();

When you invoke a method on an object, that object becomes the current object, also known as this. Inside print, the keyword this refers to the card the method was invoked on.

The toString method

Every object type has a method called toString that returns a string representation of the object. When you print an object using print or println, Java invokes the object’s toString method.

The default version of toString returns a string that contains the type of the object and a unique identifier (see Section Printing objects). When you define a new object type, you can override the default behavior by providing a new method with the behavior you want.

For example, here is a toString method for Card:

public String toString() {
    return ranks[rank] + " of " + suits[suit];
}

The return type is String, naturally, and it takes no parameters. You can invoke toString in the usual way:

Card card = new Card(1, 1);
String s = card.toString();

or you can invoke it indirectly through println:

System.out.println(card);

The equals method

In Section The sameCard method we talked about two notions of equality: identity, which means that two variables refer to the same object, and equivalence, which means that they have the same value.

The == operator tests identity, but there is no operator that tests equivalence, because what “equivalence” means depends on the type of the objects. Instead, objects provide a method named equals that defines equivalence.

Java classes provide equals methods that do the right thing. But for user defined types the default behavior is the same as identity, which is usually not what you want.

For Cards we already have a method that checks equivalence:

public static boolean sameCard(Card c1, Card c2) {
    return (c1.suit == c2.suit && c1.rank == c2.rank);
}

So all we have to do is rewrite is as an object method:

public boolean equals(Card c2) {
    return (suit == c2.suit && rank == c2.rank);
}

Again, I removed static and the first parameter, c1. Here’s how it’s invoked:

Card card = new Card(1, 1);
Card card2 = new Card(1, 1);
System.out.println(card.equals(card2));

Inside equals, card is the current object and card2 is the parameter, c2. For methods that operate on two objects of the same type, I sometimes use this explicitly and call the parameter that:

public boolean equals(Card that) {
    return (this.suit == that.suit && this.rank == that.rank);
}

I think it improves readability.

Oddities and errors

If you have object methods and class methods in the same class, it is easy to get confused. A common way to organize a class definition is to put all the constructors at the beginning, followed by all the object methods and then all the class methods.

You can have an object method and a class method with the same name, as long as they do not have the same number and types of parameters. As with other kinds of overloading, Java decides which version to invoke by looking at the arguments you provide.

Now that we know what the keyword static means, you have probably figured out that main is a class method, which means that there is no “current object” when it is invoked. Since there is no current object in a class method, it is an error to use the keyword this. If you try, you get an error message like: “Undefined variable: this.”

Also, you cannot refer to instance variables without using dot notation and providing an object name. If you try, you get a message like “non-static variable... cannot be referenced from a static context.” By “non-static variable” it means “instance variable.”

Inheritance

The language feature most often associated with object-oriented programming is inheritance. Inheritance is the ability to define a new class that is a modified version of an existing class. Extending the metaphor, the existing class is sometimes called the parent class and the new class is called the child.

The primary advantage of this feature is that you can add methods and instance variables without modifying the parent. This is particularly useful for Java classes, since you can’t modify them even if you want to.

If you did the GridWorld exercises (Chapters GridWorld: Part 1 and GridWorld: Part 2) you have seen examples of inheritance:

1
2
3
4
5
6
7
8
9
public class BoxBug extends Bug {
    private int steps;
    private int sideLength;

    public BoxBug(int length) {
        steps = 0;
        sideLength = length;
    }
}

BoxBug extends Bug means that BoxBug is a new kind of Bug that inherits the methods and instance variables of Bug. In addition:

  • The child class can have additional instance variables; in this example, BoxBugs have steps and sideLength.
  • The child can have additional methods; in this example, BoxBugs have an additional constructor that takes an integer parameter.
  • The child can override a method from the parent; in this example, the child provides act (not shown here), which overrides the act method from the parent.

If you did the Graphics exercises in Appendix Graphics, you saw another example:

public class MyCanvas extends Canvas {

    public void paint(Graphics g) {
        g.fillOval(100, 100, 200, 200);
    }
}

MyCanvas is a new kind of Canvas with no new methods or instance variables, but it overrides paint.

If you didn’t do either of those exercises, now is a good time!

The class hierarchy

In Java, all classes extend some other class. The most basic class is called Object. It contains no instance variables, but it provides the methods equals and toString, among others.

Many classes extend Object, including almost all of the classes we have written and many Java classes, like java.awt.Rectangle. Any class that does not explicitly name a parent inherits from Object by default.

Some inheritance chains are much longer, though. For example, javax.swing.JFrame extends java.awt.Frame, which extends Window, which extends Container, which extends Component, which extends Object. No matter how long the chain, Object is the common ancestor of all classes.

The “family tree” of classes is called the class hierarchy. Object usually appears at the top, with all the “child” classes below. If you look at the documentation of JFrame, for example, you see the part of the hierarchy that makes up JFrame’s pedigree.

Object-oriented design

Inheritance is a powerful feature. Some programs that would be complicated without it can be written concisely and simply with it. Also, inheritance can facilitate code reuse, since you can customize the behavior of existing classes without having to modify them.

On the other hand, inheritance can make programs hard to read. When you see a method invocation, it can be hard to figure out which method gets invoked.

Also, many of the things that can be done with inheritance can be done as well or better without it. A common alternative is composition, where new objects are composed of existing objects, adding new capability without inheritance.

Designing objects and the relationships among them is the topic of object-oriented design, which is beyond the scope of this book. But if you are interested, I recommend Head First Design Patterns, published by O’Reilly Media.

Glossary

object method:
A method that is invoked on an object, and that operates on that object. Object methods do not have the keyword static.
class method:
A method with the keyword static. Class methods are not invoked on objects and they do not have a current object.
current object:
The object on which an object method is invoked. Inside the method, the current object is referred to by this.
implicit:
Anything that is left unsaid or implied. Within an object method, you can refer to the instance variables implicitly (i.e., without naming the object).
explicit:
Anything that is spelled out completely. Within a class method, all references to the instance variables have to be explicit.

Exercises

Exercise

Download CardSoln2.java and CardSoln3.java.

CardSoln2.java contains solutions to the exercises in the previous chapter. It uses only class methods (except the constructors).

CardSoln3.java contains the same program, but most of the methods are object methods. I left merge unchanged because I think it is more readable as a class method.

Transform merge into an object method, and change mergeSort accordingly. Which version of merge do you prefer?

Exercise

Transform the following class method into an object method.

public static double abs(Complex c) {
    return Math.sqrt(c.real * c.real + c.imag * c.imag);
}

Exercise

Transform the following object method into a class method.

public boolean equals(Complex b) {
    return(real == b.real && imag == b.imag);
}

Exercise

This exercise is a continuation of this Exercise. The purpose is to practice the syntax of object methods and get familiar with the relevant error messages.

  1. Transform the methods in the Rational class from class methods to object methods, and make the necessary changes in main.
  2. Make a few mistakes. Try invoking class methods as if they were object methods and vice-versa. Try to get a sense for what is legal and what is not, and for the error messages that you get when you mess up.
  3. Think about the pros and cons of class and object methods. Which is more concise (usually)? Which is a more natural way to express computation (or, maybe more fairly, what kind of computations can be expressed most naturally using each style)?

Exercise

The goal of this exercise is to write a program that generates random poker hands and classifies them, so that we can estimate the probability of the various poker hands. If you don’t play poker, you can read about it in Wikipedia.

  1. Start with CardSoln3.java and make sure you can compile and run it.
  2. Write a definition for a class named PokerHand that extends Deck.
  3. Write a Deck method named deal that creates a PokerHand, transfers cards from the deck to the hand, and returns the hand.
  4. In main use shuffle and deal to generate and print four PokerHands with five cards each. Did you get anything good?
  5. Write a PokerHand method called hasFlush returns a boolean indicating whether the hand contains a flush.
  6. Write a method called hasThreeKind that indicates whether the hand contains Three of a Kind.
  7. Write a loop that generates a few thousand hands and checks whether they contain a flush or three of a kind. Estimate the probability of getting one of those hands. Compare your results to the probabilities at Wikipedia.
  8. Write methods that test for the other poker hands. Some are easier than others. You might find it useful to write some general-purpose helper methods that can be used for more than one test.
  9. In some poker games, players get seven cards each, and they form a hand with the best five of the seven. Modify your program to generate seven-card hands and recompute the probabilities.

«  Objects of Arrays   ::   Contents   ::   GridWorld: Part 3  »