Scarica Programmazione 2 - Java (Dragan Ahmetovic) e più Appunti in PDF di Programmazione Java solo su Docsity!
Programmazione II
1. Introduzione
Programmazione imperativa → dire al computer di eseguire una sequenza di operazioni , in una specifica sequenza. (GO) Diventa complesso comprendere a pieno tutto, soprattutto perché appesantisce il codice. Programmazione procedurale → strutturare le istruzioni in diverse procedure che siano indipendenti , richiamabili , isolate(JAVA). Dare più struttura al codice, raggruppando istruzioni in unità logiche cercando di isolare il comportamento. Decomposizione : se devo scrivere un codice e anche altre persone devono scriverlo o leggerlo, è utile scomporre un programma in sotto problemi → sottoprogrammi riutilizzabili, brevi e specifici. Astrazione: la decomposizione genera delle entità, generalizzabili con un processo di astrazione:
- Parametrica = genero entità ( codice ) che dipendono da parametri , al posto dei quali possono essere usati valori diversi;
- Per specifica = una volta che ho un codice strutturato in sotto-programmi , questi devono essere specificati , ovvero dare minimi informazioni indispensabili per dare il codice ad altri.
2. Object Orientation in Java
Differenza di approccio, tra procedurale e orientata ad oggetti:
- Procedurale → ho dei dati da memorizzare e delle azioni sui dati da fare , definite tramite funzioni.
- Orientato ad oggetti → ho un oggetto che si trova in uno stato (ovvero serie di info che lo caratterizzano) e dei comportamenti ovvero metodi che agiscono (modifiche,…) sullo stato dell’oggetto. Per definire questi oggetti in Java , si scrivono delle classi , che possono avere 2 compiti:
- Raccogliere metodi : ci possono essere metodi che non modificano lo stato di un oggetto (es funzioni). Definire nuovi tipi di dati come una nuova classe ( generica / struct di go ) che determina il suo tipo di dato. o Un oggetto è un istanza della classe che determina il suo tipo di dato
public class NomeClasse
Package – collezione classi
In Java le classi sono organizzate in Package (pacchetti), con 2 obiettivi :
- Encapsulation = ovvero suddividere del codice in moduli indipendenti , con visibilità esterna solo dei metodi indispensabili per svolgere il compito richiesto.
- Naming = separato per evitare di avere casi omonimi di classi. Nel caso di un conflitto tra i due obiettivi, è buona norma dare priorità all’incapsulamento, meglio ottimizzare il programma che un naming flessibile. I package seguono una gerarchia a filesystem , che definisce il fully qualified name (percorso), con cui riferirmi ai Package e alle loro classi. A diversi livelli di gerarchia possono esserci diverse classi, si possono vedere anche senza fully name. Si può fare import dei package per poterli richiamare direttamente (ma attenzione ai conflitti)
- Il package java.lang è automaticamente importato (per System.out.. e String).
Espressioni
In java le espressioni sono eseguite da sx a dx, sono formate da:
- Atomi : variabili, costanti, letterali
- Operatori
- Invocazioni di metodi Le espressioni danno un → valore (output).
Variabili locali
Le variabili hanno nome e tipo , possono essere:
- Primitivi (int, float, boolean) generati sulla stack
- Riferimento (array, obj(oggetti - String vista come oggetto)) con riferimento nella stack e contenute nella heap o Oggetti si inizializzano con operatore new e chiamata al costruttore (es: “a Obj = new Obj()”) o A fine vita del riferimento l’oggetto sulla heap potrà essere cancellato dal garbage collector.
Oggetti
Gli oggetti che si creano possono essere di 2 tipi(mutabili e immutabili):
- Se creo una variabile “a Obj = new Obj()“ questa allocherà uno spazio sulla heap
- Se assegno una nuova variabile ‘b=a‘ ora queste punteranno allo stesso spazio in heap (quindi stessa casella = stesso contenuto)
- Mutabili: una modifica alla variabile ‘b‘ impatta anche ‘a‘ (e viceversa)
- Immutabili: una volta che si crea, è quello e non si può modificare (come le stringhe)
Definizione Metodi
Per scrivere un metodo bisogna definire la sua intestazione definita da:
- Nome del metodo
- Parametri formali definiti dal loro tipo di dato e nome ( input )
- Tipo di valore restituito (return value), deve esserci uno solo, se vuoi si usa la keyword void
- Keyword di scope e visibilità
- E poi c’è il corpo con il codice vario.
Over loading:
- È possibile, ovvero creare metodi che abbiano lo stesso nome , ma con parametri diversi.
- Utile per avere un naming e comportamento consistente e prevedibile. Es. due metodi somma, che però una lo fa con i valori float e una con i valori int Mi permette di gestire 2 funzioni uguali, ma utilizzando diversi parametri di input.
Cicli while/do while/for /for each.
Il ciclo while , verifica come prima cosa la condizione , se viene rispettata entra nel codice ed esegui il suo interno; Il ciclo do while , al contrario del while, esegue prima il codice , almeno una volta, poi ne verifica la condizione; Il ciclo for invece ha una terna di “attributi”, con un indice ed itera da quello fino alla caratteristica inserita; Il ciclo for each serve per estrapolare i singoli dati di un array ed accedervi con facilità.
- Tipi e Dispatching
Gerarchia
Gli oggetti → formano una gerarchia (posso creare una struttura con i diversi dati/tipi) A livello pratico si può estendere una classe B, già creata , una classe A aggiungendo attributi di stato o comportamenti:
- Es. ho i dati di coordinate di un punto, potrei aggiungere un metodo che calcoli la distanza di più punti È una relazione d’ordine → poiché A < B (A è sottotipo di B) il sottotipo è il figlio(A), l’altro è il genitore(B) Si tratta di una proprietà transitività → A<B e B<C allora A<C (quindi rel ordine) Object sarebbe C, la cosa più vecchia, le cose nuove, sono sottotipi di quella Una variabile b d tipo B è sempre possibile assegnare un valore di tipo A<B Il tipo Apparente → di b è B (animal) - Il tipo Concreto è A (dog) Principio di sostituzione di tipo. Posso fare una variabile obj , qualsiasi tipo di dato che creo. In java tutte le classi sono sottotipo di obj.
Metodi utili
toString
Metodo toString : converte un oggetto qualsiasi in stringa , ad esempio per stamparlo. Quindi se ho un obj e voglio controllarlo lo stampo, dando questa conversione. Devo fare l’ over-riding / ridefinizione (procedura che si fa nella classe, si scrive lo stesso metodo, ma gli si cambia il metodo) del metodo per fare qualcosa che voglio. → sovrascrivere i metodi della classe padre, con altri metodi della classe figlio.(non viene invocato il padre) Ad esempio: se ho un tipo Studente, e voglio stampare solo una parte dell’obj uso: String toString() ← fa solo la conversione, poi stampo con System.out.println()
equals
Metodo equals : confronti con l’if nel caso di obj, (== negli oggetti con riferimento non funziona, poiché nella stack c’è il puntatore e non il vero valore). Questo metodo serve per trovare lo stesso dato all’interno di qualcosa. Ad esempio: se ho una community, con diversi utenti, con nome e nickname, se voglio trovare gli utenti sapendo il nick name collegandolo, uso questo metodo per trovare la coppia di dati. Metodo che serve per dire se 2 tipi obj sono uguali. *Restituisce un valore boolean (se sono uguali) → signature: boolean equals(Object o) È un metodo predefinito, che Java potrebbe usare automaticamente per alcune funzioni, quindi quando uso esplicitamente equals è come se venisse creato un nuovo metodo con le stesse informazioni. Quando definisco equals posso decidere di che tipo devono essere i dati che confronto.
Type safety
Strong type
Non si possono confrontare due tipi diversi. Fortemente tipato. Esempio: 3, {1,2,3}, pluto → non posso confrontare questi tipi assieme C’è una struttura di tipo induzionale, ovvero alcuni dati non esplicitamente dichiarati, prende il tipo del dato dell’espressione associata. Casting → procedura che permette di cambiare i tipi dei dati (es float→ int / int→ float) Il casting potrebbe cambiare implicitamente il tipo di dato, per poter fare un’operazione tra 2 tipi Possono avvenire in 2 modi:
- Implicito/automatiche se si passa da un tipo più piccolo a uno più grande. int myDouble = double myInt public class Main { public static void main(String[] args) { int myInt = 9 ; → double myDouble = myInt; // Automatic casting: int to double System.out.println(myInt); // Output 9 System.out.println(myDouble); // Output 9. } }
- manuali , forzando un tipo più grande in uno più piccolo. int myInt = (int) myDouble public class Main { public static void main(String[] args) { double myDouble = 9.78d; → int myInt = (int) myDouble; // Manual casting: double to int System.out.println(myDouble); // Output 9. System.out.println(myInt); // Output 9 } } Si effettua la chiamata più specifica → most specific. Se non sono possibili conversioni o non c’è una più specifica → errore di compilazione (esempio: somma(int a,doubleb) e somma(double a, int b) e ho una somma(2,3) non trova il migliore, quello che decide, quindi bisogna cambiare qualcosa, magari definendo un nuovo metodo somma(int a, int b)) Il meccanismo che decide quale metodo invocare si chiama dispatching.
Dispatching
Se ho un oggetto di tipo A<B, che metodi vengono eseguiti? → in linea di massima, quando chiamo 2 metodi , uno sottotipo dell’altro , invoco quello più specifico del sotto-tipo stesso. (esempio: in quello vecchio ho un metodo, in quello nuovo pure, chiamo quello nuovo perché, se viene rifatto è molto più specifico) Per sapere se un metodo si può eseguire → guardo il metodo apparente, nel momento di esecuzione, però uso il metodo dell’oggetto concreto (però è un po' ostico). Però posso fare una conversione esplicita a compile time, ma la validità la valuta solo a run time.
Wrapping
È definita come la generazione di obj con tipi di dati primitivi, per poterli usare con determinati metodi. I tipi primitivi → stanno nella stack, ma potrebbe essere utile fare ragionamenti come oggetti. Posso generare delle classi , con dati primitivi , però non sono i veri primitivi, quindi faccio wrapping facendoli diventare obj, perché coi metodi posso lavorare solo con oggetti ( Factory → produco nuovi oggetti ). Invece se faccio il contrario, faccio unwrapping , quindi trasformare un oggetto in un tipo primitivo , dopo aver faccio le varie funzioni. *Tutti i tipi wrapped di tipi primitivi, sono immutabili Nelle nuove versioni, permette di fare il wrapping/unwrapping automaticamente:
- Boxing (autoboxing)
- Unboxing : lo fa anche utilizzando ==
- Astrazione Procedurale e Specifiche
Meccanismi di astrazione
Astrarre → quando si scrive il codice → semplificare con operazioni simili, raggrupparle in una procedura e utilizzarla varie volte. Quindi i meccanismi di astrazione richiedono di trascurare dettagli irrilevanti nella soluzione di un problema , concentrandoci sulle questioni rilevanti.
- Astrazione per parametrizzazione: invece di dare oggetti concreti, metto dei parametri astratti, operazione che avviene tramite passaggio di parametri.
- Astrazione per specifica: do le specifiche per spiegare e capire come vengono fatte le operazioni.
Tipi di astrazione
- Astrazione Procedurale
- Astrazione sui tipi di dati
- Astrazione di iterazione
- Gerarchie di tipi
Astrazione per parametrizzazione
Trascura i valori effettivi utilizzati nei calcoli e si concentra sulle loro caratteristiche Potrò sostituire i parametri con i valori effettivi, mantenendo valido il calcolo L’astrazione per parametrizzazione è un’astrazione sui dati , l’istanza del dato che mi vincola e che mi interessa è il tipo.
Astrazione per specifica
Trascura il modo in cui viene risolto , si concentra sulle specifiche del problema , cioè, dico che devo fare qualcosa, non dice come (es faccio una somma, è questa la specifica, molto libero scegliere come – è variabile). Posso mantenere la validità del programma anche se implementato in maniera diversa, se da una specifica precisa ci sono diverse strade per trovare la soluzione. L’astrazione per specifica è un’astrazione sui vincoli dell’operazione
PROPRIETÀ DELL’ASTRAZIONE PER SPECIFICA
Il meccanismo di astrazione per specifica permette di avere due proprietà vantaggiose:
- Località: posso considerare una data astrazione in maniera disgiunta dal resto, l’astrazione in sé (la funzione) mi permette di riusare la funzione indipendentemente, quindi i programmatore può usare la stessa logica in altri posti. Chi usa l’astrazione dovrà guardare solo la specifica
- Modificabilità: posso vincolare l’astrazione in modo da poterla modificare senza problemi Aiuta a sistemare i problemi e la performance in un secondo momento Modifiche future si possono prevedere rendendo il codice facilmente modificabile
Astrazione Procedurale
Nell’astrazione procedurale, la meccanica dell’astrazione per parametrizzazione permette :
- Definizione della procedura come blocco di codice indipendente
- Parametrizzazione degli ingressi e uscite → il codice si può richiamare per tutti i problemi simili Invece, l’astrazione per specifica è resa mediante :
- Intestazioni delle procedure (header o signature): nome della procedura deve essere chiaro
- Specifiche rese attraverso commenti con l’obiettivo di:
- Indicare prerequisiti sui parametri
- Spiegare i possibili risultati
- Segnalare eventuali effetti collaterali
Linguaggio di specifica
Commenti/frasi che permettono di capire bene se sono procedure parziali o totali, dicono i vincoli sugli input, su output ecc È necessario avere un modo di scrivere specifiche, formale (tutti i pezzetti sono ben noti) o informale, ma comunque:
- chiaro - che sia noto quello che significano i termini utilizzati
- consistente - che non muti nel tempo e che tutti siano concordi sulla semantica (può essere complesso ottenere consistenza usando un linguaggio informale ma aiuta ad avere una chiarezza immediata ). Il linguaggio di specifica NON è un linguaggio di programmazione
- Ve ne sono diversi: Liskov(definisce un proprio linguaggio), javadoc(standardizzata di java)
- Evitare specifiche operazionali (non dire come[ operazioni ] ma cosa fare)
- In alcuni casi possono essere utili (se serve un metodo ben noto per le sue proprietà)
Specifiche di procedura (Liskov)
Sono definite attraverso 3+1 principali clausole di specifica:
- requires (pre-condizioni) - specifica i (possibili) vincoli sugli input della procedura (es. radice quadrata sulla variabile int [poiché deve essere maggiore di 0]) - non obbligatoria - si omette se non vi sono vincoli su input (a parte il tipo di dato)
- modifies (effetti collaterali) - specifica gli input modificati dalla procedura ()
- non obbligatoria - si omette se la procedura non modifica input
- effects (post-condizioni) - specifica gli effetti della procedura (output, modifiche causate...) una volta che ho fatto una procedura, dice cosa ho stampato, cosa ho calcolato… ( cosa ho restituito ) - deve essere presente , utile confrontare lo stato pre e post esecuzione
- overview (su classe) - specifica l’obiettivo della classe e possibili funzionalità chiave ATTENZIONE: La specifica deve essere il primo passo nella scrittura del codice e serve per progettare più agevolmente la struttura del codice Es. questo “gioca” con gli array, cerca un valore intero, all’interno dell’array Esempio: Le modifiche possono accadere non solo negli input espliciti (parametri), ma anche negli input impliciti (ambiente, variabili di classe, ...). Bisogna comunque riportare le modifiche che avvengono.
Proprietà delle specifiche
Una buona specifica ha le seguenti proprietà:
- minimalità → dovrebbe porre il meno possibile vincoli sull’implementazione
- restrittività → dovrebbe escludere le implementazioni non corrette
- generalità → dovrebbe permettere le implementazioni corrette:
- sotto-determinazione - potrebbe permettere più implementazioni possibili , quale sarà implementata?
- comportamento non determinato - le implementazioni potrebbero differire nei risultati forniti
- ma la procedura dovrebbe sempre avere un comportamento deterministico
- chiarezza → dovrebbe essere chiara semanticamente (anche considerando altre persone)
- ridondanza - dovrebbe chiarire il significato mediante ulteriori spiegazioni o esempi
- Eccezioni
Caratteristiche di una procedura parziale
Una procedura parziale non descrive il comportamento per input non gestiti. (cerco di controllare l’input scrivendolo come messaggio quando viene runnato un file) Il comportamento non è definito (non sono gestiti tutti i casi d’errore), quindi possono avvenire cose diverse:
- Procedura e il programma terminano in maniera improvvisa
- La procedura cicla senza terminare
- La procedura da un valore errato
- L’errore non viene notato(poiché non so il risultato giusto)
- Potrebbe propagarsi → continua a tenere il risultato sbagliato, salvato ecc in altri processi Tutti questi problemi → rendono il sistema non robusto
Graceful Degradation ← effetto desiderato
Non posso creare un sistema perfetto , ma posso avere un effetto desiderato , che implica che:
- Il programma gestisca l’errore in maniera desiderata
- Per recuperare il comportamento
- Oppure termino senza portare danni ulteriori
Rendere le procedure totali
Una procedura totale → Per incrementare la robustezza del programma, gestendo i casi d’errore. Non sempre è possibile o auspicabile. → potrebbe incidere troppo sulla performance(magari devo scrivere più controlli) Potrebbe essere possibile: es. radice quadrata, può essere totale, quando si gestisce il fatto che, se il nr in input è <0, stampo un errore. Però ci sono problemi. Posso gestire casi di errore sui valori in input, restituendo output particolari :
- Non è possibile se tutti il codominio è usato
- Alto rischio di propagazione
- Alto costo di gestione
- Alto rischio di runtime errors È possibile restituire più valori, usare quindi un valore per segnalare l’errore o comportamenti anomali. Wrapping: possibile restituire più valori con return , tramite qualcosa che si avvolge intorno a qualcos'altro come copertura o protezione: un involucro. In riferimento ai software, il termine indica un programma o un codice che riveste altri componenti del programma.
- Valore ok: potrebbe avere problemi comunque. Errori : propagazione dell’errore, che porti danni più complicati in avanti.
Cosa sono le eccezioni
Per gestire questi problemi in Java ci sono le ECCEZIONI → Meccanismo (particolare) che prevede la gestione di eventi eccezionali :
- Se incontra un evento eccezionale → causa l’interruzione della procedura
- È indipendente dal codominio della procedura
- Costringe a gestire l’irregolarità (poiché interrompe nel momento in cui fa qualcosa che non voglio) permette quindi di non propagare l’errore → blocca la procedura
Come vengono gestite le eccezioni
Le eccezioni sono degli oggetti → con un sottotipo (Throwable ), possiamo crearne di nuove (delle figlie). Adatto delle eccezioni che si adattino ai casi specifici. Throw → si dichiara come output nei metodi, con un nome adatto, potrebbe essere già esistente, come potrebbe essere creato da zero dal programmatore. 2 gruppi principali di throw:
- Error = errori fatali nell’esecuzioni del programma (non le useremo, poiché sono errori complessi tipo crash app) non sono controllati
- Exception = comportamenti inattesi ma che riesco a gestire
- RuntimeException : imprevedibile non sono controllati
- Altre : prevedibile sono controllati
UNCHECKED
Error e RuntimeExceprion Eventi fuori dal controllo del programmatore Irrealistico chiedere al programmatore di gestirle di principio
CHECKED
Il programmatore dovrebbe gestirle. Altrimenti porta a compile error.
Come si usano
Creo un blocco nuovo → try {…} tenta di lanciare il codice se tutto funziona giusto Seguito da catch{…} che gestisce l’eccezione e ritorna alla funzione da provare (try) Infine, ce finally{…} blocco eseguito sempre, anche se try va bene Es. try LeggiFile studenti.txt { ← non funziona catch se esiste e ritorna sul try se non ci fosse { ← esce se non funziona il try finally{… ← dice in ogni caso, per fare qualcosa in tempo, se il catch si è avviato(ovvero se c’era un errore – es chiudo lo scanner) Es try: voglio leggere file e mettere tutto in una stringa catch: il file è vuoto, parte l’eccezione FileNotFound finally: restituisco una stringa vuota Sono necessarie modifiche alla signature^1 e alla specifica dei metodi:
- Uso una keywords → throws, seguita dai tipi di eccezione che potrebbero accadere
- Quando c’è un eccezione → devo specificarla negli EFFECTS
- Se avviene per alcuni valori del dominio, questi sono validi
- Non bisogna specificarli come vincoli in requires
- Se ci sono modifiche agli input, devo scriverlo nei MODIFIES (^1) Signature: insieme di informazioni che identificano univocamente il metodo stesso fra quelli della sua classe di appartenenza.
- Astrazione dei Tipi (definizione e specifica)
Tipi di dati
Modellano classi di oggetti , di un dato tipo, costruiti da:
- Uno stato → delle informazioni che caratterizzano l’oggetto di quella classe (in quel momento - > per restituire il momento in cui si trova).
- Dei comportamenti → che tutti gli oggetti di quella classe sanno fare e possono:
- Restituire info sullo stato dell’oggetto
- Manipolare lo stato dell’oggetto
- Metodi di classe → non agiscono su un oggetto, sono delle semplici funzioni che dipendono da propri parametri.
Sintassi:
- Def della classe → può essere pubblica (public)class NomeClasse{…}
- Attributi (fields)→ dati con un tipo di dato e un nome , inizializzazione di una variabile nome ;
- Si impostano quando si crea un’istanza della classe
- Sono la rappresentazione del dato
- Metodi → servono per accedere ai dati della classe
- Metodi della classe ( public)static nomeMetodo(){…} per metodi statici
- Costruttori per creare nuovi oggetti (public) NomeClasse(){…}
- Metodi di istanza ( public) nomeMetodo(){…} che agiscono sullo stato dell’oggetto Ci possono essere diversi costruttori, si può fare un overload.
Metodi di istanza:
differiscono da quelli statici:
- Poiché agiscono su attributi dell’oggetto obj.metodo
- Sono definiti solo su oggetti specifici :
- s. charAt(0) → restituisce il primo carattere di s che è una String
- sc. nextInt() → restituisce l’ultimo intero letto da scanner che è di tipo Scanner
- l. size()→ restituisce la dimensione di l che è di tipo List
- i metodi di istanza possono essere :
- Costruttori - metodi usati alla creazione dell’oggetto che ne assegnano gli attributi (ogni classe che prevede un istanza[movimento nel metodo] ha bisogno di un costruttore per costruire lo stesso. Esempio: uno studente ha bisogno di un costruttore per creare una nuova istanza → popolare la classe con nuovi dati[es Studente __ = new Studente(info personali)]).
- Metodi di mutazione - Sono usati per cambiare lo stato dell’oggetto (attributi)
- Metodi di osservazione - Sono usati per chiedere informazioni sugli attributi dell’oggetto
- Metodi di produzione/fabbricazione - restituiscono un oggetto diverso ▪ Es ho una matrice, potrei avere la necessita di creare una matrice inversa /opposta, creare una nuova partendo da quella che già avevo.
Utilizzo di oggetti
Per usarle un oggetto devo per forza crearlo prima , per poi interagirci. (es se voglio un nuovo studente, creo una variabile di tipo Studente (che è in public class)) → Studente Angelica = new Studente(matricola[sarebbe la variabile che chiede in input il metodo]).
- Si assegna ad una variabile del tipo T un oggetto restituito dal suo costruttore.
- ‘Persona giovanni = new Persona ("Giovanni", "M", 23, 1.74, "biondo");’ ← creo l’oggetto con new
- Si possono usare i suoi metodi di osservazione per leggerne gli attributi
- ‘double altezzaDiGiovanni = giovanni.altezza(); ← mi permette di vedere la sua altezza, chiamando solamente un suo attributo
- Si possono usare i suoi metodi di mutazione per cambiare gli attributi
- ‘ giovanni.siTinge("rosso"); ← muto l’oggetto con un metodo di mutazione
L’ASTRAZIONE SUI DATI:
È necessario per manipolarli , altrimenti sono vincolato su come il dato è implementato
- Se in fase di implementazione di un dato, decido la sua struttura , qualsiasi modifica del dato (tipo cambiare array in hasMap) devo lavorarci e per tutti gli altri che lo usano devono cambiare in base alle modifiche.
- Il problema è la riusabilità del codice (stare attenti coi tipi di dati dei metodi)
PARADIGMARE GLI OGGETTI Il paradigma a oggetti → permette di fare l’astrazione sui dati, ovvero ignorare la rappresentazione effettiva:
- Concentrarmi su cosa voglio rappresentare e sul comportamento che il dato può avere.
- Devo definire cosa è il dato e cosa fa , ma non come è implementato.
- Ci concentriamo su come specificare l’astrazione.
Specifiche
La cosa iniziale da capire è che cosa deve fare la classe:
- //OVERVIEW per capire cosa fare;
- Dobbiamo capire anche se l’oggetto deve essere mutabile o no
- Specifico le intestazioni dei costruttori sotto //constructors
- Definiscono il comportamento del dato specificando tutti i metodi necassi sotto //methods (mentre sto costruendo la classe, definisco overview, i costruttori e i metodi che potrebbero servire con le loro intestazioni )
Esempi PDJ IntSet e Poly
I prossimi esempi sono da PDJ. Sono utili perchè danno un’idea di progettazione di tipi ben noti
- IntSet (su nr interi, senza ordine) mostra il processo di progettazione , specifica e implementazione di un tipo che modella un insieme di interi - Gli elementi sono numeri interi , non ripetuti, senza distinzione di ordine - I metodi sono tutte le operazioni necessarie per lavorare su insiemi di interi
- Poly mostra il processo di progettazione, specifica e implementazione di un tipo che modella i polinomi (su incognita singola x ) (qualsiasi grado) - I polinomi sono espressioni costituite da somme di monomi - Ciascun monomio è un prodotto tra una costante c e un’incognita x elevata ad una potenza e - Alcuni possono essere nulli.
- Astrazione dei tipi(implementazione)
Attributi
Bisogna capire com’è la rappresentazione , come la memorizziamo:
- Devo scegliere quali attributi servono per memorizzare il tipo (es. Studente: con matricola, nome, esami…)
- Sono la rappresentazione del tipo.
- Devono essere espressivi e usare giusti attributi e comportamenti (es. sensori di luce on off → con un booleano).
- Bisogna considerare l’efficienza e la semplicità d’uso.
Metodi di istanza
L’implementazione dei metodi deve rispettare le specifiche e interagire con gli altri attributi.
- Dai metodi dell’oggetto ci si può riferire all’oggetto stesso con → this
- this. nomeattributo → estrae l’attributo già creato.
Costruttori
Sono dei metodi di istanza speciali che creano un nuovo oggetto assegnando i suoi attributi
- Il costruttore è il metodo che si invoca quando si inizializza un nuovo oggetto
- Sintassi: ’Studente nome = new Studente(); es: ’Studente nome = new Studente("Mario Rossi", 96224);
- È possibile l’over-loading 2 dei costruttori se devo passare dei parametri
- L’implementazione del costruttore solitamente inizializza e assegna gli attributi dell’oggetto
- es: ‘ this. nomecompleto = nome + " " + cognome;‘ la keyword ‘this‘ è disponibile in tutti i metodi di istanza e si usa per riferirsi all’oggetto stesso
- Non è obbligatoria ma torna utile per disambiguare tra nomi dei parametri e attributi
- es: ‘this.matricola = matricola;‘
- questa keyword è disponibile solo nei metodi di istanza, non può esistere nei metodi di classe
Metodi di osservazione
Vanno a vedere e ispezionare gli attributi di un oggetto , diversi tipi:
- getters (più semplici) = restituiscono lo stato di un singolo attributo (il valore)
- [se faccio un get di un booleano → is…] (^2) Over-loading: possibilità di definire più metodi col medesimo nome, ma firma differente, in una stessa classe
Metodi di manipolazione
Vengono usati per cambiare gli attributi dell’oggetto:
- setters = settano uno specifico attributo a un valore
- metodi pubblici, che non restituiscono nulla, ma settano un valore che gli viene passato
- possono esserci metodi di manipolazione senza parametri :
- quando voglio cancellare qualcosa
- switchen: che può essere T o F → il funzionamento è come se fosse acceso o spento (binarie).
- se la modifica non avviene → lancia un eccezione
- la modifica può dipendere dallo stato dell’oggetto, il metodo è anche di osservazione
- possono anche ritornare un valore
Metodi di fabbricazione o costruzione
Genera dei nuovi oggetti (simile al costruttore, ma il costruttore si chiama come l’oggetto e usa new), mentre nei metodi di costruzione il tipo di ritorno è un oggetto del tipo appena creato.
- Possono partire da degli oggetti già esistenti (caso di osservazione)
- Es matrice trasposta() → creo una nuova matrice, partendo da quella esistente, modificandola
- Possono essere static se creano un oggetto nuovo senza partire da uno esistente
- Possono essere usati come wrapper dei Costruttori per creare nuove istanze con benefici.
- Es creo mezzi di trasporto , parto da quelli già creati , e li riutilizzo per nuovi oggetti
- Caching dell’istanza restituita (devo creare un entità, voglio crearla non per la prima volta, ma riusarlo da altri file)
hasCode
associa a un oggetto un intero che funge da chiave nelle HashMap o simili.
- Posso usare un oggetto , come chiave all’interno della mappa.
- Due oggetti uguali devono avere la stessa hashCode
- Possono esserci delle collisioni → oggetto diversi che producono lo stesso hashCode Da qualche parte c’è una situazione che uno stesso oggetto, venga riassunto da un intero uguale → deve esserci un meccanismo per mappare stessi spazi con stesse hashCode.
- Usa basi matematiche per evitare che ci siano molte collissioni
- Come implementare int hashCode() → nel vecchio modo: o Quello che voglio ottenere è che tutto quello che viene descritto nell’oggetto , venga “compresso” e riassunto in hashCode con però valori diversi o Tutti gli attributi che vogliamo che generino l’hashCode → siano considerati propriamente: ▪ Per i dati primitivi è facile ▪ Dati non primitivi sono complessi o Parto da un nr primo, ogni passaggio, lo moltiplico per un altro e sommare l’hashCode di ciascun attributo Vecchio modo: Nuovo metodo
clone
clone serve per creare una copia , per evitare di toccare il metodo originale (es. quando modifico il valore di qualche attributo di un oggetto per una hashMap → porta danni)
- Per creare la copia non basta copiare tutti gli attributi (shallow copy → default) Per gli attributi di tipo riferimento → entrambi gli oggetti punterebbero allo stesso dato
- È un problema per i tipi mutabili ma non lo è per quelli immutabili
- Clone → restituisce un Object , bisogna castarlo Non tutti gli attributi non sono clonable, se fosse così, bisogna ridefinirlo per tutti gli attributi. Per implementare Object clone() 8.Funzioni di astrazione e invariante di rappresentazione
Legame tra astrazione e implementazione
Le classi sono template che hanno delle funzioni sopra: Funzione di astrazione → cosa che restituisce l’astrazione di un oggetto , caso Studente Giacomino , restituisco tutte le informazioni della classe , si utilizza un To_String , restituisce ciò che stiamo modellando , quindi i suoi attributi. Quello che sto rappresentando è quello che sto modellando, quindi la utilizzo nella fase di controllo. Invariante di rappresentazione → rep Ok valuta se la funzione è in uno stato corretto , per capire se la rappresentazione è stabile. Metodo con controlli che verificano che lo stato sia a posto (serie di controlli) se un qualsiasi controllo risulta false, ritorna false. Questo metodo si usa in fase di debugging. Una volta che tutto va bene, posso togliere la rep Ok, c’è non serve per sempre, ma solo in fase di programmazione. Viene fatta in fase di debug , repOk, partendo dalle classi che sono delle strutture sui quali si costruiscono degli oggetti, l’invariante di rappresentazione, valuta lo stato dell’oggetto se è in quello cercato. Viene detto tutto quello che è l’oggetto, si valuta la sua rappresentazione → si valuta la stabilità(è un metodo con tutti i controlli). L’unico caso in cui l’invariante di rappresentazione non serve → classe e oggetto sono già a prova di “proiettile”, non rompibile, quindi quando ho oggetti immutabili , che nel momento del set, rimangono così.
Funzione di astrazione → cosa lega l’astrazione che si aveva in mente , con l’effettiva rappresentazione Invariante di rappresentazione → che è la correttezza dell’implementazione. Astrazione: definisce una serie di oggetti astratti , tutti gli oggetti astratti a di un astrazione A. La classe C implementa l’astrazione A associata a ogni oggetto astratto, quindi la rappresentazione c appartenente alla Classe; La rappresentazione di una classe è uno specifico stato degli attributi; La funzione di astrazione associa alla rappresentazione c la corrispondente astrazione C → A È vista come una funzione :
- Dominio → insieme di oggetti validi c ∈ C , ovvero tutte le combinazioni valide di stato degli attributi di c
- Codominio →: tutti gli oggetti astratti a ∈ A
- È Suriettiva , ovvero ogni oggetto astratto è mappato da una rappresentazione valida.
- È Non-iniettiva , ovvero possono esserci più rappresentazioni valide per un oggetto astratto.
- Quale sarà supportata dipende dalla implementazione scelta
- Possono esistere rappresentazioni non valide , che non corrispondono ad un oggetto astratto.
- Queste chiaramente non fanno parte del dominio della funzione
Identificazione delle rappresentazioni valide
Bisogno verificare che una rappresentazione sai corretta L’Invariante di Rappresentazione è una funzione RI : C → {True, False} RI(c) = ‘True‘ =⇒ ∃a ∈ A|AF(a) = c
- Se c è una valida rappresentazione di un oggetto astratto a, allora RI(c) è vera
- Dominio : tutti i possibili stati di c
- Codominio : True, False (è un predicato: funzioni che partono da un range di valore restituiscono true o false) Nota: l’insieme degli elementi c ∈ C|RI(c) = ‘True‘ è il dominio della Funzione di Astrazione Se valgono vincoli specifici su attributi (specificati come congiunzione di predicati)
- Es: studente deve avere un nome valido E una matricola valida Ci possono essere delle combinazioni di attributi valide o non valide o controlli più complessi
- Es: un gatto maschio non può avere il pelo di tre colori diversi
Implementazione della Funzione di Astrazione
La Funzione di Astrazione dovrebbe restituire l’oggetto astratto rappresentato da c
- Non possiamo restituire l’oggetto astratto ma possiamo dare una sua descrizione univoca
- È il comportamento desiderato di ‘toString()’ La cosa che fa restituire la rappresentazione di un oggetto, vedere cosa contengono tutti i suoi attributi.
Implementazione della Invariante di Rappresentazione
Per la Invariante di Rappresentazione Liskov suggerisce di implementare un nuovo metodo
- ‘public boolean repOk() ’
- Il dominio della funzione sono tutte le possibili combinazioni degli attributi
- Il codominio è un valore booleano
- Posso evitare rappresentazioni errate in seguito alle mutazioni lanciando un’eccezione
Validità della Invariante di Rappresentazione
L’Invariante di Rappresentazione è un vincolo interno all’implementazione che garantisce : ■ Che la rappresentazione sia valida al termine di ciascuna mutazione
- Non è però garantita la validità durante una mutazione
- Es: posso aggiungere un duplicato all’IntSet e poi levarlo ■ In ogni caso non garantita se si espone la rappresentazione (modifiche esterne agli attributi)
- Es: accesso esterno all’array che memorizza IntSet può modificarlo in maniera che non rispetta la RI
- Per implementazioni immutabili è comunque un problema perché vincola l’implementazione