๐ Can explain error handling
Well-written applications include error-handling code that allows them to recover gracefully from unexpected errors. When an error occurs, the application may need to request user intervention, or it may be able to recover on its own. In extreme cases, the application may log the user off or shut down the system. --(source)
๐ Can explain exceptions
Exceptions are used to deal with 'unusual' but not entirely unexpected situations that the program might encounter at run time.
Exception:
An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program's instructions. โ- Java Tutorial (Oracle Inc.)
๐ฆ Examples:
๐ Can do exception handling in code
Most languages allow a method to encapsulate the unusual situation in an Exception object and 'throw' that object so that another piece of code can 'catch' it and deal with it. This is especially useful when code segment that encountered the unusual situation does not know how to deal with it.
Exception objects can propagate up the method call hierarchy until it is dealt with. Usually, an exception thrown by a method is caught by the caller method. If the called method does not know how to deal with the exception it caught, it will throw the Exception object to its own caller. If none of the callers is prepared to deal with the exception, the exceptions can propagate through the method call stack until it is received by the main method and thrown to the runtime, thus halting the system.
๐ฆ In the code given below, processArray
can potentially throw an InvalidInputException
. Because of that, processInput
method invokes processArray
method inside a try{ }
block and has a catch{ }
block to specify what to do if the exception is actually thrown.
Advantages of exception handling in this way:
Which are benefits of exceptions?
(a) (c) (d) (e)
Explanation: Exceptions cannot prevent problems in the environment. They can only be used to handle and recover from such problems.
๐ Can avoid using exceptions to control normal workflow
In general, use exceptions only for 'unusual' conditions. Use normal return
statements to pass control to the caller for conditions that are 'normal'.
๐ Can explain assertions
Assertions are used to define assumptions about the program state so that the runtime can verify them. An assertion failure indicates a possible bug in the code because the code has resulted in a program state that violates an assumption about how the code should behave.
๐ฆ An assertion can be used to express something like when the execution comes to this point, the variable v
cannot be null.
If the runtime detects an assertion failure, it typically take some drastic action such as terminating the execution with an error message. This is because an assertion failure indicates a possible bug and the sooner the execution stops, the safer it is.
๐ฆ In the Java code below, suppose we set an assertion that timeout
returned by Config.getTimeout()
is greater than 0
. Now, if the Config.getTimeout()
returned -1
in a specific
execution of this line, the runtime can detect it as a assertion failure -- i.e. an assumption about the expected behavior of the code turned out to be wrong which could potentially be the result of a bug -- and take some drastic
action such as terminating the execution.
int timeout = Config.getTimeout();
๐ Can use assertions
Use the assert
keyword to define assertions.
๐ฆ This assertion will fail with the message x should be 0
if x
is not 0 at this point.
x = getX();
assert x == 0 : "x should be 0";`
...
Assertions can be disabled without modifying the code.
๐ฆ java -enableassertions HelloWorld
(or java -ea HelloWorld
) will run HelloWorld
with assertions enabled while java -disableassertions HelloWorld
will run it without verifying assertions.
Java disables assertions by default. This could create a situation where you think all assertions are being verified as true
while in fact they are not being verified at all. Therefore, remember to enable
assertions when you run the program if you want them to be in effect.
๐ก Enable assertions in Intellij (how?) and get an assertion to fail temporarily (e.g. insert an assert false
into the code temporarily) to confirm assertions are being verified.
Java asset
vs JUnit assertions: They are similar in purpose but JUnit assertions are more powerful and customized for testing. In addition, JUnit assertions are not disabled by default. We recommend you use
JUnit assertions in test code and Java assert
in functional code.
Tutorials:
Best practices:
๐ Can use assertions optimally
It is recommended that assertions be used liberally in the code. Their impact on performance is considered low and worth the additional safety they provide.
Do not use assertions to do work because assertions can be disabled. If not, your program will stop working when assertions are not enabled.
๐ฆ The code below will not invoke the writeFile()
method when assertions are disabled. ย If that method is performing some work that is necessary for your program, your program will not work correctly when assertions are disabled.
...
assert writeFile() : "File writing is supposed to return true";
Assertions are suitable for verifying assumptions about Internal Invariants, Control-Flow Invariants, Preconditions, Postconditions, and Class Invariants. Refer to [Programming with Assertions (second half)] to learn more.
Exceptions and assertions are two complementary ways of handling errors in software but they serve different purposes. Therefore, both assertions and exceptions should be used in code.
A Calculator program crashes with an โassertion failureโ message when you try to find the square root of a negative number.
(c)
Explanation: An assertion failure indicates a bug in the code. (b) is not acceptable because of the word "terminated". The application should not fail at all for this input. But it could have used an exception to handle the situation internally.
Which statements are correct?
(a)
๐ Can explain logging
Logging is the deliberate recording of certain information during a program execution for future reference. Logs are typically written to a log file but it is also possible to log information in other ways ย e.g. into a database or a remote server.
Logging can be useful for troubleshooting problems. A good logging system records some system information regularly. When bad things happen to a system ย e.g. an unanticipated failure, their associated log files may provide indications of what went wrong and action can then be taken to prevent it from happening again.
๐ก A log file is like the
Why is logging like having the 'black box' in an airplane?
(a)
๐ Can use logging
Most programming environments come with logging systems that allow sophisticated forms of logging. They have features such as the ability to enable and disable logging easily or to change the logging
๐ฆ This sample Java code uses Javaโs default logging mechanism.
First, import the relevant Java package:
import java.util.logging.*;
Next, create a Logger
:
private static Logger logger = Logger.getLogger("Foo");
Now, you can use the Logger
object to log information. Note the use of
WARNING
so that log messages specified as INFO
level (which is a lower level than WARNING
) will not
be written to the log file at all.
// log a message at INFO level
logger.log(Level.INFO, "going to start processing");
//...
processInput();
if(error){
//log a message at WARNING level
logger.log(Level.WARNING, "processing error", ex);
}
//...
logger.log(Level.INFO, "end of processing");
Tutorials:
Best Practices:
๐ Can explain defensive programming
A defensive programmer codes under the assumption "if we leave room for things to go wrong, they will go wrong". Therefore, a defensive programmer proactively tries to eliminate any room for things to go wrong.
๐ฆ Consider a MainApp#getConfig()
a method that returns a Config
object containing configuration data. A typical implementation is given below:
class MainApp{
Config config;
/** Returns the config object */
Config getConfig(){
return config;
}
}
If the returned Config object is not meant to be modified, a defensive programmer might use a more defensive implementation given below. ย This is more defensive because even if the returned Config
object is modified (although it is not meant to be) it will not affect the config
object inside the MainApp
object.
/** Returns a copy of the config object */
Config getConfig(){
return config.copy(); //return a defensive copy
}
๐ Can use defensive coding to enforce compulsory associations
Consider two classes, Account
and Guarantor
, with an association as shown in the following diagram:
Example:
Here, the association is compulsory i.e. an Account
object should always be linked to a Guarantor
. One way to implement this is to simply use a reference variable, like this:
class Account {
Guarantor guarantor;
void setGuarantor(Guarantor g) {
guarantor = g;
}
}
However, what if someone else used the Account
class like this?
Account a = new Account();
a.setGuarantor(null);
This results in an Account
without a Guarantor
! In a real banking system, this could have serious consequences! The code here did not try to prevent such a thing from happening. We can make the code more defensive
by proactively enforcing the multiplicity constraint, like this:
class Account {
private Guarantor guarantor;
public Account(Guarantor g){
if (g == null) {
stopSystemWithMessage("multiplicity violated. Null Guarantor");
}
guarantor = g;
}
public void setGuarantor (Guarantor g){
if (g == null) {
stopSystemWithMessage("multiplicity violated. Null Guarantor");
}
guarantor = g;
}
โฆ
}
For the Manager
class shown below, write an addAccount()
method that
import java.util.*;
public class Manager {
private ArrayList< Account > theAccounts ;
public void addAccount(Account acc) throws Exception {
if (theAccounts.size( ) == 8){
throw new Exception ("adding more than 8 accounts");
}
if (!theAccounts.contains(acc)) {
theAccounts.add(acc);
}
}
public void removeAccount(Account acc) {
theAccounts.remove(acc);
}
}
Implement the classes defensively with appropriate references and operations to establish the association among the classes. Follow the defensive coding approach. Let the Marriage
class handle setting/removal of reference.
public class Marriage {
private Man husband = null;
private Woman wife = null;
// extra information like date etc can be added
public Marriage(Man m, Woman w) throws Exception {
if (m == null || w == null) {
throw new Exception("no man/woman");
}
if (m.isMarried() || w.isMarried()) {
throw new Exception("already married");
}
husband = m;
m.enterMarriage(this);
wife = w;
w.enterMarriage(this);
}
public Man getHusband() throws Exception {
if(husband == null) {
throw new Exception("error state");
} else {
return husband;
}
}
public Woman getWife() throws Exception {
if(wife == null) {
throw new Exception("error state");
} else {
return wife;
}
}
// removal of both ends of 'Marriage'
public void divorce() throws Exception {
if (husband==null || wife==null) {
throw new Exception("no marriage");
}
husband.removeFromMarriage(this);
husband = null;
wife.removeFromMarriage(this);
wife = null;
}
}
Give a suitable defensive implementation to the Account
class in the following class diagram. Note that โ{immutable}โ means once the association is formed, it cannot be changed.
class Account {
private Guarantor myGuarantor; // should not be public
public Account(Guarantor g){
if (g==null) {
haltWithErrorMessage(โAccount must have a guarantorโ);
}
myGuarantor = g;
}
// there should not be a setGuarantor method
}
class City{
Country country;
void setCountry(Country country){
this.country = country;
}
}
This is a defensive implementation of the association.
False
Explanation: While the design requires a City to be connected to exactly one Country, the code allows it to be connected to zero Country objects (by passing null to the setCountry() method).
๐ Can use defensive coding to enforce 1-to-1 associations
Consider the association given below. A defensive implementation requires to ensure a MinedCell
cannot exist without a Mine
and vice versa which requires simultaneous object creation. However, Java can only create
one object at a time. Given below are two alternatives implementations, both of which violate the multiplicity for a short period of time.
Option 1:
class MinedCell {
private Mine mine;
public MinedCell(Mine m){
if (m == null) {
showError();
}
mine = m;
}
โฆ
}
Option 1 forces us to keep a Mine
without a MinedCell
(until the MinedCell
is created).
Option 2:
class MinedCell {
private Mine mine;
public MinedCell(){
mine = new Mine();
}
โฆ
}
Option 2 is more defensive because the Mine
is immediately linked to a MinedCell
.
๐ Can use defensive coding to enforce referential integrity of bi-directional associations
A bidirectional association in the design (shown in (a)) is usually emulated at code level using two variables (as shown in (b)).
class Man {
Woman girlfriend;
void setGirlfriend(Woman w) {
girlfriend = w;
}
โฆ
}
class Woman {
Man boyfriend;
void setBoyfriend(Man m) {
boyfriend = m;
}
}
The two classes are meant to be used as follows:
Woman jean;
Man james;
โฆ
james.setGirlfriend(jean);
jean.setBoyfriend(james);
Suppose the two classes were used this instead:
Woman jean; Man james, yong;
โฆ
james.setGirlfriend(jean);
jean.setBoyfriend(yong);
Now James' girlfriend is Jean, while Jean's boyfriend is not James. This situation results as the code was not defensive enough to stop this "love triangle". In such a situation, we say that the referential integrity has been violated. It simply means there is an inconsistency in object references.
One way to prevent this situation is to implement the two classes as shown below. Note how the referential integrity is maintained.
public class Woman {
private Man boyfriend;
public void setBoyfriend(Man m) {
if(boyfriend == m){
return;
}
if (boyfriend != null) {
boyfriend.breakUp();
}
boyfriend = m;
m.setGirlfriend(this);
}
public void breakUp() {
boyfriend = null;
}
...
}
public class Man{
private Woman girlfriend;
public void setGirlfriend(Woman w) {
if(girlfriend == w){
return;
}
if (girlfriend != null) {
girlfriend.breakUp();
}
girlfriend = w;
w.setBoyfriend(this);
}
public void breakUp() {
girlfriend = null;
}
...
}
When the code james.setGirlfriend(jean)
is executed, the code ensures that james
break up with any current girlfriend before he accepts jean
as the girlfriend. Furthermore, the code ensures that jean
breaks up with any existing boyfriends and accepts james
as the boyfriend.
Imagine that we now support the following feature in our Minesweeper game.
Feature ID: multiplayer
Description: A minefield is divided into mine regions. Each region is assigned to a single player. Players can swap regions. To win the game, all regions must be cleared.
Given below is an extract from our class diagram.
Minimally, this can be implemented like this.
class Player{
Region region;
void setRegion(Region r) {
region = r;
}
Region getRegion() {
return region;
}
}
// Region class is similar
However, this is not very defensive. For example, a user of this class can pass a null
to either of the methods, thus violating the multiplicity of the relationship.
Implement the two classes using a more defensive approach. Take note of the bidirectional link which requires us to preserve referential integrity at all times.
In this solution, we assume Regions
can be created without Players
(note that we cannot be 100% defensive all the time). The usage will be something like this:
Region r1 = new Region();
Player p1 = new Player(r1);
Region r2 = new Region();
Player p2 = new Player(r2);
p1.setRegion(r2);
r1.setPlayer(p2);
Here are the two classes. Get methods are omitted as they are simple. Note how much extra effort we need to be defensive.
public class Region {
private Player myPlayer;
public Region() {
// initialise region
}
public void setPlayer(Player newPlayer) {
if (newPlayer == null) {
stopSystemWithErrorMessage("Multiplicity violation");
}
if (myPlayer == newPlayer) {
return; // same player
}
if (myPlayer != null) {
// I already have a Player!
myPlayer.removeRegion(this);
}
myPlayer = newPlayer;
// set the reverse link
myPlayer.setRegion(this);
}
public void removePlayer(Player disconnectingPlayer) {
if (myPlayer == disconnectingPlayer){
myPlayer = null;
} else {
stopSystemWithErrorMessage("Unknown Player trying to disconnect");
}
}
private void stopSystemWithErrorMessage(String msg) {
...
}
}
public class Player {
private Region myRegion;
public Player(Region region) {
setRegion(region);
}
public void setRegion(Region newRegion) {
if (newRegion == null) {
stopSystemWithErrorMessage("Multiplicity violation");
}
if (myRegion == newRegion) {
return; // no change in Region!
}
if (myRegion != null) {
// previous region exists
myRegion.removePlayer(this);
}
myRegion = newRegion;
// set the reverse link
myRegion.setPlayer(this);
}
public void removeRegion(Region disconnectingRegion) {
if (myRegion == disconnectingRegion) {
myRegion = null;
}
}
private void stopSystemWithErrorMessage(String msg) {
...
}
}
Note that the above code stops the system when the multiplicity is violated. Alternatively, we can throw an exception and let the caller handle the situation.
Implement this bidirectional association. Note that the Bank
uses accNumber
attribute to uniquely identify an Account
object. Assume the Bank
class is responsible for maintaining
the links between objects.
The code below contains a method in the Bank
class to create an account; the bank field in the new account is thereby filled by the bank creating it.
We assume that once an Account
has been assigned to one Bank
, it cannot be assigned to a different Bank
. Once the Account
is removed from the Bank
, it will not
be used any more (hence, no need to remove the link from Account
to Bank
).
public class Account {
private int accNumber ;
private Bank theBank ;
public Account(int n, Bank b) {
accno = n ;
theBank = b ;
}
public int getNumber() {
return accNumber;
}
public Bank getBank() {
return theBank ;
}
}
import java.util.*;
public class Bank {
private HashMap< Integer, Account > theAccounts = new HashMap < Integer, Account > ();
public void createAccount(int n) {
addAccount(new Account(n, this)) ;
}
public void addAccount(Account a) {
theAccounts.put(a.getNumber(), a);
}
public void removeAccount(int accNumber) {
theAccounts.remove(accNumber);
}
public Account lookupAccount(int accNumber) {
return theAccounts.get(accNumber);
}
}
(a) Is the code given below a defensive translation of the associations shown in the class diagram? Explain your answer.
class Teacher{
private Student favoriteStudent;
void setFavoriteStudent(Student s){
favoriteStudent = s;
}
}
class Student{
private Teacher favoriteTeacher;
void setFavoriteTeacher(Teacher t){
favoriteTeacher = t;
}
}
(b) In terms of maintaining referential integrity in the implementation, what is the difference between the following two diagrams?
(c) Show a defensive implementation of the remove(Member m)
of the Club
class given below.
(a) Yes. Each links is mutable and unidirectional. A simple reference variable is suitable to hold the link.
Teacher
class can be made even more defensive by introducing a resetFavoriteStudent()
method to unlink the current favorite student from a teacher. In that case, setFavoriteStudent(Student)
method should not accept null. This approach is more defensive because it prevents a null value being passed to setFavoriteStudent(Student)
by mistake and being interpreted as a request to de-link the current favorite
student from the Teacher
object.
(b) First diagram has unidirectional links. Second has a bidirectional link. RI is only applicable to the second.
(c)
void removeMember(Member m) {
if (m==null) {
throw exception("this is null, not a member!");
} else if(member_count == 10) {
throw exception("we need at least 10 members to survive!");
} else if(!isMember(m)) {
throw exception ("this fellow is not a member of our club!");
} else {
members.remove(m); // members is a data structure such as ArrayList
}
}
Bidirectional associations, if not implemented properly, can result in referential integrity violations.
True
Explanation: Bidirectional associations require two objects to link to each other. When one of these links is not consistent with the other, we have a referential integrity violation.
๐ Can explain when to use defensive programming
It is not necessary to be 100% defensive all the time. While defensive code may be less prone to be misused or abused, such code can also be more complicated and slower to run.
The suitable degree of defensiveness depends on many factors such as:
Defensive programming,
(a)(b)(c)(d)(e)
Explanation: Defensive programming requires a more checks, possibly making the code longer, more complex, and possibly slower. Use it only when benefits outweigh costs, which is often.
๐ Can explain the Design-by-Contract approach
Suppose an operation is implemented with the behavior specified precisely in the API (preconditions, post conditions, exceptions etc.). When following the defensive approach, the code should first check if the preconditions have been met. Typically, exceptions are thrown if preconditions are violated. In contrast, the Design-by-Contract (DbC) approach to coding assumes that it is the responsibility of the caller to ensure all preconditions are met. The operation will honor the contract only if the preconditions have been met. If any of them have not been met, the behavior of the operation is "unspecified".
Languages such as Eiffel have native support for DbC. For example, preconditions of an operation can be specified in Eiffel and the language runtime will check precondition violations without the need to do it explicitly in the code. To follow the DbC approach in languages such as Java and C++ where there is no built-in DbC support, assertions can be used to confirm pre-conditions.
Which statements are correct?
(a)(b)(c)
Explanation: DbC is not an alternative to OOP. We can use DbC in an OOP solution.