On this page:
2.1 Overview
2.2 Introduction
2.3 Java data types
2.3.1 Primitive types
2.3.1.1 Booleans
2.3.1.2 Numbers
2.3.1.3 Characters
2.3.2 Representing data that have finite, specific values
2.3.3 Primitive vs reference types
2.3.4 private, public and final:   What are they?
2.3.5 Organization with packages
2.4 Dynamic dispatch
2.5 Exceptions
2.5.1 Writing a method with exceptions
2.5.2 Calling methods that may throw exceptions
2.5.3 Checked and unchecked exceptions
2.5.4 Testing methods with exceptions
2.5.5 Using exceptions in general
6.3

Lecture 2: Java Safari

Related files:
  Publication.java     Book.java     TypeOfBook.java     PublicationTest.java  

2.1 Overview
2.2 Introduction

In this lecture we review some topics specific to the Java programming language. We use a variation of the Book class from Lecture 1: The Essence of Objects.

2.3 Java data types

Data may come in several forms:

Java is a strongly-typed language. When we declare a variable in Java, we must specify the type of the data that it will represent.

2.3.1 Primitive types

Simple data like numbers, characters and booleans are called primitive types.

2.3.1.1 Booleans

In Java, boolean values are written true and false, and the name of the type is boolean.
boolean isDaytime = true;
boolean inNewYork = false;

2.3.1.2 Numbers

Integral numbers are written as follows:
int ten = 10;
There are several data type for integral numbers: byte, short, int and long. They use 1, 2, 4 and 8 bytes respectively, and represent correspondingly larger ranges of signed integral numbers.

Decimal numbers are written as follows:
double pointSix = 0.6;
float pointFour = 0.4f;
While both represent decimal numbers, double can store numbers in a bigger range and with greater precision than float. The “f” in “0.4f” is used to specify explicitly that we mean a “float” and not a “double.”

2.3.1.3 Characters

Java has a type char that can be used to represent single characters (letters, punctuation, special symbols, etc.).
char letterA = 'a';
char uppercaseA = 'A';
A single character is written in single quotes. But what if we wanted to represent the single-quote character itself? We use a special character ‘\’ to tell Java to take the character immediately following it literally.
char singleQuote = '\'';

2.3.2 Representing data that have finite, specific values

A book can be bought in several forms: hard cover, paperback, Kindle, etc. How can we represent this information in the Book class?

One option may be to represent this data as a simple number, and associate its values with specific forms of books. A value of 1 means Paperback, a value of 2 means Hardcover and a value of 3 means Kindle. Although we can make our class work with such a representation, it has several problems:

The form of book is information that has a unique property: it can be one of a finite set of values, nothing else. Such type of data is common: days of the week, months of the year, types of chess pieces, sizes of ice cream, etc. Java allows us to define a special type for such data: enumerated type.

We can define a type TypeOfBook for the form of book:

public enum TypeOfBook {HARDCOVER,PAPERBACK,KINDLE}

in a file TypeOfBook.java. Then we can change our Book class:

/** * Represents bibliographic information for books. */
public class Book {
private final String title, author, publisher, location;
private final int year;
private TypeOfBook bookType;
 
/** * Constructs a {@code Book} object. * * @param title the title of the book * @param type the type of the book * @param author the author of the book * @param publisher the publisher of the book * @param location the location of the publisher * @param year the year of publication */
public Book(String title, TypeOfBook type, String author, String publisher,
String location, int year) {
this.title = title;
this.type = type;
this.author = author;
this.publisher = publisher;
this.location = location;
this.year = year;
}
}

In the above code snippet the field bookType can have one of only three values: TypeOfBook.HARDCOVER, TypeOfBook.PAPERBACK and TypeOfBook.KINDLE. Attempting to assign it any other value will give us an error!

We use this enumerated type as follows:

Publication rushdie = new Book("Midnight's Children", "Salman Rushdie",
TypeOfBook.HARDCOVER, "Jonathan Cape", "London", 1980);

Enumerated types in Java are capable of doing more: please see the Java tutorial on enumerated types for more information.

2.3.3 Primitive vs reference types

Java represents primitive-type data differently from all other forms of data (e.g. objects). Consider the following code:

