Arrays of Objects in Java: A Comprehensive Guide with Examples, Exams of Design

suits[3] = Spades;. Creating an array and initializing the elements is such a common operation that. Java provides a special syntax for it:.

Typology: Exams

2022/2023

Uploaded on 02/28/2023

kourtney
kourtney 🇺🇸

4.8

(6)

220 documents

1 / 30

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
Chapter 11
Arrays of Objects
11.1 Composition
By now we have seen several examples of composition (the ability to combine
language features in a variety of arrangements). One of the first examples we
saw was using a method invocation as part of an expression. Another example
is the nested structure of statements: you can put an if statement within a
while loop, or within another if statement, etc.
Having seen this pattern, and having learned about arrays and objects, you
should not be surprised to learn that you can have arrays of objects. In fact,
you can also have objects that contain arrays (as instance variables); you can
have arrays that contain arrays; you can have objects that contain objects, and
so on.
In the next two chapters we will look at some examples of these combinations,
using Card objects as an example.
11.2 Card objects
If you are not familiar with common playing cards, now would be a good time to
get a deck, or else this chapter might not make much sense. There are 52 cards
in a deck, each of which belongs to one of four suits and one of 13 ranks. The
suits are Spades, Hearts, Diamonds and Clubs (in descending order in Bridge).
The ranks are Ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen and King. Depending
on what game you are playing, the rank of the Ace may be higher than King or
lower than 2.
If we want to define a new object to represent a playing card, it is pretty obvious
what the instance variables should be: rank and suit. It is not as obvious what
type the instance variables should be. One possibility is Strings, containing
things like "Spade" for suits and "Queen" for ranks. One problem with this
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e

Partial preview of the text

Download Arrays of Objects in Java: A Comprehensive Guide with Examples and more Exams Design in PDF only on Docsity!

Chapter 11

Arrays of Objects

11.1 Composition

By now we have seen several examples of composition (the ability to combine language features in a variety of arrangements). One of the first examples we saw was using a method invocation as part of an expression. Another example is the nested structure of statements: you can put an if statement within a while loop, or within another if statement, etc.

Having seen this pattern, and having learned about arrays and objects, you should not be surprised to learn that you can have arrays of objects. In fact, you can also have objects that contain arrays (as instance variables); you can have arrays that contain arrays; you can have objects that contain objects, and so on.

In the next two chapters we will look at some examples of these combinations, using Card objects as an example.

11.2 Card objects

If you are not familiar with common playing cards, now would be a good time to get a deck, or else this chapter might not make much sense. There are 52 cards in a deck, each of which belongs to one of four suits and one of 13 ranks. The suits are Spades, Hearts, Diamonds and Clubs (in descending order in Bridge). The ranks are Ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen and King. Depending on what game you are playing, the rank of the Ace may be higher than King or lower than 2.

If we want to define a new object to represent a playing card, it is pretty obvious what the instance variables should be: rank and suit. It is not as obvious what type the instance variables should be. One possibility is Strings, containing things like "Spade" for suits and "Queen" for ranks. One problem with this

132 Arrays of Objects

implementation is that it would not be easy to compare cards to see which had higher rank or suit.

An alternative is to use integers to encode the ranks and suits. By “encode,” I do not mean what some people think, which is to encrypt, or translate into a secret code. What a computer scientist means by “encode” is something like “define a mapping between a sequence of numbers and the things I want to represent.” For example,

Spades 7 → 3 Hearts 7 → 2 Diamonds 7 → 1 Clubs 7 → 0

The obvious feature of this mapping is that the suits map to integers in order, so we can compare suits by comparing integers. The mapping for ranks is fairly obvious; each of the numerical ranks maps to the corresponding integer, and for face cards:

Jack 7 → 11 Queen 7 → 12 King 7 → 13

The reason I am using mathematical notation for these mappings is that they are not part of the Java program. They are part of the program design, but they never appear explicitly in the code. The class definition for the Card type looks like this:

