/
March towards Generics

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); }); } }

 

  1. Modify TextFileSource so that it has a built-in functor (we are going to embed this into the class)

  2. ICollectionLoader<ArrayList<String>> functor = (c, l) -> { c.add(l); return c; };
  3. We will provide an overloaded version of loadData()

  4. 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); }

     

  5. We will need to update the IDataSource interface

  6. public 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 ); }
  7. Modify the TextFileSource so that it is a generic class (only public ArrayList<String> loadData( String name ) for now)

  8. 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.

  9. 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 following

  10. public 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

  11. The class TextFileSource is still problematic, consider the method public <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); }
  12. 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); }
  13. The method public <T> T loadData( String fname ) is now out of sync with public <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 method

    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); }
  14. So the completed code as generics now looks like this

  15. 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; } }
  16. 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(); }

 

Related content