Subtype polymorphism, types and casting review
In Java, when we use the phrase "the variable x
has type T
",
we are trying to say that the variable x
can hold any value that has the type T
where T
can be
-
A class name
-
An abstract class name
-
An interface name
A reference value in Java has type T
if and only if
-
T
is a class name, it was created using the constructor of classT
or one of it’s subclasses -
T
is an interface name, it was create using the constructor of a classC
that implementsT
-
C
can implementT
directly, i.e., the code forC
hasimplements T
-
C
can implementT
indirectly, i.e., one of the superclasses ofC
implementsT
-
Observe that the rules here allow for
-
values created from subclasses to be used as having the type of one of their superclasses
-
values created from classes to be used as having the type of an interface that they implement
This ability to view values under different types because of their position in the class hierarchy is called subtype polymorphism.
Compile-time and runtime types
Our Java programs have 2 distinct phases in their lifetimes
-
Compile time refers to your source code and the point in time when the source code is being compiled by the Java compiler
javac
-
Runtime refers to when your code is being evaluated (or executed or run) by the Java Virtual Machine (JVM). This is the
java
command.
The Java compiler checks your code at compile time to make sure that your source code is using each variable (fields, method arguments, local variable etc.) according to the type assigned to the variable. In order to perform these checks the compiler is only aware of the type used to declare our variable. We call this type the compile-time type.
The compiler will use the compile-time type to ensure that all your code only accesses fields/methods that are defined on the compile-time type.
Recall our Shapes
example
Lets look at some snippets of code and walk through what the compiler will do to establish if we have valid code or not.
private Posn pin; (1)
Posn origin = new Posn(0,0); (2)
Shape s1 = new Circle(origin, 5); (3)
AbstractShape as1 = new Circle(origin, 5); (4)
Circle c1 = new Circle(origin, 5); (5)
String ss = new Circle(origin, 6); (6)
s1.area(); (7)
as1.area(); (8)
c1.area(); (9)
s1.getRadius(); (10)
as2.getRadius(); (11)
c1.getRadius(); (12)
1 | a field declaration that specifies Posn as the compile-time type of pin |
2 | a line that both declares and initializes the variable origin . origin has compile-time type Posn . The compiler checks that the right-hand side of the assignment
(= ) is creating/using a value that has a type of Posn . In this case we are creating an object using Posn 's constructor which is-a Posn . |
3 | a line that both declares and initializes the variable s1 . s1 has compile-time type Shape . The compiler checks that the right-hand side of the assignment
(= ) is creating/using a value that has a type of Shape . In this case we are creating an object using Circle 's constructor which implements Shape , therefore is-a Shape . |
4 | a line that both declares and initializes the variable as1 . as1 has compile-time type AbstractShape . The compiler checks that the right-hand side of the assignment
(= ) is creating/using a value that has a type of AbstractShape . In this case we are creating an object using Circle 's constructor which extends AbstractShape , therefore is-a AbstractShape . |
5 | a line that both declares and initializes the variable c1 . c1 has compile-time type Circle . The compiler checks that the right-hand side of the assignment
(= ) is creating/using a value that has a type of Circle . In this case we are creating an object using Circle 's constructor which therefore is-a AbstractShape . |
6 | a line that both declares and initializes the variable ss . ss has compile-time type String . The compiler checks that the right-hand side of the assignment
(= ) is creating/using a value that has a type of String . In this case we are creating an object using Circle 's constructor which is not a Circle . The compiler signals
a compile time error informing us that the value we tried to assign to ss has incompatible type. |
7 | a line that attempts to call a method on s1 . The compiler checks that the compile-time type of s1 has a method with that name, i.e., area() . The compile-time type of s1 is Shape , Shape is an interface and it does contain a signature for area() . Since the compiler forces all values that implement Shape to provide an implementation for each signature, this is a valid method call. |
8 | a line that attempts to call a method on as1 . The compiler checks that the compile-time type of as1 has a method with that name, i.e., area() . The compile-time type of as1 is AbstractShape , AbstractShape is an abstract class that implements the interface Shape . Shape contains a signature for area() . Since the compiler forces all values that implement Shape to provide an implementation for each signature then either AbstractShape has an implementation of area() and/or any subclasses of AbstractShape must have an implementation of area() . Thus this is a valid call. |
9 | a line that attempts to call a method on c1 . The compiler checks that the compile-time type of c1 has a method with that name, i.e., area() . The compile-time type of c1 is Circle , Circle is an concrete class that extends AbstracShape and implements the interface Shape . Shape contains a signature for area() . Since the compiler forces all values that implement Shape to provide an implementation for each signature then either AbstractShape has an implementation of area() and/or Circle has an implementation of area() . Thus this is a valid call. |
10 | a line that attempts to call a method on s1 . The compiler checks that the compile-time type of s1 has a method with that name, i.e., getRadius() . The compile-time type of s1 is Shape , Shape is an interface. Shape does not contain a signature for getRadius() . This is an invalid use of Shape and the compiler issues a compile time error informing us that there is no such method on Shape . |
11 | a line that attempts to call a method on as1 . The compiler checks that the compile-time type of as1 has a method with that name, i.e., getRadius() . The compile-time type of as1 is AbstractShape , AbstractShape is an abstract class that implements the Shape interface. Shape does not contain a signature for getRadius() and neither does AbstractShape . This is an invalid use of Shape and the compiler issues a compile time error informing us that there is no such method on Shape . |
12 | a line that attempts to call a method on c1 . The compiler checks that the compile-time type of c1 has a method with that name, i.e., getRadius() . The compile-time type of c1 is Circle , Circle does contain a signature for getRadius() . This is a valid call. |
Runtime type
Observer in our preceding code snippets that our compiler is being defensive or strict. Our lines of code explicitly create a Circle
for s1
, as1
and c1
and yet
in the case of calling getRadius()
the compiler rejected s1.getRadius()
and as1.getRadius()
.
So when the above code runs, we say at runtime, we know that the values stored for s1
and as1
are in fact Circle
s.
We say that the runtime type of s1
and as1
is Circle
. Given that we compiled our program and the compiler has enforced that
-
s1
is only used and manipulated with whatShape
allows -
as1
is only used and manipulated with whatAbstractShape
allows -
c1
is only used and manipulated with whatCircle
allows
the objects at runtime,
-
are going to have runtime type
Circle
-
are able to be manipulated with what
Circle
allows -
are only going to execute what the compiler allowed, that is
-
the object pointed to by
s1
is only used and manipulated with whatShape
allows -
the object pointed to by
as1
is only used and manipulated with whatAbstractShape
allows -
the object pointed to by
c1
is only used and manipulated with whatCircle
allows
-
Casting
Casting is a Java language feature that allows us to alter the compile-time type of a variable. The runtime type is not altered because of a cast.
We can explicitly cast to a compile-time type using (T) o
or we can implicitly cast using subtype polymorphism.
Lets analyze the following code snippets with regards to casting.
Posn origin = new Posn(0, 0);
Shape s1 = new Cicle(origin, 5); (1)
Circle c1 = new Circle(origin, 5);
Shape s2 = (Shape) c1; (2)
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Circle that = (Circle) o; (3)
// elided code
}
1 | We have seen similar lines like this one before. The compile-time type of s1 is Shape . The right-hand side of = creates a Circle . The compiler
under the covers is actually casting the value created by new Circle(origin, 5) into a Shape . This is valid since Circle implements Shape . |
2 | This and the preceding line in the code are equivalent to what the first line does. When we cast from a subclass to a superclass (or interface) upcasting, since we are moving up in the class hierarchy. |
3 | On this line we know that o has the same runtime type as our current object (see the if statement and the calls to getClass() ). We thus know that at runtime
if evaluation reaches this line, the if statement was false (both sub-expressions are false ). Thus the specific o that we were given is the same runtime-type
as our current object. Because o is the same runtime type as the current object it is safe to cast to Circle . We call such casts down casts because we are moving
down the class hierarchy. |
Down casts are dangerous and we have to write code to ensure that our down cast is safe. If we do not then we run the risk of trying to change the compile-time type of an object to some type that is not compatible. For example
public boolean equals(Object o) {
//if (o == null || getClass() != o.getClass()) return false;
Circle that = (Circle) o;
(1)
// elided code
}
We have commented out the if
-statement. What will happen if we pass an o
to this equals()
method whose runtime type is String
?
java.lang.ClassCastException: java.lang.String cannot be cast to Circle
The runtime complains. For down casts, the JVM checks at runtime if our down cast is valid according to the same rules the compiler uses at compile time.
A String
is not a Circle
and the JVM exits with an error message.
Down casts are necessary for some cases. For our class and your designs you should not use down casts at all.
The only exception is |
Single Responsibility
Recall from CS5001 our rule
One task one function
The rule is still relevant for Java
One task one method
We have another rule that refers to classes
One class one responsibility
After mapping our information to data and in Java’s case classes and interfaces, we aim to add methods to our classes and interfaces based on the responsibility that we have assigned to our class.
Let’s consider out code for Shapes
again
One way to implement moveX()
in the concrete classes that implement Shape
could be
// In Class Circle
public Shape moveX(Integer dx) {
return new Circle(new Posn(this.pin.getX() + dx), this.pin.getY()), this.radius);
}
// In Class Square
public Shape moveX(Integer dx) {
return new Square(new Posn(this.pin.getX() + dx), this.pin.getY()), this.side);
}
// In Class Rectangle
public Shape moveX(Integer dx) {
return new Rectangle(new Posn(this.pin.getX() + dx), this.pin.getY()), this.width, this.height);
}
Observe
-
The code is very similar
-
The code is essentially updating a value, the
x
coordinate ofPosn
, inside a class that is notPosn
.
Who is responsible for manipulating x
and y
?
Posn
should be the entity responsible for updating any data that we captured as being part of Posn
.
Here is another implementation
//In Class Posn
public Posn moveX(Intefer dx) {
return new Posn(this.x + dx, this.y);
}
// In Class Circle
public Shape moveX(Integer dx) {
return new Circle(this.pin.moveX(dx), this.radius);
}
// In Class Square
public Shape moveX(Integer dx) {
return new Square(this.pin.moveX(dx), this.side);
}
// In Class Rectangle
public Shape moveX(Integer dx) {
return new Rectangle(this.pin.moveX(dx), this.width, this.height);
}
And here is the updated UML Class Diagram
The code is similar for moveY()
. Now any class that contains a Posn
can leverage our new methods.
Here is a new rule, the Law of Demeter [1]
Only talk to your friends
For a method m
inside a class C
your friends are
-
fields defined/inherited in the class
C
-
arguments of
m
-
methods defined/inherited in
C
-
methods available on the arguments of
m
-
methods available on the fields inherited/defined in
C
-
local values created in the body of
m
-
methods available on local values created in the body of
m
Design Recipe
-
Data Analysis
-
Data Examples
-
Template
-
-
Signature
-
Purpose
-
Examples
-
Function Definition
-
Tests
-
Review
Nothing has changed about the Design Recipe. What has changed is the outcome of each step since we are now going to create Java programs instead of Racket programs.
Racket
;;;; Data Analysis and Definitions:
(define-struct student (last first teacher))
;; A student is a (make-student Symbol Symbol Symbol)
;; INTERP: represents a student's first and last name
;; and the name of the their teacher
;;;; Data Examples:
;; (make-student 'Joe 'Doe 'Mary)
;; (make-student 'Mat 'Jones 'Fritz)
;;;; Template:
;; student-fn: Student -> ???
;; (define (student-fn a-student a-teacher)
;; ... (student-last a-student) ...
;; ... (student-first a-student) ...
;; ... (student-teacher a-student) ...)
;;;; Signature: subst-teacher : Student Symbol -> Student
;;;; Purpose: to create a student structure with a new
;;;; teacher name if the teacher's name matches 'Fritz
;;;; Examples:
;; (subst-teacher (make-student 'Mat 'Jones 'Fritz) 'Elise)
;; =
;; (make-student 'Mat 'Jones 'Elise)
;; (subst-teacher (make-student 'Joe 'Doe 'Mary) 'Elise)
;; =
;; (make-student 'Joe 'Doe 'Mary)
;;;; Function Definition:
(define (subst-teacher a-student a-teacher)
(cond
[(symbol=? (student-teacher a-student) 'Fritz)
(make-student (student-last a-student)
(student-first a-student)
a-teacher)]
[else a-student]))
;;;; Tests:
(check-expect (subst-teacher (make-student 'Mat 'Jones 'Fritz) 'Elise)
(make-student 'Mat 'Jones 'Elise))
(check-expect (subst-teacher (make-student 'Joe 'Doe 'Mary) 'Elise)
(make-student 'Joe 'Doe 'Mary))
Java
Data Analysis
We are trying to map information to data. Instead of define-struct
we are going to design Java classes.
Create a UML class diagram to capture
-
Java classes that you are going to create
-
Fields for each class that you need
-
Methods for each class that you need
-
Dependencies between classes; arrows that show relationships between classes.
For example recall the Author
example from Lab1
Data Examples
Java code that creates instance of your classes. This code ends up in
your setup()
method inside your Test
class (e.g., AuthorTest
,
EmailTest
, AddressTest
).
Templates
The goal of the template it do decompose your data. At this point data in Java means a class. So we need a template for a class that, hopefully, all class methods can use as a starting point. The contents of the template is a list of operations and fields that we can rely on. It is in fact the same list as the list of "friends" as we have defined it for the Law of Demeter.
So lets take Author
template()
contains all the elements (fields and methods) that we can access.
Observer that the template above does not include equals(Object o)
. The template that we have above
concerns only the Author
class. This template has higher chances of being used for methods that do not
take any argument.
We write a different template for every possible combination of argument types e.g.,
Observe that template(Author a)
can call any method on a
because we are still within Author
and
have access to all (private
, public
and protected
) fields and methods.
The third template template(Email e)
can only call public methods on e
which is of type Email
.
We write a template for each combination of list of argument types that we plan to use in order to implement a method in our class.
Signature
The signature that we used to write for each Racket function is now embedded into the method signature for Java. The types that we have to include as part of the Java method’s signature are essentially the signature that we used to write as comments in Racket.
public class MyInt {
private Integer val;
public MyInt(Integer val) {
this.val = val;
}
// signature : getVal() : -> Integer
public Integer getVal() {
return this.val;
}
// signature : greaterThan: Integer -> Boolean
public Boolean greaterThan(Integer other) {
return this.val > other.getVal();
}
We do have to remember however that Java methods are inside a class and we always has access to the this
class. In Racket
we had to pass a value of our struct
as an argument, in Java we are already inside the instance of the class.
Another way to visualizing this property is to assume an invinisble first argument that has the type of the class
that defines this method and it is called this
and that Java auto-populates each call with the right object for the
first argument called this
.
e.g.,
this
as an argument to each method.public class MyInt {
private Integer val;
public MyInt(MyInt this, Integer val) {
this.val = val;
}
// signature : getVal() : MyInt -> Integer
public Integer getVal(MyInt this) {
return this.val;
}
// signature : greaterThan: MyInt Integer -> Boolean
public Boolean greaterThan(MyInt this, Integer other) {
return this.val > other.getVal();
}
Purpose
Similar to Racket only in Java we need to write our purpose using Javadoc, which means that we have
-
A one sentence description of what this method does.
-
Any
WHERE
clauses that the method requires -
Any
INVARIANTS
,HALTING MEASURES
,TERMINATION ARGUMENTS
that the method requires -
A one line comment for each formal argument using
@param
-
A one line comment explaining the method’s return value using
@return
.
Examples
Examples in Java are written as part of the Javadoc comment for the method so that when we generate our documentation and provide it to clients they will have example uses for each method.
Function Definition (now Method Definition)
The name is misleading for Java since we do not have function but methods. So we will rename this step to Method Definition.
Tests
In Java our tests are found in a companion JUnit class. Instead of check-expect
and its variants we use the JUnit Assert
class along with its assertion
methods.
UML Sequence Diagrams
UML Sequence Diagrams are used to model the flow of execution within your system. Sequence Diagrams are typically used to model:
-
Usage scenarios. A sequence of operations that your code goes through.
-
The logic of methods. Designate the steps taken during the excution of a method including the objects called, arguments passed etc.
-
The logic of a component/service. Similar to methods but the entities used can be a component, a web service etc.
We will mostly use Sequence Diagrams to show the exectuion of our methods.
Consider the addition of the following method in Author
...
/**
* Return the author's first name
*
* @return the author's first name
*/
public String getFirstName() {
return this.person.getFirst();
}
...
With the following code in our Test class
...
Author a1 = new Author(new Person("John", "Smith"), ...); // elided arguments
a1.getFirstName();
...
Here is the Sequence Diagrams for this getFirstName()
.
-
Sequence Diagrams are graphs with the Y-axis being time. Time moves from top to bottom.
-
Each object appears as a line with the same header and footer. [2] The header and footer contain
-
the object’s bound variable if any, if there is no bound variable then leave this blank
-
a colon
:
-
the object’s runtime type
-
-
A method call is an arrow from left to right where
-
the arrow originates from the object that makes the method call
-
the arrow terminates on the object that receives the method call
-
the arrow is labeled with the method name and its arguments
-
-
A method return is a dotted arrow from right to left where [3]
-
the arrow originates from the object returning a value
-
the arrow terminates on the object that receives the return value
-
the arrow can be labelled with the return value
-
-
Also notice that on the line that represents an object we see vertical rectangles. These rectangles denote the execution time of a method. Think of them as representing the method’s body.
Recursive Data
Recall Racket Lists
Using an itemization, structs and a self reference.
;; +-------------+
;; \/ |
;; A List of Number (LoN) is one of |
;; - empty |
;; - (cons Number LoN) --------------+
;; Template
;; (define (lon-fn alon)
;; (cond
;; [(empty? alon) ...]
;; [(cons? alon) ... (first alon) ...
;; ... (lon-fn (rest alon)) ...]
;;
;;; Signature:
;;lon-size: LoN -> Number
;;;; Purpose
;; Given a list of numbers return the number of elements in the list.
;; +---------------+
;; \/ ;; |
(define (lon-size alon) ;; |
(cond ;; |
[(empty? alon) 0] ;; |
[(cons? alon) (+ 1 (lon-size (rest alon)))]))
Now in Java
Same idea, itemization, classes and a self reference
Adding a size()
operation will require the following modifications
-
Add method
Integer size()
onList
-
Implement
size()
in each class that implementsList
, i.e.,Empty
andCons
Here is the updated UML diagram
And here is the Java code
/**
* Represents a List of Integers
*/
public interface List {
/**
* Returns the total number of elements in the list.
*
* @return number of elements in this list
*/
Integer size(); (1)
/**
* Returns true if empty and false otherwise
*
*/
Boolean isEmpty();
/**
* Given a new element {@code element} prepend it to this list
*
* @param element new element to add to the list
* @return updated list with {@code element} prependeds
*/
List add(Integer element);
/**
* Return the last element of this list.
*
* @return the last element of this list.
*/
Integer last();
}
/**
* Represents the empty list of integers.
*/
public class Empty implements List {
@Override
public String toString() {
return "Empty{}";
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
else return true;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return 42;
}
@Override
public Integer size() { (2)
return 0; (3)
}
@Override
public Boolean isEmpty() {
return true;
}
@Override
public List add(Integer element) {
return new Cons(element, this);
}
@Override
public Integer last() throws InvalidCallException {
throw new InvalidCallException("Called last() on empty!");
}
}
/**
* Represents a non-emty list of integers
*
*/
public class Cons implements List {
private Integer first;
private List rest;
/**
* Given an integer and a list create a new list with the
* same elements as {@code rest} and with {@code first} prepended.
*
* @param first new element to add to the beginning of the list
* @param rest the list we are going to use to add our new element
*/
public Cons(Integer first, List rest) {
this.first = first;
this.rest = rest;
}
/**
* Getter for property 'first'.
*
* @return Value for property 'first'.
*/
public Integer getFirst() {
return first;
}
/**
* Getter for property 'rest'.
*
* @return Value for property 'rest'.
*/
public List getRest() {
return rest;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Cons cons = (Cons) o;
if (first != null ? !first.equals(cons.first) : cons.first != null) return false;
return rest != null ? rest.equals(cons.rest) : cons.rest == null;
}
@Override
public String toString() {
return "Cons{" +
"first=" + first +
", rest=" + rest +
'}';
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (rest != null ? rest.hashCode() : 0);
return result;
}
@Override
public Integer size() { (4)
return 1 + this.getRest().size(); (5)
}
@Override
public Boolean isEmpty() {
return false;
}
@Override
public List add(Integer element) {
return new Cons(element, this);
}
@Override
public Integer last() {
if (this.rest.isEmpty()) {
return this.first;
} else {
return this.rest.last();
}
}
}
1 | First we add the appropriate method signature on the List interface |
2 | We implement the method size() inside Empty |
3 | In the empty case the size of the list is 0 |
4 | We implement the method size() inside Cons |
5 | In the non-empty case we add 1 to the size of the rest of the list. The rest of the list
is the field rest inside Cons so we call size() on that object. |
So lets create an example in Java of a list with 3 elements. We will name the intermediary lists so that we can then refer to them in our diagrams.
// We could have written
//
// List threeElement = new Cons(10, new Cons(20, new Cons(10, new Empty())));
//
// We use a different variable to assign each created object for demonstration purposes and for
// allowing us to map 1-1 our code to our sequence diagram
List empty = new Empty();
List oneElement = new Cons(10, empty);
List twoElement = new Cons(20, oneElement);
List threeElement = new Cons(30, twoElement);
Integer threeElementSize = threeElement.size();
And here is the sequence diagram for the call to size
in the preceding
code snippet.
Observe that the bodies of the methods inside the concrete classes contain similar
code to the right-hand side expression in our cond clauses.
|
Let’s also add the following methods
-
a method called
Boolean isEmpty()
that returnstrue
if our list is empty andfalse
otherwise -
a method called
List add(Integer num)
that addsnum
to the beginning of the list. -
a method called
Integer last()
that returns the last element of the list -
a method called
List addToEnd(Integer element)
that adds the given element to the end of the list -
a method called
Integer elementAt(Integer index)
that returns the element at indexindex
Java Packages
Java allows you to organize your Java classes into groups. The grouping is similar (or inspired) to how you group files on your computer using folders.
The Java language provides the keyword package
that we can use to create a package and provide a name. For example if we wanted to keep all of our code for lists of integers into a package called integerlist
we add the following line at the top of each .java
file
package integerlist;
/**
* Represents a List of Integers
*/
public interface List {
/**
* Returns the total number of elements in the list.
*
* @return number of elements in this list
*/
Integer size(); (1)
/**
* Returns true if empty and false otherwise
*
*/
Boolean isEmpty();
/**
* Given a new element {@code element} prepend it to this list
*
* @param element new element to add to the list
* @return updated list with {@code element} prependeds
*/
List add(Integer element);
/**
* Return the last element of this list.
*
* @return the last element of this list.
*/
Integer last();
}
The naming convention for Java packages is to keep all names lowercase. In order to refer to the List
interface using it’s full qualified name we write
-
integerlist.List
We can also created nested packages, just like you can create nested folders. We separate nested packages with a .
, e.g.,
package edu.neu.ccs.cs5004.lecture3.integerlist;
/**
* Represents a List of Integers
*/
public interface List {
/**
* Returns the total number of elements in the list.
*
* @return number of elements in this list
*/
Integer size(); (1)
/**
* Returns true if empty and false otherwise
*
*/
Boolean isEmpty();
/**
* Given a new element {@code element} prepend it to this list
*
* @param element new element to add to the list
* @return updated list with {@code element} prependeds
*/
List add(Integer element);
/**
* Return the last element of this list.
*
* @return the last element of this list.
*/
Integer last();
}
When we create a packages the Java language requires that your save your .java
in a folder structure that *reflects your package’s name and nesting. So the preceding nested package example must have the following folder structure
edu
└── neu
└── ccs
└── cs5004
└── lecture3
└── List.java
If you use your IDE to create a package (by selecting New → Package instead of New → Class the IDE will create the necessary folder structure).
We use fully qualified names when we use Java’s import
to include code from other libraries. By default java.lang
is always imported so we can omit importing java.lang
. Recall that the Java Documentation as well as your documentation uses package names to organize your Javadoc’s generated HTML pages.
Accessing Class properties
Here is a table that explains what can be accessed from which location (class, package, world) for each Java modifier (public
, private
, protected
and default) [4]
Access Modifier | Same Class | Same Package | Subclass | Other Packages |
---|---|---|---|---|
|
Y |
Y |
Y |
Y |
|
Y |
Y |
Y |
N |
|
Y |
N |
N |
N |
default |
Y |
Y |
|
N |
In this course avoid using default. |
Exceptions
An exception is an event that occurs during execution of a program and disrupts the normal flow of execution.
Three kinds of exceptions
-
Checked Exceptions capture events that a well-written application must anticipate and recover from.
-
opening a file using the file name provided by the user and cannot find that file
-
opening a connection to a server whose address was provided to the program by an external entity
-
-
Errors capture exceptional conditions external to the application that the application cannot anticipate or recover from.
-
error reading from the disk
-
error reading from the connection to the database
-
-
Runtime Exception captures events that are exceptional and internal to the application. The application typically cannot anticipate them or recover from
-
passing in the incorrect values for the arguments to a function
-
logical errors; going over the length of a list
-
Java’s exception hierarchy dictates the kind of exception (checked, runtime or error).
Java provides special syntax to mark code that could throw an exception as well as catching an exception. Let’s look at an example.
/**
* Represents an exception thrown when an invalid value is given for radius
*/
public class InvalidRadiusException extends RuntimeException {
/**
* {@inheritDoc}
*/
public InvalidRadiusException(String message) {
super(message);
}
}
Since our InvalidRadiusException
extends RuntimeException
it is not a checked exception. It is a runtime exception.
We can then throw this exception when appropriate in our Circle
's constructor method using the
Java keyword throw
and passing an object of our exception.
public class Circle extends AbstractShape {
/**
* Given a pin and a radius greater than 0, creates a circle
*
* @param pin the location of this circle's pin
* @param radius this circle's radius. The radius must be greater than 0
public Circle(Posn pin, Integer radius) {
super(pin);
if (radius <= 0) {
throw new InvalidRadiusException("Radius must be > 0, given: " + radius);
}
this.radius = radius;
}
// elided code
}
Code that calls the constructor of Circle
can optionally catch the exception and accordingly do something about this abnormal case. For example one possibility would be
class StickFigure {
private Circle head;
private Body body;
public StickFigure(Integer head, Integer body) {
try {
this.head = new Circle(new Pin(0,0), head);(1)
} catch (InvalidRadiusException invalidRadius) {
String errorMessage =(2)
"We detected an incorrect value " +
"for the stick figure's head. " +
"Please provide a positive number.";
new Window(errorMessage).exit();
}
this.body = new Body(body);
}
}
1 | Code that is calling a method that can throw an exception can try to call the method. If there is not exception because of the execution of the code in the try -block then Java jumps over the catch -block and continues as normal |
2 | if the execution of the code inside the try -block does throw an exception then Java will check each catch statement to see if there type of the thrown exception matches the argument of the catch -block. If it does, then run the code inside the catch -block and exit. |
If there is no catch
-block that matches the thrown exception, Java will traverse all the callers that lead to the execution of this code. If none of the callers deal with the thrown exception the JVM will exit and print out the details of the thrown exception on your Console.
Java allows us to have more than one catch
-blocks.
try {
// try to run these expressions/statements that may throw an exception
} catch (ExceptionType ext) {
// catch an exception of type ExceptionType, bind it to the variable ext
// perform actions to recover
} catch (ExceptionType2 ext2) {
// catch an exception of type ExceptionType2, bind it to the variable ext2
// perform actions to recover
} catch (IOException | SQLException ex) {
// catching multiple exceptions for which we deal with in the same
// way
}
In the case where your exception is a checked exception, any method that might throw the checked exception must [5] declare it’s intend to do so in the method header.
Syntax to declare that a method can throw an exception (of any kind)
/**
* ...
* @return ...
* @throws RuntimeException when we receive a runtime exception from our helper method that sets up the connection
* @throws IllegalArgumentException in the case when this object is not properly initialized
* @throws NetworkException in the case we fail to connect to the server
*/
public Integer method() throws RuntimeException, ArgumentException, NetworkException {
}
A method can throw zero, one or more exceptions. Javadoc also allows us to document for each exception the conditions that will cause our code to throw the specific exception.
Always create your own exceptions. Do not rely on the String message allowed by
the already defined exceptions.
|
Testing methods that throw exceptions in JUnit
JUnit can be used to check your methods even under conditions that throw exceptions.
To tell JUnit that you expect an exception to the thrown we add to the @Test
annotation
the expected exception
@Test(expected=MyException.class)
public void testElementAt() throws Exception {
//call the method in a way that causes an the expected exception to be thrown
}
Let’s add a test for our Circle
class that will validate the use case when we pass
a value for radius
that is not a positive integer.
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class CircleTest {
private Circle incorrectCircle;
private Circle circleRadius10;
@Before
public void setUp() throws Exception {
// this.incorrectCircle = new Circle(new Posn(1,1),-1);
this.circleRadius10 = new Circle(new Posn(10,10), 10);
}
@Test
public void testGetRadius() throws Exception {
Assert.assertEquals(this.circleRadius10.getRadius(), new Integer(10));
}
@Test(expected=InvalidRadiusException.class)
public void testInvalidRadius() throws Exception {
this.incorrectCircle = new Circle(new Posn(0,0), -1);
}
}
Also let’s see what happens if we call Circle
's constructor without using a try
-catch
block.
We create a simple class UseCircle
with only one method.
/**
* Created by therapon on 5/24/16.
*/
public class UseCircle {
public Circle createAnInvalidCircle(){
return new Circle(new Posn(0,0), -1);
}
}
and a corresponding test for UseCircle
called UseCircleTest
import org.junit.Before;
import org.junit.Test;
public class UseCircleTest {
private UseCircle uc;
@Before
public void setUp() throws Exception {
this.uc = new UseCircle();
}
@Test
public void createAnInvalidCircle() throws Exception {
uc.createAnInvalidCircle();
}
}
And here is the output on our Console when we run the tests inside UseCircleTest
InvalidRadiusException: Radius must be > 0, given: -1
at Circle.<init>(Circle.java:10)
at UseCircle.createAnInvalidCircle(UseCircle.java:8)
at UseCircleTest.createAnInvalidCircle(UseCircleTest.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)