class Card { int suit, rank;

public Card () { this.suit = 0; this.rank = 0; }

public Card (int suit, int rank) { this.suit = suit; this.rank = rank; } }

As usual, I am providing two constructors, one of which takes a parameter for each instance variable and the other of which takes no parameters.

To create an object that represents the 3 of Clubs, we would use the new com- mand:

Card threeOfClubs = new Card (0, 3);

The first argument, 0 represents the suit Clubs.

134 Arrays of Objects

the expression suits[c.suit] means “use the instance variable suit from the object c as an index into the array named suits, and select the appropriate string.” The output of this code

Card card = new Card (1, 11); printCard (card);

is Jack of Diamonds.

11.4 The sameCard method

The word “same” is one of those things that occur in natural language that seem perfectly clear until you give it some thought, and then you realize there is more to it than you expected.

For example, if I say “Chris and I have the same car,” I mean that his car and mine are the same make and model, but they are two different cars. If I say “Chris and I have the same mother,” I mean that his mother and mine are one and the same. So the idea of “sameness” is different depending on the context.

When you talk about objects, there is a similar ambiguity. For example, if two Cards are the same, does that mean they contain the same data (rank and suit), or they are actually the same Card object?

To see if two references refer to the same object, we can use the == operator. For example:

Card card1 = new Card (1, 11); Card card2 = card1;

if (card1 == card2) { System.out.println ("card1 and card2 are the same object."); }

This type of equality is called shallow equality because it only compares the references, not the contents of the objects.

To compare the contents of the objects—deep equality—it is common to write a method with a name like sameCard.

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

Now if we create two different objects that contain the same data, we can use sameCard to see if they represent the same card:

Card card1 = new Card (1, 11); Card card2 = new Card (1, 11);

if (sameCard (card1, card2)) { System.out.println ("card1 and card2 are the same card."); }

11.5 The compareCard method 135

In this case, card1 and card2 are two different objects that contain the same data

11

suit 1

rank

card 11

suit 1

rank

card

so the condition is true. What does the state diagram look like when card1 == card2 is true?

In Section 7.10 I said that you should never use the == operator on Strings because it does not do what you expect. Instead of comparing the contents of the String (deep equality), it checks whether the two Strings are the same object (shallow equality).

11.5 The compareCard method

For primitive types, there are conditional operators that compare values and determine when one is greater or less than another. These operators (< and

and the others) don’t work for object types. For Strings there is a built-in compareTo method. For Cards we have to write our own, which we will call compareCard. Later, we will use this method to sort a deck of cards.

Some sets are completely ordered, which means that you can compare any two elements and tell which is bigger. For example, the integers and the floating- point numbers are totally ordered. Some sets are unordered, which means that there is no meaningful way to say that one element is bigger than another. For example, the fruits are unordered, which is why we cannot compare apples and oranges. In Java, the boolean type is unordered; we cannot say that true is greater than false.

The set of playing cards is partially ordered, which means that sometimes we can compare cards and sometimes not. For example, I know that the 3 of Clubs is higher than the 2 of Clubs, and the 3 of Diamonds is higher than the 3 of Clubs. But which is better, the 3 of Clubs or the 2 of Diamonds? One has a higher rank, but the other has a higher suit.

In order to make cards comparable, we have to decide which is more important, rank or suit. To be honest, the choice is completely arbitrary. For the sake of choosing, I will say that suit is more important, because when you buy a new deck of cards, it comes sorted with all the Clubs together, followed by all the Diamonds, and so on.

With that decided, we can write compareCard. It will take two Cards as pa- rameters and return 1 if the first card wins, -1 if the second card wins, and 0 if they tie (indicating deep equality). It is sometimes confusing to keep those return values straight, but they are pretty standard for comparison methods.

11.7 The printDeck method 137

deck[index] = new Card (suit, rank); index++; } }

The outer loop enumerates the suits, from 0 to 3. For each suit, the inner loop enumerates the ranks, from 1 to 13. Since the outer loop iterates 4 times, and the inner loop iterates 13 times, the total number of times the body is executed is 52 (13 times 4).