int number;
 
number = 10;

When we define a new variable int number, a chunk of memory of 4 bytes is allocated. When we assign the value 10 to it, this number is written directly to that memory. In other words, the variable number is a placeholder for where this integer’s value is actually stored.

Now consider the following code:

Publication rushdie = new Book("Midnight's Children", "Salman Rushdie",
TypeOfBook.HARDCOVER, "Jonathan Cape", "London", 1980);

Knowing how pointers in C++ behave will help in understanding how reference types in Java work. Unlike with pointers, Java does not allow directly manipulating memory addresses.

Similar to the above, when we define a new variable Publication rushdie, a chunk of memory is allocated. Next, the Book object is created, but at another memory location. Finally the object (its memory location) is assigned to rushdie. Thus, unlike primitive types, the variable rushdie stores a reference to the actual Book object instead of the object itself.

Although simple to understand, this representation has a profound effect on how reference type variables can be used. Here are some examples:

Checking equality among objects has many nuances. We will encounter these issues later.

Overall, much of the behavior of objects in Java can be understood by remembering the nature of a reference type.

2.3.4 private, public and final: What are they?

Given a Book object called obj, syntactically we can refer to its fields as obj.title and call its method as obj.citeApa().

When we have many classes and objects, allowing one object direct access to another object’s fields like this creates problems and confusion. Java allows us to restrict access to a field or a method of an object. This is done through "access modifiers." If a field of a class is denoted as private then that field can be directly accessed only by the methods of that class, nothing else. Similarly, if a method of a class is denoted as private then that method can be called only from other methods within that class. At the other extreme, making a field (method) public makes it accessible (callable) from any method of any other class.

Think of an ATM. It allows you to withdraw cash, deposit checks and check balance. There are parts of it that you can access and use (e.g. the keypad on the ATM). However there are parts that you cannot access (e.g. the mechanical parts that dispense notes), and with good reason! In this way the bank can rest assured that no customer has unauthorized access to the machine, while you can use the machine without ever knowing how it works. This is exactly what we must accomplish when we design objects: expose only necessary functionality and hide others (information hiding).

How does one decide whether to make something private or public? The following rules are useful and apply well in most cases:

The final keyword, when applied to fields does not allow mutating these fields after they have been initialized once in the constructor.

2.3.5 Organization with packages

As the number of classes in an application increases, it is desirable to organize them appropriately. Packages are a way to group classes.

A class is added to a package by adding package nameOfPackage; as the first line of the file that contains the class. Thus a class can belong to only one package. This class must be referred to using its full name (i.e. nameOfPackage.nameOfClass). In order to avoid repeatedly referring to the package name, we can use import statements. Packages can be grouped in other packages as well, creating a hierarchical structure. For example the Scanner class in Java can be referred to as java.util.Scanner, revealing that the Scanner class is inside the util package, which itself is inside the java package. In order to refer to it as simply Scanner, we add import java.util.Scanner; at the top of the file that refers to it. If several classes within the util package are used, we can use the shorthand import java.util.*; notation.

Most programming languages allow some mechanism to organize classes (e.g. namespaces in C++). Organizing classes into packages has several advantages. One can isolate code by components, having multiple classes with the same name but in different packages, etc.

Java mandates that the package structure be mimicked in the folder structure that stores the actual source files. For example, if the Book class is stored in packages such that it can be referred as cs5010.publication.Book then the file Book.java must be stored inside a folder publication which is inside the folder cs5010. This top-level folder must reside in a folder that Java will look into for classes when it is using this class. Such folders in a file system that contain Java packages and classes must be on the classpath, an environment variable in all operating systems that support Java. When we create a project in an IDE, it adds the folder where all *.java files in the project are stored to the classpath, so that the project can be compiled and run.

2.4 Dynamic dispatch

Consider the following code in one of the tests from Lecture 1: The Essence of Objects.

