This site is from a past semester! The current version will be here when the new semester starts.
TIC4002 2021 Jan-May
  • Full Timeline
  • Week 1 [Mon, Jan 11th]
  • Week 2 [Fri, Jan 15th]
  • Week 3 [Fri, Jan 22nd]
  • Week 4 [Fri, Jan 29th]
  • Week 5 [Fri, Feb 5th]
  • Week 6 [Fri, Feb 12th]
  • Week 7 [Fri, Feb 19th]
  • Week 8 [Fri, Mar 5th]
  • Week 9 [Fri, Mar 12th]
  • Week 10 [Fri, Mar 19th]
  • Week 11 [Fri, Mar 26th]
  • Week 12 [Fri, Apr 2nd]
  • Week 13 [Fri, Apr 9th]
  • Textbook
  • Admin Info
  • Dashboards
  •  Individual Project (iP):
  • Individual Project Info
  • iP Upstream Repo
  • iP Showcase
  • iP Code Dashboard
  • iP Progress Dashboard

  •  Team Project (tP):
  • Team Project Info
  • tP Upstream Repo (AB3)
  • Team List
  • tP Code Dashboard
  • tP Progress Dashboard
  • Report Bugs
  • Forum
  • Instructors
  • Announcements
  • Files (handouts, submissions etc.)
  • MS Teams link
  • Java Coding Standard
  • Git Conventions
  • Participation Dashboard
  • Week 7 [Fri, Feb 19th] - Topics

    • [W7.1] Error Handling: Exceptions
    • [W7.1a] Implementation → Error Handling → Introduction → What :

    • [W7.1b] Implementation → Error Handling → Exceptions → What :

    • [W7.1c] Implementation → Error Handling → Exceptions → How :

    • [W7.1d] C++ to Java → Exceptions → What are Exceptions? :

    • [W7.1e] C++ to Java → Exceptions → How to use Exceptions :

    • [W7.1f] Implementation → Error Handling → Exceptions → When :

    • [W7.2] Error Handling: Assertions
    • [W7.2a] Implementation → Error Handling → Assertions → What :

    • [W7.2b] Implementation → Error Handling → Assertions → How :

    • [W7.2c] Implementation → Error Handling → Assertions → When :

    • [W7.3] Error Handling: Logging
    • [W7.3a] Implementation → Error Handling → Logging → What :

    • [W7.3b] Implementation → Error Handling → Logging → How :

    • [W7.4] Error Handling: Defensive Programming
    • [W7.4a] Implementation → Error Handling → Defensive Programming → What

    • [W7.4b] Implementation → Error Handling → Defensive Programming → Enforcing compulsory associations

    • [W7.4c] Implementation → Error Handling → Defensive Programming → Enforcing 1-to-1 associations

    • [W7.4d] Implementation → Error Handling → Defensive Programming → Enforcing referential integrity

    • [W7.4e] Implementation → Error Handling → Defensive Programming → When

    • [W7.4f] Implementation → Error Handling → Design by Contract → Design by contract


    [W7.1] Error Handling: Exceptions

    W7.1a :

    Implementation → Error Handling → Introduction → What

    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. -- Microsoft

    W7.1b :

    Implementation → Error Handling → Exceptions → What

    Can explain exceptions

    Exceptions are used to deal with 'unusual' but not entirely unexpected situations that the program might encounter at runtime.

    Exception:

    The term exception is shorthand for the phrase "exceptional event." 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:

    • A network connection encounters a timeout due to a slow server.
    • The code tries to read a file from the hard disk but the file is corrupted and cannot be read.

    W7.1c :

    Implementation → Error Handling → Exceptions → How

    Can explain how exception handling is done typically

    Most languages allow code that encountered an "exceptional" situation to encapsulate details of the situation in an Exception object and throw/raise that object so that another piece of code can catch it and deal with it. This is especially useful when the code that encountered the unusual situation does not know how to deal with it.

    The extract below from the -- Java Tutorial (with slight adaptations) explains how exceptions are typically handled.

    When an error occurs at some point in the execution, the code being executed creates an exception object and hands it off to the runtime system. The exception object contains information about the error, including its type and the state of the program when the error occurred. Creating an exception object and handing it to the runtime system is called throwing an exception.

    After a method throws an exception, the runtime system attempts to find something to handle it in the the ordered list of methods that had been called to get to the method where the error occurredcall stack. The runtime system searches the call stack for a method that contains a block of code that can handle the exception. This block of code is called an exception handler. The search begins with the method in which the error occurred and proceeds through the call stack in the reverse order in which the methods were called. When an appropriate handler is found, the runtime system passes the exception to the handler. An exception handler is considered appropriate if the type of the exception object thrown matches the type that can be handled by the handler.

    The exception handler chosen is said to catch the exception. If the runtime system exhaustively searches all the methods on the call stack without finding an appropriate exception handler, the program terminates.

    Advantages of exception handling in this way:

    • The ability to propagate error information through the call stack.
    • The separation of code that deals with 'unusual' situations from the code that does the 'usual' work.

    Which are the benefits of exceptions?

    • a. Exceptions allow us to separate normal code from error handling code.
    • b. Exceptions can prevent problems that happen in the environment.
    • c. Exceptions allow us to handle in one location an error raised in another location.

    (a) (c)

    Explanation: Exceptions cannot prevent problems in the environment. They can only be used to handle and recover from such problems.

    W7.1d :

    C++ to Java → Exceptions → What are Exceptions?

    Can explain Java Exceptions

    Given below is an extract from the -- Java Tutorial, with some adaptations.

    There are three basic categories of exceptions In Java:

    • Checked exceptions: exceptional conditions that a well-written application should anticipate and recover from. All exceptions are checked exceptions, except for Error, RuntimeException, and their subclasses.

    Suppose an application prompts a user for an input file name, then opens the file by passing the name to the constructor for java.io.FileReader. Normally, the user provides the name of an existing, readable file, so the construction of the FileReader object succeeds, and the execution of the application proceeds normally. But sometimes the user supplies the name of a nonexistent file, and the constructor throws java.io.FileNotFoundException. A well-written program will catch this exception and notify the user of the mistake, possibly prompting for a corrected file name.

    • Errors: exceptional conditions that are external to the application, and that the application usually cannot anticipate or recover from. Errors are those exceptions indicated by Error and its subclasses.

    Suppose that an application successfully opens a file for input, but is unable to read the file because of a hardware or system malfunction. The unsuccessful read will throw java.io.IOError. An application might choose to catch this exception, in order to notify the user of the problem — but it also might make sense for the program to print a stack trace and exit.

    • Runtime exceptions: conditions that are internal to the application, and that the application usually cannot anticipate or recover from. Runtime exceptions are those indicated by RuntimeException and its subclasses. These usually indicate programming bugs, such as logic errors or improper use of an API.

    Consider the application described previously that passes a file name to the constructor for FileReader. If a logic error causes a null to be passed to the constructor, the constructor will throw NullPointerException. The application can catch this exception, but it probably makes more sense to eliminate the bug that caused the exception to occur.

    Errors and runtime exceptions are collectively known as unchecked exceptions.

    W7.1e :

    C++ to Java → Exceptions → How to use Exceptions

    Can use Java Exceptions

    The content below uses extracts from the -- Java Tutorial, with some adaptations.

    A program can catch exceptions by using a combination of the try, catch blocks.

    • The try block identifies a block of code in which an exception can occur.
    • The catch block identifies a block of code, known as an exception handler, that can handle a particular type of exception.

    The writeList() method below calls a method process() that can cause two type of exceptions. It uses a try-catch construct to deal with each exception.

    public void writeList() {
        print("starting method");
        try {
            print("starting process");
            process();
            print("finishing process");
    
        } catch (IndexOutOfBoundsException e) {
            print("caught IOOBE");
    
        } catch (IOException e) {
            print("caught IOE");
    
        }
        print("finishing method");
    }
    

    Some possible outputs:

    No exceptions IOException IndexOutOfBoundsException
    starting method
    starting process
    finishing process
    finishing method
    starting method
    starting process
    finishing process
    caught IOE
    finishing method
    starting method
    starting process
    finishing process
    caught IOOBE
    finishing method

    You can use a finally block to specify code that is guaranteed to execute with or without the exception. This is the right place to close files, recover resources, and otherwise clean up after the code enclosed in the try block.

    The writeList() method below has a finally block:

    public void writeList() {
        print("starting method");
        try {
            print("starting process");
            process();
            print("finishing process");
    
        } catch (IndexOutOfBoundsException e) {
            print("caught IOOBE");
    
        } catch (IOException e) {
            print("caught IOE");
    
        } finally {
            // clean up
            print("cleaning up");
        }
        print("finishing method");
    }
    

    Some possible outputs:

    No exceptions IOException IndexOutOfBoundsException
    starting method
    starting process
    finishing process
    cleaning up
    finishing method
    starting method
    starting process
    finishing process
    caught IOE
    cleaning up
    finishing method
    starting method
    starting process
    finishing process
    caught IOOBE
    cleaning up
    finishing method
    • The try statement should contain at least one catch block or a finally block and may have multiple catch blocks.

    • The class of the exception object indicates the type of exception thrown. The exception object can contain further information about the error, including an error message.

    You can use the throw statement to throw an exception. The throw statement requires a Throwable objects are instances of any subclass of the Throwable class.throwable object as the argument.

    Here's an example of a throw statement.

    if (size == 0) {
        throw new EmptyStackException();
    }
    

    In Java, Checked exceptions are subject to the Catch or Specify Requirement: code that might throw checked exceptions must be enclosed by either of the following:

    • A try statement that catches the exception. The try must provide a handler for the exception.
    • A method that specifies that it can throw the exception. The method must provide a throws clause that lists the exception.

    Unchecked exceptions are not required to follow to the Catch or Specify Requirement but you can apply the requirement to them too.

    Here's an example of a method specifying that it throws certain checked exceptions:

    public void writeList() throws IOException, IndexOutOfBoundsException {
        print("starting method");
        process();
        print("finishing method");
    }
    
    Some possible outputs:
    
    No exceptions IOException IndexOutOfBoundsException
    starting method
    finishing method
    starting method
    finishing method
    starting method
    finishing method

    Java comes with a collection of built-in exception classes that you can use. When they are not enough, it is possible to create your own exception classes.

    The Main class below parses a string descriptor of a rectangle of the format "WIDTHxHEIGHT" e.g., "3x4" and prints the area of the rectangle.

    public class Main {
    
        public static void printArea(String descriptor){
            //TODO: modify the code below
            System.out.println(descriptor + "=" + calculateArea(descriptor));
        }
    
        private static int calculateArea(String descriptor) {
            //TODO: modify the code below
            String[] dimensions = descriptor.split("x");
            return Integer.parseInt(dimensions[0]) * Integer.parseInt(dimensions[1]);
        }
    
        public static void main(String[] args) {
            printArea("3x4");
            printArea("5x5");
        }
    }
    

    3x4=12
    5x5=25
    
    1. Update the code of printArea to print an error message if WIDTH and/or HEIGHT are not numbers e.g., "Ax4"
      calculateArea will throw the unchecked exception NumberFormatException if the code tries to parse a non-number to an integer.

    2. Update the code of printArea to print an error message if the descriptor is missing WIDTH and/or HEIGHT e.g., "x4"
      calculateArea will throw the unchecked exception IndexOutOfBoundsException if one or both dimensions are missing.

    3. Update the code of calculateArea to throw the checked exception IllegalShapeException if there are more than 2 dimensions e.g., "5x4x3" and update the printArea to print an error message for those cases. Here is the code for the IllegalShapeException.java

    public class IllegalShapeException extends Exception {
      //no other code needed
    }
    

    Here is the expected behavior after you have done the above changes:

    public class Main {
    
        //...
    
        public static void main(String[] args) {
            printArea("3x4");
            printArea("3xy");
            printArea("3x");
            printArea("3");
            printArea("3x4x5");
        }
    }
    

    3x4=12
    WIDTH or HEIGHT is not a number: 3xy
    WIDTH or HEIGHT is missing: 3x
    WIDTH or HEIGHT is missing: 3
    Too many dimensions: 3x4x5
    
    public class Main {
    
        public static void printArea(String descriptor){
            try {
                System.out.println(descriptor + "=" + calculateArea(descriptor));
            } catch (NumberFormatException e) {
                System.out.println("WIDTH or HEIGHT is not a number: " + descriptor);
            } // add more catch blocks here
        }
    
        private static int calculateArea(String descriptor) throws IllegalShapeException {
            String[] dimensions = descriptor.split("x");
    
            //throw IllegalShapeException here if dimensions.length > 2
    
            return Integer.parseInt(dimensions[0]) * Integer.parseInt(dimensions[1]);
        }
    
    
    }
    

    W7.1f :

    Implementation → Error Handling → Exceptions → When

    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'.

    [W7.2] Error Handling: Assertions

    W7.2a :

    Implementation → Error Handling → Assertions → What

    Video

    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 takes 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 you set an assertion that timeout returned by Config.getTimeout() is greater than 0. Now, if Config.getTimeout() returns -1 in a specific execution of this line, the runtime can detect it as an 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(); 
    

    W7.2b :

    Implementation → Error Handling → Assertions → How

    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 assert 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:

    W7.2c :

    Implementation → Error Handling → Assertions → When

    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.

    • The raising of an exception indicates an unusual condition created by the user (e.g. user inputs an unacceptable input) or the environment (e.g., a file needed for the program is missing).
    • An assertion failure indicates the programmer made a mistake in the code (e.g., a null value is returned from a method that is not supposed to return null under any circumstances).

    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. Use assertions to indicate that the programmer messed up; use exceptions to indicate that the user or the environment messed up.
    • b. Use exceptions to indicate that the programmer messed up; use assertions to indicate that the user or the environment messed up.

    (a)

    [W7.3] Error Handling: Logging

    Video

    W7.3a :

    Implementation → Error Handling → Logging → What

    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 actions can then be taken to prevent it from happening again.

    A log file is like the flight data recorderblack box of an airplane; they don't prevent problems but they can be helpful in understanding what went wrong after the fact.

    Why is logging like having the 'black box' in an airplane?

    (a)

    W7.3b :

    Implementation → Error Handling → Logging → How

    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 how much information to recordintensity.

    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 a INFO, WARNING etc.logging level for each message. When running the code, the logging level can be set to WARNING so that log messages specified as having 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:

    • A video tutorial by SimplyCoded:

    Best Practices:

    [W7.4] Error Handling: Defensive Programming

    Video

    W7.4a

    Implementation → Error Handling → Defensive Programming → What

    Can explain defensive programming

    A defensive programmer codes under the assumption "if you 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 method MainApp#getConfig() 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
        }
    

    W7.4b

    Implementation → Error Handling → Defensive Programming → Enforcing compulsory associations

    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. You 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

    • restricts the maximum number of Accounts to 8
    • avoids adding duplicate Accounts

    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 associations among the classes. Follow the defensive coding approach. Let the Marriage class handle the setting/removal of references.

    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).

    W7.4c

    Implementation → Error Handling → Defensive Programming → Enforcing 1-to-1 associations

    Can use defensive coding to enforce 1-to-1 associations

    Consider the association given below. A defensive implementation requires us to ensure that 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 alternative 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.

    W7.4d

    Implementation → Error Handling → Defensive Programming → Enforcing referential integrity

    Can use defensive coding to enforce referential integrity of bidirectional 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 like 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 is a result of the code not being defensive enough to stop this "love triangle". In such a situation, you could say that the referential integrity has been violated. This means that 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 james.setGirlfriend(jean) is executed, the code ensures that james breaks up with any current girlfriend before he accepts jean as his girlfriend. Furthermore, the code ensures that jean breaks up with any existing boyfriends before accepting james as her boyfriend.

    Imagine that you 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, assume Regions can be created without Players (note that you 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 you 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, you can throw an exception and let the caller handle the situation.

    Implement this bidirectional association. Note that the Bank uses the 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.

    Assume that once an Account has been assigned to a Bank, it cannot be assigned to a different Bank. Once the Account is removed from the Bank, it will not be used anymore (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) {
            accNumber = 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) method of the Club class given below.

    (a) Yes. Each link is mutable and unidirectional. A simple reference variable is suitable to hold the link.

    The 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, the 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, you have a referential integrity violation.

    W7.4e

    Implementation → Error Handling → Defensive Programming → When

    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:

    • How critical is the system?
    • Will the code be used by programmers other than the author?
    • The level of programming language support for defensive programming
    • The overhead of being defensive

    Defensive programming,

    • a. can make the program slower.
    • b. can make the code longer.
    • c. can make the code more complex.
    • d. can make the code less susceptible to misuse.
    • e. can require extra effort.

    (a)(b)(c)(d)(e)

    Explanation: Defensive programming requires a lot more checks, possibly making the code longer, more complex, and slower. Use it only when benefits outweigh costs, which is often.

    W7.4f

    Implementation → Error Handling → Design by Contract → Design by contract

    Can explain the Design-by-Contract approach

    term was coined by Bertrand MeyerDesign by contract (DbC) is an approach for designing software that requires defining formal, precise and verifiable interface specifications for software components.

    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. It is not natively supported by Java and C++.
    • b. It is an alternative to OOP.
    • c. It assumes the caller of a method is responsible for ensuring all preconditions are met.

    (a)(b)(c)

    Explanation: DbC is not an alternative to OOP. You can use DbC in an OOP solution.