I used the variable index to keep track of where in the deck the next card should go. The following state diagram shows what the deck looks like after the first two cards have been allocated:

2

suit 0

1 rank

suit 0

rank

deck

0 1 2 3 51

Exercise 11.1 Encapsulate this deck-building code in a method called buildDeck that takes no parameters and that returns a fully-populated array of Cards.

11.7 The printDeck method

Whenever you are working with arrays, it is convenient to have a method that will print the contents of the array. We have seen the pattern for traversing an array several times, so the following method should be familiar:

public static void printDeck (Card[] deck) { for (int i=0; i<deck.length; i++) { printCard (deck[i]); } }

Since deck has type Card[], an element of deck has type Card. Therefore, deck[i] is a legal argument for printCard.

11.8 Searching

The next method I want to write is findCard, which searches through an array of Cards to see whether it contains a certain card. It may not be obvious why this method would be useful, but it gives me a chance to demonstrate two ways to go searching for things, a linear search and a bisection search.

138 Arrays of Objects

Linear search is the more obvious of the two; it involves traversing the deck and comparing each card to the one we are looking for. If we find it we return the index where the card appears. If it is not in the deck, we return -1.

public static int findCard (Card[] deck, Card card) { for (int i = 0; i< deck.length; i++) { if (sameCard (deck[i], card)) return i; } return -1; }

The arguments of findCard are named card and deck. It might seem odd to have a variable with the same name as a type (the card variable has type Card). This is legal and common, although it can sometimes make code hard to read. In this case, though, I think it works.

The method returns as soon as it discovers the card, which means that we do not have to traverse the entire deck if we find the card we are looking for. If the loop terminates without finding the card, we know the card is not in the deck and return -1.

If the cards in the deck are not in order, there is no way to search that is faster than this. We have to look at every card, since otherwise there is no way to be certain the card we want is not there.

But when you look for a word in a dictionary, you don’t search linearly through every word. The reason is that the words are in alphabetical order. As a result, you probably use an algorithm that is similar to a bisection search:

  1. Start in the middle somewhere.
  2. Choose a word on the page and compare it to the word you are looking for.
  3. If you found the word you are looking for, stop.
  4. If the word you are looking for comes after the word on the page, flip to somewhere later in the dictionary and go to step 2.
  5. If the word you are looking for comes before the word on the page, flip to somewhere earlier in the dictionary and go to step 2.

If you ever get to the point where there are two adjacent words on the page and your word comes between them, you can conclude that your word is not in the dictionary. The only alternative is that your word has been misfiled somewhere, but that contradicts our assumption that the words are in alphabetical order.

In the case of a deck of cards, if we know that the cards are in order, we can write a version of findCard that is much faster. The best way to write a bisection search is with a recursive method. That’s because bisection is naturally recursive.

140 Arrays of Objects

if (comp == 0) { return mid; } else if (comp > 0) { return findBisect (deck, card, low, mid-1); } else { return findBisect (deck, card, mid+1, high); } }

I added a print statement at the beginning so I could watch the sequence of recursive calls and convince myself that it would eventually reach the base case. I tried out the following code:

Card card1 = new Card (1, 11); System.out.println (findBisect (deck, card1, 0, 51));

And got the following output:

0, 51 0, 24 13, 24 19, 24 22, 24 23

Then I made up a card that is not in the deck (the 15 of Diamonds), and tried to find it. I got the following:

0, 51 0, 24 13, 24 13, 17 13, 14 13, 12

These tests don’t prove that this program is correct. In fact, no amount of testing can prove that a program is correct. On the other hand, by looking at a few cases and examining the code, you might be able to convince yourself.

The number of recursive calls is fairly small, typically 6 or 7. That means we only had to invoke compareCard 6 or 7 times, compared to up to 52 times if we did a linear search. In general, bisection is much faster than a linear search, and even more so for large arrays.

Two common errors in recusive programs are forgetting to include a base case and writing the recursive call so that the base case is never reached. Either error will cause an infinite recursion, in which case Java will (eventually) throw a StackOverflowException.

