A. Intro
Learning Goals
This lab will focus on Java primitives and objects. Our goals for this lab will be as follows.
- Learn the different Java primitives and when to use them.
- Learn how to define classes and use reference-typed variables.
- Learn how to work with box-and-pointer diagrams to identify common usage errors.
We'll use several exercises to demonstrate this. First we will enhance a bank account class by providing deposit and withdraw functionalities. We will additionally code how to merge accounts and provide overdraft protection.
Next, we will be coding our own object to represent pursuit curves, a powerful way of rendering paths on a computer. Make sure to read this lab carefully as it will explain important design practices that will help develop your Java and general coding skills!
Beginning the Lab
You'll be working in partners again, but this time please find someone different. Before you begin the exercises in this lab, make sure to pull the skeleton code as we talked about in the previous lab. You can choose to use an IDE for this lab or work in your preferred text editor; we'll go more into IDE usage in the next lab.
B. Primitives and Objects
Java Primitives
As you may have noticed, when initializing a variable in Java you must put the type next to it.
int number = 10;
The above line tells Java that the variable number
is an integer that
holds the value 10
. Types represent things such as integers and
decimals and are fundamental to the operation of a language. In Java, there are
a predefined set of primitive types.
- boolean : a
boolean
represents the two possible values oftrue
andfalse
- byte : a
byte
represents an 8-bit integer. - short : a
short
represents a 16-bit integer. - int : an
int
represents a 32-bit integer. This is the most commonly used integer type and can hold values between -2,147,483,648 to 2,147,483,647 inclusive. - long : a
long
represents a 64-bit integer. Sometimes when we need to express large integral numbers we will use this as it ranges from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. - float : a
float
represents a 32-bit single precision floating point number. Floating point numbers can approximate a range real numbers including integers, decimals, and special values like infinity. - double : a
double
represents a 64-bit double precision floating point number. Most of our decimal numbers will use this type as it provides greater precision. - char : a
char
represents an ascii letter (like the English alphabet).
These words are reserved in Java. That is we cannot use int
and
double
in any other context besides declaring a variable of that type.
Declaring a primitive is very simple. For example, if we wanted to declare a double, we can write the following.
double pi = 3.14;
Certain primitives require an extra letter after the initial value. For example,
to declare a long
or a float
, we write the following.
long num = 9223372036854775807L;
float num2 = 42.0f;
Finally, we can declare a char
using a single-quoted literal. For example, if
we want to initialize variable a
to the letter "a", we would write the following.
char a = 'a';
We need not always initialize the value of a primitive. Sometimes it is useful to just a declare a variable and allow later blocks of code to determine its value. We do so by writing the following.
char a;
double dooble;
Of course to use both a
and dooble
, we must provide both a value.
Guide to Writing Java Objects
Java is an object-oriented language. This means that everything we want to represent in Java is defined in terms of Objects.
Objects are bundles of code that define the state and behavior of the construct we wish to represent. Suppose we wish to represent a potato. A potato's state can be described by its variety and age, and it also has behaviors such as grow and flower.
Now suppose Sarah and Alan both have potatoes; Sarah has a Yukon Gold and Alan has
a Red Pontiac. Even though Sarah and Alan have different varieties of potatoes, they are
both still potatoes. They each have an age, color and variety. Critically, we can
describe an entire group of Potatoes with a set of common descriptors.
In Java we define an Object via its Class. Sarah's Yukon Gold and Alan's Red
Pontiac would then be called instances of the Potato
class. Let us see how we can
implement a Potato
class in Java.
Example
For this section, we will be using Potato code found below. This can be found in
lab02/Potato.java
.
public class Potato {
/* An instance variable representing the potato's species. */
private String variety;
/* An instance variable representing the potato's age. */
private int age;
/** A constructor that returns a very young russet burbank potato. */
public Potato() {
this.variety = "Russet Burbank";
this.age = 0;
}
/** A constructor that allows you to specify its variety and age. */
public Potato(String variety, int age) {
this.variety = variety;
this.age = age;
}
/** A getter method that returns the potato's type. */
public String getVariety() {
return this.variety;
}
/** A getter method that returns the potato's age. */
public int getAge() {
return this.age;
}
/** A setter method that sets the potato's age to AGE. */
public void setAge(int age) {
this.age = age;
}
/** A method that grows the potato. Note it increases its age by 1. */
public void grow() {
System.out.println("Photosynthesis!");
this.age = this.age + 1;
}
/** Did you know potatoes can flower? No? Neither did I... */
public void flower() {
System.out.println("I am now a beautiful potato");
}
}
We will also be looking at lab02/Potato1.java
later on!
Defining a Class
Let's see how to define our Potato
class. To define a Java Class, create a new
.java
file and encompass the class's code with the following header
class Potato {
/** Potato code goes here! */
}
There are two things to keep in mind when writing Java classes.
- Java requires the class name to be the same as the file name. This is why
the
Potato
class is written inPotato.java
. - The name of a class must always begin with a capital letter and is generally named using camel case (ex: ThisIsCamelCase)
Constructors
Now to initialize a Potato
object, we must call its constructor. The
constructor is a special method that initializes all the variables associated
with the class's instance.
It's possible to define a constructor that takes in no arguments.
public Potato() {
this.variety = "Russet Burbank";
this.age = 0;
}
Here the constructor returns a baby Russet potato because, as we all know, provided no guidance, the potato obviously becomes a baby russet potato.
We can also specify arguments in our constructor.
public Potato(String variety, int age) {
this.variety = variety;
this.age = age;
}
This constructor returns a potato where we can define its variety and age. Now we can construct potatoes such as Sarah's 3 year old Yukon Gold potato.
We will discuss how to declare objects in more detail during the Boxes and Pointers section.
Caveat if no constructors are defined in the object file, then the Java compiler will provide a default constructor that accepts no argument. However, if a constructor is defined, then the compiler will not provide a default constructor. Read more about it here.
Instance Variables
Instance variables allow us to represent the state of an object and can be both
primitives and objects. Within our Potato
class, we see that there are two
instance variables: variety
and age
.
/* An instance variable representing the potato's species. */
private String variety;
/* An instance variable representing the potato's age. */
private int age;
As with any variables we must declare what type it is. The String
keyword
tells us variety
is a String object and int
tells us the age is an integer
primitive.
We can (sort of) access the age and variety of the Potato via dot notation. This is similar to Python's dot notation, which you may have encountered in CS61A.
Potato sarahsPotato = new Potato("Yukon Gold", 3); // Sarah's potato!
sarahsPotato.variety; // returns the variety of Sarah's potato
sarahsPotato.age; // returns the age
Notice that we had to first instantiate a new potato object before we could access
variety
or age
. Remember that instance variables are particular to the object.
Thus we need to create an object first in order to have age and variety.
When writing object code within its class, we can also employ the this
keyword.
Its usage is similar to that of self
in Python.
this.variety; // returns the current instance's variety
this.age; // returns the current instance's age
One notable difference, however, is that this
cannot be reassigned whereas
self
in Python can be reassigned.
Now we say "sort of" because we also have a private
keyword placed in
front of the variety
and age
declaration. This means we cannot access
the age and variety via dot notation outside of Potato.java
. We will see more
about why we may want to do this in the Getter and Setter Method section
below.
Finally, it's important to stress that even though all instances of Point will
have the variables variety
and age
, their values will be specific to each
instance - hence the name instance variable.
Instance Methods
To facilitate behavior, we can define instance methods. For example, Potato
has defined in it the grow()
method.
/** A method that grows the potato. Note it increases its age by 1. */
public void grow() {
System.out.println("Photosynthesis!");
this.age = this.age + 1;
}
Like instance variables, we can access instance methods using dot notation as well.
sarahsPotato.grow(); // Sarah's potato grows!
We also have a few special instance methods prefixed by the words "get" and "set". These are aptly named getters and setters, which we'll learn more about them below!
Getter and Setter Methods
As we have seen, the private
keyword limits our ability to access instance
variables directly. This is called an access modifier
and we will be
discussing them in more detail later on in the course.
For now, just know that in general it is good practice to make instance variables private. A consequence of making our instance variables private is that we must now define instance methods to access them.
This is where we introduce getter and setter methods. Within Potato
we have
these methods.
/** A getter method that returns the potato's type. */
public String getVariety() {
return this.variety;
}
/** A getter method that returns the potato's age. */
public int getAge() {
return this.age;
}
The above two blocks are called getter methods since they get the value
of their respective instance variables for programs outside of Potato.java
.
Of course, due to advancements in genetic modification technology, it is also
possible to set the age of our potato.
/** A setter method that sets the potato's age to AGE. */
public void setAge(int age) {
this.age = age;
}
This is called a setter method as it allows us to set the value of an instance variable.
Interestingly enough, we don't have a setter method for the variety
instance
variable. This is because until we develop the technology to support
spud-transmutation (#PotatoDreams), Sarah's Yukon Gold potato will forever remain
a Yukon Gold potato.
Of course, this is important in an application sense because now external
programs cannot maliciously spoof the identity of a potato. Take a look at
Potato1.java
/* An instance variable representing the potato's species. */
String variety;
/* An instance variable representing the potato's age. */
int age;
The variety
and age
are not private meaning we can write a program to
change the identity of Sarah's potato.
/* sarahsPotato is an instance with variety = "Yukon Gold" */
sarahsPotato.variety = "Red Pontiac"; // A POTATO IMPOSTER!
This practice is called information hiding and it prevents external programs from unintentionally (or intentionally!) changing the value of our instance variables.
In an exercise below, we will be considering a bank account. Without a doubt, we will
want the balance of our bank account to be private, so that other programs cannot simply set account.balance = 0;
.
Boxes and Pointers
Throughout this class it will be extraordinarily helpful to draw pictures of the variables in our program to help us with debugging. The diagrams we'll teach you to use in this class are often referred to as box and pointer diagrams, or sometimes box and arrow diagrams.
Let's start off with something simple. When we declare a primitive, we draw a box for it, and label the box with the type of primitive, and the name of the variable. Here, primitives will be in red boxes. For example,
int x;
When we assign a value to the primitive, we fill in the box with the value of the primitive.
x = 3;
Variables can also refer to objects. For example, it can refer to a Potato
instance. We can declare a Potato
object the same way as we declare an int
.
Potato p;
This variable is called a reference, because it will refer to an object. When
we first declare the reference but don't assign an object to it like in the code
above, we say the reference contains nothing, or null
. Here's how we draw it:
Here we're drawing references in green to emphasize that they are different from primitives.
Now let's assign a reference to the Potato
object by calling its constructor.
This instantiates, or creates, a new instance of the Potato
class.
Instantiating an object via its constructor always requires the new
keyword.
p = new Potato();
Here an object is drawn in blue, to emphasize that it is different from a primitive and a reference. We can now store primitives within the object as instance variables!
One critical thing about the object: unlike the primitive integer, 3, drawn inside
the box for x
, the Potato
object is not drawn inside the variable p
.
Instead p
simply contains an arrow that points to the Potato
object. This is
why p
is called a reference or pointer because it just refers to the object
but does not contain it. The true value of the variable p
is a pointer to
a Potato
object rather than the Potato
object itself. This is a very, very
important distinction!
Of course, when we call the no argument constructor, it will initialize the variety
to "Russet Burbank"
and the age
to 0
. Our diagram looks like the following.
Is this what you expected?
Remember that a String
in Java is an object, not a primitive. Objects are not
drawn inside other objects, so when we initialize variety
, we make sure the
reference points outside the object.
Discussion: Intuition for Drawing Objects
Discuss with your partner to see if you can come up with intuition as to why these diagrams are drawn the way they are. Why does it make sense that objects are not stored inside variables, but are only referred to them? Why does it make sense that objects are not drawn inside other objects? Why isn't the blue object box labeled with the name of the variable? There aren't necessarily correct answers to these question, so just see if you can come up with explanations that make sense to you.
Discussion: When are Primitives Used?
Discuss with your partner the purpose of each primitive and any idiosyncrasies of declaring a variable of that type.
int
double
char
boolean
long
Discussion: Drawing a char Variable
Some students might incorrectly draw the result of the code
char c;
c = 'c';
as follows:
Explain this misconception.
Self-test: Assignment Statements
Consider a main program for the Counter
class mentioned in the readings:
Counter c1 = new Counter();
c1.increment();
// c1 now represents the value 1
Counter c2 = new Counter();
// c2 now represents the value 0
Indicate which of the box-and-pointer diagrams best represents the effect of the assignment statement
c1 = c2;
If you get this wrong, consult with your partner.
|
Incorrect. Notice the assignment statement is assigning the
references
equal to each other, not the objects.
|
|
|
Incorrect. References can only point to objects, not other references.
|
|
|
Correct! The assignment statement sets the references to point to the same value.
|
Self-test: Error Messages
Before you try it for yourself, answer the following question: What error message is caused by the following code?
public class Counter {
int count = 0;
void increment() {
count = count + 1;
}
public static void main (String[] args) {
Counter c1 = new Counter ( );
increment();
c1.count = 0;
}
}
c1
cannot be resolved
|
Incorrect. Try it out for yourself and see!
|
|
count must be private
|
Incorrect. Even though it is generally bad practice to leave an instance variable at anything besides
private
, it does not necessarily generate an error.
|
|
Cannot make a static reference to the non-static method
increment()
from the type
Counter
|
Correct! Increment must be called on the object pointed to by
c1
|
|
The constructor Counter(int) is undefined
|
Incorrect. Nowhere do we try to construct a counter using an argument.
|
|
The method
increment()
in the type
Counter
is not applicable for the arguments (int)
|
Incorrect. We never try to pass in a value to
increment
|
|
Cannot make a static reference to the non-static field
count
.
|
Incorrect. We only reference
count
by calling it from
c1
, which is a non-static reference
|
Self-test: Error Messages 2
Before you try it yourself, answer the question: What error message is caused by the following code?
public class Counter {
int count = 0;
void increment() {
count = count + 1;
}
public static void main (String[] args) {
Counter c1 = new Counter();
c1.increment();
count = 0;
}
}
c1
cannot be resolved
|
Incorrect. Try it out for yourself and see!
|
|
count must be private
|
Incorrect. Even though it is generally bad practice to leave an instance variable at anything besides
private
, it does not necessarily generate an error.
|
|
Cannot make a static reference to the non-static method
increment()
from the type
Counter
|
Incorrect. We correctly call
increment
on an instance of a
Counter
object
|
|
The constructor Counter(int) is undefined
|
Incorrect. Nowhere do we try to construct a counter using an argument.
|
|
The method
increment()
in the type
Counter
is not applicable for the arguments (int)
|
Incorrect. We never try to pass in a value to
increment
|
|
Cannot make a static reference to the non-static field
count
.
|
Correct!
count
must be accessed from an instance of a
Counter
object
|
Self-test: Error Messages 3
Before you try it yourself, answer the question: What error message is caused by the following code?
public class Counter {
private int cnt = 0;
void increment () {
count = count + 1;
}
void setMyCount(int count) {
count = count;
}
public static void main(String [] args) {
Counter c1 = new Counter();
c1.increment(2);
c1.setMyCount(0);
}
}
c1
cannot be resolved
|
Incorrect. Try it out for yourself and see!
|
|
Cannot make a static reference to the non-static method
increment()
from the type
Counter
|
Incorrect. We correctly call
increment
on an instance of a
Counter
object
|
|
The constructor Counter(int) is undefined
|
Incorrect. Nowhere do we try to construct a counter using an argument.
|
|
The method
increment()
in the type
Counter
is not applicable for the arguments (int)
|
Correct! Nowhere did we define a method
increment
that takes in an
int
|
|
Cannot make a static reference to the non-static field
count
.
|
Incorrect. We only reference
count
by calling it from
c1
, which is a non-static reference
|
Self-test: Error Messages 4
Before you try it yourself, answer the question: What error message is caused by the following code?
public class Counter {
private int cnt = 0;
void increment () {
count = count + 1;
}
void setCount(int count) {
this.count = count;
}
public static void main(String [ ] args) {
Counter c1 = new Counter();
c1.increment();
c1.setCount(0);
}
}
c1
cannot be resolved
|
Incorrect. Try it out for yourself and see!
|
|
Cannot make a static reference to the non-static method
increment()
from the type
Counter
|
Incorrect. We correctly call
increment
on an instance of a
Counter
object
|
|
The constructor Counter(int) is undefined
|
Incorrect. Nowhere do we try to construct a counter using an argument.
|
|
Cannot make a static reference to the non-static field
count
.
|
Incorrect. We only reference
count
by calling it from
c1
, which is a non-static reference
|
|
c1.count
cannot be resolved or is not a field.
|
Correct! Notice the declared instance variable is
cnt
not
count
.
|
Self-test: Assignment Statements
What gets printed by the following program? Try to figure out the answer without using the computer with the help of a box-and-pointer diagram.
import java.awt.Point;
public class Test1 {
public static void main (String [ ] args) {
Point p1 = new Point ();
p1.x = 1;
p1.y = 2;
Point p2 = new Point ();
p2.x = 3;
p2.y = 4;
// now the fun begins
p2.x = p1.y;
p1 = p2;
p2.x = p1.y;
System.out.println (p1.x + " " + p1.y
+ " " + p2.x + " " + p2.y);
}
}
4 4 4 4
C. Bank Account Methods and More
Bank Account Management
The next several exercises involve modifications to an Account
class, which
models a bank account. The file you will be working with is lab02/Account.java
.
The Account
class allows deposits and withdrawals. Instead of warning about a
balance that's too low, however, it merely disallows a withdrawal request for
more money than the account contains.
Remember, the balance
instance variable is private thus we can only access it via
the getBalance()
instance method. Think about how bad it would be if a
hacker wrote a single line of code to empty your account! Thank you, information hiding.
Exercise: Modifying Withdrawal Behavior
The withdraw
method is currently defined as a void
method. Modify it to
return a boolean
: true
if the withdrawal succeeds (along with actually
performing the withdrawal) and false
if it fails.
Exercise: Merging Accounts
Define a merge
method on accounts that takes the balance of the argument
account, adds it to the current balance of this account, and sets the argument's
balance to zero. We've provided a skeleton of the method in Account.java
.
Exercise: Overdraft Protection
A convenient feature of some bank accounts is overdraft protection; rather than bouncing a check when the balance would go negative, the bank will deduct the necessary funds from a second account. One might imagine such a setup for a student account, provided the student's parents are willing to cover any overdrafts (!). Another use is to have a checking account that is tied to a savings account where the savings account covers overdrafts on the checking account. In our system, we'll be keeping things simple with only one type of account so we don't have to worry about student or savings accounts.
Implement and test overdraft protection for Account
objects by completing
the following steps.
- Add a
parentAccount
instance variable to theAccount
class; this is the account that will provide the overdraft protection, and it may have overdraft protection of its own. - Add a two-argument constructor. The first argument will be the initial
balance as in the existing code. The second argument will be an
Account
reference with which to initialize the instance variable you defined in step 1. - In the one-argument constructor, set the parent account to
null
. - Modify the
withdraw
method so that, if the requested withdrawal can't be covered by this account, the difference is withdrawn from the parent account. This may trigger overdraft protection for the parent account, and then its parent, and so on. The number of accounts connected in this way may be unlimited. If the account doesn't have a parent and it can't cover the withdrawal, thewithdraw
method should merely print an error message as before and not change any account balances. Here's an example of the desired behavior, with theAccount
objectkathy
providing overdraft protection for theAccount
objectmegan
.
kathy balance |
megan balance |
attempted withdrawl from megan |
desired result |
---|---|---|---|
500 | 100 | 50 | megan has 50, kathy has 500 |
500 | 100 | 200 | megan has 0, kathy has 400 |
500 | 100 | 700 | return false without changing either balance |
Discussion: Merging Revisited
One proposed solution for merging accounts is the following:
public void merge (Account otherAccount) {
this.balance = this.balance + otherAccount.balance;
otherAccount = new Account(0);
}
This doesn't work. Explain why not.
D. Pursuit Curves
You will now create a class representing a pursuit curve.
Pursuit curves provide a powerful way to render curves on a computer. The traditional method for drawing a path is to analytically define it via some algebraic formula like \(y(t) = t^2\) and trace it point-wise. Consider an alternative where we define two points: the pursuer and the pursued.
Now suppose the pursued point (in black) follows some fixed path \(F(t)\). Then the pursuer (in red) will seek the pursued in the following manner.
We notice that the pursuer always follows the pursued along its tangent, which gives some serious first order differential equation vibes. Letting the pursuer's path be given by \(x(t)\), then the closed form solution for its path is given by the following equation.
Of course, we won't require you to solve a differential equation. In fact, let's see what your task will be!
Programming Task
Implement a simpler version of pursuit curves in order to create a
cool visual by filling out lab02/Path.java
. An additional
file lab02/PathHarness.java
is provided containing code that will render
your code in Path.java
using Java's graphics framework.
Path.java
will represent the path traveled by the pursuer. You will need to
keep track of the following two points:
- The
currPoint
will represent where the path currently ends. This will be a Point object. - The
nextPoint
will represent where the path (and thus, thecurrPoint
) will travel to next. This will also be a Point object.
Next, you will need to define a constructor that, given an x and y coordinate,
sets nextPoint
to the starting point (x, y). The constructor may look
something like this.
public Path(double x, double y) {
// more code goes here!
}
When the Path
object is first constructed, the currPoint
can be set to a Point
instance with any coordinate so long as it is not null
.
Try playing around with initial currPoint
values to see what you can get!
Finally, you will need to implement the following instance methods.
method name | return type | functionality |
---|---|---|
getCurrX() |
double |
Returns the x-coordinate of the currPoint |
getCurrY() |
double |
Returns the y-coordinate of the currPoint |
getNextX() |
double |
Returns the x-coordinate of the nextPoint |
getNextY() |
double |
Returns the y-coordinate of the nextPoint |
iterate(double dx, double dy) |
void |
Sets currPoint to nextPoint and updates the position of nextPoint to be the currPoint with movement defined by dx and dy . |
A note on iterate(double dx, double dy)
. If you were to implement a pursuit
curve in full generality, then this is where you would solve a differential
equation. But again, we won't have you do that. Instead we're giving you \(dx\)
and \(dy\) which will tell you how the path travels on each call to iterate
.
To summarize your task:
- Keep track of
currPoint
andnextPoint
. - Implement a constructor taking in a
double x
anddouble y
. - Implement the methods listed in the table above.
Here are some tips to keep you on the right track!
- As
currPoint
andnextPoint
are bothPoint
objects, we've provided a class definingPoint
. Make sure to read through and understand what each method and constructor does! - When defining
iterate(double dx, double dy)
you may find that yourcurrPoint
andnextPoint
are not being set to what they are coded to be. Think about object references and try drawing a box-and-pointer diagram. - There is a secret to
Path.java
andPathHarness.java
see if you can find it! - If you want to learn more about pursuit curves, Wolfram's MathWorld provides a very interesting read.
E. Student Survey
As a final part of this lab, we'd like you to complete this quick survey to tell us a
little about yourself! After completing the survey, take the provided secret phrase and place it into Survey.java
where it says PUT SECRET WORD HERE!
.
F. Conclusion
Summary
Coding is not easy! Keeping track of what references point to what, modifying code (which you first have to understand), and systematically finding bugs are definitely not skills that develop overnight. Make sure to practice! You can get your partner or another classmate involved and generate variants of the lab exercises to provide extra practice.
The exercises on complicated uses of references are easy to produce and can be verified online using tools such as Java Visualizer or by simply running your code through intelliJ.
The internet is also a great boon for more coding practice. Checkout Reddit's /r/dailyprogrammer and topcoder's online exercises. Project Euler also provides a ton of questions with solutions that a potential interviewer might one day ask you!
For coding, practice is crucial so make sure to do so! Finally, if you or anyone you know is struggling, let a TA know and we'll be more than happy to help.
Submission
Submit the following as lab02
:
- your
Account.java
that provides overdraft protection and account merging - your
Path.java
program - your
Survey.java
program
Reading
- HFJ Ch 3 to pg 59, 4 to pg 80, 9, pg 287-289