Simple Resource Clean-up
Roger Hernandez
rhvarona at gmail.com
Mon Mar 2 21:02:08 PST 2009
I saw how the previous Automatic Resource Management proposal got torn to
pieces but I feel strongly enough about this issue to post a similar
proposal.
Some points:
I am not a programming language designer. I have very little experience
with byte code and with design and implementation of compilers. What I do
have is many years of experience in writing code for large business
applications, both in house-custom programs and shrink-wrapped products sold
to customers.
About 1/3 of the Java code I write contributes almost nothing to the
functionality or flexibility of the program. It is composed of very simple
and very repetitive resource clean-up code. JDBC code is an especially bad
case. I know we should be using JDO or JPA, but: 1) most of the Java
database code in existence is using JDBC to some extent or another, 2) for
the kind of processing our software does with large datasets having hundreds
of megabytes of row data and millions of rows per account per month, custom
JDBC code still beats the other solutions in memory utilization and
throughput.
In most business code we don't do complex exception processing. If we get a
exception we rollback the transaction and unroll the stack until we reach
some program level error handler that logs the error for some administrator
to review at a later date. So if this proposal is not applicable to complex
error handling scenarios, that is fine. Taking care of the simple scenarios
will still get rid of most of that 1/3 of the code I write, allowing me to
concentrate on the actual program logic, not the resource clean-up noise.
I also program quite a bit in C++ and C# and when I work in Java I sorely
miss RAII (Resource Acquisition Is Initialization) and the "using" statement
respectively.
At the end of the day, what I would like is a solution to minimize all the
resource clean-up boiler plate.
-----------------------------------------------------------------------------------------
PROJECT COIN SMALL LANGUAGE CHANGE PROPOSAL FORM v1.0
AUTHOR: Roger Hernandez, rogerh at velocityconsultinginc.com
OVERVIEW
FEATURE SUMMARY: Syntactic sugar for simple cases of the common
new/try/finally/close language idiom. The object being created must
implement the Closeable interface.
MAJOR ADVANTAGE: Significantly reduces lines of code when working with
objects that encapsulate external resources that should be closed as soon as
possible. Examples are Stream, Reader, Writer classes in java.io, and
Connection, Statement, PreparedStatement, ResultSet in java.sql.*.
MAJOR BENEFIT: It allows writing code that uses these kinds of object to
more clearly express the both the lifetime of the utilization of each
resource, and allows the logic flow of the code to be more visible.
MAJOR DISADVANTAGE: Either a new keyword, or an additional overloaded
meaning on an existing keyword.
ALTERNATIVES: You can always use the standard idiom: SomeType val = new
val(...); try { ... } finally { val.close(); }
EXAMPLES
SIMPLE EXAMPLE: A simple Java version of the command line utility "tee".
//This is the existing way of doing it.
//Lines of code: 19
package com.vci.projectcoin.using;
import java.io.*;
public class SimpleExample {
public static void main(String[] args) throws IOException {
byte []buffer = new byte[1024];
FileOutputStream out = new FileOutputStream(args[0]);
try {
for (int count; (count = System.in.read(buffer)) != -1;) {
out.write(buffer, 0, count);
System.out.write(buffer, 0, count);
}
} finally {
out.close();
}
}
}
//This is the proposed way of doing it, the compiler converts the
syntactic sugar into the same byte codes
//I am adding a new use to the the "try" keyword to avoid adding more
to the language, but it would work
//just a well with a "using" keyword.
//Lines of code: 16
package com.vci.projectcoin.using;
import java.io.*;
public class SimpleExample {
public static void main(String[] args) throws IOException {
byte []buffer = new byte[1024];
try (FileOutputStream out = new FileOutputStream(args[0])) {
for (int count; (count = System.in.read(buffer)) != -1;) {
out.write(buffer, 0, count);
System.out.write(buffer, 0, count);
}
}
}
}
ADVANCED EXAMPLE: A simple utility to execute a query and write values as
a comma delimited file.
//This is the existing way of doing it
//Lines of code: 55
package com.vci.projectcoin.using;
import java.sql.*;
import java.io.*;
public class AdvancedExample {
final static String EOL = System.getProperty("line.separator");
//Command Line: <JDBC url> <query sql> [<output file>]
static public void main(String []args) throws SQLException,
FileNotFoundException {
String url = args[0];
String sql = args[1];
PrintWriter out = new PrintWriter(args.length > 2 ? new
FileOutputStream(args[2]) : System.out);
try {
Connection conn = DriverManager.getConnection(url);
try {
Statement query =
conn.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY);
try {
ResultSet results = query.executeQuery(sql);
try {
ResultSetMetaData meta = results.getMetaData();
int colCount = meta.getColumnCount();
while (results.next()) {
for (int index = 1; index <= colCount; index++) {
int colType = meta.getColumnType(index);
boolean quoted = colType == Types.CHAR ||
colType == Types.LONGNVARCHAR || colType == Types.LONGVARCHAR ||
colType == Types.NCHAR ||
colType == Types.NVARCHAR || colType == Types.VARCHAR;
if (quoted) {
System.out.append('"');
}
System.out.append(results.getString(index));
if (quoted) {
System.out.append('"');
}
if (index < colCount) {
System.out.print(',');
} else {
System.out.print(EOL);
}
}
}
} finally {
results.close();
}
} finally {
query.close();
}
} finally {
conn.close();
}
} finally {
out.close();
}
}
}
//This is the proposed way of doing it
//This proposal gets rid of the finally clean up per object. It lets
one write robust resource clean-up code without a lot of effort.
//Lines of code: 43
package com.vci.projectcoin.using;
import java.sql.*;
import java.io.*;
public class AdvancedExample {
final static String EOL = System.getProperty("line.separator");
//Command Line: <JDBC url> <query sql> [<output file>]
static public void main(String []args) throws SQLException,
FileNotFoundException {
String url = args[0];
String sql = args[1];
try (PrintWriter out = new PrintWriter(args.length > 2 ? new
FileOutputStream(args[2]) : System.out)) {
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement query =
conn.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)) {
try (ResultSet results = query.executeQuery(sql)) {
ResultSetMetaData meta = results.getMetaData();
int colCount = meta.getColumnCount();
while (results.next()) {
for (int index = 1; index <= colCount; index++) {
int colType = meta.getColumnType(index);
boolean quoted = colType == Types.CHAR ||
colType == Types.LONGNVARCHAR || colType == Types.LONGVARCHAR || colType ==
Types.NCHAR || colType == Types.NVARCHAR || colType == Types.VARCHAR;
if (quoted) {
System.out.append('"');
}
System.out.append(results.getString(index));
if (quoted) {
System.out.append('"');
}
if (index < colCount) {
System.out.print(',');
} else {
System.out.print(EOL);
}
}
System.out.println();
}
}
}
}
}
}
}
//This is an additional syntactic sugar proposal, allowing multiple
objects to be allocated inside one try block. The compiler converts all
three programs into the same bytecode
//This proposal gets rid of the additional indentation level and
closing brace per object. It further minimize the clean-up boiler-plate,
allowing the point of the program logic to be clearer.
//Lines of code: 38
package com.vci.projectcoin.using;
import java.sql.*;
import java.io.*;
public class AdvancedExample {
final static String EOL = System.getProperty("line.separator");
//Command Line: <JDBC url> <query sql> [<output file>]
static public void main(String []args) throws SQLException,
FileNotFoundException {
String url = args[0];
String sql = args[1];
try (PrintWriter out = new PrintWriter(args.length > 2 ? new
FileOutputStream(args[2]) : System.out),
Statement query =
conn.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY),
ResultSet results = query.executeQuery(sql)) {
ResultSetMetaData meta = results.getMetaData();
int colCount = meta.getColumnCount();
while (results.next()) {
for (int index = 1; index <= colCount; index++) {
int colType = meta.getColumnType(index);
boolean quoted = colType == Types.CHAR || colType ==
Types.LONGNVARCHAR || colType == Types.LONGVARCHAR || colType == Types.NCHAR
|| colType == Types.NVARCHAR || colType == Types.VARCHAR;
if (quoted) {
System.out.append('"');
}
System.out.append(results.getString(index));
if (quoted) {
System.out.append('"');
}
if (index < colCount) {
System.out.print(',');
} else {
System.out.print(EOL);
}
}
System.out.println();
}
}
}
}
DETAILS
The specification requires that the object in the try () block have a
"close()" method. Wether the method throws any or no exception, or if it
returns a value or no value does not matter. The proposal is not trying to
introduce any new intelligence into the try finally clause, it is just
syntactic sugar to minimize simple resource clean-up code.
SPECIFICATION:
The "try" keyword will have an overloaded meaning
CASE 1
------
try (ClassWithCloseMethod value = new ClassWithCloseMethod(...)) {
//work gets done here
}
Will be syntactic sugar for:
ClassWithCloseMethod value = new ClassWithCloseMethod(...);
try {
//work gets done here
} finally {
value.close();
}
CASE 2
------
try (ClassWithCloseMethod value = new ClassWithCloseMethod(...)) {
//work gets done here
} finally {
//additional clean-up code
}
Will be syntactic sugar for:
ClassWithCloseMethod value = new ClassWithCloseMethod(...);
try {
//work gets done here
} finally {
value.close();
//additional clean-up code
}
CASE 3
------
try (ClassWithCloseMethod value = new ClassWithCloseMethod(...)) {
//work gets done here
} catch (Exception ex) {
//exception handling code
} finally {
//additional clean-up code
}
Will be syntactic sugar for:
ClassWithCloseMethod value = new ClassWithCloseMethod(...);
try {
//work gets done here
} catch (Exception ex) {
//exception handling code
} finally {
value.close();
//additional clean-up code
}
CASE 4
------
try (Class1WithCloseMethod value1 = new Class1WithCloseMethod(...),
Class2WithCloseMethod value2 = new Class2WithCloseMethod(...),
Class3WithCloseMethod value3 = new Class3WithCloseMethod(...)) {
//work gets done here
}
Will be syntactic sugar for:
Class1WithCloseMethod value1 = new Class1WithCloseMethod(...);
try {
Class2WithCloseMethod value2 = new Class2WithCloseMethod(...);
try {
Class3WithCloseMethod value3 = new Class3WithCloseMethod(...);
try {
//work gets done here
} finally {
value3.close();
}
} finally {
value2.close();
}
} finally {
value1.close();
}
CASE 5
------
try (Class1WithCloseMethod value1 = new Class1WithCloseMethod(...),
Class2WithCloseMethod value2 = new Class2WithCloseMethod(...),
Class3WithCloseMethod value3 = new Class3WithCloseMethod(...)) {
//work gets done here
} finally {
//additional clean-up code
}
Will be syntactic sugar for:
Class1WithCloseMethod value1 = new Class1WithCloseMethod(...);
try {
Class2WithCloseMethod value2 = new Class2WithCloseMethod(...);
try {
Class3WithCloseMethod value3 = new Class3WithCloseMethod(...);
try {
//work gets done here
} finally {
value3.close();
}
} finally {
value2.close();
}
} finally {
value1.close();
//additional clean-up code
}
CASE 6
------
try (Class1WithCloseMethod value1 = new Class1WithCloseMethod(...),
Class2WithCloseMethod value2 = new Class2WithCloseMethod(...),
Class3WithCloseMethod value3 = new Class3WithCloseMethod(...)) {
//work gets done here
} catch (Exception ex) {
//exception handling code
} finally {
//additional clean-up code
}
Will be syntactic sugar for:
Class1WithCloseMethod value1 = new Class1WithCloseMethod(...);
try {
Class2WithCloseMethod value2 = new Class2WithCloseMethod(...);
try {
Class3WithCloseMethod value3 = new Class3WithCloseMethod(...);
try {
//work gets done here
} finally {
value3.close();
}
} finally {
value2.close();
}
} catch (Exception ex) {
//exception handling code
} finally {
value1.close();
//additional clean-up code
}
COMPILATION: The SPECIFICATION section above shows the desugaring for
each case. Byte code would be identical to the desugared constructs.
TESTING: Byte code comparison of common code constructs. If the byte
code is not identical to the desugared version, test fails.
LIBRARY SUPPORT: No.
REFLECTIVE APIS: No.
OTHER CHANGES: No.
MIGRATION: For each case in the SPECIFICATION section, convert the
existing code to the syntactic sugar proposal.
COMPATIBILITY
BREAKING CHANGES: None.
EXISTING PROGRAMS: Compile accepts both existing and new forms of the
"try" statement. Byte code does not change.
REFERENCES
EXISTING BUGS: None
--
Roger Hernandez
More information about the coin-dev
mailing list