Lezione 10 · Canale 2 · lunedì 12 ottobre 2026
Package, accessi ed ereditarietà in Java
Progettazione del Software
Package e import
Un package raccoglie classi correlate. Le librerie Java sono package (java.io, java.util); solo java.lang, con String, System e Math, è importato automaticamente.
Ogni classe appartiene a uno e un solo package. Se manca la dichiarazione, appartiene al package predefinito, che non ha nome. La dichiarazione package è la prima istruzione del file, e il file sta in una cartella con il nome del package.
// mio_package/C.java
package mio_package;
public class C {
public void F_C() {
System.out.println("Sono F_C()");
}
}
Fuori dal package una classe si nomina con il nome completo, mio_package.C, oppure si importa: import mio_package.C; per una sola classe, import mio_package.*; per tutte quelle del package.
Un package può avere sottopackage, come mio_package.mio_subpackage, con una sottocartella per ciascuno. I sottopackage non hanno privilegi: import mio_package.*; non importa le loro classi, che vanno importate a parte.
// mio_package/mio_subpackage/D.java
package mio_package.mio_subpackage;
public class D {
public void F_D() {
System.out.println("Sono F_D()");
}
}
// Esempio3.java
import mio_package.*;
public class Esempio3 {
public static void main(String[] args) {
C c = new C();
c.F_C();
mio_package.mio_subpackage.D d =
new mio_package.mio_subpackage.D();
d.F_D();
}
}
Livelli di accesso
Una classe di primo livello è public, visibile anche fuori dal package, oppure non qualificata, cioè senza modificatore: è visibile solo nel package e fa da ausilio alle classi pubbliche. Un file contiene al più una classe public, che dà il nome al file, e altre classi non qualificate. La dichiarazione di package vale per tutto il file.
Campi e metodi hanno quattro livelli: public, protected, non qualificato e private. Un membro è visibile da questi punti:
| Accesso da | public | protected | non qualificato | private |
|---|---|---|---|---|
| Stessa classe | Sì | Sì | Sì | Sì |
| Altra classe dello stesso package | Sì | Sì | Sì | No |
| Classe derivata, in un altro package | Sì | Sì | No | No |
| Classe non derivata, in un altro package | Sì | No | No | No |
Da sinistra a destra i diritti decrescono. C e il suo costruttore sono pubblici, stampa è non qualificato e x è privato:
// vis_a/C.java
package vis_a;
public class C {
private int x, y;
public C(int a, int b) {
x = a;
y = b;
}
void stampa() {
System.out.println("x: " + x + ", y: " + y);
}
}
// vis_b/Altro.java
package vis_b;
import vis_a.*;
public class Altro {
public static void main(String[] args) {
C c = new C(7, 12); // corretto
c.stampa(); // errore: non qualificato
int val = c.x; // errore: privato
}
}
Una classe dello stesso package vis_a chiama stampa senza errori, ma non legge x: è privato e resta dentro C.
Information hiding
Limitare la visibilità serve a nascondere i dettagli interni di una classe. Se la rappresentazione è nascosta, si può cambiare senza toccare i clienti, purché i metodi pubblici restino gli stessi. I clienti inoltre non possono modificare i campi e lasciare l'oggetto in uno stato incoerente.
Le classi dello stesso package vedono i membri pubblici, protetti e non qualificati le une delle altre, e così dipendono dai dettagli interni reciproci. Conviene quindi riunire nello stesso package classi davvero legate. Java non impedisce a una classe di dichiararsi in un package: chi scrive la classe sceglie questo accoppiamento in modo esplicito.
Derivazione tra classi
D extends B costruisce D a partire da B, che è la classe base (superclasse); D è la classe derivata.
// B.java
public class B {
int x;
void G() { x = x * 20; }
}
// D.java
public class D extends B {
void H() { x = x * 10; }
}
Una classe derivata
Quattro principi reggono la derivazione:
- Eredità: le proprietà di
Bsono definite anche inD, che haxeG()senza riscriverli. I membri privati diBfanno parte dell'oggetto, maDnon li raggiunge. - Aggiunta:
Dpuò avere altre proprietà, comeH(). - Compatibilità: ogni oggetto di
Dè anche un oggetto diB(relazione «è un»), quindi si usa dove serve unB. - Non il viceversa: un oggetto di
Bnon è un oggetto diDe non haH().
B b = new B();
b.H(); // errore: B non ha H()
D d = b; // errore: B non è un D
b = new D(); // corretto: D è un B
Una derivata può fare da base ad altre (gerarchia in profondità), e una classe può avere più derivate (in ampiezza). Due classi sorelle come D e Dbis non sono compatibili fra loro. Ogni classe ha una sola classe base: Java non ha ereditarietà multipla.
Gerarchia di classi
Casting
Riferimenti di classi diverse possono denotare lo stesso oggetto, ma il compilatore controlla i membri sul tipo del riferimento. Con x_d aggiunto da D, b.x_d è un errore anche se b denota un D. Il cast cambia il tipo con cui il riferimento è visto, non l'oggetto:
// D.java
public class D extends B { int x_d; }
D d = new D();
d.x_d = 10;
B b = d; // stesso oggetto di d
b.x_d = 20; // errore: B non ha x_d
((D) b).x_d = 20; // corretto
Il cast lungo lo stesso cammino della gerarchia compila sempre, e la correttezza resta al programmatore. Con B b = new B(); D d = (D) b; il controllo a runtime lancia ClassCastException, perché l'oggetto è un B e non un D. Un cast fra classi sorelle è invece rifiutato in compilazione.
Accesso protected
Una classe derivata, anche in un altro package, non è un cliente qualsiasi della base: deve poterne riusare il comportamento. Un membro protected è accessibile alle classi derivate e alle classi dello stesso package, ma non ai clienti generici.
Costruttori delle classi derivate
Un oggetto derivato ha anche i campi ereditati, alcuni dei quali privati. Il costruttore della derivata chiama quindi un costruttore della base con super(...), che è la prima istruzione: prima si inizializza la parte di base, poi quella aggiunta. I costruttori non si ereditano.
// B.java
public class B {
private int x_b;
public B(int a) { x_b = a; }
public int getXb() { return x_b; }
}
// D.java
public class D extends B {
private int x_d;
public D(int b, int c) {
super(b);
x_d = c;
}
public int getXd() { return x_d; }
}
Stampando getXb() e getXd(), new D(3,4) dà 3, 4. Se manca la chiamata esplicita, Java inserisce super(); se la base non ha un costruttore senza parametri, c'è un errore. Dichiarare un costruttore con parametri non fa generare quello senza parametri.
Se B ha anche B() { x_b = 10; } e D ha solo D(int c) { x_d = c; }, con new D(4) si stampa 10, 4. Se D non ha costruttori, Java genera uno standard senza parametri, che chiama super() e lascia i campi di D al valore predefinito (0, false, null): new D() stampa 10, 0.
this(...) chiama un altro costruttore della stessa classe, per riusare le inizializzazioni in un punto solo. È anch'essa la prima istruzione, quindi un costruttore non può chiamare sia super(...) sia this(...).
// Persona.java
public class Persona {
private String nome;
private String residenza;
public Persona() { this("Mario Rossi"); }
public Persona(String n) { this(n, null); }
public Persona(String n, String r) {
nome = n;
residenza = r;
}
}
Overloading e overriding
Nell'overloading più metodi hanno lo stesso nome e parametri diversi; il compilatore sceglie in base agli argomenti. Nell'overriding la derivata ridefinisce un metodo ereditato con la stessa segnatura, cioè stesso nome e stessi tipi di parametri. L'annotazione @Override fa verificare al compilatore che il metodo ne ridefinisca davvero uno: senza di essa un errore nei parametri crea un overload involontario.
// B.java
public class B {
public void f(int i) {
System.out.println(i * i);
}
}
// D.java
public class D extends B {
// overloading
public void f(String s) {
System.out.println(s);
}
// overriding
@Override
public void f(int n) {
System.out.println(n * n * n);
}
}
// Esempio2.java
public class Esempio2 {
public static void main(String[] args) {
D d = new D();
d.f(10); // stampa 1000
B b = d;
b.f(10); // stampa 1000, non 100
}
}
Overloading e overriding
Con l'overriding, ogni chiamata su un oggetto di D esegue il corpo ridefinito, qualunque sia il tipo del riferimento. I clienti di D non possono più chiamare B.f(int); dentro D si può, con super.f(n). Il livello di accesso del metodo ridefinito si può solo allargare, per esempio da protected a public, così un oggetto derivato resta usabile dove si attende la base. Un metodo final non si ridefinisce, e una classe final non si estende (String lo è).
Late binding
In compilazione si controlla solo che nel tipo del riferimento esista un metodo con la segnatura richiesta. Il corpo da eseguire si sceglie a runtime, in base alla classe dell'oggetto e non a quella del riferimento: il tipo statico dice quali metodi si possono chiamare, il tipo dinamico quale versione parte.
// B.java
public class B {
protected int id;
public B(int i) { id = i; }
public boolean get() { return id < 0; }
}
// D.java
public class D extends B {
protected char ch;
public D(int i, char c) {
super(i);
ch = c;
}
public boolean get() { return ch != 'a'; }
}
// LateBinding.java
public class LateBinding {
public static void main(String[] args) {
D d = new D(1, 'b');
B b = d;
System.out.println(b.get()); // true
System.out.println(d.get()); // true
}
}
Con id uguale a 1, la versione di B darebbe false; entrambe le chiamate eseguono quella di D, che dà true perché ch è diverso da 'a'.
Campi nascosti
L'overriding dei campi non esiste. Un campo di D con lo stesso nome di uno di B lo nasconde, ma nell'oggetto restano entrambi: quello di B si raggiunge con un riferimento di tipo base o, dentro D, con super. Riusare un nome ereditato rende il codice difficile da leggere.
// B.java
public class B { int i; }
// D.java
public class D extends B {
char i;
void stampa() {
System.out.println(i); // f
System.out.println(super.i); // 9
}
}
// SovrascritturaCampiDati.java
public class SovrascritturaCampiDati {
public static void main(String[] args) {
D d = new D();
d.i = 'f';
((B) d).i = 9;
d.stampa();
}
}Formulario
Package
package p;: prima istruzione- una classe, un solo package
import p.*;: no sottopackage
Accesso, dal più largo
publicprotected: package e derivate- non qualificato: package
private: la classe
Derivazione
D extends B: D è un B- cast: non cambia l'oggetto
ClassCastException: runtime
Costruttori
super(...)othis(...)- prima istruzione, mai insieme
- senza chiamata:
super()
Metodi
- overloading: parametri diversi
- overriding: stessa segnatura
- late binding: tipo dinamico
- i campi si nascondono