High Quality Java Code: Exception-Safe Code (I)

Similar documents
High Quality Java Code: Contracts & Assertions

Thread Safety. Review. Today o Confinement o Threadsafe datatypes Required reading. Concurrency Wrapper Collections

Chapter 4 Defining Classes I

1B1b Inheritance. Inheritance. Agenda. Subclass and Superclass. Superclass. Generalisation & Specialisation. Shapes and Squares. 1B1b Lecture Slides

Import Statements, Instance Members, and the Default Constructor

Exceptions and assertions

Exception Safety. CS 311 Data Structures and Algorithms Lecture Slides Wednesday, October 28, Glenn G. Chappell. continued

AXIOMS OF AN IMPERATIVE LANGUAGE PARTIAL CORRECTNESS WEAK AND STRONG CONDITIONS. THE AXIOM FOR nop

Chapter01.fm Page 1 Monday, August 23, :52 PM. Part I of Change. The Mechanics. of Change

Lab 5: Java IO 12:00 PM, Feb 21, 2018

Module 10A Lecture - 20 What is a function? Why use functions Example: power (base, n)

Dealing with Bugs. Kenneth M. Anderson University of Colorado, Boulder CSCI 5828 Lecture 27 04/21/2009

Test-Driven Development (TDD)

Software Engineering

CS125 : Introduction to Computer Science. Lecture Notes #38 and #39 Quicksort. c 2005, 2003, 2002, 2000 Jason Zych

CMSC 330: Organization of Programming Languages. Threads Classic Concurrency Problems

Software Engineering Testing and Debugging Testing

Comp215: Thinking Generically

Principle of Complier Design Prof. Y. N. Srikant Department of Computer Science and Automation Indian Institute of Science, Bangalore

CS103 Spring 2018 Mathematical Vocabulary

Testing Exceptions with Enforcer

Software Design and Analysis for Engineers

(Refer Slide Time: 02.06)

COMP322 - Introduction to C++ Lecture 10 - Overloading Operators and Exceptions

Module 6. Campaign Layering

CSE 331 Software Design & Implementation

Lecture 9 Stacks & Queues

Lecture 9 Notes Stacks & Queues

Assertions, pre/postconditions

Lab 10: Sockets 12:00 PM, Apr 4, 2018

Intro. Scheme Basics. scm> 5 5. scm>

XP: Backup Your Important Files for Safety

The Dining Philosophers Problem CMSC 330: Organization of Programming Languages

(Refer Slide Time: 01:25)

Deviations are things that modify a thread s normal flow of control. Unix has long had signals, and these must be dealt with in multithreaded

COSC 2P95. Procedural Abstraction. Week 3. Brock University. Brock University (Week 3) Procedural Abstraction 1 / 26

Exceptions. References. Exceptions. Exceptional Conditions. CSE 413, Autumn 2005 Programming Languages

9 th CA 2E/CA Plex Worldwide Developer Conference 1

MSO Lecture Design by Contract"

CMSC 330: Organization of Programming Languages. The Dining Philosophers Problem

MITOCW ocw f99-lec07_300k

Exceptions. CSE 142, Summer 2002 Computer Programming 1.

Exceptions. Readings and References. Exceptions. Exceptional Conditions. Reading. CSE 142, Summer 2002 Computer Programming 1.

These are notes for the third lecture; if statements and loops.

Type Checking in COOL (II) Lecture 10

CATCH Me if You Can Doug Hennig

Final Examination CS 111, Fall 2016 UCLA. Name:

SML Style Guide. Last Revised: 31st August 2011

A dictionary interface.

3 Nonlocal Exit. Quiz Program Revisited

Foundations, Reasoning About Algorithms, and Design By Contract CMPSC 122

Programming Data Structures and Algorithms Prof. Shankar Balachandran Department of Computer Science Indian Institute of Technology, Madras

Type Checking and Type Equality

(Refer Slide Time: 1:27)

Engineering Abstractions in Model Checking and Testing. Michael Achenbach Klaus Ostermann

An Introduction to Business Disaster Recovery

CISC-124. Casting. // this would fail because we can t assign a double value to an int // variable

Kotlin for Android Developers

12 Key Steps to Successful Marketing

Checked and Unchecked Exceptions in Java

How Rust views tradeoffs. Steve Klabnik

