Programmazione 2 Cambiago Silvia Anno Accademico 2021-2022
Prof.ssa Daniela Micucci
Classi e oggetti
Una classe è una definizione di un tipo di oggetto, da cui si possono istanziare oggetti di quello stesso tipo. Specifica attributi e metodi delle istanze, ma non il valore degli attributi.
Per rappresentare le classi è largamente utilizzato il diagramma delle classi UML (Universal Modeling Language), che si presenta come di seguito:
- Nome classe
- Attributi (e relativo tipo)
- Metodi (e relativo tipo)
Un esempio di scrittura di una classe è il seguente:
public class Gatto {
public String nome;
public int eta;
public boolean vivo;
public void Miagola {
System.out.print("Miao");
}
public void scriviOutput {
System.out.println("Nome: " + nome);
System.out.print("Età: " + eta);
System.out.print("Vivo? " + vivo);
}
}
Un esempio di oggetto della classe creata sopra è il seguente:
public class GattoDemo {
public static void main (String [] args) {
Gatto g1 = new Gatto();
g1.nome = "Duke";
g1.eta = 2;
g1.vivo = true;
g1.scriviOutput();
g1.Miagola();
}
}
Associazioni
Ogni sistema è composto da più classi. Le associazioni collegano classi e, attraverso di esse, gli oggetti possono interagire (analogamente a come avviene per i database e i diagrammi relazionali).
Un’associazione è un legame semantico tra le classi: fra i corrispondenti oggetti c’è un link, traducibile come istanza di un’associazione (analogamente a come un oggetto è istanza di una classe). Di default è bidirezionale, ma si può rendere unidirezionale all’occorrenza.
Un esempio di associazione in notazione UML è il seguente:
In notazione UML il simbolo + davanti ad attributi o metodi indica che il rispettivo modificatore è public, - indica private e # indica protected. Il diamante (al posto della freccia) indica una relazione di aggregazione. I metodi statici sono sottolineati.
Trascrizione in codice:
public class Motore {
public int numeroCilindri;
public int potenza;
}
public class Auto {
public String marca;
public String modello;
public Motore ilMotore;
}
public class Test {
public static void main (String [] args) {
Auto a = new Auto();
Motore m = new Motore();
a.ilMotore = m;
}
}
Costruttore
Il metodo costruttore viene invocato nel momento in cui si istanzia un oggetto tramite l’operatore new. Ciò che esegue è fondamentalmente la creazione dell’oggetto in questione e l’inizializzazione delle variabili di istanza ad un valore di default. Ha lo stesso nome della classe e non ha tipo di ritorno. Possono esserci molteplici costruttori per la medesima classe (ovviamente a patto che abbiano parametri diversi). Un costruttore che non prende nessun parametro in input è detto costruttore di default.
Un esempio di costruttore applicato alla classe precedente sarebbe:
public class Gatto {
public String nome;
public int eta;
public boolean vivo;
public Gatto (String n, int e, boolean f) {
nome = n;
eta = e;
vivo = f;
}
}
Per invocare uno specifico costruttore è quindi necessario accostare la dicitura new ai parametri da inserire, concordi con quelli del costruttore che si vuole utilizzare.
public class GattoDemo {
public static void main (String [] args) {
Gatto g1 = new Gatto("Duke", 2, true);
}
}
La parola chiave this
Nello scope di un costruttore o di un metodo di istanza, la parola chiave this è il riferimento all'istanza stessa, ossia l'istanza specifica alla quale appartiene quel costruttore/metodo invocato. Sempre in riferimento all’esempio di prima, il costruttore si può anche definire così:
public class Gatto {
public String nome;
public int eta;
public boolean vivo;
public Gatto (String nome, int eta, boolean vivo) {
this.nome = nome;
this.eta = eta;
this.vivo = vivo;
}
}
In questo caso, l’uso di this è necessario per distinguere gli attributi di istanza dalle variabili locali definite all’interno del costruttore.
Un altro esempio di impiego della parola chiave this è:
public class Gatto {
public String nome;
public int eta;
public boolean vivo;
public Gatto () {
this("", 0, true);
}
public Gatto (String nome, boolean vivo) {
this(nome, 0, vivo);
}
}
In questo caso, this rappresenta un’invocazione esplicita di un costruttore. Qualora si voglia richiamare un costruttore di una classe dentro un altro costruttore della stessa classe, tale invocazione deve comparire come prima istruzione nel corpo del costruttore.
Information hiding e incapsulamento
L’information hiding consente di progettare un metodo in modo tale che possa venire implementato senza che il fruitore ne conosca i dettagli del codice. L’incapsulamento consiste nel nascondere i dettagli della definizione di una classe non necessari all’utilizzo delle istanze di tale classe.
Variabili di tipo classe
Nell’istanziazione di variabili di tipo classe viene allocata all’oggetto una specifica area di memoria, dove sono memorizzati i dati delle variabili. Di conseguenza, analogamente a quanto avviene per gli array, l’operazione di assegnamento (=) assegna all’indirizzo dell’oggetto a destra dell’uguale quello dell’oggetto a sinistra. L’operatore == controlla se i due oggetti puntano allo stesso indirizzo di memoria.
Modificatori public, private e protected
I modificatori agiscono sulla visibilità di variabili, metodi o classi a cui vengono applicati.
- Public: visibile ovunque;
- Protected: visibile all’interno del package e nelle sottoclassi (sia dentro che fuori dal package);
- Private: visibile solo all’interno della classe;
Metodi set e get
Quando le variabili di una classe vengono rese private, l’unico modo per renderle accessibili all’esterno è creare dei metodi di accesso, o metodi get, che ritornano i dati contenuti in una variabile di istanza.
public class Gatto {
private String nome;
public String getNome() {
return nome;
}
}
Quando invece è necessario modificare tali dati, bisogna creare metodi di modifica, o metodi set.
public class Gatto {
private String nome;
public void setNome(String nome) {
this.nome = nome;
}
}
Un metodo set può verificare se una modifica dei dati è opportuna, e, qualora non lo sia, può mandare in output un messaggio per l’utente. In questo modo non viene annullato lo scopo del modificatore di accesso private.
Variabili e metodi statici
Le variabili che presentano il modificatore sono anche dette variabili di classe. I metodi statici possono essere invocati senza usare alcun oggetto. Quando viene invocato, prima del nome del metodo, si aggiunge il nome della classe. Possono accedere alle variabili statiche, ma non agli attributi delle istanze. Non possono invocare metodi di istanza, se non tramite un oggetto passato come parametro.
Classi wrapper
In Java, per ogni tipo di dato primitivo, è fornita una classe wrapper, che definisce i metodi che possono essere applicati sui valori di un tipo primitivo.
| Tipo Primitivo | Classe Wrapper |
|---|---|
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
Ad esempio, se si vuole convertire un valore di tipo primitivo x in un oggetto della classe Integer si effettua un’operazione detta boxing, effettuata automaticamente da Java, e si scrive:
Integer n = Integer.valueOf(x);
Se invece occorre convertire un’istanza di Integer in un valore di tipo int si effettua un’operazione di unboxing (anch’essa eseguita in automatico da Java), e si opera nel seguente modo:
int x = n.intValue();
Caratteristiche di un oggetto
- Stato: è dato dal valore dei suoi attributi (variabili di istanza) ed è una delle possibili condizioni in cui può trovarsi;
- Comportamento: è la modalità con cui un oggetto risponde alle richieste da parte di altri oggetti ed è definito dalle operazioni;
- Identità: due oggetti, anche se nello stesso stato, hanno due indirizzi di memoria distinti. Ogni oggetto ha un proprio OID (Object Identifier) univoco e immutabile.
Aree di memoria
Un programma Java accede a due aree di memoria che svolgono funzioni diverse: lo stack e l’heap. Nello stack sono memorizzati lo stato di esecuzione, le variabili locali (i loro valori per quelle primitive e gli indirizzi di memoria per quelle complesse). È bene ricordare che lo stack ha una dimensione limitata di pochi MB. Se viene superata si genera un errore di StackOverflowError.
Lo stack è assegnato dalla JVM all'inizio dell'esecuzione del programma. È una pila in modalità LIFO (Last Input First Output). Contiene i record di attivazione, ossia le informazioni necessarie per invocare i metodi. Ogni record di attivazione contiene i parametri, l'oggetto invocato (this), le variabili locali, il valore di ritorno, il punto di ritorno dell'invocazione di un metodo e l'istruzione successiva alla chiamata.
Nell’heap sono memorizzati gli oggetti (in seguito alla scrittura dell’operatore new) e il loro stato (variabili di istanza). È un’area di memoria dinamica e può essere modificata dalla JVM, durante l’esecuzione. Qui avviene il processo di “garbage collection”, ossia l’eliminazione degli oggetti non più referenziati. In sostanza, ogni volta che in un programma viene creato un qualsiasi oggetto, esso viene sempre memorizzato nell’heap, ed un riferimento allo stesso è mantenuto nello stack.
Un esempio di come funzionano questi spazi di memoria è fornito di seguito:
Ovviamente il codice scritto sopra ha senso solo se si assume che la classe Demo sia dichiarata, completa di attributi e metodi.
Package
Un package è una libreria di classi. Ciascun file del package deve contenere, all’inizio del file, la dicitura:
package nomePackage;
Un programma può usufruire delle classi contenute in un package, inserendo un’istruzione di import, sempre all’inizio del file:
import nomePackage.nomeClasse;
Se si vogliono importare tutte le classi di un package si scrive invece:
import nomePackage.*;
Ereditarietà
Attraverso il meccanismo dell’ereditarietà, concetto chiave della OOP, una classe, detta superclasse (classe base), può essere specializzata definendo una sottoclasse (classe derivata) che ne contenga casi particolari e che estenda la superclasse senza ridefinire variabili e metodi d’istanza già presenti.
Il costrutto principale dell’ereditarietà in Java è la keyword extends, che permette di dichiarare una classe come sottoclasse di un’altra classe.
public class Europeo extends Gatto {
private int lenPelo;
private int lenCoda;
public Europeo (int lenPelo, int lenCoda) {
this.lenPelo = lenPelo;
this.lenCoda = lenCoda;
}
public void scriviOutput () {
System.out.println("Nome: " + getNome());
System.out.println("Lunghezza pelo: " + lenPelo);
System.out.println("Lunghezza coda: " + lenCoda);
}
}
È bene ricordare che in Java non è permessa l’ereditarietà multipla, quindi ogni sottoclasse eredita da una e una sola superclasse.
- Riuso: la superclasse può essere riusata in contesti diversi;
- Economia: attributi, metodi e associazioni comuni si eseguono una volta sola;
- Consistenza: se un attributo, metodo o associazione viene modificato nella superclasse, anche le sottoclassi subiscono il cambiamento;
- Estendibilità: è sempre possibile aggiungere nuove sottoclassi senza riscrivere la parte comune contenuta nella superclasse.
Costruttori nelle classi derivate
Quando si definisce un costruttore per la classe derivata, di solito si invoca prima il costruttore della classe base. Per fare ciò si usa la keyword super tassativamente come prima istruzione, seguita dai parametri che fanno riferimento al costruttore della superclasse che si intende implementare.
public class Europeo extends Gatto {
public Europeo () {
super();
lenPelo = 0;
lenCoda = 0;
}
}
Se si omette super in ogni costruttore della sottoclasse, Java includerà automaticamente una chiamata al costruttore di default della superclasse. Qualora non sia presente, l’omissione di super genera un errore di compilazione.
La parola chiave super è utile anche se si vuole richiamare un metodo che è stato ridefinito nella classe derivata esattamente come è stato scritto in quella base. Si scrive:
super.[nomeMetodo];
Overriding
L’overriding è la sovrascrittura di un metodo della classe derivata, che mantiene inalterato numero e tipo di parametri. Quando si esegue l’overriding di un metodo, non è permesso modificarne il tipo di ritorno, a meno che quest’ultimo non sia una classe che nell’esecuzione dell’overriding viene sostituito con una sua sottoclasse.
Overloading
L’overloading è la definizione, all’interno della stessa classe di più metodi con lo stesso nome ma tipo o numero di parametri diversi (firma diversa). Quando si effettua l’overloading di un metodo, Java distingue i metodi sulla base del numero e del tipo di parametri. Se l’invocazione di un metodo corrisponde alla sua definizione in termini di nome e tipo di parametri, allora verrà eseguito quel metodo. Se non si trova corrispondenza immediata, Java passa alla conversione di tipo dei parametri attuali. Se ancora non si trova corrispondenza, si verificherà un errore di compilazione. Non si può effettuare overloading di un metodo fornendo due definizioni differenti solo nel tipo di ritorno.
Polimorfismo (binding)
Con il termine polimorfismo si indica la proprietà di associare più significati ad un nome di metodo, attraverso un meccanismo detto binding dinamico. Come già è noto, un metodo ereditato può essere riscritto diversamente in tutte le sottoclassi. Il binding è il processo con cui l’invocazione di un metodo viene associata ad una definizione specifica del metodo (quindi ad uno specifico tra i metodi ereditati e modificati che hanno lo stesso nome).
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.