By the end of next lecture, you'll understand everything in this class definition.
package edu.gatech.cs1331.card;
import java.util.Arrays;
public class Card {
public static final String[] VALID_RANKS = {"2", ... , "ace"};
public static final String[] VALID_SUITS = {"diamonds", ... };
private String rank;
private String suit;
public Card(String aRank, String aSuit) {
// ...
}
public String toString() {
return rank + " of " + suit;
}
private boolean isValidRank(String someRank) { ... }
}
In this two-part lecture we'll use a running example stored in a Git repo. Go to https://github.com/cs1331/card, click on "Clone or download" and copy the clone URL. Then open your terminal and do this:
cd cs1331
git clone https://github.com/cs1331/card.git
cd card
Note that if you've uplaoded your public SSH key you may use an SSH clone URL.
Now you're ready to follow along.
Consider how to represent a Card ADT:
Here's how you would check out version 0.0:
git checkout v0.0
Card
, v0.0And your Card.java
will then contain:
public class Card {
String rank;
String suit;
}
rank
and suit
are instance variablesCard
has its own copy of instance variables.Let's "do something" with Card
by adding a main method and bumping to v0.1.
Card
v0.1public class Card {
String rank;
String suit;
public static void main(String[] args) {
Card c = new Card();
System.out.println(c);
}
}
Note that we can put a main
method in any class. This is useful for exploratory testing, like we're doing here.
The String
representation isn't very appealing. (What does it print?)
Card
v0.2public class Card {
String rank;
String suit;
public String toString() {
return rank + " of " + suit;
}
public static void main(String[] args) {
Card swedishPop = new Card();
swedishPop.rank = "ace";
swedishPop.suit = "base";
Card handy = new Card();
handy.rank = "jack";
handy.suit = "all trades";
System.out.println(swedishPop);
System.out.println(handy);
}
}
Now we have a nice String
representation, but we havve an "ace of base" card and a "jack of all trades", which aren't valid cards.
Let's protect the instance variables by making them private:
public class Card {
private String rank;
private String suit;
public String toString() {
return rank + " of " + suit;
}
public static void main(String[] args) {
Card c = new Card();
c.rank = "ace";
c.suit = "base";
System.out.println(c);
}
}
Why does this still compile?
main
method in Card
-- can see Card
's private partspublic class Dealer {
public static void main(String[] args) {
Card c = new Card();
c.rank = "ace";
c.suit = "base";
System.out.println(c);
}
}
This won't compile (which is what we want). Why?
public class Card {
private String rank;
private String suit;
public void setRank(String rank) {
rank = rank;
}
public void setSuit(String suit) {
suit = suit;
}
}
setRank
and setSuit
.setX
is the Java convention for a setter method for an instance variable named x
.Card
, v1.2Let's try out our new Card
class.
public class Dealer {
public static void main(String[] args) {
Card c = new Card();
c.setRank("ace");
c.setSuit("base");
System.out.println(c);
}
}
Oops. Prints "null of null". Why?
The parameters in the setters "shadowed" the instance variables:
public void setRank(String rank) {
rank = rank;
}
public void setSuit(String suit) {
suit = suit;
}
rank
in setRank
refers to the local rank
variable, not the instance variable of the same namesuit
in setSuit
refers to the local suit
variable, not the instance variable of the same namethis
: Card, v1.2.1public class Card {
private String rank;
private String suit;
public void setRank(String rank) {
this.rank = rank;
}
public void setSuit(String suit) {
this.suit = suit;
}
}
this
reference which refers to the instance on which a method is being called.this.rank
refers to the rank
instance variable for the Card
instance on which setRank
is being called.this.rank
is different from the local rank
variable that is a parameter to the setRank
method.Card
, v1.2.1public class Dealer {
public static void main(String[] args) {
Card c = new Card();
c.setRank("ace");
c.setSuit("base");
System.out.println(c);
}
}
Now we have encapsulation, but we can still create invalid Card
s, e.g., "base" is not a valid suit. How to fix?
Class invariant: a condition that must hold for all instances of a class in order for instances of the class to be considered valid.
Invariants for Card class:
rank
must be one of {"2", "3", "4", "5", "6", "7", "8", "9", "10", "jack", "queen", "king", "ace"}suit
must be one of {"diamonds", "clubs", "hearts","spades"}rank
invariant can be maintained by adding:
public class Card {
private final String[] VALID_RANKS =
{"2", "3", "4", "5", "6", "7", "8", "9",
"10", "jack", "queen", "king", "ace"};
public void setRank(String rank) {
if (!isValidRank(rank)) {
System.out.println(rank + " is not a valid rank.");
System.exit(0);
}
this.rank = rank;
}
private boolean isValidRank(String someRank) {
return contains(VALID_RANKS, someRank);
}
private boolean contains(String[] array, String item) {
for (String element: array) {
if (element.equals(item)) {
return true;
}
}
return false;
}
// ...
}
Now we can't write code that instantiates an invalid Card
object:
public class Dealer {
public static void main(String[] args) {
Card c = new Card();
c.setRank("ace");
c.setSuit("base");
System.out.println(c);
}
}
yields:
$ java Dealer
base is not a valid suit.
Dealer
v1.3.1Version 1.3.1 fixes the invalid suit:
public class Dealer {
public static void main(String[] args) {
Card c = new Card();
c.setRank("ace");
c.setSuit("spades");
System.out.println(c);
}
}
Card
now ensures that we don't create card objects with invalid ranks or suits. But consider this slight modification to Dealer
in v1.4:
public class Dealer5 {
public static void main(String[] args) {
Card c = new Card();
System.out.println(c); // Printing a new Card instance
c.setRank("ace");
c.setSuit("base");
System.out.println(c);
}
}
What if we printed our Card
instance, c
, before we called the setters?
Two ways to initialize the instance variables of an object:
public class Card {
private String rank = "2";
// ...
}
public class Card {
public Card() {
rank = "2";
}
// ...
}
A constructor is what's being called when you invoke operator new
.
Since we didn't write our own constructor, Java provided a default no-arg constructor - default no-arg ctor sets instance variables (that don't have their own declaration-point intializations) to their default values.
That's why Card
objects are null of null
after they're instantiated. We have to call the setters on a Card
instance before we have a valid object.
In general, it's poor style to require multi-step initialization.
new Card()
is called, instance variables have useless defaults.The way to fix this is by writing our own constructor.
If we write a constructor, Java won't provide a default no-arg constructor. (We may choose to provide one.)
public class Card {
// ...
public Card(String rank, String suit) {
setRank(rank);
setSuit(suit);
}
// ...
}
Dealer
v2.0Now this won't even compile:
public class Dealer {
public static void main(String[] args) {
Card c = new Card();
// ...
}
}
$ javac Dealer.java
Dealer.java:4: error: constructor Card in class Card cannot be applied to given types;
Card c = new Card();
^
required: String,String
found: no arguments
reason: actual and formal argument lists differ in length
1 error
Now we have a safer, more consistent way to initialize objects:
public class Dealer {
public static void main(String[] args) {
Card c = new Card("queen", "hearts");
System.out.println(c);
}
}
Source: Wikipedia
Let's review our progress with our Card class design:
Card
).Card
) with mutator methods (Card
) to set their values.Card
).Card
has a constructor, which ensures that instance variables are initialized when an instance of Card
is created.Do we need a separate instance of VALID_RANKS
and VALID_SUITS
for each instance of our Card class?
static
members are shared with all instances of a class:
public static final String[] VALID_RANKS =
{"2", "3", "4", "5", "6", "7", "8", "9",
"10", "jack", "queen", "king", "ace"};
public static final String[] VALID_SUITS =
{"diamonds", "clubs", "hearts","spades"};
Given the declarations above:
VALID_RANKS
and a single copy of VALID_SUITS
final
, we can safely make them public
so clients of our Card class can use themCard
v2.1 is pretty good, but we can write code like this:
public class Dealer {
public static void main(String[] args) {
Card c = new Card("queen", "hearts");
System.out.println(c);
c.setRank("jack"); // modifying c
System.out.println(c);
}
}
Does this make sense? Should Card objects be mutable?
Card objects don't change. We can model this behavior by removing the setters and putting the initialization code in the constructor (or making the setters private and calling them from the constructor):
public Card(String aRank, String aSuit) { // constructor
if (!isValidRank(rank)) {
System.out.println(aRank + " is not a valid rank.");
System.exit(0);
}
rank = aRank;
if (!isValidSuit(aSuit)) {
System.out.println(aSuit + " is not a valid suit.");
System.exit(0);
}
suit = aSuit;
}
Note the use of another idiom for disambiguating constructor paramters from instance variables (as opposed to using this
).
An immutable class is a class whose instances cannot be modified. To make a class immutable:
final
so it can't be extended (there's another way to accomplish this, but making the class final
is good enough for now)final
private
In general, make your classes immutable unless you have a good reason to make them mutable. Why? Because immutable objects
Take a look at the final evolution of our Card class. It contains a few more enhancements:
IllegalArgumentException
on invalid input so that client code can choose to deal with the exception at run-time.equals()
method.==
means identity equality (aliasing) for reference types (objects).equals(Object)
tests value equality for objects.Given our finished Card class with a properly implemented equals(Object)
method, this code:
Card c1 = new Card("ace", "spades");
Card c2 = new Card("ace", "spades");
Card c3 = c1;
System.out.println("c1 == c2 returns " + (c1 == c2));
System.out.println("c1.equals(c2) returns " + c1.equals(c2));
System.out.println("c1 == c3 returns " + (c1 == c3));
System.out.println("c1.equals(c3) returns " + c1.equals(c3));
produces this output:
c1 == c2 returns false
c1.equals(c2) returns true
c1 == c3 returns true
c1.equals(c3) returns true
By the way, what if we left off the parentheses around c1 == c2
in System.out.println("c1 == c2 returns " + (c1 == c2))
?
Using the encapsulation techniques we just learned, write a class named Person
with a name
field of type String
and an age
field of type int
.
Write a suitable toString
method for your Person
class.
Add a main method that:
Person
objects