Java Tutorial - Parte 1 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Organizziamo i packages

Introduzione

Fino a questo momento non ci siamo mai preoccupati del nome del file sorgente nel quale scriviamo la nostra classe. Non solo, nel nostro piccolo programma di gestione delle figure geometriche abbiamo definito due classi all'interno di un unico file sorgente (vedi I files del capitolo).

Adesso però vogliamo organizzare meglio il nostro progetto; i sorgenti dovranno essere suddivisi in cartelle, ognuna delle quali tratterà un argomento specifico. Inoltre, vogliamo evitare che i nomi delle classi del nostro progetto possano andare in conflitto con i nomi di classi di altri progetti.
Per esempio, la classe Rectangle che rappresenta la figura piana di un rettangolo potrebbe probabilmente avere lo stesso nome in altri progetti e questo potrebbe causare un conflitto tra i nomi delle classi.

In questo capitolo e nei successivi scriveremo una piccola applicazione che estrae e conteggia i numeri primi minori di / uguale ad un dato numero n che sarà di tipo long. Useremo un algoritmo leggermente migliore della "divisione per tentativi", noto come Crivello di Eratostene ed organizzeremo i sorgenti in cartelle separate che rappresentano i packages del nostro progetto.
Cercheremo di ottimizzare il codice per ottenere migliori performances ed imparerete ad interfacciare i programmi Java con il codice nativo scritto in linguaggio "C" che consente di superare alcuni limiti di Java.

Il conflitto dei nomi

Prendiamo come esempio il progetto di gestione delle figure geometriche nel quale abbiamo definito la classe Rectangle (vedi Un primo approccio) e poniamoci una domanda: quante altre classi Rectangle potrebbero esserci nei programmi Java? Sicuramente molte; lo stesso Java ne definisce una nella sua libreria standard: java.awt.Rectangle.

Java, come tutti i linguaggi di programmazione, impedisce che due o più identificatori possano avere lo stesso nome poichè ci sarebbe un conflitto: il compilatore non saprebbe quale essere usato.
La soluzione al problema del conflitto nei nomi delle classi sono i packages. Come abbiamo avuto già modo di osservare in Il disegno dei poligoni prima di usare un qualsiasi tipo di dato non-primitivo è necessario definirlo oppure importare il package in cui esso è definito.
Per esempio, per creare un oggetto di classe BufferedImage abbiamo dovuto importare il tipo di dato dal suo package:

import java.awt.image.BufferedImage;
public BufferedImage draw( int flags, Color color, Font font )
{
BufferedImage img = new BufferedImage( width, heigth, NufferedImage.TYPE_INT_ARGB );
... omissis ...

Una alternativa alla importazione del package è quella di specificare il fully qualified name del tipo di dato cioè il nome qualificato completo, composto dal nome della classe preceduto dal nome del package nel quale è stata definita.

java.awt.image.BufferedImage image;

I nomi di dominio Internet

Ci sarebbe ancora una questione da risolvere: anche se completamente qualificato, un nome di classe potrebbe andare ancora in conflitto con un altro se anche i nomi dei packages coincidono. La strategia introdotta in Java per evitare il conflitto è quello di usare i nomi di dominio Internet rovesciati per qualificare i packages. Per esempio, se la società Acme Spa che possiede il dominio acme.com, sviluppa un package per la gestione delle figure geometriche, potrebbe dare al package il nome:

com.acme.geometry 

Ben difficilmente potrà esistere un package con lo stesso nome, a meno che non sia sviluppato dalla stessa Acme SpA. Quella suesposta è una regola non scritta: il programmatore è comunque libero di scegliere il nome di package che preferisce.
Il nome che io ho scelto per questo package è javatutor.primes dal momento che il sottoscritto, al momento, non possiede alcun dominio Internet.

La struttura di un package

Il package ha una struttura che ricalca la organizzazione a cartelle e sottocartelle del filesystem del nostro sistema. Per creare il package javatutor.primes creiamo una cartella chiamata primes della cartella javatutor/projects; questa sarà la cartella radice del progetto..

 |-> javatutor1 
     |
      -> projects
         |
          -> hellow 
         |
          -> mcmd 
         |
          -> geometry 
         |
          -> primes    = cartella radice del progetto

Dalla cartella radice creiamo la sottocartella javatutor/primes e, quando il progetto diventerà mano a mano più complesso, avremo una gerarchia simile a questa:

 |-> projects
     |
      -> primes    = cartella radice del progetto
        |
         -> javatutor
            |
             -> primes
                |
                 -> crivello
                    |
                     -> jni
                    |
                     -> ffm

Il contenuto del package

La cartella del package javatutor/primes contiene i seguenti files:

Nome del file package Bote
OverflowException.java javatutor.primes La classe eccezione
ListPrimes.java javatutor.primes La interfaccia per la lista dei primi
DivisionLoop.java javatutor.primes Algortimo per tentativi di divisione
package-info.java javatutor.primes documentazione per il package
overview.html   il file HTML per javadoc
docoptions.txt   il file opzioni per javadoc

Analizziamo alcuni files:

  • overview.html è il file della pagina principale della documentazione ottenuta con l'utility javadoc che il lettore ha già incontrato in Il file overview.txt
  • docoptions.txt è il file delle opzioni per l'utility javadoc il lettore ha già incontrato in Il file docoptions.txt
  • package-info.java è il file della pagina principale della documentazione del singolo package ottenuta con la utility javadoc

La interfaccia ListPrimes

Aprite il file sorgente ListPrimes.java che definisce i metodi che restituiscono il conteggio e/o la lista dei numeri primi minori/uguali ad un dato numero n. Analizziamo il sorgente in questione. Innanzitutto, perchè una interfaccia e non una classe? Avevo già accennato al concetto di interfaccia nel capitolo relativo al disegno dei poligoni, ricordate? (vedi Le interfacce).
Una interfaccia è simile ad una classe base astratta ma con alcuni vantaggi:

  • una classe può derivare da una sola classe base ma può implementare due o più interfacce
  • i metodi definiti nella interfaccia non hanno corpo e sono, a tutti gli effetti, astratti
  • la classe che implementa una interfaccia DEVE implementare tutti i metodi della interfaccia a meno che la classe stessa non sia una classe astratta
  • una interfaccia definisce un nuovo tipo di dato che può essere usato esattamente come tutti gli altri tipi di dato

Inoltre, una interfaccia può essere implementata da classi che non hanno nulla in comune e che non potrebbero mai derivare da un capostipite comune. questo si può applicare anche alle classi che restituiscono una lista di numeri primi: in futuro potrebbero essere sviluppati algoritmi che nulla hanno in comune con quelli che scriviamo oggi.

La prima istruzione di ogni file sorgente che fà parte di un package deve essere la parola chiave package seguita dal nome del package del quale fa parte la classe che stiamo scrivendo. Solitamente, subito dopo la dichiarazione del package, si trovano le direttive di importazione. Esempio:

/* ListPrimes.java - ritorna i numeri primi
... omissis ...
*/
package javatutor.primes;
import java.util.List;
import java.util.ArrayList;
import java.util.Scanner;

Successivamente, viene definita la interfaccia:

public interface ListPrimes
{
... definizione dei metodi
}

Gli attributi di accesso

Abbiamo imparato che gli attributi di accesso servono ad impedire l'acceso ai membri dati ed ai metodi da parte di metodi esterni alla classe: questo consente di mantenere coerenti i dati interni di una classe oltre a poter usare un dato interno in un formato più semplice da gestire (vedi Astrazione dei dati). Gli attributi visti finora sono:

  • public: l'accesso è garantito a tutti i metodi sia interni che esterni alla classe
  • protected: l'accesso è garantito a tutti i metodi interni della classe e di tutte le sue derivate
  • private: l'accesso è garantito ai soli metodi interni alla classe

Ma se non viene specificato alcun attributo di accesso, quale è quello di default? Per una qualsiasi classe, membro dati o metodo, l'attributo di default è una modalità che ancora non abbiamo affrontato: package private. Questo tipo di attributo indica che il membro dati o il metodo è accessibile da parte dei metodi di tutte le classi che sono definite nello stesso package.
Fà eccezione a questa regola la interfaccia: l'attributo di default dei suoi metodi è public.

Il nome delle classi

Anche la classe in quanto tipo di dato può avere un attributo di accesso: può essere public oppure private e, se non viene specificato alcun attributo, anche la classe può essere package private. Se vogliamo dare accesso pubblico ad una classe, dobbiamo specificarlo esplicitamente:

public class MyClass
{
...
}

Finora non abbiamo mai definito una classe pubblica: non ce ne era bisogno poichè tutti i sorgenti delle nostre piccole aplicazioni erano ubicati nella stessa cartella e non facevano parte di alcun package o, meglio, facevano parte di un unnamed package (=package anonimo).

Nella realtà, invece, la maggior parte delle classi sono sempre definite pubbliche; questo però comporta una limitazione: il compilatore Java pretende che ogni classe pubblica sia definita in un file sorgente a se stante e che il nome del file sorgente sia uguale a quello della classe definita in esso. Per questo motivo, se vogliamo rendere pubblica la interfaccia ListPrimes dobbiamo definirla nel file sorgente ListPrimes.java e nessuna altra classe pubblica può essere definita nello stesso file sorgente.

I metodi getPrimesCount

La interfaccia espone due versioni del metodo getPrimesCount:

long getPrimesCount(long start, long end);
default long getPrimesCount(long end)

La prima versione è un metodo astratto che deve essere implementato nelle classi che implementano la interfaccia. Il metodo accetta due parametri:

  • start: il numero di partenza del conteggio
  • end: il numero finale del conteggio

chiameremo questi due parametri i limiti entro i quali devono essere conteggiati i numeri primi; essi vengono valutati come valore assoluto e start è sempre il minore dei due argomenti mentre end è il maggiore.

La seconda versione del metodo non è astratta: nella interfaccia vi è la definizione del metodo:

default long getPrimesCount( long end ) throws OverflowException
{
return getPrimesCount( 2, end );
}

Poichè questa versione sovrascritta del metodo getPrimesCount è una particolare forma del metodo astratto in cui start è uguale a 2, la implementazione di default nella interfaccia è quella di richiamare il metodo astratto con argomento start uguale a 2, il più piccolo dei numeri primi.
Pertanto è possibile implementare un metodo in una interfaccia usando l'attributo default. Vi è sempre la possibilità, per le classi che implementano la interfaccia, di sovrascrivere questo metodo di default.

I metodi getPrimesList

Anche questo metodo possiede due versioni sovrascritte le quali hanno lo stesso significato del metodo getPrimesCount ma, invece di restituire solo il conteggio dei numeri primi ne restuiscono la lista:

public List<Long> getPrimesList(long start, long end) throws OverflowException;
default List<Long> getPrimesList(long end) throws OverflowException

Analogamente a quanto visto per i metodi precedenti, la prima versione è un metodo astratto che deve essere implementato nelle classi che implementano la interfaccia. Gli argomenti start ed end sono i due limiti entro i quali devono essere estratti i numeri primi; questi argomenti vengono valutati in valore assoluto e start è sempre il minore dei due argomenti mentre end è il maggiore.
Anche in questo caso, la implementazione di default del secondo metodo è quella di richiamare il metodo astratto fornendo come argomento start il valore 2.

La condizione di Overflow

Come abbiamo visto, il nostro piccolo programmino non restituisce solo il conteggio dei numeri primi esistenti tra due estremi ma anche la lista di essi in una collection di tipo List.

public List<Long> getPrimesList(long start, long end) throws OverflowException;

Quanti elementi, al massimo, può contenere una List? E quanti potrebbero essere, potenzialmente, i numeri primi esistenti tra il numero 2 ed il massimo valore memorizzabile in un long?

Come accennato in La interfaccia ListPrimes non esiste una formula analitica che fornisce il numero esatto di:

P(n) = numeri_primi <= n

ma esiste una sua approssimazione che è data dalla formula:

P(n) = n / log(n)

dove log(n) è il logaritmo naturale di n. Applicando la formula approssimativa otteniamo che:

 P(n) = Long.MAX_VALUE / log( Long.MAX_VALUE ) = 211214493616576547

che è pari a circa 2,1x1017, un numero decisamente più alto del numero massimo di elementi che si possono aggiungere ad una lista o ad una array che è pari a Integer.MAX_VALUE.

Pertanto, se n è abbastanza grande possiamo avere una condizione di overflow (=superamento della capacità) della lista che memorizza i numeri primi. Questa condizione deve essere testata ed è necessario interrompere la elaborazione se dovesse verificarsi.
Inoltre, questa condizione deve essere segnalata all'utente. Come? Ma sollevando una eccezione, ovviamente!

// dimensione prevista approssimata della lista dei numeri primi
long size = (long) ( (end - start) / Math.log( end - start ));
// se la dimensione prevista è maggiore di Integer.MAX_VALUE solleva la eccezione
if ( size > Integer.MAX_VALUE ) {
throw new OverflowException( "cannot allocate list of primes of " + size + " elements" );
}

Ecco perchè i metodi getPrimesList definiti nella interfaccia possono sollevare eccezioni di tipo OverflowException implementata nel file sorgente con lo stesso nome della classe, vedi I metodi getPrimesList.

Arrays e Collections

Avremmo potuto restituire la lista di numeri primi in una array, questo è certo:

public long[] getPrimesList( long start, long end )
{
long[] primes = new long[???];
... omissis ...
}

ma quanti elementi avremmo dovuto allocare? Non lo sappiamo con precisione. Potremmo usare il metodo getPrimesCount per conoscerlo con esattezza ma avremmo perso tempo inutilmente dal momento che le operazioni di estrazione e conteggio dei numeri primi sono le stesse.
La soluzione a questo problema è quella di restituire un oggetto che implementi la interfaccia List e, nello specifico, un oggetto di classe ArrayList: vi sono molte classi che implementano la interfaccia List; esse prendono il nome di collections.

Una collection incapsula il concetto di array e ne condivide la organizzazione in quanto è un contenitore di oggetti ma possiede molti vantaggi:

  • possono essere create vuote ed esse si espandono dinamicamente mano a mano che gli elementi vengono aggiunti
  • possono essere create con un numero di elementi previsto ma mantengono la possibilità di espandersi all'occorrenza
  • possiedono il concetto di iterator (=iteratore) che rende molto semplice iterare su tutti o su una parte della collezione
  • è possibile modificare ed eliminare gli elementi della array in modo semplice e veloce
  • possono essere fortemente tipizzati oppure generici
  • alcune classi delle collezioni forniscono metodi sincronizzati con i threads che vi accedono in modo da evitare condizioni di inconsistenza

Quindi la soluzione migliore è quella di istanziare una ArrayList specificando nel costruttore il numero previsto di elementi calcolato con la formula approssimativa e lasciare che sia la classe ArrayList a gestirne il contenuto.

public List<Long> getPrimesList( long n )
{
// calcola la dimensione approssimata della lista dei numeri primi
long size = (long) (n / Math.log(n));
if ( size > Integer.MAX_VALUE ) {
throw new OverflowException( "cannot allocate list of " + size + " elements" );
}
List<Long> primes = new ArrayList( (int) size );
primes.ensureCapacity( (int) size );
... omissis ...

Dopo aver calcolato la dimensione approssimativa della lista, istanziamo l'oggetto primes specificando nel costruttore il numero previsto di elementi. Questo consente di avere un certo vantaggio perchè è pur vero che una ArrayList può crescere dinamicamente ma l'operazione di ridimensionamento ha un costo in termini di tempo.
In seguito, richiamiamo il metodo ensureCapacity in modo da costringere la ArrayList ad allocare il numero di elementi minimo che è sempre pari ad almeno il numero previsto: se l'operazione dovesse fallire verrà sollevata una eccezione.

Per approfondire il concetto delle collezioni vedi The Java Tutorial - Generics

Siete curiosi di sapere di quanto è errato il risultato approssimato rispetto al conteggio reale dei numeri primi? Nella tabella sottostante potete vedere, per N fino a quarante-miliardi, il conteggio di P(n) ricavato dalla formula e quello reale, la differenza in valore assoluto ed in percentuale tra le due modalità di calcolo:

N n / log(n) count difference % error
100.000 8.686 9.592 906 9,45%
1.000.000 72.382 78.498 6.116 7,79%
5.000.000 324.150 348.513 24.363 6,99%
10.000.000 620.421 664.579 44.158 6,64%
40.000.000 2.285.141 2.433.654 148.513 6,10%
100.000.000 5.428.681 5.761.455 332.774 5,78%
200.000.000 10.463.629 11.078.937 615.308 5,55%
400.000.000 20.194.906 21.336.326 1.141.420 5,35%
1.000.000.000 48.254.942 50.847.534 2.592.592 5,10%
5.000.000.000 223.886.908 234.954.233 11.067.325 4,71%
10.000.000.000 434.294.482 455.052.511 20.758.029 4,56%
20.000.000.000 843.205.936 882.206.716 39.000.780 4,42%
40.000.000.000 1.638.528.672 1.711.955.433 73.426.761 4,29%

Come il lettore potrà osservare la differenza tra il conteggio di P(n) stimato e quello reale è piuttosto importante.

Il metodo isPrime

La interfaccia definisce un metodo di default, quindi implementato, per stabilire se il numero fornito come argomento, valutato come valore assoluto, è primo oppure no. L'algortimo usato è quello "a divisione per tentativi", quindi poco efficiente, ma è anche l'unico di cui disponiamo al momento che non sia probabilistico:

default boolean isPrime( long num )
{
num = Math.abs( num );
if ( num <= 1 ) {
return false;
}
if ( num == 2 ) {
return true;
}
long lim = (long) ( Math.sqrt( num ));
assert( lim > 0 );
boolean r = true;
for ( long i = 2; i <= lim; i++ ) {
if ( num % i == 0 ) {
r = false;
break;
}
}
return r;
}

Il metodo getKnownPrimes

Il metodo getKnownPrimes della inetrfaccia ListPrimes è un metodo statico e ritorna la lista dei numeri primi compresi tra due estremi start ed end con end minore o uguale a 10.000. La lista ritornata non viene calcolata con un algoritmo ma è hardcoded nella interfaccia stessa. Questo metodo viene usato per verificare che gli algoritmi da noi scritti ritornino i risultati corretti confrontando la lista dei numeri primi ritornata dal algoritmo con la lista dei numeri primi conosciuti:

static List<Long> getKnownPrimes( long start, long end ) throws IllegalArgumentException
{
... omissis ...
List<Long> primes = new ArrayList<Long>(1230);
// usa uno scanner per leggere la stringa contenente i numeri primi
Scanner scan = new Scanner( knownPrimes );
while ( scan.hasNext()) {
... omissis ...
}
scan.close();
return primes;
}
public static final String knownPrimes =
"2 3 5 7 11 13 17 19 23 29" +
" 31 37 41 43 47 53 59 61 67 71" +
... omissis ...

La classe DivisionLoop

Il file sorgente DivisionLoop.java contiene la omonima classe che è la implementazione della interfaccia ListPrimes: essa usa l'inefficente algoritmo di iterare su tutti i numeri compresi nei due estremi e di verificare se essi sono primi o meno usando il metodo di default isPrime. L'algoritmo usato è praticamente uguale a quello già visto in La interfaccia ListPrimes.

Il metodo getPrimesCount

Questo metodo è davvero identico a quello visto nel codice a riferimento con solo due piccoli accorgimenti:

  • i due estremi entro cui estrarre il conteggio dei numeri primi vengono valutati come valori assoluti
  • l'argomento start è sempre il minore dei due estremi mentre l'argomento end è il maggiore dei due
public long getPrimesCount( long start, long end )
{
start = Math.abs( start );
end = Math.abs( end );
long s = Math.min( start, end );
long e = Math.max( start, end );
.. omissis ...
return count;
}

Il metodo getPrimesList

La implementazione di questo metodo ricalca quello del conteggio ma con gli accorgimenti che abbiamo visto nelle sezioni precedenti:

  • viene allocata una ArrayList con una capacità iniziale pari alla dimensione stimata del numero di elementi da inserirvi
  • se la dimensione stimata è superiore al numero massimo di elementi inseribili in una ArrayList viene sollevata una eccezione di tipo OverflowException
  • la capacità minima della lista è di 100 elementi

L'algoritmo vero e proprio è davvero molto semplice (ma anche molto inefficente):

public List<Long> getPrimesList( long start, long end ) throws OverflowException
{
ArrayList<Long> primes = new ArrayList<Long>( (int) size );
primes.ensureCapacity((int) size );
for ( long i = s; i <= e; i++ ) {
if ( isPrime(i)) {
primes.add( i );
}
}
... omissis ...

Il metodo isPrime

Non c'è alcun bisogno di implementare questo metodo della interfaccia dal momento che l'algoritmo DivisionLoop usa quello di default della interfaccia.

Misuriamo le performances

Conosciamo già, almeno in parte, le performances di Java nel conteggio dei numeri primi presenti da 2 ad un certo numero limite e sappiamo che non è affatto un linguaggio lento, anzi, i suoi tempi si avvicinano a quelli di un linguaggio compilato come il "C"; vedi I risultati del test.
Quello che però vogliamo sapere è se questi eccellenti risultati si ottengono anche con algoritmi diversi e non solo: possiamo anche capire che Java è veloce nel contare i numeri primi ma quando si tratta di memorizzarli in una lista come se la cava?
Benchè il nostro algoritmo prevede una capacità iniziale per la lista che si avvicina alla dimensione necessaria, l'inserimento di in elemento in una ArrayList ha un overhead (=penalizzazione, sovraccarico) apprezzabile?

In secondo luogo, la lista di numeri primi a cui siamo interessati potrebbe essere molto più corta di quelle totale; in altre parole, anche se la estrazione dei numeri primi deve avvenire tra 2 e end potremmo essere interessati solo agli ultimi 10 numeri primi della lista. Peranto prevederemo una specifica opzione per questo.

Performance: la sintassi

La applicazione che andremo a scrivere dovrebbe avere una sintassi come questa:

java -ea Performance [OPTIONS] ALGO [START] END

in cui OPTIONS può essere:

  • -h: visualizza l'help del comando, come di consueto
  • -t: estrae e visualizza la lista dei numeri primi
  • -k: visualizza la lista dei numeri primi conosciuti e minori di 10000
  • -e: estrae la lista ma non la visualizza
  • -aNUM: estrae la lista ma visualizza solo gli ultimi NUM numeri primi

Se il comando viene eseguito senza opzioni, la app esegue solo il conteggio dei numeri primi compresi tra START, il cui default è 2, ed END.
L'argomento ALGO è il codice del algoritmo da utilizzare; al momento il solo algoritmo disponibile è il DivisionLoop che abbreviamo in DL ma nei prossimi capitoli ne scriveremo altri.

Performance: gli algoritmi

A volte è possibile ottenere migliori performances semplicemente scrivendo algoritmi migliori o usando alcuni trucchetti di programmazione. E' quello che cercherò di insegnare al lettore nei prossimi capitoli; non è necessario scrivere tutte le features (=funzionalità) oppure cercare la migliore ottimizzazione fin da subito.
Java non è affatto un linguaggio lento, anzi, ma ha dei limiti soprattutto con la gestione della memoria. Però lo stesso Java mette a disposizione alcuni strumenti che possono aggirare questi limiti e, nello specifico, Java permette di richiamare dai suoi metodi, codice nativo, qundi compilato con un linguaggio come il "C".
Questi strumenti sono due; la Java Native Interface (JNI) e la Foreign Function and Memory (FFM) che utilizzeremo nei prossimi capitoli.

Tornando agli algoritmi di estrazione e conteggio dei numeri primi, la seguente tabella elenca quelli che andremo ad implementare in questo tutorial:

Codice Classe Descrizione algoritmo
DL DivisionLoop per tentativi di divisione
C1 Crivello Crivello di Eratostene
C2 Crivello2 Crivello di Eratostene migliorato
C3 Crivello3 Crivello ulteriormente migliorato
JNI CrivelloJNI Crivello in JNI, estrazione a singolo bit
JNIby CrivelloJNIby Crivello in JNI, estrazione a byte
JNIar CrivelloJNIar Crivello in JNI, estrazione in una array
JNIcb CrivelloJNIcb Crivello in JNI, estrazione con callback
FFM CrivelloFFM Crivello in FFM, estrazione a singolo bit
FFMcb CrivelloFFMcb Crivello in FFM, estrazione con callback
FFMar CrivelloFFMar Crivello in FFM, estrazione con segmento nativo

Performance: il sorgente

Arrivato a questo punto, il lettore non dovrebbe avere particolari problemi a leggere il sorgente della applicazione Performance.java: a parte i due metodi main e parseCmdLine che oramai già conosciamo a memoria, vi è praticamente un solo altro metodo: il metodo run:

public void run( String[] args ) throws Throwable, OverflowException
{
... omissis ...
// ottiene il tempo iniziale
long startTime = System.currentTimeMillis();
List<Long> primes = null;
long count = -1;
... omissis ...
else {
listPrimes = switch( algo ) {
case "DL" -> new DivisionLoop();
case "C1" -> new Crivello();
case "C2" -> new Crivello2();
case "C3" -> new Crivello3();
case "JNI" -> new CrivelloJNI();
case "JNIby" -> new CrivelloJNIby();
case "JNIcb" -> new CrivelloJNIcb();
case "JNIar" -> new CrivelloJNIar();
case "EXE" -> new CrivelloExtern();
case "FFM" -> new CrivelloFFM( dllDebug );
case "FFMby" -> new CrivelloFFMby( dllDebug );
case "FFMar" -> new CrivelloFFMar( dllDebug );
default -> throw new IllegalArgumentException( "Uknown "
+ " implementation: " + algo );
};
}
// ottiene il tempo finale
long endTime = System.currentTimeMillis();
... omissis ...
// se l'argomento 'end' è minore/uguale a 10000, la app verifica che
// la lista dei numeri primi ritornati dal nostro algoritmo sia uguale
// a quella ritornata dalla lista conosciuta
if ( k == false && primes != null && end <= 10000 ) {
verifyList( primes );
}
// visualizza il tempo trascorso
System.out.println( "Time elapsed: " +
String.format( "%.1f", (float) (endTime - startTime) /1000.0 ));
}

Dopo aver ottenuto il tempo di sistema iniziale, il metodo istanzia la classe specifica per l'algoritmo desiderato con una espressione switch; in seguito, richiama il metodo che estrae la lista o il solo conteggio dei primi e, alla fine delle operazioni, ottiene il tempo di sistema finale.
La differenza tra i due tempi rappresenta il tempo impiegato nella operazione: da notare che il tempo finale viene preso prima di visualizzare eventuali risultati: anche questa operazione richiede tempo ma noi siamo interessait solo ai tempi di elaborazione, non di visualizzazione.

Performance: compilazione

Compiliamo i sorgenti e procediamo a verificare i tempi sul conteggio. Fino a questo momento abbiamo sempre compilato i sorgenti spostandoci nella cartella che li conteneva ma ora che abbiamo organizzato i nostri sorgenti all'interno di un package, questo non è possibile.
Per compilare ed eseguire una applicazione Java che fà parte di un package dobbiamo tenere come directory corrente la "cartella radice" del package:

>cd javatutor1\projects\primes

>javac javatutor\primes\*.java

Per cominciare eseguiremo il test con il solo conteggio dei primi, fino a 1.000.000 come già fatto in I risultati del test.

>java -ea javatutor.primes.Performance DL 1000000

Test performances of ListPrimes implementations
Starting number: 2
Ending number: 1000000

Using implementation: DivisionLoop

count of primes: 78498
Time elapsed: 0,5

Il tempo impiegato è lo stesso di quanto già ottenuto. Proviamo se la estrazione dei numeri primi è penalizzante:

>java -ea javatutor.primes.Performance -e DL 1000000

Test performances of ListPrimes implementations
Starting number: 2
Ending number: 1000000

Using implementation: DivisionLoop

count of primes: 78498
Time elapsed: 0,5

Ora vogliamo sapere quali sono i dieci numeri primi più grandi minori di un-milione:

>java -ea javatutor.primes.Performance -s10 DL 1000000

Test performances of ListPrimes implementations
Starting number: 2
Ending number: 1000000

Using implementation: DivisionLoop
List of the last 10 prime numbers:
999863 999883 999907 999917 999931 999953 999959 999961 999979 999983
count of primes: 78498
Time elapsed: 0,6

Abbiamo appurato che il tempo impiegato per contare i numeri primi o per estrarli è sostanzialmete lo stesso; questo indica che la implementazione della classe ArrayList in Java è davvero efficiente.

Argomento precedente - Argomento successivo - Indice Generale