Today, we'll talk about the thing that makes an object-oriented programming (OOP) language object-oriented. And that's the notion of a class. We've been using them already, but without really talking about what they are. Essentially, a class is a template for a kind of object. For example, we've used the String
class a number of times already. Every time we create a string, we're creating a new instance of the class String
. (Instances are also called objects, which is where we get the name object-oriented programming.) Every instance of a String
has one piece of data associated with it (an array of char
s) and a number of methods (such as .length()
) which are functions that can be run on that object. So every string gets its own version of the .length()
method.
Up until this point, most of the classes we've created have been static classes, which only ever have one instance, and so they don't really feel like templates at all. But today, we'll create a non-static class and explore how to use it and what it's good for.
Our example today is going to have more than one file, so let's create a directory for all those files. If you're doing this as part of a homework assignment, remember to make the new directory readable and executable for everyone or your graders won't be able to get into it.
[ewennstr@silo java]$ mkdir BookStore
[ewennstr@silo java]$ chmod a+rx BookStore/
[ewennstr@silo java]$ cd BookStore
Suppose we're creating an app to work with an online bookstore. We'll want to have a special data type for the books that we sell. So we create a class called Book
. Each book has an ISBN, a title, an author, and a price, so we make sure that every instance of the Book
class will have member variables to store each of those values.
public class Book {
String isbn;
String title;
String author;
double price;
}
The first thing that should jump out at you is that this class has no main()
method. This is because this isn't a program to be run; it's a framework for a kind of data object. This also means that you can't run it by itself, but then again, what would that even meant?
Note that ISBN's aren't always only numerical, and since they can start with 0, we don't want to treat them as integers or long integers. It's true that we don't really want high accuracy for price (we only care to the nearest cent), but integer dollar amounts aren't enough, so for the time being, let's go with a double
. We'll do something better later on.
Now when you create a new instance of a class by using the new
command, what happens is that the class's constructor method is called. A number of things happen behind the scenes (like allocating memory), but what the constructor does is something we (the ones creating the class) control. The name of the constructor method is always exactly the same as the name of the class, which can be confusing, but you should get used to it because almost all OOP languages do it this same way. Let's take a look at one way we could write a constructor for our Book
class:
public class Book {
String isbn;
String title;
String author;
double price;
public Book(String i, String t, String a, double p) {
this.isbn = i;
this.title = t;
this.author = a;
this.price = p;
}
}
If your instinct was to write public void Book(...)
, then your instincts are good, but they're wrong in this case. The constructor doesn't have any output, but since constructors in Java never have any output, we don't specify this by putting void
in the declaration. Confusing, I know, but that's the way it is in Java.
When you're writing a method for a class like this, you specify the names of all the arguments in the declaration (here it's i
, t
, a
, and p
), but you might also want to refer to the object itself. The keyword this
does that job for you. In this case, because this is a constructor, this
refers to the object being created. But if you had called something like myBook.toString()
, then this
would rever to myBook
.
Adding this constructor method means that we can now create new Book
objects (this means new instances of the class Book
) by using the command new Book(isbn, title, author, price)
, and plugging in the appropriate values for isbn, title, author, and price.
So let's actually test all this. We put the above code into a file named Book.java
, and then we create a new file BookTester.java
to create a short program to test out our new class:
public class BookTester {
public static void main(String args[]) {
Book book1 = new Book("468408636", "American Gods", "Neil Gaiman", 24.99);
System.out.println("Book1's title is " + book1.title);
System.out.println("Book1's price is " + book1.price);
}
}
Now we can compile the book tester program. Note that because BookTester.java
mentions a class called Book
, the Java compiler will look for a file called Book.java
and compile that (which results in the file Book.class
) too. This is one of the reasons why the Java is so particular about making sure that a public class called classname is always defined in a file named classname.java
. It's so that the compiler knows where to find the class definitions when it sees them used.
[ewennstr@silo BookStore]$ javac BookTester.java
Picked up _JAVA_OPTIONS: -Xms512m -Xmx512m
[ewennstr@silo BookStore]$ ls
Book.class Book.java BookTester.class BookTester.java
[ewennstr@silo BookStore]$ java BookTester
Picked up _JAVA_OPTIONS: -Xms512m -Xmx512m
Book1's title is American Gods
Book1's price is 24.99
Every object automatically comes with a method called toString()
, which creates a string that describes the object. It can be called explicitly (for example, book1.toString()
), but it's also called automatically any time you try to use the object as a string (for example, System.out.println(book1)
). Of course, the default version of toString()
isn't terribly useful, since it has to work for any conceivable object, even ones that don't have any data at all. Here's the output you get right now if you try to print the Book
object book1
(using the command System.out.println(book1);
): Book@6a50549
. It basically just tells you what class of object it is and where it's stored in memory. We can do better than that by defining our own version of the toString()
method. We add the following code to the Book
class definition.
public String toString() {
return this.title + ", written by " + this.author;
}
And now, if we run System.out.println(book1)
, the result is: American Gods, written by Neil Gaiman
.
Note: in class, we defined a similar function, but called it describe()
. It's better to use the toString()
name, so that it improves the behavior when someone tries to print one of your objects.
So far, we've seen methods to create objects (constructors) and methods that use the data stored in the object, but we can also have methods that change the objects. To demonstrate this, let's create a method that takes as an argument a percentage to be discounted (written as a decimal, so a 15% discount is calculated by inputting 0.15) and changes the price accordingly. We'll call the method percentDiscount()
, and here is its code:
public void percentDiscount(double discount) {
this.price = this.price * (1 - discount);
}
The details here are as much math as they are computer programming, but if it's still confusing, try calculating a few simple examples (try discounting a $100 by 20%) yourself using this formula.
We add a few more test lines to our tester just to make sure it works.
public class BookTester {
public static void main(String args[]) {
Book book1 = new Book("468408636", "American Gods", "Neil Gaiman", 24.99);
System.out.println("Book1's title is " + book1.title);
System.out.println("Book1's price is " + book1.price);
System.out.println("Book1 is " + book1);
book1.percentDiscount(0.15);
System.out.println(book1 + " costs $" + book1.price + " after discount.");
}
}
And the results:
[ewennstr@silo test]$ java BookTester
Picked up _JAVA_OPTIONS: -Xms512m -Xmx512m
Book1's title is American Gods
Book1's price is 24.99
Book1 is American Gods, written by Neil Gaiman
American Gods, written by Neil Gaiman costs $21.2415 after discount.
If you're anything like me, then that extra .0015 in the price after discount is very annoying and just begging to be fixed. There are a number of ways of doing this, but I'm going to use one that allows us to talk about encapsulation, another core principle of object-oriented programming. In a nutshell, encapsulation is a technique where a member variable of a class (such as price
) is implemented in such a way that it never needs to be (and sometimes can't be) directly accessed. Encapsulation is especially useful when the way the class internally stores the data differs from the way code outside of the class will use or refer to it. It's also useful when some sort of clean-up is needed every time the data is altered.
Here, we're going to solve the rounding-to-the-nearest-cent problem by storing the price of each book as an integer storing the number of cents, as opposed to a floating point number storing the number of dollars. This is not the only way of solving the problem, of course, but this has a number of different advantages. First of all, using a Java int
allows us to store penny-precise values up to 231 cents (or about 20 million dollars). Using a float
takes up the same amount of space as an int
(32 bits), and it can store even larger values, but if you want your values to be accurate to the nearest cent, your maximum value is 224 cents (almost 168 thousand dollars).* Okay, so not a very important difference for most books. But it also allows us to take advantage of all the nice built-in features for rounding integers. And most importantly for our current purposes, it gives us an opportunity to practice encapsulation.
*We can be penny-precise all the way up to almost 100 quadrillion dollars using a 64-bit long
, or up to just over 90 trillion dollars using a 64-bit double
.
The first step for us is to replace the double
variable storing the price in dollars (defined by the line double price;
with an int
variable storing the price in cents: private int priceInCents;
. Here in the notes, I'm going to name the variable priceInCents
to make it clear to anyone who looks at my code (including possibly myself after a few weeks have passed and I've forgotten the details) that I'm storing the price in cents. also, since I'll only ever have to explicitly refer to this value a few times, I can get away with a longer name without having to type it over and over again. In class, we used protected
, but that's actually less restrictive than just leaving it at the default level. You can read more about access level modifiers here, or you can look at the notes to tomorrow's lecture. For now, it's enough to know that declaring this member variable to be private
, means that if we try to reference Book.priceInCents
anywhere outside of this class definition, it won't work.
So how will we set, get, or change the price if we can't refer to Book.priceInCents
directly? Well the value can be initially set by the constructor, which we'll modify in just a moment, and it can be changed or retrieved by special methods we'll create just for that purpose.
But first, let's look at the constructor. Now we don't want to actually change the way we used the constructor (giving it three strings
and a double
for the ISBN, title, author, and price in dollars), so the definition of the constructor method starts the same way as before. But we need to convert that dollar price to a cent price and round it to the nearest cent. So here's the code for the new constructor:
public Book(String i, String t, String a, double priceInDollars) {
this.isbn = i;
this.title = t;
this.author = a;
this.priceInCents = Math.round(priceInDollars * 100);
}
Next, we create a method to retrieve the data stored in the priceInCents
variable. A method like this is technically called an accessor, but most of the time people just call it a getter. Now we're almost always going to want a dollar amount to work with, so our price getter should convert to dollars first before returning the value requested. It's traditional to start the names of getters with the word get
. Its code looks like this:
public double getPriceInDollars() {
return (this.priceInCents / 100.0);
}
"Why 100.0?" I hear you scream. Well if we just did this.priceInCents / 100
Java would recognize that both this.priceInCents
and 100
represent int
s, and it will do integer division, ignoring any decimal part as an unneeded remainder. When Java sees a number with a decimal in it, it automatically assumes that it is a double
, and when you try to divide an int
by a double
, it automatically casts* the int
as a double
and then does the division using double
s.
So now we have a method that let's us refer to the data stored in the price. We could create another getter to get the price in cents if we needed it, but this should be fine for now. The next step is to create a method that allows us to change the price. Such a method is officially called a mutator, but mostly, I've heard them referred to as setters, which might be a bit misleading because they're mostly used for changing values, not for setting them in the first place. "Mutator" also sounds cooler than "setter", but "setter" rhymes with "getter", so that's what we use. You can probably guess what word is usually used to start the names of setters. (Hint: it starts with an "s" and rhymes with "get".) Here's the code for our setter:
public void setPriceInDollars(double priceInDollars) {
this.priceInCents = round(priceInDollars * 100);
}
The content here is very similar to what's in the constructor. In fact, since we're duplicating code (multiplying by 100 and then rounding) here, it's probably a good idea to rewrite the constructor so it uses the setPriceInDollars()
method. While we're at it, we should also change the percentDiscount()
method to take advantage of the new getter and setter. It's a good general programming principle to look for places where you've repeated the same block of code, and find a way to remove that redundancy. In this case, the repeated code is simple enough that it's unlikely to be written wrong, and the US government isn't likely to change their mind about the number of cents in a dollar), but it is possible that we might change our minds later about how to store the price internally. And if that happens, the only methods that we'll have to alter will be the getters and setters, because they are the only ones that explicitly refer to the member variable PriceInCents
. So here's where the Book
class now:
public class Book {
String isbn;
String title;
String author;
private long priceInCents;
public Book(String i, String t, String a, double PriceInDollars) {
this.isbn = i;
this.title = t;
this.author = a;
this.price = Math.round(PriceInDollars * 100);
}
public String toString() {
return this.title + ", written by " + this.author;
}
public void percentDiscount(double discount) {
this.setPriceInDollars( (1-discount) * this.getPriceInDollars() );
}
public double getPriceInDollars() {
return this.price / 100.0;
}
public void setPriceInDollars(double dollarPrice) {
this.price = Math.round(dollarPrice * 100);
}
}
Now since percentDiscount()
is defined as part of the Book
class, it still can refer directly to priceInCents
, but it shouldn't. When it comes to other classes (like our BookTester
class), it is necessary to use the getters and setters because we set the member variable priceInCents
to be private
. So we'll need to change our tester as well:
public class BookTester {
public static void main(String args[]) {
Book book1 = new Book("468408636", "American Gods", "Neil Gaiman", 24.99);
System.out.println("Book1's title is " + book1.title);
System.out.println("Book1's price is " + book1.getPriceInDollars());
System.out.println("Book1 is " + book1);
book1.percentDiscount(0.15);
System.out.println(book1 + " costs $" + book1.getPriceInDollars() + " after discount.");
}
}
The output is exactly the same as before, except that we now have prices accurate to only the nearest cent.
[ewennstr@silo test]$ java BookTester
Picked up _JAVA_OPTIONS: -Xms512m -Xmx512m
Book1's title is American Gods
Book1's price is 24.99
Book1 is American Gods, written by Neil Gaiman
American Gods, written by Neil Gaiman costs $21.24 after discount.
Overloading is what happens when you give two different definitions for the same method, using a different number of arguments or arguments with different data types. We've seen it in practice already. The Java operator +
is heavily overloaded. If both arguments are int
s, then it does integer addition (for example 4+7
returns the int
value 11
). If one of the arguments is a double
and the other is an int
, it casts the int
as a double
, adds the number and returns the result as a double
(for example, 4+7.0
returns the double
value 11.0
). Or even stranger, if one of the arguments is a String
, the operator converts the other argument into a String
and does concatenation (e.g., 4+"7"
returns the String
with the value "47
").
We can overload our methods too, and this is very commonly done with constructor methods. Our current constructor always takes four arguments: three String
s (for the ISBN, title, and author), followed by a double
(for the price in dollars). (Note that Java will automatically take care of converting other numerical types into double
s if needed.) Let's build another constructor, that leaves out the author and only takes two String
s and a double
. In that case, we'll just declare the author to be "unknown
".
public Book(String i, String t, double PriceInDollars {
this(i, t, "unknown", p);
}
Note that we're using this()
to call the other, more comprehensive constructor instead of rewriting all the constructor code again for this version of the constructor. In class, we just rewrote the code, but remember that duplicating code is often bad, so try to avoid when possible.
We added an extra couple of lines to the tester, to demonstrate the new constructor:
public class BookTester {
public static void main(String args[]) {
Book book1 = new Book("468408636", "American Gods", "Neil Gaiman", 24.99);
System.out.println("Book1's title is " + book1.title);
System.out.println("Book1's price is " + book1.getPriceInDollars());
System.out.println("Book1 is " + book1);
book1.percentDiscount(0.15);
System.out.println(book1 + " costs $" + book1.getPriceInDollars() + " after$
Book book2 = new Book("89622162", "Beowulf", 5.99);
System.out.println("Book2 is " + book2);
}
}
And here it is in action:
[ewennstr@silo test]$ javac BookTester.java
Picked up _JAVA_OPTIONS: -Xms512m -Xmx512m
[ewennstr@silo test]$ java BookTester
Picked up _JAVA_OPTIONS: -Xms512m -Xmx512m
Book1's title is American Gods
Book1's price is 24.99
Book1 is American Gods, written by Neil Gaiman
American Gods, written by Neil Gaiman costs $21.24 after discount.
Book2 is Beowulf, written by unknown