Lecture 5: Programming, With Class
Overview
Classes are more than example data
Instantiating objects, and what tester does with Examples
Constructors
Methods and this
5.1 Representing Compound Data
So far, we’ve seen three kinds of data:
Numbers, represented in Java as ints
Sequences of characters, represented in Java as Strings
Yes/no answers, represented in Java as booleans
There are many more kinds of information in the world than these simple datatypes.
A record of student information can’t be represented as just a single string, or a number, or a boolean. Rather, it’s several pieces of related data, each of which might individually be one of these types: a boolean for whether the student is from within the state or not, a String for the student ID and username, an int for birth year, and so on. Each of these pieces of information means less on its own, but put together fully describes a student record that might be saved in a database.
In the context of math and graphing, we often talk about points, which (in two dimensions), aren’t represented as a single number, but as a pair of numbers. A point is defined as having an x- and y-coordinate, and from the perspective of defining a point, both are necessary.
A song in a music playlist (like in iTunes or Spotify), has a number of fields for data: the title (a String), the length of the song in seconds (a int), the file the song data is stored in (a String), and album and artist information. The album information might be a compound of other data as well, like the year it was released and the title.
In Java, we use classes to represent these shapes of compound data. A class for a simple student record might look like this:
class Student { String studentID; boolean inState; int birthYear; Student(String studentID, boolean inState, int birthYear) { this.studentID = studentID; this.inState = inState; this.birthYear = birthYear; } }
We can use the class to create data that describes several different students. To test it out, we can put the Student class definition above into the same file as an Examples class. Then in the Examples class we define several examples, using the new operator, which we haven’t seen before:
class ExamplesLec { Student s1 = new Student("A12345678", true, 1996); Student s2 = new Student("A98765432", false, 1993); Student s3 = new Student("A18273645", true, 1999); }
If we run this, we get:
ExamplesLec: |
--------------- |
|
new ExamplesLec:1( |
this.s1 = |
new Student:2( |
this.studentID = "A12345678" |
this.inState = true |
this.birthYear = 1996) |
this.s2 = |
new Student:3( |
this.studentID = "A98765432" |
this.inState = true |
this.birthYear = 1993) |
this.s3 = |
new Student:4( |
this.studentID = "A18273645" |
this.inState = true |
this.birthYear = 1999)) |
There are several pieces of this program that are new to us.
Fields without Values
First, in the class definition for Student, the first three lines are:
String studentID; boolean inState; int birthYear;
These look like field definitions, but they are missing the part with the equals sign and the value. We call these uninitialized field definitions, and this is actually the most common variety of field definition we’ll see in the course. We’ll see shortly why they don’t have a specific value associated with their definition.
(Simple) Constructors
Second, also in the class definition for Student, there’s this block of code:
Student(String studentID, boolean inState, int birthYear) { this.studentID = studentID; this.inState = inState; this.birthYear = birthYear; }
At first glance, this looks like a method – it has the name Student, a list of parameters, and what looks like a method body. Remember, though, that a method has two components before the parameter list.
Do Now!
What are they?
In a few lectures, we’ll talk about constructors in detail. For now, it’s more important for us to move on and see how new works, and how we can define methods.
Classes as Types
Third, we use Student in the position where we have previously written types like int or boolean. Just like int describes the set of integers, which is a type of value, when we create a class, its name describes a set of values. A field with a class type, like Student, can only hold references to objects of that type. We’ll define objects and references next.
The new Operator
Fourth, in the Examples at the bottom, we use the new operator, for instance in this example field:
Student s1 = new Student("A12345678", true, 1996);
A new expression has the keyword new, followed by the name of a class (in this case Student), followed by arguments in parentheses. These arguments must match the order and types of the parameters in the constructor, so in this case we have the String value "A12345678" for studentID, followed by the boolean value true for inState, followed by the int value 1996 for birthYear.
This new expression packages up these three pieces of data into what we call an object, or instance, of the class that appears after new. In this case, that class is Student. To understand what an object is, we can first refer to a fragment of what the tester library printed out:
new Student:2( |
this.studentID = "A12345678" |
this.inState = true |
this.birthYear = 1996) |
From this printing, we could conclude that objects are collections of fields that each have a particular value, and are labelled with the class that was used to construct them. The particular object above has class Student and the given values in its fields.
We can also represent objects through pictures. We’ll build up a pictorial representation of objects in this course, because many times it will be useful to draw out our programs to work through the relationship between different values and objects. For the object created in stored in the field s1, we’ll draw it like this:
The Student in the top right is the name of the object’s class, and the three fields’ values are listed in boxes within the class.
Behind the Scenes
That covers the four new pieces of syntax: uninitialized field definitions, constructors, class names as types, and the new operator. It also helps us understand the behind-the-scenes work that happens when printing out the values of the ExamplesLec class.
In the printed output, we see the field s1 is printed as holding the whole value of the student object, containing all three fields:
new ExamplesLec:1( |
this.s1 = |
new Student:2( |
this.studentID = "A12345678" |
this.inState = true |
this.birthYear = 1996) |
... |
On interesting thing to notice here is that the Student object that was created is printed in the same style as the ExamplesLec printing, which prints new, the name of the class, and all the fields’ values formatted as this.fieldName = fieldValue. This immediately suggests what’s going on when we run a program: an ExamplesLec object is created, and it and all its fields are printed! This is in fact exactly what happens behind the scenes when we use ./run: the infrastructure for the course simply uses new on ExamplesLec class and prints out the resulting value nicely formatted.
References
It’s useful to apply our pictorial representation idea to the fully-constructed ExamplesLec object above, focusing first on just the field s1. Based on the earlier picture, that suggests that the picture for the ExamplesLec object is something like this shape:
We just need to understand what goes in the box for s1. We might try to understand this by putting the entirety of the picture above into the box. However, we’re not going to do that, because it’s not an accurate model of how these objects are laid out in the running Java program. Instead, here’s the accurate picture of this situation:
We say that s1 contains a reference to the object. The program always ends up acting on references to objects, never the objects themselves. We’ll use small, distinct colored shapes to represent these references. The one at the top-left corner of an object serves to identify it, so when we see a reference in another field (or used as a parameter), we know which object it is referring to. The numbers 1 and 2 in the references are the same numbers that appear after the colons in the printout:
--------here! |
| |
v |
new ExamplesLec:1( |
this.s1 = |
new Student:2( <------------------and here! |
this.studentID = "A12345678" |
this.inState = true |
this.birthYear = 1996) |
... |
The tester library keeps a count of created objects, and labels each object in sequence with the order it was created in. This helps us distinguish objects in the printout. The full printout was:
ExamplesLec: |
--------------- |
|
new ExamplesLec:1( |
this.s1 = |
new Student:2( |
this.studentID = "A12345678" |
this.inState = true |
this.birthYear = 1996) |
this.s2 = |
new Student:3( |
this.studentID = "A98765432" |
this.inState = false |
this.birthYear = 1993) |
this.s3 = |
new Student:4( |
this.studentID = "A18273645" |
this.inState = true |
this.birthYear = 1999)) |
Which corresponds to this picture:
We can see this notion of a numbered reference play out if we create a field that uses a value from an existing object:
class ExamplesLec { Student s1 = new Student("A12345678", false, 1996); Student s2 = new Student("A98765432", true, 1993); Student s3 = new Student("A18273645", true, 1999); Student sReferToS1 = s1; }
When this is printed, we get the printout below, with the line added by me for emphasis. Java doesn’t copy the object, it just keeps track of another reference to it.
ExamplesLec: |
--------------- |
|
new ExamplesLec:1( |
this.s1 = |
new Student:2( <------------------------ |
this.studentID = "A12345678" | |
this.inState = false | |
this.birthYear = 1996) | |
this.s2 = | |
new Student:3( | |
this.studentID = "A98765432" | |
this.inState = true | |
this.birthYear = 1993) refers back to |
this.s3 = | |
new Student:4( | |
this.studentID = "A18273645" | |
this.inState = true | |
this.birthYear = 1999) | |
this.sReferToS1 = Student:2) ------------ |
In picture form, what happens is we copy the reference to the first object that was created, and store that in the sReferToS1 field:
5.2 More on Methods
We saw earlier that methods are a useful way to describe computations. Methods definitions appeared within the Examples classes we were working with. Methods can be defined in any class, including new ones like Student that we are working with in this section. For example, we could write a method that answers whether a student was born before a certain year. We write this method in the class that has the data we care about. In this case, the method needs to know the birthYear of a student, so the method will be defined in that class.
The first thing we need to do is figure out the header of this new method. The input is the year we’re comparing against. The name bornBefore makes sense, and since it’s a yes/no question, it should return a boolean. We typically write methods after the field definitions and the constructor. Here is the setup:
class Student { String studentID; boolean inState; int birthYear; Student(String studentID, boolean inState, int birthYear) { this.studentID = studentID; this.inState = inState; this.birthYear = birthYear; } /* @param year The year to check against, which should be less than the current year and greater than 0 @return true if this student's birth year is less than the given year, false otherwise */ boolean bornBefore(int year) { } }
Next, we need to write examples. This brings up an important point about how we’ll write programs in this course – the examples will always go in the Examples class at the end of the file. So we don’t add them inside the Student class, but in the Examples class, where our example data is defined, as well:
class ExamplesLec { Student s1 = new Student("A12345678", true, 1996); Student s2 = new Student("A98765432", false, 1993); Student s3 = new Student("A18273645", true, 1999); boolean bb1 = this.s1.bornBefore(1999); // should be true boolean bb2 = this.s1.bornBefore(1995); // should be false boolean bb3 = this.s2.bornBefore(1995); // should be true boolean bb4 = this.s3.bornBefore(10); // should be false }
Note a key difference here – we don’t write this in front of the dot for these method calls. We use the field containing a reference to a Student object. We must do this because the bornBefore method is defined only for Student objects, since it is in the Student class. If we used this.bornBefore, we’d get an error. This is a key point: we must call methods using a reference to an object whose class has the method that is being called.
Note also that the four method calls use three different object references. The first two use the reference stored in s1, the third and fourth use those stored in s2 and s3 respectively. The behavior of the method must somehow depend on the particular reference used, since we expect bb2 and bb3 to have different answers, even though the same argument is passed in.
Here’s the key idea that makes this work out: when calling a method, the value of this used when calculating that method is always the same as the reference that was used to call the method. So, for example, in the call for bb2, the reference used to call it is the reference stored in s1, while for bb3, the reference is the one stored in s2. That means uses of this within the method will reflect that difference for the two calls. With that in mind, let’s fill in the body of the bornBefore method:
/* @param year The year to check against, which should be less than the current year and greater than 0 @return true if this student's birth year is less than the given year, false otherwise */ boolean bornBefore(int year) { return this.birthYear < year; }
In each of the examples, we can use this rule about this to understand what value this.birthYear evaluates to. In the first two examples, it evaluates to 1996. In the call for bb3, it evaluates to 1993, and in the call for bb4, it evaluates to 1999. That tells us that the answers should work out correctly, since the four method calls end up perfoming these four comparisons:
1996 < 1999 // is true, using the value from the first student 1996 < 1995 // is false, using the value from the first student 1993 < 1995 // is true, using the value from the second student 1999 < 10 // is false, using the value from the third student
Exercise
Use the design recipe to write a method ageIn that takes a year as a parameter, and returns the age (in years) of the student in the given year.
Exercise
A person needs to be a resident of California and at least 30 years old to represent California in the U.S. senate. Use the design recipe to write a method canBeSenator in the Student class that has no parameters, and returns whether the Student could be a senator.
5.3 Summary
We refined what goes in a class definition – it can also contain a constructor, which lets us set the fields differently for different instances.
We saw that fields don’t necessarily need to come with a value; they can be provided with their value when using new, rather than always having the same value.
We introduced the new operator, which allows us to create instances of a class with specified values for the different fields.
We distinguished between objects and references, both in the printed output we get when running programs, and in a new picture-based representation of values.
The special name this is available in methods and is a reference to the object that was to the left of the dot when the method was called.