MokaByte 75 - Giugno 2003 
Corso di programmazione Java
XIV parte: Classi Astratte, Contesto Statico e Package
di
Andrea Gini
Dopo aver introdotto il formalismo della programmazione ad oggetti, è giunto il momento di introdurre alcuni aspetti avanzati della programmazione ad oggetti. Attraverso le classi astratte è possibile creare classi dotate di metodi astratti, ossia privi di implementazione. Il contesto statico è invece un insieme di attributi e di metodi comuni a tutte le istanze di un determinato oggetto. Infine i package sono un costrutto di Java che permette di organizzare le classi all'interno di una struttura simile al file system.

Classi astratte
La classificazione di oggetti software permette la realizzazione incrementale di entità software con gradi crescenti di specializzazione. Ogni elemento può essere sviluppato e testato in modo indipendente: le migliorie apportate ad una classe in cima alla una gerarchia verranno trasmes-se automaticamente a tutte le sottoclassi.

Durante il lavoro di classificazione, tuttavia, capitano anche situazioni in cui emergono entità che pur avendo attributi e dei metodi ben precisi, non corrispondono ad entità concrete. Nella classificazione degli esseri viventi, ad esempio, esiste la categoria dei mammiferi, che racchiu-de un insieme di attributi comuni a diverse specie viventi (cani, gatti, mucche…), ma che di per sé non rappresenta nessun animale.

Procedendo con la classificazione è possibile trovare ulteriori esempi. In Figura 11.1, le catego-rie Erbivoro, Carnivoro, Felino, Ovino e Bovino non corrispondono ad alcun animale concreto, ma rappresentano comunque passaggi fondamentali nella classificazione.


Figura 1
- Classificazione delle specie viventi

Nella progettazione di software si presenta spesso un problema simile: le gerarchie molto arti-colate presentano spesso dei nodi che corrispondono a categorie astratte di oggetti, indispensa-bili come passaggi logici, ma di fatto non istanziabili. Tali classi vengono dette Astratte, ed of-frono la possibilità di definire degli speciali metodi abstract privi di implementazione, che delegano la proipria concretizzazione a chi implementa le sottoclassi.

Per definire una classe astratta, è necessario aggiungere il modificatore abstract sia nella di-chiarazione della classe, sia in quella dei metodi privi di implementazione. L'esempio seguente riporta il caso della classificazione degli esseri viventi. La classe astratta mammifero dichiara il metodo concreto mangia(), che è definito come l'esecuzione in sequenza di tre passaggi: ingerisci(), digerisci() ed evacua():

public abstract class Mammifero {

  public void mangia(Cibo c) {
    ingerisci(Cibo c);
    digerisci();
    evacua();
  }
  public abstract void ingerisci(Cibo c);
  public abstract void digerisci();
  public abstract void evacua();
}

Le modalità di ingestione, digestione ed espulsione sono differenti in ognuna delle sottocatego-rie, tuttavia esse sono sempre presenti. Ogni sottoclasse concreta di Mammifero sarà tenuta a fornire una implementazione dei metodi astratti, che rifletta la natura particolare dell'entità rappresentata (la digestione nei mammiferi carnivori è sostanzialmente differente da quella dei mammiferi erbivori ruminanti).
Classi interne
Il linguaggio Java permette di definire classi all'interno di altre classi, con un livello di nidifi-cazione arbitrario:

public class MyClass {

  public method() {
    …
  }

  public class MyInnerClass {
    public method() {
      ...
    }
  }  
}

La classe interna può essere richiamata all'interno della classe di livello superiore utilizzando il suo nome senza particolari differenze rispetto a come si utilizza qualunque altra classe presente nel name space del sorgente; di contro, all'esterno del sorgente in cui viene dichiarata essa è accessibile solamente utilizzando il percorso completo:

MyClass.MyInnerClass m = new MyClass.MyInnerClass();
Nel codice di esempio, si può notare che la classe interna definisce un metodo utilizzando lo stesso nome di un metodo della classe di livello superiore. In casi come questo, qualora la clas-se interna desideri chiamare il metodo omonimo della classe di livello superiore, dovrà accede-re all'istanza della classe contenitrice attraverso un uso particolare di this:

MyClass.this.metodo();

lo stesso nome Uso di this e nomeclasse.this

Le classi interne sono state introdotte a partire dal JDK 1.1, principalmente per supportare il modello di eventi tipico delle interfacce grafiche. Pertanto esse verranno trattate in modo ap-profondito solamente in tale contesto.
Il contesto statico: Variabili e metodi di classe
Gli attributi visti fino ad ora sono associati alle istanze di una classe: ogni singola istanza pos-siede una copia privata di tali attributi, e i metodi della classe lavorano sul tale copia privata. Esiste la possibilità di definire attributi statici, ossia variabili legate alla classe, presenti in co-pia unica e accessibili da tutte le istanze. Allo stesso modo è possibile definire metodi statici, ossia metodi non legati alle singole istanze, che possono operare esclusivamente su variabili statiche. Attributi e metodi statici vengono chiamati anche "attributi e metodi di classe", per distinguerli dagli attributi e dai metodi visti fino ad ora, che vengono detti "di istanza".

Metodi e variabili di classe costituiscono il contesto statico di una classe. Per accedere al con-testo statico di una classe non è necessario crearne un'istanza: ogni metodo statico è accessibile direttamente attraverso la sintassi:

NomeClasse.metodoStatico();

All'interno di un metodo di classe è possibile accedere solamente a metodi ed attributi statici; inoltre in tale contesto gli identificatori this e super sono privi di significato.

Attributi e i metodi di classe sono utili per rappresentare caratteristiche comuni a tutto l'insieme di oggetti appartenenti ad una classe. Ad esempio, in un'ipotetica classe che denoti le finestre grafiche di un desktop compariranno sia attributi di istanza (tipo la posizione e la di-mensione, che sono caratteristiche di ogni singola finestra) sia attributi di classe (come il colore della barra del titolo, uguale per tutte le finestre presenti sul sistema). Questa situazione può essere rappresentata dal seguente esempio:

public class Window {
  private int x;
  private int y;
  private int height ;
  private int width;
  private static Color titleBarColor;

  public void setX(int x) {
    this.x = x;
  }

  ...
  
  public static void setTitleBarColor(Color c) {
    titleBarColor = c;
  }
}


In questo esempio, se si possiede un oggetto di tipo Window e si desidera impostarne le dimen-sioni, si provvederà ad effettuare una chiamata a metodo come di consueto:

w.setWidth(400);
w.setHeight(300);

Queste chiamate, come ormai dovrebbe essere chiaro, operano sull'istanza referenziata dalla variabile w. Al contrario, la chiamata:

Window.setTitleBarColor(new Color("Red"));

va a modificare l'attributo statico titleBarColor, con la conseguenza di modificare tutti gli og-getti di tipo Window presenti in memoria.

Nota: le variabili e i metodi statici possono essere richiamati anche attraverso l'identificatore di una va-riabile; pertanto, riferendosi all'esempio precedente, le seguenti istruzioni

Window. setTitleBarColor (new Color("Red"));
w.setTitleBarColor (new Color("Red"));

sortiranno il medesimo effetto. Dal momento che metodi e variabili di classe operano esclusivamente sul contesto statico, si preferisce seguire la convenzione di richiamarli unicamente attraverso l'identificatore della classe, in modo da sottolineare la differenza.

 

I package
Il linguaggio Java offre la possibilità di organizzare le classi in package, uno strumento di clas-sificazione gerarchico simile alle directory di un file system. Un package è un contenitore che può raccogliere al suo interno sia classi che altri package, secondo una struttura gerarchica. Grazie ai package è possibile suddividere un software in moduli, raggruppando insiemi di clas-si che svolgono un determinato compito. Una simile forma di organizzazione diventa indispen-sabile nei moderni sistemi software, composti di solito da centinaia o migliaia di classi.