11.9 Decks and subdecks 141

11.9 Decks and subdecks

Looking at the interface to findBisect

public static int findBisect (Card[] deck, Card card, int low, int high)

it might make sense to think of three of the parameters, deck, low and high, as a single parameter that specifies a subdeck. This way of thinking is quite common, and I sometimes think of it as an abstract parameter. What I mean by “abstract,” is something that is not literally part of the program text, but which describes the function of the program at a higher level.

For example, when you invoke a method and pass an array and the bounds low and high, there is nothing that prevents the invoked method from accessing parts of the array that are out of bounds. So you are not literally sending a subset of the deck; you are really sending the whole deck. But as long as the recipient plays by the rules, it makes sense to think of it, abstractly, as a subdeck.

There is one other example of this kind of abstraction that you might have noticed in Section 9.7, when I referred to an “empty” data structure. The reason I put “empty” in quotation marks was to suggest that it is not literally accurate. All variables have values all the time. When you create them, they are given default values. So there is no such thing as an empty object.

But if the program guarantees that the current value of a variable is never read before it is written, then the current value is irrelevant. Abstractly, it makes sense to think of such a variable as “empty.”

This kind of thinking, in which a program comes to take on meaning beyond what is literally encoded, is a very important part of thinking like a computer scientist. Sometimes, the word “abstract” gets used so often and in so many contexts that it comes to lose its meaning. Nevertheless, abstraction is a central idea in computer science (as well as many other fields).

A more general definition of “abstraction” is “The process of modeling a complex system with a simplified description in order to suppress unnecessary details while capturing relevant behavior.”

11.10 Glossary

encode: To represent one set of values using another set of values, by con- structing a mapping between them.

shallow equality: Equality of references. Two references that point to the same object.

deep equality: Equality of values. Two references that point to objects that have the same value.

abstract parameter: A set of parameters that act together as a single param- eter.

Chapter 12

Objects of Arrays

12.1 The Deck class

In the previous chapter, we worked with an array of objects, but I also men- tioned that it is possible to have an object that contains an array as an instance variable. In this chapter we are going to create a new object, called a Deck, that contains an array of Cards as an instance variable.

The class definition looks like this

class Deck { Card[] cards;

public Deck (int n) { cards = new Card[n]; } }

The name of the instance variable is cards to help distinguish the Deck object from the array of Cards that it contains. Here is a state diagram showing what a Deck object looks like with no cards allocated:

deck cards

As usual, the constructor initializes the instance variable, but in this case it uses the new command to create the array of cards. It doesn’t create any cards to go in it, though. For that we could write another constructor that creates a standard 52-card deck and populates it with Card objects:

public Deck () { cards = new Card[52]; int index = 0; for (int suit = 0; suit <= 3; suit++) {

144 Objects of Arrays

for (int rank = 1; rank <= 13; rank++) { cards[index] = new Card (suit, rank); index++; } } }

Notice how similar this method is to buildDeck, except that we had to change the syntax to make it a constructor. To invoke it, we use the new command:

Deck deck = new Deck ();

Now that we have a Deck class, it makes sense to put all the methods that pertain to Decks in the Deck class definition. Looking at the methods we have written so far, one obvious candidate is printDeck (Section 11.7). Here’s how it looks, rewritten to work with a Deck object:

public static void printDeck (Deck deck) { for (int i=0; i<deck.cards.length; i++) { Card.printCard (deck.cards[i]); } }

The most obvious thing we have to change is the type of the parameter, from Card[] to Deck. The second change is that we can no longer use deck.length to get the length of the array, because deck is a Deck object now, not an array. It contains an array, but it is not, itself, an array. Therefore, we have to write deck.cards.length to extract the array from the Deck object and get the length of the array.

For the same reason, we have to use deck.cards[i] to access an element of the array, rather than just deck[i]. The last change is that the invocation of printCard has to say explicitly that printCard is defined in the Card class.

