|
New for/in loop gymnastics
2005-02-07 The Java Specialists' Newsletter [Issue 103] -
New for/in loop gymnastics
Author:
Dr. Heinz M. Kabutz JDK version: Sun JDK 1.5.0_01
If you are reading this, and have not subscribed, please consider doing it now by going to our
subscribe page. You can subscribe either via email or RSS.
Welcome to the 103rd edition of The Java(tm) Specialists' Newsletter. After a weekend at
home, I am back in Johannesburg, continuing with the Java
5 Delta Course - Letting the Tiger out of the cage.
Please
let me know if your company would benefit from this
training. Because this is a new course, I am offering
it at excellent prices.
This morning, I read with great sadness in the Cape
Times that my Applied Mathematics professor Dr Brian
Hahn at the University of Cape Town passed away after being
brutally assaulted by a student. I attended Dr Hahn's
lectures in 1990, and besides being a great lecturer, I was
also impressed by his boldness in publicly declaring his
faith. This is a great loss for Cape Town, one of the most
beautiful cities in the world. So, here is a salute to
Dr Hahn - thanks for your hard work, and strength to your
family in this difficult time! [Oh, just to add fuel to the
fire, the suspect was released on bail of US$ 83]
And now, let us get our teeth into the new for/in construct
of Java.
New for/in loop gymnastics
The new for/in loop gives us the ability to iterate through
any object that implements the Iterable interface. In this
newsletter, I will show some applications of Iterable.
Result Set Iterable
The most obvious place of iteration that I immediately
thought of was the ResultSet. Currently, this does not
implement the Iterable interface, but you can add this using
an Object
Adapter. This class is a bit tricky, since the
columns could each have different types. My solution returns
a String[] of the columns for each row.
import java.sql.*;
import java.util.*;
/**
* This class allows you to iterate over a ResultSet using the
* standard for/in construct. It always returns String[] for
* each row, irrespective of the real types of the objects.
*
* @author Heinz Kabutz
* @since 2005/02/07
*/
public class ResultSetIterable implements Iterable<String[]> {
private final ResultSet rs;
private final int columns;
public ResultSetIterable(ResultSet rs) throws SQLException {
this.rs = rs;
columns = rs.getMetaData().getColumnCount();
}
public Iterator<String[]> iterator() {
return new Iterator<String[]>() {
private boolean nextCalled = true;
private boolean moreObjects;
public boolean hasNext() {
if (nextCalled) {
try {
moreObjects = rs.next();
nextCalled = false;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return moreObjects;
}
public String[] next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
try {
String[] values = new String[columns];
for (int i = 0; i < values.length; i++) {
values[i] = rs.getString(i + 1);
}
nextCalled = true;
return values;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
We can then create the ResultSetIterable from any ResultSet
and iterate through it with the new for/in loop, or use the
standard Iterator. This approach is better than copying
everything into a Collection and returning a handle to its
Iterator, since with a large ResultSet, that could cause an
OutOfMemoryError.
Here is a short example. You will have to replace the URL
and driver Strings with your own:
import java.sql.*;
import java.util.Arrays;
/**
* Here I am connecting to a database table and selecting all
* rows, then using the new for/in construct to iterate through
* the ResultSet.
*
* @author Heinz Kabutz
* @since 2005/02/07
*/
public class ResultSetIterableTest {
public static void main(String[] args) throws Exception {
// you have to set up your JDBC connection yourself
Class.forName("...");
Connection con = DriverManager.getConnection("...");
Statement st = con.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM customers");
for (String[] row : new ResultSetIterable(rs)) {
// Arrays.toString() is a new Tiger function
System.out.println(Arrays.toString(row));
}
}
}
I was impressed that this was so easy to do. We now have a
consistent approach to iteration :)
Iterable Input Streams and Readers
An idea that I learnt from the O'Reilly book on Tiger 1.5 Tiger: A Developer's Notebook by
Brett McLaughlin and David Flanagan (excellent book
btw, it will teach you many of the practical applications of
Tiger), is to read Strings from a file, or in our case, from
a stream of characters.
In this code, I am reading either from an InputStream of from
a Reader, one line at a time. When we construct the
InputParser, we specify what objects must be returned with
the Iterator. However, there is a slight problem. The
objects that are read are Strings, so how do we convert from
those to the objects that we want to return? Very easily, we
simply define an interface Parser<T> that takes a String
and converts it to your object. You need to implement that,
but I have provided some default Parsers in the InputParser
class.
The rest of the code is fairly self-explanatory. With the
Iterator, I return true if there are
more lines in the stream. One easier approach would be to
read all the lines into a collection, and then return an
iterator over that. However, the disadvantage is that you
have to read all the lines into memory. If you are reading
a large file, you might run out of memory.
import java.io.*;
import java.util.*;
/**
* I thought for a while for a good name for this class. This
* was the best I could come up with, but I am not 100% happy
* with it either.
*
* @author Heinz Kabutz
* @since 2005/02/07
*/
public class InputParser <T> implements Iterable<T> {
private final BufferedReader reader;
private final Parser<? extends T> parser;
public InputParser(Reader in, Parser<? extends T> parser) {
this.parser = parser;
reader = new BufferedReader(in);
}
public InputParser(InputStream in, Parser<? extends T> parser) {
this(new InputStreamReader(in), parser);
}
public Iterator<T> iterator() {
return new Iterator<T>() {
private String nextLine;
private boolean lineReadFromFile = false;
public boolean hasNext() {
if (!lineReadFromFile) {
try {
nextLine = reader.readLine();
} catch (IOException e) {
return false;
}
lineReadFromFile = true;
}
return nextLine != null;
}
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
lineReadFromFile = false;
return parser.convert(nextLine);
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* You use this interface to convert a String to your type of
* object T. I prefer defining interfaces closely to where
* they will be used, hence the inner interface Parser.
*/
public interface Parser<T> {
T convert(String s);
}
/**
* The default implementation simply returns the strings.
*/
public static final Parser<String> STRING_PARSER =
new Parser<String>() {
public String convert(String s) {
return s;
}
};
public static final Parser<Double> DOUBLE_PARSER =
new Parser<Double>() {
public Double convert(String s) {
return Double.parseDouble(s);
}
};
public static final Parser<Integer> INT_PARSER =
new Parser<Integer>() {
public Integer convert(String s) {
return Integer.parseInt(s.replaceAll("\\..*", ""));
}
};
/**
* A simple CSVParser that should probably be expanded to
* handle delimited separators, e.g. \,
*/
public static final Parser<String[]> CSV_PARSER =
new Parser<String[]>() {
public String[] convert(String s) {
List<String> result = new ArrayList<String>();
StringTokenizer st = new StringTokenizer(s, ",");
while (st.hasMoreTokens()) {
result.add(st.nextToken());
}
return result.toArray(new String[0]);
}
};
}
We can now read in numbers from any input stream, such as a
file, or a database, or a TCP/IP stream, and iterate through
them with the new for/in construct. For example:
import java.io.*;
/**
* @author Heinz Kabutz
* @since 2005/02/07
*/
public class NumberParserTest {
public static void main(String[] args) {
// Create a byte array that contains 10 random numbers less
// than 10000.
ByteArrayOutputStream bout = new ByteArrayOutputStream();
PrintStream out = new PrintStream(bout);
for (int i = 0; i < 10; i++) {
out.println(Math.random() * 10000);
}
out.close();
byte[] data = bout.toByteArray();
// Read the Strings in and convert them to doubles.
InputParser<Double> doubleFile = new InputParser<Double>(
new ByteArrayInputStream(data), InputParser.DOUBLE_PARSER);
double total = 0;
for (double d : doubleFile) { // this uses autoboxing
total += d;
}
System.out.println(total);
InputParser<Number> numberFile = new InputParser<Number>(
new ByteArrayInputStream(data), InputParser.INT_PARSER);
for (Number i : numberFile) {
System.out.println(i);
}
}
}
You can therefore iterate through InputParsers and read the
type that you have specified. On my run, it output the
following:
37817.79477180377
4572
5802
6213
3851
2402
5578
3894
263
13
5225
Parsing CSV to Generate Objects
Sometimes we need to read the construction parameters of
objects from a CSV file, so I have provided a default
CSVParser that converts a String to a String[]. Let us use
that together with a Person class:
/**
* @author Heinz Kabutz
* @since 2005/02/07
*/
public class Person {
private final String firstName;
private final String surname;
private final int age;
public Person(String firstName, String surname, int age) {
this.firstName = firstName;
this.surname = surname;
this.age = age;
}
@Override // always use this when you are overriding
public String toString() {
return firstName + " " + surname + ", " + age;
}
}
To generate a Person object from a String, we use the
CSVParser from the InputParser:
/**
* @author Heinz Kabutz
* @since 2005/02/07
*/
public class PersonParser implements InputParser.Parser<Person> {
public Person convert(String s) {
String[] values = InputParser.CSV_PARSER.convert(s);
String firstName = values[0];
String surname = values[1];
int age = Integer.parseInt(values[2]);
return new Person(firstName, surname, age);
}
}
And we can then use that PersonParser class to read in a
CSV file, and generate Person objects using the new for/in
loop:
import java.io.*;
/**
* @author Heinz Kabutz
* @since 2005/02/07
*/
public class PersonParserTest {
private static final String FILE = "persons.txt";
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(new FileWriter(FILE));
out.println("Heinz,Kabutz,33");
out.println("John,Green,33");
out.println("Anton,de Swardt,33");
out.println("Zachary,Thalla,32");
out.close();
InputParser<Person> personFile = new InputParser<Person>(
new FileInputStream(FILE), new PersonParser());
for (Person person : personFile) {
System.out.println(person);
}
}
}
Now, when we run this code, we end up with this output:
Heinz Kabutz, 33
John Green, 33
Anton de Swardt, 33
Zachary Thalla, 32
Until next time, and remember that if any of this is unclear
to you, we offer an excellent Java
5 delta course, that covers the new constructs :-)
The course is not for Java beginners, but rather for
experienced programmers. i.e. I will not go over elementary
constructs again.
Kind regards
Heinz
This material from The Java(tm)
Specialists' Newsletter by Maximum Solutions (South Africa). Please contact Maximum
Solutions for more information.
|