L'organizzazione delle classi in package permette inoltre di risolvere il problema del conflitto di nomi (in inglese name clash). Il conflitto di nomi e' un problema molto sentito nella comuni-tà dei programmatori ad oggetti: all'interno di progetti di grandi dimensioni, la necessità di da-re un nome ad ogni cosa conduce a dover riutilizzare più volte nomi comuni tipo "Persona", "Cliente" o "Studente". La divisione in package risolve il problema del conflitto di nomi, dal momento che in questo caso e' sufficiente che i nomi siano unici all'interno di un package.
Dichiarazione di Package
Per includere una classe in un package è necessario inserire in testa al sorgente la dichiarazio-ne:

package nomepackage;

Se si desidera inserire la classe in un sottopackage, bisogna utilizzare come separatore il carat-tere ".":

package pakage1.package2.myPackage;

Oltre alla dichiarazione di appartenenza, è necessario che il file sorgente venga posto fisica-mente all'interno di una struttura di directory analoga a quella dei package sottostanti, a partire da una locazione che prenderà il nome di radice. Si osservi la figura 11.2:


Figura 2
- Esempio di organizzazione in package all'interno di un file system

Se si desidera creare una classe "userInterface" all'interno del package it.mokabyte.sampleApplication.userInterface, è necessario creare nella directory c:/progetto/it/mokabyte/sampleApplication/userInterface un sorgente "MainFrame.java" do-tato di un'intestazione di questo tipo:

package it.mokabyte.sampleProject.userInterface;

  public class MainFrame {
    ...
  }

In uno scenario come questo, la directory "progetto" svolge il ruolo di radice: la posizione dei package all'interno della gerarchia viene considerata in relazione a quest'ultima.
Compilazione ed esecuzione
L'organizzazione di un progetto in package richiede una certa attenzione in fase di compilazio-ne ed esecuzione, al fine di evitare errori comuni che generano una grossa frustrazione nei principianti.

In primo luogo, per identificare univocamente una classe e' necessario specificarne per esteso il nome assoluto, comprensivo di identificatori di package:

it.mokabyte.sampleApplication.userInterface.MainFrame

In secondo luogo, prima di invocare i comandi bisogna posizionarsi sulla radice dell'albero di package; in caso contrario il compilatore e la JVM non riusciranno a trovare i files. Riprenden-do l'esempio precedente, prima di compilare o eseguire la classe MainFrame sarà necessario effettuare una chiamata di questo tipo:

cd c:/progetto

Per compilare la classe MainFrame.java bisogna digitare:

javac it\mokabyte\sampleApplication\userInterface\MainFrame.java

su piattaforma windows e:

javac it/mokabyte/sampleApplication/userInterface/MainFrame.java

sotto Unix-Linux.

Per eseguire il main della classe invece bisogna invocare il comando java specificando il nome assoluto:

java it.mokabyte.sampleApplication.userInterface.MainFrame

Quando si comincia a lavorare su progetti di grandi dimensioni organizzati a package, e' bene prendere l'abitudine di separare i file sorgenti dalle classi in uscita dal compilatore. Il flag -d del compilatore javac permette di specificare la directory di destinazione del compilatore; per-tanto, se si desidera compilare la classe MainFrame in modo tale da piazzare i file .class nella directory C:\classes, si dovrai invocare il comando:

javac -d C:\classes it\mokabyte\sampleApplication\userInterface\MainFrame.java

All'interno della directory C:\classes verranno automaticamente generate le cartelle corri-spondenti ai package, con i file .class nelle posizioni corrette.
Dichiarazione Import
Per accedere alle classi contenute in un package è necessario utilizzare il nome della classe per esteso, sia in fase di dichiarazione che di assegnamento:

it.mokabyte.sampleApplication.userInterface.MainFrame m =
new it.mokabyte.sampleApplication.userInterface.MainFrame();


La scomodità di un simile approccio e' evidente: l'utilizzo di nomi composti così lunghi può alla fine generare altrettanti problemi del conflitto di nomi. Per non essere costretti a specifica-re ogni volta tutto il percorso verso la classe MainFrame, e' possibile importare la classe all'interno del name space del sorgente su cui si sta lavorando, aggiungendo un'apposita clau-sola import all'intestazione del file:

import it.mokabyte.sampleApplication.userInterface.MainFrame;

public class MyClass {
  public static void main(String argv[]) {
    MainFrame f = new MainFrame();
  }
}

Grazie alla import, l'identificatore MainFrame entra a far parte del name space del sorgente: sarà compito del compilatore associare il nome

MainFrame

con la classe

it.mokabyte.sampleApplication.userInterface.MainFrame

La import viene spesso utilizzata per importare interi package, utilizzando il carattere "*" al posto del nome della classe:

import it.mokabyte.sampleApplication.userInterface.*;

In questo caso, il name space del sorgente comprenderà I nomi di tutte le classi contenute nel package importato. Ovviamente l'import di interi package può a sua volta generare conflitti sui nomi, dal momento che package differenti possono contenere nomi uguali. Queste eventualità verranno in ogni caso segnalate in fase di compilazione, e potranno essere risolte ricorrendo, solo in quei casi, all'uso di nomi completi.
Convenzioni di Naming dei Package
I nomi di package seguono la consueta convenzione camel case, con iniziale minuscola. Per l'organizzazione in sottopackage, le specifiche Sun consigliano di seguire la regola del nome di dominio invertito: ad esempio la IBM (dominio ibm.com) inserisce le proprie classi in package che hanno come prefisso com.ibm; allo stesso modo la Sun Microsystem (sun.com) utilizza il prefisso com.sun. Grazie a questa convenzione è possibile raggiungere l'obiettivo di garantire l'unicità dei nomi di classe senza il bisogno di ricorrere ad una qualche autorità centralizzata.

 

Principali package del JDK
A differenza di altri linguaggi di programmazione, le classi di sistema di Java non sono riunite in librerie, ma in package. Ogni package contiene classi che permettono di svolgere un deter-minato compito: grafica, comunicazione in rete, gestione del file system… Di seguito, vengono presentati i package più importanti del linguaggio:

  • java.lang: classi base del linguaggio, tra le quali si trovano Object, String, le wrapper class (Integer, Boolean…) e la classe Math (che contiene le più importanti funzioni matematiche). A differenza degli altri package, java.lang non necessita di essere im-portato, dato che esso fa già parte della dotazione standard del linguaggio.
  • java.io: contiene tutte le classi necessarie a gestire il file system e l'Input Output
  • java.util: classi di utilità, come Vector, Hashtable o Date
  • java.net: qui si trovano tutte le classi di supporto alla comunicazione via rete
  • java.awt: toolkit grafico
  • javax.swing: classi per la gestione della grafica a finestre

L'uso delle classi presente in questi ed altri package verrà chiarito nei prossimi capitoli.

 

Modificatori
I modificatori sono parole riservate del linguaggio che permettono di impostare determinate caratteristiche di classi, metodi e attributi. Alcuni di questi modificatori, come abstract e sta-tic, sono già stati trattati ampiamente nei precedenti paragrafi; altri, come public, private e protected, verranno finalmente illustrati in modo completo e formale.

 

Modificatori di Accesso
I modificatori di accesso permettono di impostare il livello di visibilità dei principali elementi di un sorgente Java, ossia di classi, metodi e attributi. Ad ognuno di questi elementi è possibile assegnare uno dei seguenti livelli di protezione:

private: l'elemento è visibile solo all'interno di una classe
nessun modificatore (package protection): l'elemento è visibile a tutte le classi che fanno parte dello stesso package
protected: l'elemento è accessibile a tutte le classi del package e a tutte le sottoclassi, anche se dichiarate in package differenti
public: l'elemento è visibile ovunque.

Il modificatore private è caratteristico degli attributi. I principi base della programmazione ad oggetti suggeriscono di rendere tutti gli attributi private, e di permetterne l'accesso in modo programmatico attraverso metodi getter. Ci sono comunque diverse occasioni in cui conviene dichiarare private anche un metodo, qualora esso venga chiamato solamente all'interno della classe.

