Suprisingly few developers really master Java generics in their full extent. As apparently not being one of them, generic throws feature surprised me lately.
Following code snippet shows the idea - generic <X extends Exception> is used in throws part of go() method. Implementing class defines Exception subclass that will be allowed to be thrown.
import java.io.FileNotFoundException; interface Something<X extends Exception> { public abstract void go() throws X; } class SomethingImpl implements Something<FileNotFoundException> { public void go() throws FileNotFoundException { throw new FileNotFoundException("Yeah! Not found!"); } }
Interesting trick, but how can this be useful? Well, for example, you can pimp Strategy pattern with cunning Exception passing/handling!
In Strategy pattern, you basicaly have two choices how to deal with exceptions- Add throws Exception on both strategy interface and context method to pass every exception back to client - especially poor choice
- Add throws Exception on strategy interface method, catch it inside context and rethrow it wrapped inside RuntimeExcetpion back to client - usual solution, but it mixes strategy and context exceptions together
This is interface allowing Client to implement his own LoaderStrategy and it also allows him to choose Exception which can be thrown from his LoaderStrategy via generic throws X
/** * Strategy interface - To be implemented by client */ interface LoaderStrategy<X extends Exception> { public String load(long id) throws X; }
And this is simple Context using LoaderStrategy interface. It is also passing chosen exceptions from strategy back to client, again via generic throws X
/** * Context using LoaderStrategy, passing selected exceptions (X) */ class StrategyContext { public <X extends Exception> void perform(LoaderStrategy<X> oader) throws X { long resourceId = getIdFromSomewhere(); String data = loader.load(resourceId); //throws X - don't have to hadle exception here try { sendDataToRemoteSystem(data); } catch (RemoteException rx) { //Other checked Exception must be wrapped inside ContextException throw new ContextException("Failed to send data for resourceId " + resourceId, rx); } } private long getIdFromSomewhere() { return System.currentTimeMillis(); //CurrentTimeMillis is best id ever! } private void sendDataToRemoteSystem(String data) throws RemoteException { //invoke some remoting operation here... } } /** * Context exception wrapper - delivers other then LoaderStrategy exceptions to client */ class ContextException extends RuntimeException { public ContextException(String string, Exception cause) { super(string, cause); } }
Use case 1 - Passing checked Exceptions
This is client's LoaderStrategy implementation. It throws IOException/** * Client provided LoaderStrategy implementation throwing IOException */ class ReaderLoader implements LoaderStrategy<IOException> { private Reader reader; public ReaderLoader(Reader reader) { this.reader = reader; } public String load(long id) throws IOException { String sid = String.valueOf(id); BufferedReader br = new BufferedReader(reader); String line = null; while ((line = br.readLine()) != null) { if (line.startsWith(sid)) { return line; } } return null; } }Then IOException is propagated back into client code to be handeled here
StrategyContext context = new StrategyContext(); ReaderLoader readerLoader = new ReaderLoader(new StringReader("Blah! Blah! Blah!")); try { context.perform(readerLoader); } catch (IOException iox) { //ReaderLoader thrown IOException is propagated through StrategyContext and delivered here } catch (ContextException cx) { //StrategyContext thrown exception wrapper - All sorts of NON ReaderLoader originated Exceptions Throwable cause = cx.getCause(); }This does not brings much value, but in client code we can easily distinguish our ReaderLoader originated exceptions from Context originated ones and handle them differently. Simple two catch clauses.
Use case 2 - Customized checked Exceptions
Client also provides custom Exception he want to be thrown from his Strategy and propagated through StrategyContext back to client/** * Client provided custom Exception */ class InvalidRecordCountException extends Exception { private long recordId; public InvalidRecordCountException(long recordId, String message) { super(message); this.recordId = recordId; } public InvalidRecordCountException(long recordId, SQLException exception) { super(exception); this.recordId = recordId; } public long getRecordId() { return recordId; } } /** * Client provided LoaderStrategy implementation throwing custom InvalidRecordCountException */ class JdbcLoader implements LoaderStrategy<InvalidRecordCountException> { private DataSource dataSource; private String select; public JdbcLoader(DataSource dataSource, String select) { this.dataSource = dataSource; this.select = select; } @Override public String load(long id) throws InvalidRecordCountException { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; try { connection = dataSource.getConnection(); statement = connection.prepareStatement(select); statement.setLong(1, id); resultSet = statement.executeQuery(); if (resultSet.next()) { String string = resultSet.getString(1); if (resultSet.next()) { //here we go... throw new InvalidRecordCountException(id, "Too many somethings in somewhere!"); } return string; } else { //here we go... throw new InvalidRecordCountException(id, "Not a single something in somewhere!"); } } catch (SQLException sqlx) { //here we go... throw new InvalidRecordCountException(id, sqlx); } finally { //TODO close resultSet, statement, connection } } }Then SQLExceptions are handled while InvalidRecordCountException propagated
StrategyContext context = new StrategyContext(); DataSource dataSource = null; //get it from somewhere JdbcLoader jdbcLoader = new JdbcLoader(dataSource, "SELECT something FROM somewhere WHERE column = ?"); try { context.perform(jdbcLoader); } catch (InvalidRecordCountException ircx) { //JdbcLoader thrown InvalidRecordCountException is propagated through StrategyContext and delivered here long badRecordId = ircx.getRecordId(); //we can take some action when knowing failing record id } catch (ContextException cx) { //StrategyContext thrown exception wrapper - SQLException will be cause propably... Throwable cause = cx.getCause(); }Now we can easily pass additional error information inside custom exception.
Well, I understand that nobody loves to write lots of single purpose exceptions, but adopting "generic throws idiom" quite increases exception reusability.
All used code can by found in kitchensink repository
No comments:
Post a Comment