Implementazione della Map
Per implementare la funzione MAP, creiamo una classe nel package map
e la chiamiamo Map
. Durante la creazione della classe, dobbiamo avere cura di ereditare la classe Mapper
. Per fare ciò, possiamo parametrizzare la maschera di creazione di una classe come nella figura sottostante:
Come possiamo notare nel campo "Superclass" della maschera, Mapper
può essere parametrizzata sostituendo gli input e gli output, noti dalla fase di progettazione. La classe ha la struttura seguente:
public class Map extends Mapper {
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {}
}
La classe estesa permette di mappare gli oggetti di input <KeyIN,ValueIN>
della funzione Map
rappresentati dai primi due parametri di Mapper
(parametrizzati nel nostro caso con oggetti LongWritable
e Text
) con gli oggetti di output <KeyOUT,ValueOUT> (Text
e IntWritable
) che a loro volta costituiranno gli input della funzione Reduce
. Possiamo quindi immaginare il motivo della presenza dei primi due parametri dello stesso tipo di <KeyIN,ValueIN>
per il metodo map()
.
L'oggetto value
contiene una riga letta dal file di input. Ottenuta la stringa contenuta in value
, possiamo tokenizzarla, incapsularla in un oggetto locale di tipo Text
che verrà dato in output incapsulandolo nell'oggetto context
(ultimo parametro di ingresso di map
). Traducendo in codice quanto detto otteniamo la seguente implementazione del metodo map
:
public class Map extends Mapper {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
StringTokenizer tokenizer = new StringTokenizer(line);
while (tokenizer.hasMoreTokens()) {
word.set(tokenizer.nextToken());
context.write(word, one);
}
}
}
Notiamo quindi l'utilizzo di due oggetti della classe Map
(one
e word
) che vengono utilizzati come l'output intermedio e inoltrati al Reduce
attraverso il metodo write()
dell'oggetto context
. Con un procedimento analogo creiamo la classe Reduce
all'interno del relativo package:
Ancora una volta la classe Reducer può essere parametrizzata sostituendo opportunamente gli input e gli output. La classe creata ha la struttura seguente:
public class Reduce extends Reducer {
public void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {}
}
I valori di interesse in input sono incapsulati nell'oggetto Iterable
. Scorrendo questo elenco infatti, possiamo recuperare i singoli valori e ricostruire il numero di occorrenze sommandoli ad ogni iterazione:
public class Reduce extends Reducer {
public void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
context.write(key, new IntWritable(sum));
}
}
Il risultato dell'elaborazione della funzione reduce
, viene fornito attraverso il metodo write()
dell'oggetto context
. All'intero del codice non è stato necessario gestire i file di input e di output: delle operazioni di lettura/scrittura dei file di I/O infatti se ne occupa il framework. Lo sviluppatore si occuperà solo di specificare quelle che sono le directory di input e di output (classe Main).