March towards Generics
In this article, I want to describe my thought process as I worked towards the design of a Java Generic class
I will start with some pre-existing code
package com.celestial.generics;
/**
*
* @author selvy
*/
public interface IDataSource
{
public Iterable<String> loadData( String fname, ICollectionLoader func );
}
And the implementation class
package com.celestial.generics;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* We want to able to load data from any medium, not just of the disk, so we are going to turn the
* Iterable<String> loadFile(String fileName, ICollectionLoader func) into a contract,
* see the IDataSource interface
*/
public class TextFileSource implements IDataSource
{
@Override
public Iterable<String> loadData(String fileName, ICollectionLoader func)
{
// Currently, the collection used by the functor is hardcoded, but we change this shortly
ArrayList<String> lines = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = br.readLine()) != null) {
// Process the line
// we use a lambda here to do the heavy lifting
func.addElement(lines, line);
}
} catch (FileNotFoundException ex) {
Logger.getLogger(App.class.getName()).log(Level.SEVERE, "File not found: " + fileName, ex);
} catch (IOException ex) {
Logger.getLogger(App.class.getName()).log(Level.SEVERE, "I/O error occurred", ex);
}
return lines;
}
}
Here is the target test
package com.celestial.generics;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
class TextFileSourceTest
{
@Test
void howto_use_loadData_method_with_single_param()
{
TextFileSource<ArrayList tfl = new TextFileSource();
String fname = "C:\\tmp\\KeyboardHandler.java.txt";
ArrayList<String> lines = tfl.loadData(fname);
lines.forEach((element) ->{
System.out.println(">> " + element);
});
}
}
Modify
TextFileSource
so that it has a built-in functor (we are going to embed this into the class)ICollectionLoader<ArrayList<String>> functor = (c, l) -> { c.add(l); return c; };
We will provide an overloaded version of
loadData()
public ArrayList<String> loadData( String fname ) { // We create a lambda expression to do the work in the TextFileLoader ICollectionLoader<ArrayList<String>> functor = (c, l) -> { c.add(l); return c; }; return loadData(fname, functor); }
We will need to update the
IDataSource
interfacepublic interface IDataSource { public ArrayList<String> loadData( String fname ); public ArrayList<String> loadData( String fname, ICollectionLoader func ); }
public interface IDataSource<T> { public <T> T loadData(String fname ); public <T> T loadData( String fname, ICollectionLoader func ); }
Modify the TextFileSource so that it is a generic class (only
public ArrayList<String> loadData( String name )
for now)public class TextFileSource<T> implements IDataSource<T> { @Override public <T> T loadData( String fname ) { // We create a lambda expression to do the work in the TextFileLoader ICollectionLoader<ArrayList<String>> functor = (c, l) -> { c.add(l); return c; }; return (T) loadData(fname, functor); }
Notice lines 1, 4, and 10. Observe how the generics are being used.
You’ll get quite a few errors. This is because the method declaration no longer matches the method declaration in the IDataSource interface. Currently the operations on IDataSource all return
ArrayList<String>
. This is a highly constrained implementation, so we are going to change the interface to the followingpublic interface IDataSource<T> { public <T> T loadData(String fname ); public <T> T loadData( String fname, ICollectionLoader func ); }
We don’t know what collection type will be used this is why the return type has been declared as generic
The class
TextFileSource
is still problematic, consider the methodpublic <T> T loadData(String fileName, ICollectionLoader func)
, it creates an ArrayList<String> object at line 5. We need to remove this constraint.@Override public <T> T loadData(String fileName, ICollectionLoader func) { // Currently, the collection used by the functor is hardcoded, but we change this shortly ArrayList<String> lines = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new FileReader(fileName))) { String line; while ((line = br.readLine()) != null) { // Process the line // we use a lambda here to do the heavy lifting func.addElement(lines, line); }
We need to modify the method so that the collection is passed in and not created inside the method. We will also need to update the interface
IDataSource
public <T> T loadData( String fname, T lines, ICollectionLoader<T> func ) { try (BufferedReader br = new BufferedReader(new FileReader(fname))) { String line; while ((line = br.readLine()) != null) { // Process the line // we use a lambda here to do the heavy lifting func.addElement(lines, line); }
The method
public <T> T loadData( String fname )
is now out of sync withpublic <T> T loadData( String fname, T lines, ICollectionLoader<T> func )
because the latter method requires a collection to be passed in that will be the recipient of the data loaded from a source. We will temporarily create an ArrayList<String> in the former method and pass it to the latter methodpublic <T> T loadData( String fname ) { ArrayList<String> lines = new ArrayList<>(); // We create a lambda expression to do the work in the TextFileLoader ICollectionLoader<ArrayList<String>> functor = (c, l) -> { c.add(l); return c; }; return (T)loadData(fname, lines, functor); }
So the completed code as generics now looks like this
TextFileSource
package com.celestial.generics; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; /** * We want to able to load data from any medium, not just of the disk, so we are going to turn the * Iterable<String> loadFile(String fileName, ICollectionLoader func) into a contract, * see the IDataSource interface */ public class TextFileSource<T> implements IDataSource<T> { @Override public <T> T loadData( String fname ) { ArrayList<String> lines = new ArrayList<>(); // We create a lambda expression to do the work in the TextFileLoader ICollectionLoader<ArrayList<String>> functor = (c, l) -> { c.add(l); return c; }; return (T)loadData(fname, lines, functor); } @Override public <T> T loadData( String fname, T lines, ICollectionLoader<T> func ) { try (BufferedReader br = new BufferedReader(new FileReader(fname))) { String line; while ((line = br.readLine()) != null) { // Process the line // we use a lambda here to do the heavy lifting func.addElement(lines, line); } } catch (FileNotFoundException ex) { Logger.getLogger(App.class.getName()).log(Level.SEVERE, "File not found: " + fname, ex); } catch (IOException ex) { Logger.getLogger(App.class.getName()).log(Level.SEVERE, "I/O error occurred", ex); } return (T)lines; } }
TextFileSourceTest
package com.celestial.generics; import org.junit.jupiter.api.Test; import java.util.ArrayList; import static org.junit.jupiter.api.Assertions.*; class TextFileSourceTest { @Test public void test_count_chars_in_DataSource_no_mocking() { TextFileSource<ArrayList<String>> tfl = new TextFileSource<>(); String fname = "C:\\tmp\\KeyboardHandler.java.txt"; ArrayList<String> lines = tfl.loadData(fname); lines.forEach((element) ->{ System.out.println(">> " + element); }); } @Test void howto_use_LoadData_method_with_3_params() { TextFileSource<ArrayList<String>> tfl = new TextFileSource<>(); // We create a lambda expression to do the work in the TextFileLoader ICollectionLoader<ArrayList<String>> functor = (c, l) -> { c.add(l); return c; }; String fname = "C:\\tmp\\KeyboardHandler.java.txt"; ArrayList<String> data = new ArrayList<>(); ArrayList<String> lines = tfl.loadData(fname, data, functor); lines.forEach((element) ->{ System.out.println(">> " + element); }); } }
Deeper Generics
I still have a problem with the codebase. Line 21 of TextFileSource file above. The hard-coded creation of the ArrayList<String>
. I really want the collection object that is created in TextFileSource.loadData( String name)
to be of the type <T> by the method itself.
You can’t instantiate generics like this T var = new T()
where T is a generic parameter.
There is an interesting article on this subject at this location. We will gradually move towards one of the solutions shown in this article.
Firstly we will update IDataSource
to the following
public interface IDataSource<T extends Collection>
{
public <T extends Collection> T loadData(String fname );
public <T extends Collection> T loadData( String fname, T line, ICollectionLoader<T> func );
}
Now we need to update TextFileSource
to the following (we will break it down)
The loadData( String fname, T lines, ICollectionLoader<T> func )
method is now
@Override
public <T extends Collection> T loadData( String fname, T lines, ICollectionLoader<T> func )
{
try (BufferedReader br = new BufferedReader(new FileReader(fname))) {
String line;
while ((line = br.readLine()) != null) {
// Process the line
// we use a lambda here to do the heavy lifting
func.addElement(lines, line);
}
} catch (FileNotFoundException ex) {
Logger.getLogger(App.class.getName()).log(Level.SEVERE, "File not found: " + fname, ex);
} catch (IOException ex) {
Logger.getLogger(App.class.getName()).log(Level.SEVERE, "I/O error occurred", ex);
}
return (T)lines;
}
And the loadData( String fname )
method is now
@Override
public <T extends Collection> T loadData( String fname )
{
//ArrayList<String> lines = new ArrayList<>();
// We create a lambda expression to do the work in the TextFileLoader
ICollectionLoader<ArrayList<String>> functor = (c, l) -> {
c.add(l);
return c;
};
return (T)loadData(fname, lines, functor);
}
We want to create the collection object based on the class type bound to T
. Java does not allow us to create an object using the new keyword on generic types
public class GenericClass<T> {
private T t;
public GenericClass() {
this.t = new T(); // DOES NOT COMPILE
}
}
Generics in Java lose their type info (type erasure) once the code has been compiled - all generic types become Object
once the code is compiled. This means the compile cycle is where type checking is accomplished.
We need some way of injecting the type info into the code for runtime use. We are going to use a factory object to hold the type info
package com.celestial.generics;
import java.util.Collection;
public class CollectionFactory<T extends Collection>
{
private Class<T> clazz;
public CollectionFactory(Class<T> clazz) {
this.clazz = clazz;
}
public <T> T createInstance() {
try {
T result = (T)clazz.getDeclaredConstructor().newInstance();
return result;
} catch (Exception e) {
throw new RuntimeException("Error while creating an instance.");
}
}
}
TextFileSource
is now modified to look like this
public class TextFileSource<T extends Collection> implements IDataSource<T>
{
private T lines;
public <T extends Collection<T>> TextFileSource(Class<T> clazz)
{
CollectionFactory<T> service = new CollectionFactory<>( clazz );
lines = service.createInstance();
}