Classi e interfacce di utilità#
Nelle API di Java, per diversi tipi T
è comune incontrare una classe di nome
Ts
(di seguito vedremo ad esempio le classi Objects
, Arrays
e
Collections
). Tali classi (che non possono essere istanziate, avendo solo il
costruttore di default e con visiilità privata), sono dei “contenitori” di
metodi statici di utilità che riguardano il tipo T
.
Questa sezione presenta un paio di tali classi e alcuni dei loro metodi che possono risultare molto utili per la prova pratica; inoltre richiama brevemente le interfacce che definiscono come comparare gli oggetti secondo le API di Java.
La classe Objects
#
La classe
java.util.Objects
contiene alcuni metodi statici di utilità generale che possono essere adoperati
per tutti gli oggetti, indipendentemente dal loro tipo.
Sovrascrivere hashCode
#
Nel caso in cui si intendano sovrascrivere i metodi equals
e hashCode
di un
oggetto, il metodo hash
(che è variadico) può risultare molto comodo.
Se equals
viene sovrascritto come congiunzione dell’uguaglianza di (un
sottoinsieme degli) attributi dell’oggetto (diciamo attr_1
, attr_2
,
attr_N
), allora hashCode
può essere sovrascritto come
@Override
public int hashCode() {
return Objects.hash(attr_1, attr_2, attr_N);
}
piuttosto che implementando direttamente la ricetta proposta nell’Item 11 del Capitolo 3 del libro di testo “Effective Java”.
Gestire i null
#
Il metodo
requireNonNull
consente di verificare se una espressione è null
e, nel caso, sollevare una
NullPointerException
col messaggio indicato; ad esempio
Objects.requireNonNull(espressione, "Messaggio");
può essere usato invece di:
if (espressione == null)
throw new NullPointerException("Messaggio");
Dal momento che qualora l’espressione non sia null
il metodo ne restituisce il
valore, esso può essere convenientemente usato in un assegnamento o invocazione
di metodo; ad esempio
variabile = Objects.requireNonNull(espressione, "Messaggio");
può essere usato invece di:
if (espressione == null)
throw new NullPointerException("Messaggio");
variabile = espressione;
e similmente
Objects.requireNonNull(espressione, "Messaggio").metodo();
può essere usato invece di:
if (espressione == null)
throw new NullPointerException("Messaggio");
espressione.metodo();
Possono risultare comodi anche i metodi statici
equals
,
toString
e
hashCode
che possono essere usati anche su riferimenti null
; ad esempio
String stringa = Objects.toString(oggetto);
boolean uguali = Objects.equals(questo, quello);
int hash = Objects.hashCode(oggetto);
possono essere rispettivamente usati invece di
String stringa = oggetto == null ? "null" : oggetto.toString();
boolean uguali = questo == null ? quello == null : questo.equals(quello);
int hash = oggetto == null ? 0 : oggetto.hashCode();
Controllare indici e intervalli#
In molte circostanze può capitare di dover controllare se un indice (o un intervallo di indici interi, che può essere specificato dandone gli estremi, oppure l’estremo sinistro e la dimensione) è contenuto in un segmento iniziale dei numeri naturali (specificato tramite la sua dimensione).
Il metodo
checkIndex
e le sue varianti possono essere comodamente utilizzati a tale scopo: nel caso
la condizione sia soddisfatta, essi restituiscono il valore dell’indice (o il
limite inferiore dell’intervallo), viceversa sollevano una
IndexOutOfBoundException
.
Le interfacce Comparable
e Comparator
#
Se si è interessati a definire una relazione d’ordine sugli oggetti di una certo tipo sono possibili due strategie:
se gli oggetti sono dotati di un ordinamento naturale, generalmente si rendono comparabili facendo in modo che il loro tipo lo realizzi implementando l’interfaccia
Comparable
,se viceversa si vogliono tenere in considerazione più ordinamenti, si ricorre di volta in volta ad un comparatore diverso, ottenuto implementando opportunamente l’interfaccia
Comparator
.
Le due interfacce descritte prescrivono rispettivamente l’implementazione di un
metodo compareTo
(che compara l’oggetto corrente con un altro oggetto del
medesimo tipo), o di un metodo compare
(che compara due oggetti dello stesso
tipo tra loro).
Una discussione esaustiva di queste interfacce esula dagli scopi di questo documento, chi volesse approfondire è invitato a consultare la documentazione delle API e a leggere l’Item 14 del Capitolo 3 del libro di testo “Effective Java”.
Può essere però utile richiamare alcuni metodi (di default e statici) di
Comparator
che consentono di ottenere dei comparatori d’uso comune:
il metodo di default
reversed
che consente di ottenere il comparatore corrispondente all’ordine inverso;i metodi statici
naturalOrder
ereverseOrder
che restituiscono rispettivamente i comparatori dell’ordine naturale e del suo inverso;i metodi statici
nullsFirst
enullsLast
che restituiscono i comparatori ottenuti dal comparatore specificato che, in aggiunta, considerano i riferimentinull
rispettivamente minori o maggiori di ogni altro valore.
Osservate che i metodi relativi all’ordine naturale non hanno argomento, devono pertanto inferire il tipo del comparatore da restituire o dal contesto, come ad esempio in
Comparator<Integer> DA_GRANDE_A_PICCOLO = Comparator.reverseOrder();
DA_GRANDE_A_PICCOLO.compare(1, 2)
1
dove il tipo è dedotto da quello deella variabile a cui assegnare il risultato, oppure da uno hint, come in
Comparator.<Integer>reverseOrder().compare(2, 1)
-1
Un esempio di uso#
Si consideri una classe che rappresenti un orario della mattina (a prescindere dall’opportunità di sviluppare un tipo del genere, accennato qui a solo a titolo esemplificativo). Una implementazione minimale di tale tipo è data da
class OrarioMattina implements Comparable<OrarioMattina> {
private static final String[] NUMERO_A_PAROLE = {"mezzanotte", "una", "due", "tre", "quattro", "cinque", "sei", "sette", "otto", "nove", "dieci", "undici", "dodici"};
public final int ore, minuti;
public OrarioMattina(final int minutiDaMezzanotte) {
if (minutiDaMezzanotte < 0 || minutiDaMezzanotte >= 12 * 60 ) throw new IllegalArgumentException();
ore = minutiDaMezzanotte / 60;
minuti = minutiDaMezzanotte % 60;
}
public String ore() {
return NUMERO_A_PAROLE[ore];
}
@Override
public String toString() {
if (minuti == 0) return ore();
return ore() + " e " + minuti;
}
@Override
public int compareTo(OrarioMattina altro) {
int result = Integer.compare(ore, altro.ore);
if (result == 0) result = Integer.compare(minuti, altro.minuti);
return result;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof OrarioMattina)) return false;
final OrarioMattina altro = (OrarioMattina)obj;
return ore == altro.ore && minuti == altro.minuti;
}
@Override
public int hashCode() {
return Objects.hash(ore, minuti);
}
}
Il toString
della classe indca le ore in parole, seguite dai minuti (se non
pari a 0). Gli oggetti della classe sono comparabili secondo l’ordine naturale
dello scorrere del tempo; si osservi che (come da specifiche dell’interfaccia)
l’implementazione di compareTo
porta con se la necessità di implementare
equals
e hashCode
in modo coerente.
Dati due orari di questo tipo
OrarioMattina
colazione = new OrarioMattina(8 * 60 + 30),
merenda = new OrarioMattina(10 * 60);
è possibile confrontarli secondo l’ordine naturale come segue
colazione.compareTo(merenda) < 0
true
con l’atteso risultato che la colazione si fa prima della merenda.
Se ora volessimo confrontarli in base all’ordine lessicografico delle loro rappresentazioni testuali potremmo definire (qui facendo uso di una classe anonima) il comparatore
final Comparator<OrarioMattina> LESSICOGRAFICO_ORE = new Comparator<>() {
@Override
public int compare(OrarioMattina primo, OrarioMattina secondo) {
return primo.ore().compareTo(secondo.ore());
}
};
secondo quest’ultimo, l’ordine tra i due orari scelti in precedenza si ribalta
LESSICOGRAFICO_ORE.compare(colazione, merenda) > 0
true
in quanto “sette” viene lessicograficamente dopo “dieci” (dato che la “s” è dopo la “d” nell’ordine alfabetico).
La classe Arrays
#
La classe
java.util.Arrays
contiene alcuni metodi statici di utilità generale che riguardano gli array.
Per i metodi illustrati di seguito sono state scelte le segnature con argomenti
di tipo generico o Object
, osservate però che di ciascuno di essi esiste
(per ragioni di semplicità ed efficienza) una versione sovraccaricata per
ciascun tipo primitivo di argomento (come è ovvio sarà il compilatore a
scegliere la segnatura adatta di volta in volta, sulla scorta del tipo apparente
degli argomenti).
Il metodo toString
#
Può capitare molte volte di dover emettere il contenuto di un array sotto forma
di stringa, purtroppo l’implementazione del metodo toString
di Object
ereditata dagli array non è particolarmente leggibile; è però possibile usare il
metodo
toString
di questa classe per ottenere una rappresentazione molto semplice; ad esempio
int[] arr = new int[] {1, 2, 3, 4};
Arrays.toString(arr) + " è più leggibile di " + arr
[1, 2, 3, 4] è più leggibile di [I@3eef321c
Riempire o copiare#
Usando il metodo fill
è possibile riempire (un segmento) di un array con un valore di default; ad esempio
String[] slot = new String[6];
Arrays.fill(slot, 0, 3, "primi tre");
Arrays.fill(slot, 4, 6, "ultimi due");
Arrays.toString(slot)
[primi tre, primi tre, primi tre, null, ultimi due, ultimi due]
Esistono diversi metodi per ottenere una copia di un array (non tutti realizzati
tramite la classe Arrays
); è necessario ricordare che se gli elementi
dell’array non sono di tipo primitivo, tutti effettuano una copia per
indirizzo, ossia copiano solo
i riferimenti degli elementi dall’array d’origine a quello copiato, senza però
copiare gli elementi stessi. Questo significa, tra l’altro, che se gli elementi
sono di tipo mutabile attraverso la copia dell’array è possibile modificare
gli elementi dell’array di origine (ovviamente a prescindere dal fatto che i
riferimenti in cui sono memorizzati gli array siano dichiarati come final
!).
Il primo modo di ottenere una copia è data dal metodo
copyOf
;
osservate che tale metodo può produrre una copia con un numero di elementi
maggiore di quello dell’originale (popolando con null
le posizioni
aggiuntive). Questo può essere molto utile nel caso in cui, memorizzando valori
in un array, si stia per eccederne la dimensione: sarà sufficiente copiarlo in
uno di dimensione doppia e quindi procedere. Di ciascun metodo esistono anche
delle versioni sovraccaricate senza i limiti del segmento (che vengono assunti
coincidere con l’inizio e la fine dell’array). Con il metodo
copyOfRange
è invece possibile ottenere una copia di (un segmento) di un array; ad esempio
String[] subslot = Arrays.copyOfRange(slot, 2, 5);
Arrays.toString(subslot)
[primi tre, null, ultimi due]
Una delle versioni sovraccaricate di
copyOf
può essere usata per effettuare una sorta di “casting” tra array i cui elementi
siano l’uno il sottotipo dell’altro. Come è ben noto, anche se S
è sottotipo
di T
ed è certo che gli elementi di un array t
di tipo T[]
siano tutti di
tipo concreto S
, non è possibile effettuare il cast di tale array come
(S[])t
; per fare un esempio
Number[] numeri = new Number[] {1, 2, 3};
try {
Integer[] interi = (Integer[])numeri;
} catch (ClassCastException e) {
System.err.println(e);
}
java.lang.ClassCastException: class [Ljava.lang.Number; cannot be cast to class [Ljava.lang.Integer; ([Ljava.lang.Number; and [Ljava.lang.Integer; are in module java.base of loader 'bootstrap')
solleva una eccezione: evidentemente, il cast non può avvenire solo sul
riferimento, perché la cosa avesse senso, dovrebbe venir applicato anche
elemento per elemento (ad esempio con un ciclo); ma si può anche usare copyOf
nella versione che accetta una istanza di
Class
per
determinare il tipo degli elementi
Integer[] interi = Arrays.copyOf(numeri, numeri.length, Integer[].class);
Arrays.toString(interi)
[1, 2, 3]
Per concludere, vale la pena ricordare anche due modi di ottenere una copia
indipendenti dalla classe Arrays
.
Il più elementare è usare il metodo clone
dell’array stesso. Il secondo è
usare il metodo statico
arraycopy
della classe System
. Questo metodo invece di restituire la copia in nuovo
array, copia i riferimenti da un array sorgente ad uno destinazione (che deve
essere già allocato e della dimensione opportuna); ad esempio
int[] positivi = new int[] {1, 2, 3, 4, 5, 6, 7};
int[] negativi = new int[] {-1, -2, -3, -4, -5, -6, -7};
System.arraycopy(positivi, 2, negativi, 1, 3);
ha l’effetto di copiare 3 elementi dalla posizione 2 di positivi
alla posizione 1 di negativi
Arrays.toString(negativi)
[-1, 3, 4, 5, -5, -6, -7]
Adattare la dimensione di un array#
Vogliamo raccogliere in un array di long
di nome pows
gli elementi
dell’insieme \(\{n < 10^{12} | n = 2^{2k}, k \geq 0 \}\); supponendo di non
conoscere a priori la cardinalità dell’insieme, possiamo riempire iterativamente
l’array, inizialmente di dimensione 1, raddoppiandone la dimensione ogni volta
che il numero i
di potenze individuate ne uguaglia la lunghezza
long[] pows = new long[1];
long n = 0;
int i = 0, k = 0;
while ((n = (long)Math.pow(2, 2 * k++)) < 1_000_000_000_000L) {
if (i == pows.length) pows = Arrays.copyOf(pows, pows.length * 2);
pows[i++] = n;
}
pows = Arrays.copyOf(pows, i);
Arrays.toString(pows)
[1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824, 4294967296, 17179869184, 68719476736, 274877906944]
Osservate come, oltre al modo comodo di scrivere la costante \(10^{12}\) come
1_000_000_000_000L
interponendo per leggibilità il separatore _
, al termine
del riempimento, si possono eliminare le posizioni rimaste vuote dell’array
copiandolo in uno di dimensione esattamentepari al numero totale di potenze
individuate.
Confrontare#
Il metodo
equals
può essere usato per decidere se due array contengono lo stesso numero di
elementi e tutti gli elementi in posizione corrispondente risultano uguali
(secondo il metodo equals
del tipo degli element dell’array, o la comparazione
con ==
nel caso di tipi primitivi).
In modo analogo, per i tipi primitivi e quelli che sono comparabili il metodo
compare
permette di determinare l’ordine
lessicografico tra i due
array, basandosi sull’ordine naturale degli elementi. Nel caso in cui gli
elementi non siano comparabili (o si voglia utilizzare un ordine diverso da
quello naturale), esiste una versione sovraccaricata del metodo
compare
che accetta un
Comparator
come argomento.
Ordinare e cercare#
Dato un vettore, è possibile ordinarlo in loco secondo l’ordine naturale dei suoi elementi tramite il metodo sort
, oppure specificando esplicitamente un comparatore con la versoine sovraccaricata sort
.
Riutilizzando la classe OrarioMattina
della sezione precedente, ad esempio
OrarioMattina[] orari = new OrarioMattina[] {colazione, merenda, new OrarioMattina(5 * 60 + 10)};
Arrays.sort(orari);
Arrays.toString(orari)
[cinque e 10, otto e 30, dieci]
permette di ordinare gli orari secondo l’ordine naturale, mentre
Arrays.sort(orari, LESSICOGRAFICO_ORE);
Arrays.toString(orari)
[cinque e 10, dieci, otto e 30]
li ordina secondo l’ordine lessicografico dell’ora (in parole). La versione in cui è possibile specificare il comparatore può essere utile per invertire l’orine; ad esempio
Arrays.sort(orari, LESSICOGRAFICO_ORE.reversed());
Arrays.toString(orari)
[otto e 30, dieci, cinque e 10]
oppure, basandosi sull’ordine naturale,
Arrays.sort(orari, Comparator.reverseOrder());
Arrays.toString(orari)
[dieci, otto e 30, cinque e 10]
Dato un vettore ordinato, è possibile cercare la posizione di un elemento nel
vettore (o scoprire se non è contenuto nel vettore), usando la ricerca
dicotomica, tramite il metodo
binarySearch
che si basa sull’ordine naturale, o la versione sovraccaricata di
binarySearch
che consente di specificare un comparatore (che, evidentemente, deve essere il
medesimo che era stat usato per ordinare l’array prima della ricerca).
Un esempio di ricerca e inserimento#
Come esempio della ricerca, consideriamo l’array cifreOrdinate
che contenga le
parole corrispondenti alle cifre decimali ordinato lessicograficamente, ottenuto
come
String[] cifreInParole = new String[] {"zero", "un", "due", "quattro", "cinque", "sei", "sette", "otto", "nove" };
String[] cifreInParoleOrdinate = cifreInParole.clone();
Arrays.sort(cifreInParoleOrdinate);
Arrays.toString(cifreInParoleOrdinate)
[cinque, due, nove, otto, quattro, sei, sette, un, zero]
Usiamo la ricerca per ottenere l’inversa della funzione che mappa i
in
cifreInParoleOrdinate[i]
, ossia la funzione cifraAPosizione
che mappa la
parola cifra
corrispondente a una cifra in parole nell’indice i
tale che
cifreInParoleOrdinate[i].equals(cifra)
sia vero
static final int cifraAPosizione(final String cifra) {
int valore = Arrays.binarySearch(cifreInParoleOrdinate, cifra);
if (valore < 0) throw new IllegalArgumentException();
return valore;
}
che si comporta come atteso
cifraAPosizione("zero")
8
dato che “z” è certamente l’ultima lettera dell’alfabeto.
Come è facile accorgersi, ci siamo scordati del tre, cercandolo infatti otteniamo un valore negativo dell’indice!
int idx = Arrays.binarySearch(cifreInParoleOrdinate, "tre");
idx
-8
Secondo il contratto del metodo di ricerca, un risultato negativo non solo
indica che l’elemento non è stato trovato, ma suggerisce la posizione pos
dove
dovrebbe venir inserito nell’array secondo la formula idx = -pos - 1
(che
ovviamente rende idx
sempre negativo); usando questa informazione è possibile
sistemare la mancanza
int pos = -idx - 1;
pos
7
A questo punto è sufficiente allocare un nuovo array corretto
con una
posizione in più, copiare dall’array cifreInParoleOrdinate
le posizioni fino a
pos
esclusa, aggiungere in tale posizione di corretto
la stringa "tre"
e
quindi copiare le rimanenti cifreInParoleOrdinate.length - pos
posizioni da
cifreInParoleOrdinate
in corretto
a partire da pos + 1
String[] corretto = new String[cifreInParoleOrdinate.length + 1];
System.arraycopy(cifreInParoleOrdinate, 0, corretto, 0, pos);
corretto[pos] = "tre";
System.arraycopy(cifreInParoleOrdinate, pos, corretto, pos + 1, cifreInParoleOrdinate.length - pos);
Arrays.toString(corretto)
[cinque, due, nove, otto, quattro, sei, sette, tre, un, zero]
Nota
A maggior conferma del fatto che nessuna delle conoscenze di questo documento è
strettamente necessaria al superamento della prova pratica, quanto mostrato sin
qui consente di costruire e mantenere un array (eventualmente ordinato) di
dimensione adattabile facendo uso soltanto di concetti elementari (appresi
dall’insegnamento di “Programmazione” del primo anno) che, peraltro, possono
essere molto facilmente implementati anche usando esclusivamente array e cicli
for
.
Con un array di dimensione adattabile è molto semplice implementare buona parte dei comportamenti delle collezioni che saranno illustrate nella seguente sezione (può essere un ottimo esercizio provare a farlo!). Ai fini del superamento della prova pratica, le liste possono essere banalmente sostituite da un array di dimensione adattabile, così come lo possono gli insiemi (e sufficiente prestare attenzione ai duplicati); persino le mappe possono essere implementate senza scomodare nessuna struttura dati tra quelle più sofisticate (incontrate nell’insegnamento di “Algoritmi e strutture dati” del secondo anno), ma usando semplicemente due array “paralleli”; tale implementazione può essere persino resa ragionevolmente efficiente se le chiavi sono comparabili!
Concatenare stringhe#
Le stringhe in Java sono immutabili, questo comporta che se è necessario concatenarne un numero variabile occorre prestare attenzione ai risultati intermedi. Il codice
String[] parti = {"queste", "stringhe", "vengono", "concatenate", "in", "modo", "inefficiente"};
String risultato = "";
for (int i = 0; i < parti.length - 1; i++) risultato += parti[i] + " ";
risultato += parti[parti.length - 1];
risultato
queste stringhe vengono concatenate in modo inefficiente
produrrà, ad ogni iterazione, un prefisso del risultato finale che resterà nello heap finché il garbage collector non lo eliminerà (in quanto non riferito da alcuna variabile). Attenzione che questo non vale se il numero di stringhe è fisso e noto a priori; il codice
String risultato = "queste" + " " + "stringhe" + " " + "vengono" + " " + "concatenate" + " " + "in" + " " + "modo" + " " + "corretto!";
risultato
queste stringhe vengono concatenate in modo corretto!
non presenta alcun problema di efficienza perché non produce alcun risultato intermedio (di cui doversi liberare).
Le API mettono a disposizione tre modi (di complessità e versatilità crescente) per ottenere la concatenazione di un numero variabile di stringhe.
Usando un metodo di String
#
Il primo è il metodo
String.join
,che funziona con un array, o con un numero variabile di argomenti, e la
versione
String.join
,
che funziona con un iterabile. L’esempio precedente si può riscrivere come
String risultato = String.join(" ", parti);
risultato
queste stringhe vengono concatenate in modo inefficiente
Usando la classe StringJoiner
#
Se si vuole specificare non solo il separatore ma anche il prefisso e
suffisso del risultato è possibile usare la classe
StringJoiner
come nel seguente esempio
StringJoiner sj = new StringJoiner(", ", "<", ">");
for (String parte : parti) sj.add(parte);
sj.toString()
<queste, stringhe, vengono, concatenate, in, modo, inefficiente>
Usando la classe StringBuilder
#
Per finire, volendo avere il controllo completo del processo, ad esempio usando
separatori diversi, è possibile usare la classe
StringBuilder
come nel seguente esempio
String[] separatori = {":", ";", "."};
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parti.length - 1; i++) {
sb.append(parti[i]).append(separatori[i % separatori.length]).append(" ");
}
sb.append(parti[parti.length - 1]);
sb.toString()
queste: stringhe; vengono. concatenate: in; modo. inefficiente