public class PublicationTest {
private Publication rushdie;
private Publication turing;
 
@Before
public void setup() {
rushdie = new Book("Midnight's Children", "Salman Rushdie",
TypeOfBook.HARDCOVER, "Jonathan Cape", "London", 1980);
 
turing = new Article("Computing machinery and intelligence",
"A. M. Turing", "Mind", 59, 236, 1950);
}
 
@Test
public void testCiteApa() {
 
String expectedOutput =
"Salman Rushdie (1980). Midnight's Children. London: Jonathan Cape.";
assertEquals(expectedOutput, rushdie.citeApa());
 
expectedOutput = "A. M. Turing (1950). Computing machinery and "
+ "intelligence. Mind, 59(236).";
assertEquals(expectedOutput, turing.citeApa());
}
}

It may be a bit surprising that the above test passes. Java called the citeApa() method of the Book class when we said assertEquals(expectedOutput, rushdie.citeApa()); but called the citeApa() method of the Article class when we said assertEquals(expectedOutput, turing.citeApa());, even though the types of rushdie and turing are the same: Publication. Indeed by looking at just those lines and even the types of those variables, even we cannot tell where it should go!

If we now look at the context we see that rushdie is set to a Book object and turing is set to a Article object (this was done in setup()). Thus we expect those method calls to work the way they do. But how does Java know?

Dynamic binding is supported in many languages. In dynamically typed languages like Python, dynamic binding allows the programmer to call a function using any variable: if at runtime that variable is an object that offers that function, it works. Many strongly-typed languages also support dynamic binding (e.g. virtual functions in C++)

The process of matching a method call to a method body is called binding or dispatch. This can be done in one of two ways. One way would be to bind code during compiling (i.e. statically). The other way would be to wait until the program is run, and then bind only when it must execute that line (i.e. dynamically). Java chooses the latter option, hence the term dynamic dispatch.

We see above an advantage of this: we can write the citeApa implementations for each publication in its own class, and then Java will figure out where to go depending on which object it is working with! In fact dynamic dispatch is one of the reasons Design 2 in Lecture 1 works correctly. Look at the Python code from Lecture 1 for the citeApa function: it explicitly determines what kind of publication it was passed (via the if-else block) and acts accordingly. The Java implementation handles that transparently for us (the code in Python can be redesigned to do this as well). The code looks clean: each citeApa() method written in Java has code for one publication, and nothing else.

Dynamic dispatch is convenient to distribute functionality in this way across related types (called union types, to be discussed later). Whenever you are working with related types and feel the need to check what type you have: stop and try to use dynamic dispatch to eliminate that checking.

Consider another piece of magic thanks to dynamic dispatch: we can now reassign objects to variables (as all variables are of type Publication) and the code that uses them, without changing any of its code, will now change its behavior accordingly!

We will see dynamic dispatch and its repercussions in design repeatedly in the course, so it is important to understand this feature.

2.5 Exceptions

Let us look at the constructor of the Book class. What would happen if we used it as follows:

Publication rushdie = new Book("Midnight's Children", "Salman Rushdie",TypeOfBook.HARDCOVER, "Jonathan Cape", "London", -1980);

We have attempted to create a Book object with a negative year. A negative year does not make sense in the given context. Java was not able to catch this error because -1980 is a valid number. Ideally the constructor should inform its caller “thou shall not pass me a negative number for the year,” instead of using the number and creating an object that has an invalid year of publication. Exceptions allow us to do that.

An exception occurs when something unexpected happens, whether it be invalid input, an operation that cannot be completed (e.g. the square root of a negative number) or even something that is beyond our control (e.g. attempting to read from a file that no longer exists). Exceptions offer us a dignified way of aborting a method and sending a message to its caller that something went wrong.

2.5.1 Writing a method with exceptions

In the constructor, an exception should occur if a negative number is passed as the year of publication of the book. We can change its signature to the following:

/** * Constructs a {@code Book} object. * * @param title the title of the book * @param author the author of the book * @param type the type of the book * @param publisher the publisher of the book * @param location the location of the publisher * @param year the year of publication */
public Book(String title, String author, TypeOfBook type,
String publisher, String location, int year)
throws IllegalArgumentException {
if (year < 0) {
throw new IllegalArgumentException("Year of publication "+
"cannot be a negative number");
}
this.title = title;
this.author = author;
this.type = type;
this.publisher = publisher;
this.location = location;
this.year = year;
}