For some of the other methods, it is not obvious whether they should be included in the Card class or the Deck class. For example, findCard takes a Card and a Deck as arguments; you could reasonably put it in either class. As an exercise, move findCard into the Deck class and rewrite it so that the first parameter is a Deck object rather than an array of Cards.

12.2 Shuffling

For most card games you need to be able to shuffle the deck; that is, put the cards in a random order. In Section 10.6 we saw how to generate random numbers, but it is not obvious how to use them to shuffle a deck.

One possibility is to model the way humans shuffle, which is usually by dividing the deck in two and then reassembling the deck by choosing alternately from each deck. Since humans usually don’t shuffle perfectly, after about 7 iterations the order of the deck is pretty well randomized. But a computer program would have the annoying property of doing a perfect shuffle every time, which is not

146 Objects of Arrays

Again, the pseudocode helps with the design of the helper methods. In this case we can use swapCards again, so we only need one new one, called findLowestCard, that takes an array of cards and an index where it should start looking.

Once again, I am going to leave the implementation up to the reader.

12.4 Subdecks

How should we represent a hand or some other subset of a full deck? One pos- sibility is to create a new class called Hand, which might extend Deck. Another possibility, the one I will demonstrate, is to represent a hand with a Deck object that happens to have fewer than 52 cards.

We might want a method, subdeck, that takes a Deck and a range of indices, and that returns a new Deck that contains the specified subset of the cards:

public static Deck subdeck (Deck deck, int low, int high) { Deck sub = new Deck (high-low+1);

for (int i = 0; i<sub.cards.length; i++) { sub.cards[i] = deck.cards[low+i]; } return sub; }

The length of the subdeck is high-low+1 because both the low card and high card are included. This sort of computation can be confusing, and lead to “off-by-one” errors. Drawing a picture is usually the best way to avoid them.

Because we provide an argument with the new command, the contructor that gets invoked will be the first one, which only allocates the array and doesn’t allocate any cards. Inside the for loop, the subdeck gets populated with copies of the references from the deck.

The following is a state diagram of a subdeck being created with the parameters low=3 and high=7. The result is a hand with 5 cards that are shared with the original deck; i.e. they are aliased.

deck cards

sub cards

12.5 Shuffling and dealing 147

I have suggested that aliasing is not generally a good idea, since changes in one subdeck will be reflected in others, which is not the behavior you would expect from real cards and decks. But if the objects in question are immutable, then aliasing is less dangerous. In this case, there is probably no reason ever to change the rank or suit of a card. Instead we will create each card once and then treat it as an immutable object. So for Cards aliasing is a reasonable choice.

12.5 Shuffling and dealing

In Section 12.2 I wrote pseudocode for a shuffling algorithm. Assuming that we have a method called shuffleDeck that takes a deck as an argument and shuffles it, we can create and shuffle a deck:

Deck deck = new Deck (); shuffleDeck (deck);

Then, to deal out several hands, we can use subdeck:

Deck hand1 = subdeck (deck, 0, 4); Deck hand2 = subdeck (deck, 5, 9); Deck pack = subdeck (deck, 10, 51);

This code puts the first 5 cards in one hand, the next 5 cards in the other, and the rest into the pack.

When you thought about dealing, did you think we should give out one card at a time to each player in the round-robin style that is common in real card games? I thought about it, but then realized that it is unnecessary for a computer program. The round-robin convention is intended to mitigate imperfect shuffling and make it more difficult for the dealer to cheat. Neither of these is an issue for a computer.

This example is a useful reminder of one of the dangers of engineering metaphors: sometimes we impose restrictions on computers that are unnecessary, or expect capabilities that are lacking, because we unthinkingly extend a metaphor past its breaking point. Beware of misleading analogies.

12.6 Mergesort

In Section 12.3, we saw a simple sorting algorithm that turns out not to be very efficient. In order to sort n items, it has to traverse the array n times, and each traversal takes an amount of time that is proportional to n. The total time, therefore, is proportional to n^2.