Write for your audience

Question 1. (2 points) What is the difference between a stream and a file?

Kotlin for Android Developers

Credit where Credit is Due. Lecture 29: Test-Driven Development. Test-Driven Development. Goals for this lecture

1 Dynamic Memory continued: Memory Leaks

The Dynamic Typing Interlude

Notes on Non-Chronologic Backtracking, Implication Graphs, and Learning

Programming Languages. Streams Wrapup, Memoization, Type Systems, and Some Monty Python

Emergency safety apps: which one is right for me?

Application Authorization with SET ROLE. Aurynn Shaw, Command Prompt, Inc. PGCon 2010

Stanko Tadić

CS193j, Stanford Handout #26. Files and Streams

Software Design and Analysis for Engineers

CPS122 Lecture: Detailed Design and Implementation

6. Java Errors and Exceptions

Lecture 9 Stacks & Queues

Lecture 8 Data Structures

Lecture Notes on Queues

Chapter 1 Getting Started

CSE 374 Programming Concepts & Tools. Hal Perkins Spring 2010

Cache Coherence Tutorial

6. Java Errors and Exceptions. Errors, runtime-exceptions, checked-exceptions, exception handling, special case: resources

Heuristic Evaluation of Covalence

Midterm Exam, October 24th, 2000 Tuesday, October 24th, Human-Computer Interaction IT 113, 2 credits First trimester, both modules 2000/2001

Lecture 21: The Many Hats of Scala: OOP 10:00 AM, Mar 14, 2018

Ch. 12: Operator Overloading

It s possible to get your inbox to zero and keep it there, even if you get hundreds of s a day.

Lecture 10 Notes Linked Lists

The Contract Pattern. Design by contract

Bounding Object Instantiations, Part 2

Error handling and logging

Animator Friendly Rigging Part 1

CSE 143 Sp03 Midterm 2 Sample Solution Page 1 of 7. Question 1. (2 points) What is the difference between a stream and a file?

Welcome to CS61A! Last modified: Thu Jan 23 03:58: CS61A: Lecture #1 1

step is to see how C++ implements type polymorphism, and this Exploration starts you on that journey.

Lesson 14 Transcript: Triggers

Within Kodi you can add additional programs called addons. Each of these addons provides access to lots of different types of video content.

Assertions. Assertions - Example

Exceptions. What exceptional things might our programs run in to?

Transcription:

High Quality Java Code: Exception-Safe Code (I) by Pedro Agulló Soliveres W riting quality code is not easy, and writing code that is really safe in the presence of errors is very difficult. What s more, what safe means is not all that clear. In this article we will investigate the different meanings safe code can have, as well as the implications of the different possibilities. Then, we will investigate how to write code that can guarantee that we achieve our objectives, and show several standard techniques that can be put to good use. What we are not going to cover Let me state what this article will not try to cover. We won t try to study all the issues related to exception handling, interesting as they might be. We are not going to study when and how to raise exceptions, nor when to use exceptions to report failure of an operation, and when to use alternative techniques, such as returning a boolean value. We are not going to study how to distinguish between legitimate errors (the network went down and the file is no longer availble) and what we might consider programming defects/bugs (a method receives a null object, something the method caller could have prevented), which we might want to handle differently. Assertions, preconditions and postconditions are useful to trap defects, but we won t talk about them here. We won t study the subject of how to write exceptions classes or exception hierarchies, nor will we comment the standard exceptions provided by the standard library, nor which to use in each case. We won t comment on the implications of exceptions moving through architectural layers. How distributed systems affect exception handling is another issue we will not study. By not trying to solve all exception-related issues, we will gain a lot of depth. If your code has to deal with exceptions raised either by your own or other s code, as is always the case, then this article will be of interest to you. Setting the scene First of all, we must forbid using exceptions as a way of transferring control from one place to another: don t use them that way. We will assume that exceptions always communicate a problem, in order to write code that systematically handles errors in a correct way. This errors are reported by raising exceptions policy does not conflict with the fact that we can report failure in several ways. Suppose that we write a MyFile class with a read method. What do you think should happen when you call read and you have already reached the end of the file? You can raise an exception, but alternatively you can do nothing in read, and return a special value so 1/8

