Introduzione
Realizzare un buon software significa trovare il miglior equilibrio fra diversi fattori:
- Soddisfazione dell'utente
- Facilità di estensione del programma (in caso di cambiamento delle normative deve essere facile da modificare)
- Comprensione delle soluzioni adatte
Significa adottare tecniche adeguate a gestire la crescente complessità delle applicazioni (complessità per chi ci ha lavorato e ci deve lavorare).
Significa fare in modo che l'investimento per produrre un'applicazione venga utilizzato al meglio, garantendo:
- Il maggior tempo di vita possibile
- La possibilità di riutilizzare in altri progetti parte del codice prodotto
La produzione del software è un processo legato alla progettazione, non alla produzione.
Decomposizione algoritmica e orientata agli oggetti
Decomposizione algoritmica: identificare il problema e scomporlo in modalità strutturata secondo la logica top-down. Dal problema generale si scompone in modo strutturato fino ad una complessità gestibile.
Decomposizione object-oriented: si ragiona in termini di entità, ruoli e responsabilità. Si cerca di individuare un insieme di entità che interagiscono fra di loro e si suddividono le responsabilità concorrendo in tal modo a compiere il processo richiesto.
La prospettiva algoritmica evidenzia l'ordine degli eventi, mentre quella ad oggetti si concentra sugli agenti che causano l'azione o la subiscono.
Vantaggi della decomposizione ad oggetti
- Riuso
- Adattamento ai cambiamenti di specifiche
- Approccio di tipo incrementale che riduce notevolmente i rischi nella costruzione di grandi sistemi
Limiti modello procedurale
È basato su un dualismo di fondo tra due entità: strutture dati e procedure.
- Strutture dati: entità passive che contengono informazioni.
- Procedure: entità attive che modificano le informazioni.
Il problema è che le due entità sono scollegate tra di loro: per una variabile globale, tutte le procedure possono modificarla senza controllo.
Modello ad oggetti
Modularizzazione e decomposizione
Nel modello ad oggetti, le strutture dati e le procedure sono collegate: le entità incapsulano informazioni e procedure. Con questa organizzazione è possibile controllare meglio il sistema, perché le procedure operano solo su variabili dell'entità stessa.
Astrazione
Una struttura organizzata a livelli consente di concentrarsi solo sugli aspetti importanti ignorando i dettagli già risolti ai livelli inferiori. Non si ha idea di come sia fatta la struttura dati e se cambiasse il programma non ne deve risentire.
Incapsulamento
Non si può agire direttamente sulla struttura dati, ma si ha a disposizione un riferimento alla struttura e si può operare su di esso solo attraverso le funzioni primitive. Viene garantita la sicurezza e protezione della struttura dati.
Dinamicità
La struttura dati su cui operare esiste per il tempo che serve: viene creata con un riferimento e cancellata liberando la memoria. Il tempo di vita delle variabili è gestito da chi le utilizza. Nei sistemi procedurali invece le variabili globali hanno una vita continua dal lancio del programma al suo termine.
Programmazione OOP - Modello classico
Un oggetto è un'entità dotata di stato e di comportamento. Un oggetto aggrega in un'entità unica e indivisibile una struttura dati (stato) e l'insieme delle operazioni che possono essere svolte su di essa (comportamento). Le variabili vengono chiamate attributi. Le procedure vengono chiamate metodi. In questo modo non è più possibile avere modifiche incontrollate.
Incapsulamento e astrazione
Lo stato di un oggetto:
- Non è accessibile dall'esterno
- Può essere visto e manipolato solo attraverso i metodi
Lo stato di un oggetto è quindi protetto (incapsulamento): si ha una separazione netta fra interfaccia e implementazione.
Classi e oggetti
È necessario:
- Definire una struttura e un comportamento
- Creare un numero qualunque di oggetti con stessa struttura e comportamento, ma con identità diverse
Questi due ruoli sono svolti da un'entità chiamata classe. Gli oggetti creati da una classe si chiamano istanze della classe. Nel modello OOP classico esistono solo classi e oggetti. Gli oggetti sono entità dinamiche: possono essere creati in qualunque momento a partire da una classe. Le classi sono entità statiche: sono sempre disponibili durante l'esecuzione di un'applicazione.
Rappresentazione delle classi
Programmazione OOP - Modello di Java
Tipi primitivi
Sono strutture dati slegate da qualunque comportamento. Per i tipi primitivi:
- Non esistono tipi la cui dimensione dipende dalla piattaforma (come int in C)
- Interi, caratteri e booleani sono tipi ben distinti tra di loro
- Tutti i tipi primitivi sono definiti in modo univoco e preciso, sono quindi sicuramente informazioni intercambiabili con il sistema operativo.
Caratteri
I caratteri in Java seguono lo standard UNICODE e hanno una rappresentazione su 2 byte. Non esiste alcuna identità fra numeri interi e caratteri: sono due tipi completamente diversi fra di loro.
Tipi primitivi: interi
- Byte (1 byte)
- Short (2 byte)
- Int (4 byte)
- Long (8 byte)
Tipi primitivi: reali
- Float (4 byte)
- Double (8 byte)
Tipi primitivi: booleani
Sono un tipo separato dagli interi: non si convertono boolean in interi e viceversa. Ha dimensione 1 bit e può valere false o true. False non vale 0 e true non vale 1.
Variabili e costanti
float f = 5; Le costanti sono identificate dalla parola chiave final
final int n = 8;final boolean b = false;
Gli operatori di relazione (<> == e !=) danno come risultato un boolean.
Metodi statici
Sono metodi che possono essere invocati anche se non esiste alcuna istanza.
Visibilità
Nel modello classico:
- tutte le variabili sono invisibili dall'esterno
- tutti i metodi sono visibili dall'esterno
Java permette di modificare i vari livelli di visibilità, permettendo di avere:
- metodi non visibili dall'esterno (privati): utili per nascondere dettagli implementativi e migliorare l'incapsulamento
- variabili visibili all'esterno (pubbliche): pericolose perché rompono l'incapsulamento
Programmazione in Java
Deve esistere almeno una classe definita come pubblica che implementi un metodo static chiamato main.
Dichiarazione e riferimento
Counter c1; Crea il riferimento in memoria.
c1 = new Counter(); Crea l'istanza della classe.
Un oggetto viene distrutto quando non c'è più un riferimento verso la sua istanza, attraverso il garbage collector.
Assegnamento
c2=c1; Non crea una copia dell'oggetto, ma le variabili puntano alla stessa zona di memoria.
Copia (copy)
public void copy(Counter x) { val = x.val; }
Dove val è l'istanza che prende valore dall'esterno e x.val è lo stato dell'istanza che arriva dall'esterno.
c2.copy(c1);
Uguaglianza tra riferimenti (equals)
c1 == c2; Due riferimenti sono uguali se puntano allo stesso oggetto e non se gli oggetti a cui puntano sono uguali.
public boolean equals(Counter x) { return val == x.val; }
b2 = c1.equals(c2);
Costruttori
È un metodo che inizializza una nuova istanza: assegna un valore iniziale alle variabili, crea altri oggetti, etc.
- ha lo stesso nome della classe
- non ha tipo di ritorno (nemmeno void)
- ha una visibilità, comunemente public
Java ammette l'esistenza di più costruttori che hanno lo stesso nome (quello della classe) ma si differenziano per il numero e il tipo dei parametri.
I costruttori con i parametri permettono di inizializzare un'istanza con valori passati dall'esterno. Il costruttore privo di parametri viene detto costruttore di default. Se non viene definito esplicitamente, il sistema ne crea automaticamente uno vuoto.
Overloading
Due metodi con lo stesso nome ma con parametri diversi si dicono overloaded. Non è sufficiente che sia diverso il valore di ritorno, deve essere diverso almeno uno dei parametri.
Passaggio dei parametri
I tipi primitivi vengono passati sempre per valore: un parametro di tipo primitivo viene copiato, e la funzione riceve la copia. Gli oggetti vengono passati sempre per riferimento: un riferimento viene copiato e la funzione riceve la copia, ma con ciò accede all'oggetto originale.
Oggetti composti
È possibile dichiarare, all'interno di una classe, variabili che non sono primitive ma degli oggetti.
public class Orologio { Counter ore, minuti; }
Ogni oggetto di classe Orologio è composto da due oggetti di classe Counter: c'è una relazione di composizione fra la classe Orologio e la classe Counter. Orologio può usare gli oggetti di classe Counter contenuti al suo interno:
- può accedere ai metodi pubblici
- non può accedere agli attributi e ai metodi privati
Costruzione degli oggetti
È necessario definire un costruttore per la classe e creare i due oggetti al suo interno.
public class Orologio { Counter ore, minuti; public Orologio() { ore = new Counter(); minuti = new Counter(); } }
Distruzione
Quando l'oggetto di classe Orologio non è più usato da nessuno viene distrutto dal Garbage Collector: la distruzione dell'istanza di Orologio fa sparire i due attributi ore e minuti.
Separazione tra interfaccia e implementazione
Nell'utilizzo di oggetti composti si nota la separazione tra interfaccia e implementazione: basta sapere quali sono i metodi pubblici della classe Counter, non sapere come sono implementati. Utilizzando i metodi pubblici della classe Counter non è necessario il loro codice, ma la loro interfaccia e quello che svolgono. Stessa cosa, nel main vengono utilizzati metodi della classe Orologio senza preoccuparsi della loro implementazione.
Package
Classi e file
Ogni classe deve stare in un file separato. Il nome del file deve essere esattamente uguale al nome della classe con l'estensione .java. Java, inoltre, è case sensitive.
I package
Un package è un gruppo di classi che costituiscono una unità logica. Un package può comprendere parecchie classi, contenute in file separati. Se si vuole indicare l'appartenenza di una classe ad un package si deve mettere all'inizio del file
package <nomepackage>;
Rappresentazione UML
Package e file system
Esiste una corrispondenza biunivoca fra il nome del package e la posizione nel file system del package. Se non inserito niente, si fa riferimento al package di default, ovvero la directory corrente.
Package come namespace
I package consentono di definire uno spazio di nomi (namespace), ovvero un modo di puntare in modo univoco a qualsiasi entità che si sta puntando in Java. Possono infatti esistere classi con lo stesso nome, se appartenenti a package diversi. Utilizzando la sintassi
package.Classe
si possono avere classi con lo stesso nome in package diversi.
Importazione
Ogni volta che si usa una classe, Java richiede che venga denotata con il suo nome assoluto, ovvero il suo percorso completo di package. Inserendo all'inizio del file, per esempio,
import java.awt.print.Book;
si può usare semplicemente Book al posto del nome completo. Per importare tutti i nomi pubblici di un package, si scrive
import java.awt.print.*;
Package e visibilità
I package definiscono anche un ambito di visibilità: oltre a public e private, in java esiste un terzo livello di visibilità intermedio fra i due, ovvero la visibilità package. È il default per classi e metodi: dati e metodi sono accessibili solo per altre classi dello stesso package in qualunque file siano definite.
Stringhe
In Java le stringhe non sono semplicemente un array di caratteri, ma oggetti appartenenti alla classe String. È possibile:
- Dichiarare variabili di tipo String
- Creare istanze di String
- Invocare metodi sulle istanze della classe String
Java consente di dichiarare le stringhe come se fossero dei dati primitivi, ovvero
String s = "hello";
Al posto di
String s = new String("hello");
La trasformazione nella forma completa viene fatta dal compilatore.
Concatenazione
L'operatore + consente di concatenare diverse stringhe.
s = "hello" + "world";
La concatenazione di due stringhe genera automaticamente una nuova istanza di String il cui contenuto è costituito dai caratteri della prima e della seconda messi insieme. Alla fine dell'istruzione non esistono più riferimenti alla prima e alla seconda istanza, che vengono quindi distrutte dal garbage collector.
Immutabilità
In Java le stringhe sono immutabili, ovvero il loro valore non può essere più cambiato dopo che l'istanza viene creata. Per eseguire la concatenazione vengono create stringhe diverse che occupano aree di memoria differenti, con le stringhe intermedie che verranno eliminate dal garbage collector.
Metodi
- length()
- charAt(index)
- indexOf('c')
- equals(s2)
- substring(10,18)
- replace('E','X')
StringBuffer
È una classe che permette di creare stringhe mutabili. Risolve un problema fondamentale: se si ha una stringa molto lunga che si vuole manipolare carattere per carattere, è necessario creare ogni volta una nuova istanza, portando ad un eccessivo utilizzo della memoria.
Utilizzo:
String s; StringBuffer sb; s = "hello"; sb = new StringBuffer(s); //Manipolazione sulla stringa StringBuffer s = sb.toString();
Array
Un array è una sequenza di locazioni di memoria, che contengono entità dello stesso tipo, a cui si può fare riferimento con un nome comune. In Java gli array si comportano a tutti gli effetti come oggetti senza esserlo del tutto, infatti:
- Sono gestiti per riferimento
- Vengono creati dinamicamente con new
- Vengono distrutti automaticamente dal garbage collector quando non servono più
Però non sono istanze di una classe.
Dichiarazione
int[] a; a = new int[50];
In C, se si cerca di accedere a elementi con indice al di fuori dell'intervallo, si hanno effetti imprevedibili perché non c'è alcuna verifica sugli indici, con l'effetto di sporcare la memoria. In Java il sistema genera un errore e il programma si blocca: non è quindi possibile accedere in modo errato alla memoria.
Dimensioni
La dimensione di un array non può essere cambiata, può essere stabilita solo al momento della creazione.
Array di oggetti
La sintassi è la seguente:
Counter[] a; a = new Counter[4]; for(int i=0; i<4; i++) { a[i] = new Counter(); }
È necessario creare per ogni elemento l'istanza: conviene quindi utilizzare un ciclo.
Parametro del main
public static void main(String[] args) args è un array di stringhe che contiene gli argomenti passati sulla riga di comando quando si lancia un programma Java. Non c'è argc come in C, perché la dimensione dell'array si può ricavare dall'attributo length. A differenza del C, args[0] non è il nome del programma ma il primo argomento.
Classi Wrapper e classi di servizio
Una classe wrapper
incapsula una variabile in un dato primitivo, ovvero "trasforma" un tipo primitivo in un oggetto equivalente. Le classi sono:
- Boolean
- Double
- Integer
- Character
Funzionamento
Ogni classe wrapper ha come stato semplicemente un attributo del tipo che incapsula, per esempio Integer ha un attributo di tipo int. Le classi wrapper sono immutabili: assumono un valore al momento della creazione e non può essere cambiato.
Esempio:
int x = 5; Integer ix = new Integer(x); int y = ix.intValue();
Classi di servizio
Le classi wrapper mettono a disposizione un gran numero di metodi static, ovvero metodi che si possono invocare senza creare un'istanza. Un esempio tipico è:
public static int parseInt(String s);
Due modi per convertire stringhe in interi
-
Integer in; int n; in = new Integer("23"); n = in.intValue(); -
int n; n = Integer.parseInt("23");
intValue() viene invocato su un'istanza mentre parseInt() viene invocato sulla classe.
Classi di servizio
Sono classi che svolgono solo il ruolo di fornitori di servizi e non hanno la capacità di creare istanze. Sono classi che:
- Non hanno costruttori
- Hanno tutti i metodi dichiarati come static
Attributi di classe
Sono attributi che:
- Non appartengono ad una istanza ma alla classe
- Esistono per tutto il tempo di vita dell'applicazione
Per usarli si fa riferimento al nome della classe e non alla variabile di un'istanza. Esempio:
public static final double PI;
Per il valore di pi greco, si usa:
x = Math.PI;
Per una stessa classe di possono avere:
- Attributi e metodi d'istanza: utilizzabili se è stato istanziato un oggetto della classe
- Attributi e metodi statici (di servizio): non interagiscono con lo stato delle classi istanziate, metodo a disposizione un servizio
ArrayList
Gli array non possono cambiare la propria dimensione, in quanto il numero di elementi viene stabilito al momento della creazione. ArrayList è una classe che permette di rappresentare sequenze di oggetti di lunghezza variabile.
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.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
-
Appunti completi corso Ingegneria del Software
-
Appunti corso completi per superare esame di Ingegneria del Software
-
Appunti Ingegneria del software
-
Appunti esame Ingegneria del software