In this section I will sketch a more efficient algorithm called mergesort. To sort n items, mergesort takes time proportional to n log n. That may not seem impressive, but as n gets big, the difference between n^2 and n log n can be enormous. Try out a few values of n and see.

12.7 Glossary 149

Then, if you get that working, the real fun begins! The magical thing about mergesort is that it is recursive. At the point where you sort the subdecks, why should you invoke the old, slow version of sort? Why not invoke the spiffy new mergeSort you are in the process of writing?

Not only is that a good idea, it is necessary in order to achieve the performance advantage I promised. In order to make it work, though, you have to add a base case so that it doesn’t recurse forever. A simple base case is a subdeck with 0 or 1 cards. If mergesort receives such a small subdeck, it can return it unmodified, since it is already sorted.

The recursive version of mergesort should look something like this:

public static Deck mergeSort (Deck deck) { // if the deck is 0 or 1 cards, return it

// find the midpoint of the deck // divide the deck into two subdecks // sort the subdecks using mergesort // merge the two halves and return the result }

As usual, there are two ways to think about recursive programs: you can think through the entire flow of execution, or you can make the “leap of faith.” I have deliberately constructed this example to encourage you to make the leap of faith.

When you were using sortDeck to sort the subdecks, you didn’t feel compelled to follow the flow of execution, right? You just assumed that the sortDeck method would work because you already debugged it. Well, all you did to make mergeSort recursive was replace one sorting algorithm with another. There is no reason to read the program differently.

Well, actually, you have to give some thought to getting the base case right and making sure that you reach it eventually, but other than that, writing the recursive version should be no problem. Good luck!

12.7 Glossary

pseudocode: A way of designing programs by writing rough drafts in a com- bination of English and Java.

helper method: Often a small method that does not do anything enormously useful by itself, but which helps another, more useful, method.

12.8 Exercises

Exercise 12.1 Write a version of findBisect that takes a subdeck as an argument, rather than a deck and an index range (see Section 11.8). Which version is more error- prone? Which version do you think is more efficient?

150 Objects of Arrays

Exercise 12.2 In the previous version of the Card class, a deck is implemented as an array of Cards. For example, when we pass a “deck” as a parameter, the actual type of the parameter is Card[].

In this chapter, we developed an alternative representation for a deck, an object type named Deck that contains an array of cards as an instance variable. In this exercise, you will implement the new representation of a deck.

a. Add a second file, named Deck.java to the program. This file will contain the definition of the Deck class. b. Type in the constructors for the Deck class as shown in Section 12.1. c. Of the methods currently in the Card class, decide which ones would be more appropriate as members of the new Deck class. Move them there and make any changes necessary to get the program to compile and run again. d. Look over the program and identify every place where an array of Cards is being used to represent a deck. Modify the program throughout so that it uses a Deck object instead. You can use the version of printDeck in Section 12.1 as an example. It is probably a good idea to make this transformation one method at a time, and test the program after each change. On the other hand, if you are confident you know what you are doing, you can make most of the changes with search- and-replace commands.

Exercise 12.3 The goal of this exercise is to implement the shuffling and sorting algorithms from this chapter.

a. Write a method called swapCards that takes a deck (array of cards) and two indices, and that switches the cards at those two locations. HINT: it should switch the references to the two cards, rather than the contents of the two objects. This is not only faster, but it makes it easier to deal with the case where cards are aliased. b. Write a method called shuffleDeck that uses the algorithm in Section 12.2. You might want to use the randomInt method from Exercise 10.2. c. Write a method called findLowestCard that uses the compareCard method to find the lowest card in a given range of the deck (from lowIndex to highIndex, including both). d. Write a method called sortDeck that arranges a deck of cards from lowest to highest.

Exercise 12.4 In order to make life more difficult for card-counters, many casinos now use automatic shuffling machines that can do incremental shuffling, which means that after each hand, the used cards are returned to the deck and, instead of reshuffling the entire deck, the new cards are inserted in random locations.

Write a method called incrementalShuffle that takes a Deck and a Card and that inserts the card into the deck at a random location. This is an example of an incre- mental algorithm.