that the caller can know that he is already at the end of the file. Both options can be valid, and which one you select doesn t affect what we have to say in this article. What we demand is that, in no event should the application be left in an unrecoverable state, an exception must be raised before that happens. If this rule is followed, the advice provided in this article will apply. What to do when an exception arrives When your methods receive an exception raised by code they have called, you have three choices. First of all, you can handle the error/exception definitively. Whether you reattempt the operation, cancel it, etc., is largely irrelevant for this discussion, what matters is that the error will not continue moving upwards. The second option is to handle the error/exception and raise a different exception. Why and when to do this is an interesting subject, but we won t discuss it here. Suffice is to say that this typically happens when an error moves from a layer to a higher level layer: when you can t write a record to a database, this might be due to a primary key violation, but when this error arrives to upper layers, it will be better to transform it into a customer already registered error or whatever makes sense at the current abstraction level. This scenario boils down to a slightly decorated version of the next scenario. The third option is to perform cleanup in your method, and pass the exception upwards. This is the most common case, because most code doesn t have enough information about what to do to handle an error that arrives from lower level layers. When an exception arrives to a method that can t solve the problem by itself, it has to clean things up, and then pass the error upwards so that somebody with a better perspective can handle it adequately. If you decide to catch the exception and handle it without letting it move upwards (the first option), this is because you know exactly what to do: this case is of little interest to our discussion. We won t consider this scenario further in this article. What to do when a method doesn t know how to handle an error (second and third options), is a different matter. It is perfectly clear that it must perform adequate cleanup, if needed, and then let the exception continue moving upwards until it finds code that knows how to handle the problem. But, what does adequate cleanup mean? This requires careful consideration. What safe means: levels of safety Ideally, we all know that to perform cleanup adequately, a method should restore the state of all data and objects it has modified, leaving them as they were before the method began to execute. Furthermore, the method should release all resources it acquired, be them files, sockets, database connections, etc. Clearly, we don t want to leave files or socket connections open, because they are limited resources. What s more, even if the system can release the resources automatically when your application is closed, you don t want to leave that task to the system, because those resources will not be available meanwhile, and you might overload the system. And you will surely find some kind of resource that the system will not 2/8

take care of. Therefore, freeing resources as soon as possible is a must. Once the method has left the program as it was before it began executing, and has released all the resources it has acquired, we let the exception continue moving upwards. This is needed so that other methods have an opportunity either to definitively handle it or just do their own cleanup and pass the exception upwards again. While restoring the system to the state it had prior to the error is highly desirable, there are cases when this might not be possible, or where performing a full rollback to the previous state of the program has a cost we find unacceptable. Adding full rollback support can add overhead each time your code executes, an overhead that might not be justifiable if exceptions are highly unlikely. Let s consider the different behaviours a method can show when it raises an exception of it arrives from lower level layers. It is possible that the method doesn t guarantee anything (level 1 safety). In this scenario, resources allocated by your method are not freed by it, and calling their cleanup methods later (i.e., close for streams or connections, etc.) is not guaranteed to work. What s worse, data or objects modified by your method are unstable, making it impossible to use them with guarantees. In other words, you are stuck. Level 1 safety means no safety at all. Level 2 safety means that the method guarantees that somebody else can release resources, nothing about modified data or objects. Resources allocated by your method are not freed by it, it is up to other code to release them. Data or objects modified by the method are unstable, making it impossible to call any method that accesses them with guarantees. However, cleanup methods can be safely called. In this scenario you are stuck, but the program might be closed without harm. Level 3 safety means that the code guarantees that all resources are released, nothing about modified data or objects. Resources allocated by your method are actually freed by it. However, data and objects modified by your method are unstable, making it impossible to call methods that access them with guarantees. However, cleanup methods can be safely called. This is better, but you ll have to throw away all modified data or objects, and won t be able to use the modified objects because you can t assume they are in good shape. Dangerous! Level 4 safety code guarantees that all resources are freed, and modified data/objects are left in a stable state. Resources allocated by your method are freed by it. Data and objects modified by your method are left in a stable state. Their state might be unknown, but you will be able to use them safely. To illustrate this case: if the method modified a list, it might not be left as it was before, but the list will be left in a usable state -say empty. This is acceptable, just take into account that data/objects might have been modified, don t assume much about them, but know you can still use them. At safety level 5, the method guarantees that all is left as it was before the method began its execution. Congratulations, you have transactional semantics. Your method is a really good citizen. 3/8