Java has many kinds of exceptions. Since our problem here is an invalid argument, we use the IllegalArgumentException class.

The method signature explicitly declares that it may throw an IllegalArgumentException object. A method can throw multiple types of exceptions, declared in its signature separated by commas. The Javadoc-style comments document this possibility.

Before initializing the fields we check if the year passed to the constructor is negative and if so, we throw an exception. This involves creating an IllegalArgumentException object with a helpful message in it and throwing it.

This method now works as follows:

A method may throw multiple types of exceptions, and it may declare some or all of them in its method signature.

2.5.2 Calling methods that may throw exceptions

Let us test this constructor, specifically by passing it a negative year. Whenever a method is called that may throw one or more exceptions we can enclose it in a try-catch block as follows:

Publication rushdie;
 
try {
rushdie = new Book("Midnight's Children", "Salman Rushdie",
TypeOfBook.HARDCOVER,"Jonathan Cape", "London", -1980);
}
catch (IllegalArgumentException e) {
//This will be executed only if an IllegalArgumentException is //thrown by the above method call }

Thus we try to call such a method, and if an exception is thrown we catch it. If no exception is thrown then the catch block is ignored.

2.5.3 Checked and unchecked exceptions

If a method can throw an exception to its caller, it is a good idea to declare it explicitly in its signature using the throws clause irrespective of whether the exception is checked or not. This informs the client explicitly so that it may address it (e.g. by enclosing a call to this method in a try-catch block).

Java has two categories of exceptions: checked and unchecked. If there is a chance that a method may throw a checked exception (either using the throw clause or by calling another method that throws it) Java mandates that the method do one of two things. Either the method must catch the exception using a try-catch block, or it must explicitly declare in its method signature using a throws clause that it may throw this exception to its caller. This is not mandated for unchecked exceptions. IllegalArgumentException and ArrayIndexOutofBoundsException are examples of unchecked exceptions, whereas FileNotFoundException is a checked exception.

More information about exceptions is available in the Java documentation.

2.5.4 Testing methods with exceptions

We can test whether a method throws exceptions when expected using the try-catch blocks as above

@Test
public void testIfInvalidYear() {
Publication rushdie;
 
try {
rushdie = new Book("Midnight's Children", "Salman Rushdie",
TypeOfBook.HARDCOVER,"Jonathan Cape", "London", 1980);
//normal tests for the rushdie object } catch (IllegalArgumentException e) {
fail("An exception should not have been thrown");
}
 
try {
rushdie = new Book("Midnight's Children", "Salman Rushdie",
TypeOfBook.HARDCOVER,"Jonathan Cape", "London", -1980);
fail("The above line should have thrown an exception");
} catch (IllegalArgumentException e) {
//do not do anything except catch the exception and let the test continue }
}

We expect the first constructor call to work correctly. If an exception is thrown the catch block will fail this test case. We expect the second constructor call to throw an exception. If an exception was indeed thrown then the try block will abort and the statement fail("The above line should have thrown an exception"); will not be executed. Catching the (expected) exception allows this test to pass. If an exception was not thrown then the fail("The above line should have thrown an exception"); fails the test case.

JUnit allows us to test for exceptions in a more concise way. We must divide the above test case into two.

@Test
public void testIfValidYear() {
rushdie = new Book("Midnight's Children", TypeOfBook.HARDCOVER,
"Salman Rushdie", "Jonathan Cape",
"London", 1980);
//test the object }
 
@Test(expected = IllegalArgumentException.class)
public void testIfInvalidYear2() {
rushdie = new Book("Midnight's Children", "Salman Rushdie", TypeOfBook.HARDCOVER,
"Jonathan Cape",
"London", -1980);
}

The first test is as before, checking if the object was created correctly with a valid year. The second test has this annotation: @Test(expected = IllegalArgumentException.class). This declares that this test expects the IllegalArgumentException to be thrown, and thus will abort and pass when it encounters this exception for the first time during its execution. If this method ends without an IllegalArgumentException thrown even once, this test fails.

2.5.5 Using exceptions in general

Java has several inbuilt exception types, which are all classes. Exceptions provide us with a way to handle errors. Follow this procedure when you write a new method: