Generics tends to address the problem of what I deem "naive casts" in Java 1.4 or earlier when dealing with Collections. In Java 1.5+, the line you've placed:
ArrayList<Row> rows = new ArrayList();
would give a warning, the proper generic code is
ArrayList<Row> rows = new ArrayList<Row>();
This tells the compiler that your ArrayList object should only ever contain the Row type.
However, since Java 1.5 is backwards-compatible with huge sets of libraries that don't contain that syntax, but rather your previous code:
ArrayList rows = new ArrayList();
Generics obviously wouldn't work with these older libraries - therefore generics are only a compile time option - the 1.5 and 1.4 classes are effectively equivalent (minus any internal refactoring/new methods added later) because they are really ArrayList implementations that handle any Object type.
The 1.5 code simply adds in a direct cast for you.
In 1.4 code, say you wanted to iterate over the ArrayList. The naive cast way to do this is the following:
for(Iterator rowIterator = rows.iterator(); rowIterator.hasNext(); ) {
Row row = (Row) rowIterator.next();
// Do something with the row.
}
Java 1.5's code is exactly equivalent to the naive cast version. It takes the fact that you are telling the compiler that it is a row and does that code for you. So, the syntactic sugar benefits are nicer (this uses the newer for each loop syntax, but it generates the same code as the above loop):
for(Row row : rows) {
// Do something with the row
}
So, if you want to use an ArrayList containing only rows, you still can. But there's no way to get the compiler to check that the ArrayList contains only all rows (although, even though the compiler provides this check, it is still possible to send in an ArrayList that contains other types of objects, since, again, the ArrayList still only really handles the Object type, and the generics are erased at runtime - all that remains is the naive cast code that you don't see anymore).
The non-naive variant is to check each instance and throw a ClassCastException yourself with an informative message (rather than have the program throw one with its default message):
for(Iterator rowIterator = rows.iterator(); rowIterator.hasNext(); ) {
Object shouldBeRow = rowIterator.next();
if(!(shouldBeRow instanceof Row)) {
throw new ClassCastException("The object " + shouldBeRow + " is not an instance of Row - only Rows should be present in the list!");
}
Row row = (Row) shouldBeRow;
// Do something with the row.
}
However, generally no one does this - good documentation can make this issue moot, as it places the burden of providing the correct collection on the caller and thus you can just have the ClassCastException thrown by the JVM.