If you can write level 3 code, then you can write equivalent level 4 code almost without a doubt. Level 4 safety is the minimum you should try to get. Sorry, but code that provides less guarantees is not good enough for production. Of course, level 5 is the target we should set ourselves. However, we should be realistic and take into account that there are cases in which attaining level 5 safety is feasible, but has an additional cost. This being the case, you have to take an informed decision about what level to support. For high performance code you will find yourself retreating to level 4 in some cases: however, if there is a possibility of moving your code to level 5, consider it, and take a decision explicitly. Level 5 is too good to lose it without a fight. Now that we have arrived to the conclusion that only levels 4 and 5 are good enough, let me unify our vocabulary with that of the C++ community, the pioneers in the exception safety arena. Code that provides level 4 guarantees are said to provide the basic guarantee, while code that supports level 5 are said to provide the strong guarantee. Only in these two cases will we consider our code to be exception-safe. The hello world of exception-safe code The easiest way to lose resources is forgetting to ensure that resources are closed. Listing 1 shows a typical scenario, where the writer object resources (an open file) are not guaranteed to be released in all cases. public static void sample1() throws IOException { Writer writer = new FileWriter( "myfile.txt"); writer.write( "Hello world"); writer.close(); Listing 1. Level 1 code: not good enough. In Listing 1, if the call to write fails with an exception close won t be called and the resources used by writer will be released at an unknown time. Listing 2 solves this problem by adding a finally block to ensure that close will be called no matter what happens. public static void sample2() throws IOException { Writer writer = new FileWriter( "myfile.txt"); try { writer.write( "Hello world"); finally { writer.close(); Listing 2. Using try-finally blocks to ensure resources are released whether there is an exception or not. We all know this, but believe me: real-world experience shows that forgetting to write try-finally blocks where needed is the most common programming defect leading to broken exception-safety. 4/8

Beware of the code you call While we all love to write new code, the fact is we consume far more code than we create, and the quality of the code you consume has a big impact in the quality of the code you create. For example, in the sample3 method in Listing 3, you will notice that we have the mandatory try-finally block to acquire/release writer wrapping our call to the copy method. Since the copy method frees all allocated resource and does not alter the program state otherwise, it provides the strong guarantee, and sample3 will be able to provide that guarantee too. But, if you call badcopy instead, you will find that badcopy might fail to release resources if there is some error before writer.close is invoked. Now, there is no way your sample3 method can provide neither the basic nor the strong guarantee. public static void sample3() throws IOException { Writer writer = new FileWriter( "myoutputfile.txt"); try { copy( "myinputfile.txt", writer); finally { writer.close(); Listing 3. public static void copy( String inputfile, Writer writer ) throws IOException { Reader reader = new FileReader( inputfile ); try { int c; while( (c = reader.read())!= -1 ) { writer.write(c); finally { reader.close(); // Code written in a different file... public static void badcopy(string inputfile, Writer writer) throws IOException { Reader reader = new FileReader( inputfile ); int c; while( (c = reader.read())!= -1 ) { writer.write(c); reader.close(); This is distressing: it means that in some cases you will not be able to write exception-safe code unless you write the whole code on your own. As should be expected, quality requires an ecosystem: it does not grow in the trees. As a rule of thumb, your methods can t provide greater guarantees than the methods you call. You can 5/8

devise ways to solve some issues, but they will make the code brittle and much more difficult to. By the way, if copy were an operation guaranteed to throw no exception, that would mean that you would be able to write the sample3 method withou the need to have a try-finally block. Code that can t raise exceptions is called no-throw code. This is a very important feature, as we will see later. Maintaining data consistent Releasing resources is just one part of exception-safety. You need to keep data consistent too when there is some error (the basic guarantee) or, even better, to restore it to its original state (the strong guarantee). Just to make things as clear as possible, I will write some code that frankly speaking makes no much sense, but I hope will make things crystal clear. Bear with me! In Listing 4 we have a hypothetical Customer class that keeps track of invoices via its addinvoices, getinvoice and getinvoicescount methods. public static class Customer { private int invoicescount; private List<Invoice> invoices = new ArrayList<Invoice>(); public void addinvoices( Collection<Invoice> invoices) { this.invoicescount += invoices.size(); Listing 4. A version of addinvoces that provides no guantee. for( Invoice invoice : invoices ) { dosomething (invoice); this.invoices.add(invoice); public int getinvoicescount() { return this.invoicescount; public Invoice getinvoice( int i ) { return this.invoices.get(i); If you take a look at addinvoices, you will find that if dosomething ends upd throwing an exception, then the internal invoicescount field will be lying with respect to the real size of the invoices list. This code is faulty because it just does not maintain the consistency of data: next time you call getinvoicescount to iterate using getinvoice, you will be in big trouble. And, yes, I m well aware that you need not keep an invoicescount field, this is just for llustrative purposes. So, let s keep pretending that we must keep invoicescount updated at all costs. An easy fix to addinvoices is to write it as follows: public void addinvoices( Collection<Invoice> invoices) { Listing 5. A version of 6/8

for( Invoice invoice : invoices ) { dosomething (invoice); this.invoices.add(invoice); this.invoicescount ++; addinvoces that provides the basic guarantee. The addinvoices implementation in Listing 5 provides the basic guarantee, because it leaves the data in a consistent state, but it does not provide the strong guarante, because it does not restores the data to the state it had before addinvoices was called. That is not the ideal situation, but it is manageable and safe. Listing 6 shows an implementation that provides the strong guarantee. Here, we create a temporary copy of data, perform the operations that change the state of data on the temporary data, and then swap the temporary and current data. public void addinvoices( Collection<Invoice> invoices) { // 1. Create a copy of the data to modify. List<Invoice> newinvoices = new ArrayList<Invoice>(this.invoices); int newinvoicescount = this.invoicescount; Listing 6. A version of addinvoces that provides the strong guarantee. // 2. Perform state changing operations with the // temporary copy of data for( Invoice invoice : invoices ) { dosomething(invoice); newinvoices.add(invoice); newinvoicescount ++; // 3. Consolidate the changes to data via operations // that just can t fail. this.invoices = newinvoices; this.invoicescount = newinvoicescount; Why does this work? To begin with, restoring state can be really, really hard, so we create a copy of existing data with which to work temporarily, and from then on then we forget about having to restore current data state: if there is a problem, we will throw away the copied data. We solve the problem or restoring data state by making it unnecesary. Next, we perform the operations that can fail (i.e., throw an exception) on the copied data. Only when we have performed all operations that modify data have do we modify the real data, by the simple expendient of swapping the current data with the temporary data And, know what? These swapping operations can t fail, they are no-throw operations. Therefore, the operation is completely safe in the presence of exceptions: this copy data, modify the copy, swap modified copy and actual data pattern is an elegant one. But, what if dosomething fails? As we already said, your code can t be exception safe if the code you call is not again, unless you are really willing to spend an inordinate amount of effort that will lead to badly delimited code responsibilities. We must insist: you can t inject 7/8

quality after the fact. Quality is organic, you just can t restore it using pills. In this (admittedly contrived) example, achieving the strong guarantee for addinvoices is relatively easy. My advice is that you should not take for granted that the strong guarantee will be much more difficult to get than the basic guarantee. Surprisingly, the same happens with performance: don t assume that providing the strong guarantee will end up producing slower code. More to come There is still more to say about exception safety, but I hope this article will have made you think about the importance of exception safety, as well as to what levels of safety are there. I plan to write a second article that will deal with what I had no space for here. Oh, and by the way, what if addinvoices receives a null collection, or one of its elements is null? I consider this to be a bug, a defect in the caller s code. From my point of view, that kind of problem should be handled in a completely different way. This will be the subject of yet another article. About the author Pedro Agulló is a consultant, programmer and freelance writer in Barcelona, Spain. He specialises in mentoring on agile methodologies and techniques, building & leading agile teams, design and development of complex business models using TDD (Test Driven Development) and all kinds of programming with a degree of complexity. He has published more than one hundred technical articles in printed magazines such as Delphi Magazine, Revista Profesional para Programadores, Programación Actual, Delphi Informant, etc. He is the author of DirectJNgine, an open source Ajax library that allows direct access to Java objects from Javascript. You can find it in code.google.com/p/directjngine/ 8/8