Dans cet article nous montrons comment rendre générique une classe java afin de simplifier le code.
Les classes génériques ont été introduite en java 5 et améliorées en Java 8.
Nous allons effectuer la transformation sur une classe très utilisée dans notre code. Pour gérer de façon temporaire des données d’un tableau, nous avons souvent besoin d’une zone de type Hashtable mais avec 2 entrées : lignes et colonnes.
Nous avons créé la classe SquareMap sur le modèle des collections java.
Cette classe pourra être utilisée dans du code multithread. Aussi, nous avons choisi de la rendre compatible en utilisant la classe ConcurrectHashMap et en particulier la méthode de cette classe putIfAbsent.
Une Hashtable multithread à deux entrées
Le principe est de traiter des entrées put et des sorties get de la façon suivante :
- put(x, y, object)
- object=get(x,y)
La classe SquareMap.java
import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class SquareMap { private ConcurrentHashMap<String, ConcurrentHashMap> records = new ConcurrentHashMap<String, ConcurrentHashMap>(); private ConcurrentHashMap<String, Object> getOrCreate(String x) { ConcurrentHashMap<String, Object> record = records.get(x); if (record == null) { // record does not yet exist ConcurrentHashMap<String, Object> newRecord = new ConcurrentHashMap<String, Object>(); record = records.putIfAbsent(x, newRecord); if (record == null) { // put succeeded, use new value record = newRecord; } } return record; } public Object get(String x, String y) { ConcurrentHashMap<String, Object> hy = records.get(x); if (hy == null) { return null; } return hy.get(y); } public Object put(String x, String y, Object value) { if (x == null || y == null || value == null) { //PlugsetLog.println("Error : x" + x + " y=" + y + " value=" + value); return null; } ConcurrentHashMap<String, Object> hy = getOrCreate(x); return hy.put(y, value); } public Object remove(String x, String y) { ConcurrentHashMap<String, Object> hy = records.get(x); if (hy == null) { return null; } return hy.remove(y); } public boolean containKey(String x) { return records.containsKey(x); } public boolean containKey(String x, String y) { ConcurrentHashMap<String, Object> hy = records.get(x); if (hy == null) { return false; } return hy.containsKey(y); } public Set<String> keySet() { return records.keySet(); } public Set<String> keySet(String x) { ConcurrentHashMap<String, Object> hy = records.get(x); return hy.keySet(); } public int size() { return records.size(); } public int size(String x) { ConcurrentHashMap<String, Object> hy = records.get(x); return hy.size(); } }
Maintenant, nous avons des cas pour lesquels nous souhaitons parcourir des tableaux avec des lignes numérotées par un entier. Nous avons besoin que notre première valeur soit un Integer et non une String.
Inutile de créer une copie de cette classe pour changer les types des paramètres. Java a introduit les classes génériques. Nous pouvons donc rendre générique notre classe SquareMap.
Modification de la classe SquareMap en classe générique
La classe générique consiste à offrir aux utilisateurs de la classe la possibilité de définir les classes à utiliser pour les paramètres x, y, object. Nous allons utiliser des noms de classe génériques : LINE pour x, COLUMN pour y et VALUE pour object.
Transformation de SquareMap en classe générique :
import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class SquareMap <LINE,COLUMN,VALUE> { private ConcurrentHashMap<LINE, ConcurrentHashMap> records = new ConcurrentHashMap<LINE, ConcurrentHashMap>(); private ConcurrentHashMap<COLUMN, VALUE> getOrCreate(LINE line) { ConcurrentHashMap<COLUMN, VALUE> record = records.get(line); if (record == null) { // record does not yet exist ConcurrentHashMap<COLUMN, VALUE> newRecord = new ConcurrentHashMap<COLUMN, VALUE>(); record = records.putIfAbsent(line, newRecord); if (record == null) { // put succeeded, use new value record = newRecord; } } return record; } public VALUE get(LINE line, COLUMN column) { ConcurrentHashMap<COLUMN, VALUE> record = records.get(line); if (record == null) { return null; } return record.get(column); } public VALUE put(LINE line, COLUMN column, VALUE value) { if (line == null || column == null || value == null) { //PlugsetLog.println("Error : x" + x + " y=" + y + " value=" + value); return null; } ConcurrentHashMap<COLUMN, VALUE> record = getOrCreate(line); return record.put(column, value); } public VALUE remove(LINE line, COLUMN column) { ConcurrentHashMap<COLUMN, VALUE> record = records.get(line); if (record == null) { return null; } return record.remove(column); } public boolean containKey(LINE line) { return records.containsKey(line); } public boolean containKey(LINE line, COLUMN column) { ConcurrentHashMap<COLUMN, VALUE> record = records.get(line); if (record == null) { return false; } return record.containsKey(column); } public Set<LINE> keySet() { return records.keySet(); } public Set<COLUMN> keySet(LINE line) { ConcurrentHashMap<COLUMN, VALUE> record = records.get(line); return record.keySet(); } public int size() { return records.size(); } public int size(LINE line) { ConcurrentHashMap<COLUMN, VALUE> record = records.get(line); return record.size(); } }
Par convention, les nom des classes génériques sont définies sur un seul caractère. Nous allons donc définir notre classe avec L=LINE, C=COLUMN et V=VALUE :
Classe SquareMap en respectant la convention des classes génériques :
import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class SquareMap <L,C,V> { private ConcurrentHashMap<L, ConcurrentHashMap> records = new ConcurrentHashMap<L, ConcurrentHashMap>(); private ConcurrentHashMap<C, V> getOrCreate(L line) { ConcurrentHashMap<C, V> record = records.get(line); if (record == null) { // record does not yet exist ConcurrentHashMap<C, V> newRecord = new ConcurrentHashMap<C, V>(); record = records.putIfAbsent(line, newRecord); if (record == null) { // put succeeded, use new value record = newRecord; } } return record; } public V get(L line, C column) { ConcurrentHashMap<C, V> record = records.get(line); if (record == null) { return null; } return record.get(column); } public V put(L line, C column, V value) { if (line == null || column == null || value == null) { //PlugsetLog.println("Error : x" + x + " y=" + y + " value=" + value); return null; } ConcurrentHashMap<C, V> record = getOrCreate(line); return record.put(column, value); } public V remove(L line, C column) { ConcurrentHashMap<C, V> record = records.get(line); if (record == null) { return null; } return record.remove(column); } public boolean containKey(L line) { return records.containsKey(line); } public boolean containKey(L line, C column) { ConcurrentHashMap<C, V> record = records.get(line); if (record == null) { return false; } return record.containsKey(column); } public Set<L> keySet() { return records.keySet(); } public Set<C> keySet(L line) { ConcurrentHashMap<C, V> record = records.get(line); return record.keySet(); } public int size() { return records.size(); } public int size(L line) { ConcurrentHashMap<C, V> record = records.get(line); return record.size(); } }
Nous avons montré comment remplacer notre classe adhoc par une classe générique. Lors de l’utilisation de la classe, nous devons maintenant spécifier les types de classes pour LINE, COLUMN et VALUE.
Rendre générique une classe java, c’est bien, l’utiliser c’est mieux !
Nous allons pleinement bénéficier de la classe générique grâce à l’auto-boxing qui permet d’utiliser des entiers directement.
Exemple de mise en œuvre :
// création d'une classe avec les des clés sous forme de String SquareMap<String, String, Object> mySquareMap= new SquareMap<String, String, Object>(); //exemple pour des enregistrements dans un tableau avec des numéro de ligne SquareMap<Integer, String, Object> intSquareMap=new SquareMap<Integer, String, Object>(); //on notera que nous avons un type Integer. //Toutefois, l'unboxing permettra d'utiliser des entiers int, ce qui va vraiment simplifier le travail : intSquareMap.put(0,"name","Pierre"); intSquareMap.put(1,"name","Xavier"); intSquareMap.put(2,"name","Yves"); intSquareMap.put(3,"name","André"); intSquareMap.put(4,"name","Jacques"); intSquareMap.put(5,"name","Jean"); intSquareMap.put(6,"name","Philippe"); intSquareMap.put(7,"name","Luc"); intSquareMap.put(8,"name","Matthieu"); intSquareMap.put(9,"name","Paul"); for(int i=0;i<10;i++){ Object value=intSquareMap.get(i,"name"); System.out.println("name["+i+"]="+value); }
Grâce à l’autoboxing et le unboxing, nous pouvons utiliser des entiers int à la place de Integer.
Nous créons une classe de test dans Android Studio :
import static org.junit.Assert.assertEquals; import org.junit.Test; public class SquareMapTest { @Test public void string_SquareMap() { SquareMap<String, String, Object> mySquareMap= new SquareMap<String, String, Object>(); mySquareMap.put("key1","key2","value"); assertEquals("value", mySquareMap.get("key1","key2")); } @Test public void integer_SquareMap() { String [] prenoms = { "Pierre", "Xavier", "Yves", "André", "Jacques", "Jean", "Philippe", "Luc", "Matthieu", "Paul" }; SquareMap<Integer, String, Object> intSquareMap= new SquareMap<Integer, String, Object>(); for(int i=0;i<10;i++) { intSquareMap.put(i, "name", prenoms[i]); } for(int i=0;i<10;i++){ Object value=intSquareMap.get(i,"name"); System.out.println("name["+i+"]="+value); assertEquals(value, prenoms[i]); } } }
Pour executer le test dans Android Studio, nous activons l’icone [Run] (1).
Le résultat est conforme, et nous le voyons en (2) et en (3) : « Test Passed ».
Conclusion
Nous avons montré comment utiliser les types génériques pour rendre générique une classe java.
Nous utilisons cette classe dans nos programmes pour traiter des fichiers en csv (comma separated value). Ainsi, la ligne d’en-tête donne la liste des noms de colonnes. Enfin, chaque ligne suivante donne les valeurs des enregistrements.