La package protection viene utilizzata soprattutto per classi di appoggio che hanno una loro utilità all'interno di un package, ma che si desidera restino nascoste al resto del sistema.

Si ricorre a protected in tutte le occasioni in cui si voglia rendere un metodo visibile solo alle sottoclassi: tipicamente vengono dichiarati protected i metodi getter relativi ad attributi che si desidera restino inaccessibili al di fuori del dominio della classe e delle sue sottoclassi.

Infine il modificatore public rende un elemento visibile ovunque: si raccomanda di utilizzare tale modificatore con parsimonia e di limitarne l'uso a classi e metodi. Si ricordi che l'insieme dei metodi pubblici di una classe è l'interfaccia attraverso la quale una classe comunica con il mondo esterno, e pertanto si raccomanda di scegliere con cura quali metodi rendere pubblici e quali no, avendo la stessa accortezza che il progettista di un elettrodomestico ha nel non lascia-re fili o ingranaggi scoperti.

 

Modificatore final
Il modificatore final assume un significato diverso a seconda del contesto di utilizzo. Se abbinato ad un attributo, esso lo rende immutabile; tale circostanza impone che l'assegnamento di una variabile final venga effettuata nello stesso momento della dichiarazione. Spesso final viene utilizzato in abbinamento a static per definire delle costanti;

public static final float pigreco = 3.14;

Bisogna fare attenzione al fatto che quando si dichiara final un reference ad un oggetto, a risul-tare immutabile è il reference, non l'oggetto in se. Se si definisce un attributo del tipo:

public static final Vector list = new Vector();

L'uso di final vieta l'operazione di assegnamento sulla variabile:

list = new Vector() // errore: la variabile list è final

Al contrario è assolutamente legale chiamare i metodi che modificano lo stato dell'oggetto:

list.add("Nuovo Elemento");

L'uso di final in abbinamento ad un metodo ha invece l'effetto di vietarne l'overridding nelle sottoclassi; se associato ad una classe ha l'effetto di proibire del tutto la creazione di sottoclassi (in una classe final, tutti i metodi sono final a loro volta). L'uso di final su metodi e classi, oltre ad avere dei ben precisi contesti di utilizzo, presenta l'ulteriore vantaggio di permettere al compilatore di effettuare delle ottimizzazioni, rendendo l'esecuzione più efficiente. Per questa ragione alcune classi di sistema, come String e StringBuffer, sono state definite final.

 

native
Il modificatore native serve a dichiarare metodi privi di implementazione; a differenza dei me-todi abstract, i metodi native vanno implementati in un qualche linguaggio nativo (tipicamente in C o C++). La filosofia di Java non incoraggia la dichiarazione di metodi nativi, ossia dipen-denti dalla macchina. Per questa ragione l'effettiva creazione di metodi nativi risulta piuttosto macchinosa.

 

strictfp
Il modificatore strictfp, usabile sia su metodi che su classi, ha lo scopo di forzare la JVM ad attenersi strettamente allo standard IEEE 754 nel calcolo di espressioni in virgola mobile. Il linguaggio Java infatti utilizza di norma una variante di tale standard che offre una maggior precisione; esistono tuttavia delle situazioni in cui è necessario fare in modo che i risultati delle operazioni siano assolutamente conformi allo standard industriale, anche se questo implica una minor precisione.

 

transient, volatile e synchronized
Il modificatore transient permette di specificare che un determinato attributo non concorre a definire lo stato di un oggetto. Esso verrà studiato in profondità nei capitoli relativi ad IO e Ja-vaBeans. L'uso di volatile e synchronized, due modificatori dotati di una semantica piuttosto complessa, verrà invece illustrato nel capitolo sui Thread e sulla concorrenza.

 

Conclusioni
In questo articolo sono stati approfonditi i costrutti delle classi astratte, di contesto statico e l'uso dei package. Il mese prossimo studieremo il costrutto delle interfacce.

 

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it