Strutture condizionali e iterative. Array e metodi

Giovedì, 9 Ottobre 2008

OBIETTIVI DELLA LEZIONE

In questa lezione:

  1. perfezioneremo la nostra conoscenza delle strutture condizionali;
  2. impareremo a eseguire più volte blocchi di codice;
  3. vedremo nuovi dettagli sugli oggetti e un nuovo oggetto: gli array.
  4. impareremo come creare ed invocare metodi.

Ripasso: nomi identificatori

Java è un linguaggio "case-sensitive", cioè differenzia tra lettere minuscole e maiuscole (come il C ma diversamente dal Fortran, ad es.).

Attenzione: è un errore molto comune chiamare una variabile con un nome tutto minuscolo e referenziarla poi con una iniziale maiuscola!

        int saldo;

        Saldo = ... // SBAGLIATO! saldo è tutto minuscolo

Un altro errore comune è quello di indicare il nome di un tipo primitivo con la maiuscola. Attenzione: i tipi primitivi non hanno iniziale maiuscola! Ma hanno iniziale maiuscola le pseudoclassi che contengono i metodi che agiscono sui tipi primitivi (sono pur sempre classi). Esempio chiarificatore:

        Int numero; // SBAGLIATO, i tipi primitivi hanno nomi minuscoli
        Stringa input; // Giusto, le stringhe sono oggetti!

        numero = int.ParseInt(input); // Sbagliato, int è un tipo primitivo e non ha metodi!
        numero = Integer.ParseInt(input); // Giusto, Integer è una classe

Strutture di controllo del flusso 2/4

Strutture condizionali: l’operatore condizionale ?:

Consideriamo il caso in cui si debba effettuare un assegnamento condizionale

if(x >= 0)
  y = 2;
else
  y = -1;

Un alternativa efficace è l'utilizzo dell'operatore condizionale ternario ?:

y = (x>=0) ? 2 : -1;

La sintassi dell'opertore è espressioneCondizionale ? valoreSeTrue : valoreSeFalse

Strutture condizionali: switch

La seconda (e ultima) struttura condizionale che vediamo è il costrutto switch - case. E' un costrutto sopravvissuto all'evoluzione della specie e deriva direttamente dalle jump-table (goto) dei linguaggi di seconda generazione (assembly). E' sopravvissuto perché in alcuni casi è molto utile! Serve quando dobbiamo confrontare una variabile di tipo intero (assolutamente non boolean, né float, né double, né long) con una serie di possibilità. Tipico esempio: scelta multipla.

import javax.swing.*;

class Multipla {
  public static void main(String[] args) {
    String stringaInput;
    int numero;
    char risposta;

    stringaInput = JOptionPane.showInputDialog("Scegli un numero");
    numero = Integer.parseInt(stringaInput);

    stringaInput = JOptionPane.showInputDialog("Cosa vuoi farne?\n" +
      "Premi R per radice quadrata,\n" +
      "      E per il quadrato,\n" +
      "      C per il cubo e\n" +
      "      n o N per uscire.");
    risposta = stringaInput.charAt(0);

    switch(risposta) { // risposta è un char ma viene 
                       // implicitamente trasformato in un int
      case 'R':        // inizia qui se risposta == 'R'
        JOptionPane.showMessageDialog(null, "La radice quadrata è " +
           Math.sqrt(numero));
        break;         // finisce qui
      case 'E':        // inizia qui se risposta == 'E'
        JOptionPane.showMessageDialog(null, "Il quadrato è " + Math.pow(numero,2.0));
        break;         // finisce qui
      case 'C':        // inizia qui se risposta == 'E'
        JOptionPane.showMessageDialog(null, "Il cubo è " + Math.pow(numero,3.0));
        break;         // finisce qui
      case 'n':        // inizia qui se risposta == 'n'
      case 'N':        // inizia qui se risposta == 'N'
        JOptionPane.showMessageDialog(null, "Faccio come vuoi tu.");
        break;         // finisce qui
      default:         // in ogni altro caso inizia qui
        JOptionPane.showMessageDialog(null, "Non sai stare al gioco.");
        break;         // finisce qui, ma il break non sarebbe stato necessario visto 
                       // che siamo alla fine del blocco e saremmo usciti comunque.
    }

    System.exit(0); // da non dimenticare!
  }
}

Strutture di controllo del flusso 3/4

Spesso è necessario ripetere numerose volte lo stesso blocco di istruzioni e tutti i linguaggi di programmazione propongono strutture di controllo per i cicli. In questo il Java assomiglia quasi completamente al C (ad onor del vero la versione 1.5 di Java introduce un nuovo tipo di ciclo, somigliante al foreach del PHP ma non presente in C).

Fondamentalmente, un ciclo è una struttura di controllo che esegue un blocco di istruzioni fino a che una condizione logica rimane vera.

Cicli: while e do-while

Esempio: calcoliamo la somma dei numeri interi minori ed uguali del numero N (dall'algebra elementare sappiamo che la somma sarà pari a N*(N+1)/2). Per prima cosa costruiamoci un diagramma a blocchi:

Una possibile soluzione è la seguente
import javax.swing.*;

class SommaInteri {
  public static void main(String[] args) {
    String stringaInput;
    int numero, N, somma;

    stringaInput = JOptionPane.showInputDialog("Scegli il numero N");
    N = Integer.parseInt(stringaInput);

    somma = 0;
    numero = 1;
    while(numero <= N) {
      somma += numero;
      numero++;
    }

    JOptionPane.showMessageDialog(null, "La somma è " + somma);
  }
}

Se si vuole che la condizione logica sia valutata alla fine del ciclo, si può utilizzare il costrutto do-while

do {
  istruzioniDelCiclo
} while(condizioneLogica);

Cicli: for

Il costrutto for è, per quanto ci riguarda, assolutamente equivalente al while. Le parti fondamentali del ciclo sono raggruppate in un costrutto più sintetico: for(condizioneIniziale; condizioneLogica; incremento). La parte centrale dell'esempio precedente andrebbe riscritta come:

    somma = 0;
    for(numero = 1; numero <= N; numero++) {
      somma += numero;
    }

In definitiva, i due costrutti seguenti sono equivalenti:

istrInizializzazione;
while(condizioneLogica) {
  istruzione1;
  istruzione2;
  ...
  istrIncremento;
  }
for(istrInizializzazione; condizioneLogica; istrIncremento) {
  istruzione1;
  istruzione2;
  ...
  }

Prediligeremo il for quando il numero di iterazioni è prefissato, il while altrimenti. Ma si potrebbe anche usare sempre uno dei due costrutti...

Attenzione alla condizione di permanenza nel ciclo! Evitare se possibile test di uguaglianza su valori in virgola mobile!

Cicli: operatori break e continue

break: permette di interrompere l’esecuzione del ciclo

continue: permettere di interrompere l’iterazione corrente

Entrambi gli operatori sono tipicamente utilizzate con una struttura condizionale. Le istruzioni non sono mai necessarie in un programma ed è meglio evitarle per migliorare la leggibilità.

Metodi

Metodo = insieme di istruzioni raggruppate insieme per svolgere una determinata operazione. Perché utilizzare dei metodi:

  1. Riutilizzare il codice;
  2. Nascondere l’implementazione (encapsulation). System.out.printl() non sappiamo come funziona ma lo abbiamo utilizzato senza problemi!

Sintassi:

modificatori tipoValoreRestituito nomeMetodo(tipo1 par1, tipo2 par2, ...) {
  istruzione1;
  istruzione2;
  ...
  return valore;
}

Un po' di terminologia:

Per invocare un metodo:

Un esempio:

import javax.swing.*;

class SommaInteriMetodo {

  public static int sommaFinoN(int numeroMassimo) {
    int numero, somma;

    somma = 0;
    numero = 1;
    while(numero <= numeroMassimo) {
      somma += numero;
      numero++;
    }
    
    return somma;
  }
    
  public static void main(String[] args) {
    String stringaInput;
    int N;

    stringaInput = JOptionPane.showInputDialog("Scegli il numero N");
    N = Integer.parseInt(stringaInput);

    JOptionPane.showMessageDialog(null, "La somma è " + sommaFinoN(N));
  }
}

Abbiamo già utilizzato dei metodi. Non ve lo avevo fatto notare ma, sono di due tipologie:

In un linguaggio OO il centro del programma sono gli oggetti stessi. Gli oggetti possono ricevere o inviare messaggi: con un messaggio un oggetto può trasferire dati o chiedere ad un secondo oggetto di manifestare un comportamento. L'oggetto che riceve il messaggio manifesterà il comportamento tramite l'esecuzione del metodo corrispondente. La classe specifica quali metodi l’oggetto dispone.

Vediamo un esempio. Nel codice sopra per avere il primo carattere della stringa abbiamo scritto la seguente istruzione:

    risposta = stringaInput.charAt(0);     

Scomponiamola:

Passaggio parametri

Per comprendere bene come funzionino i metodi in Java ed evitare errori significativi, iniziamo con un breve ripasso sulla gestione della memoria in C, e su come funziona il "call stack". Lo potete trovare in queste slides.

Anticipando quanto vedremo meglio la prossima lezione, le variabili di tipo primitivo e riferimento ad oggetto sono disposte sullo stack. Gli oggetti risiedono invece sullo, lo spazio tipicamente riservato per l'allocazione di memoria dinamica. Le variabili riferimento ad oggetto altro non sono che riferimenti (puntatori) ai relativi oggetti.

Dunque, supponiamo di invocare il metodo:

public static double Math.sqrt(double a);

Al momento dell’invocazione in Java:

  1. vengono creati sullo stack delle variabili che corrispondono ai parametri formali
  2. i parametri formali vengono inizializzati con il valore dei parametri attuali (o “argomenti”): a <- 3.0
  3. il controllo del flusso di esecuzione è trasferito al metodo
  4. al termine del metodo le variabili vengono cancellate dallo stack

Vediamolo per l'esecuzione della classe SommaInteriMetodo in modo semplificato.

123456
main
args
stringaInput
N
main
args
stringaInput
N
message
showInputDialog
+++
main
args
stringaInput
N
s
parseInt
+++
main
args
stringaInput
N
numeroMassimo
sommaFinoN
numero
somma
main
args
stringaInput
N
parentComponen
message
showMessageDialog
+++
main
args
stringaInput
N

Un errore tipico

Il passaggio parametri in Java è sempre per valore! Non esiste il passaggio parametro per indirizzo. Attenzione però che quando passate ad un metodo una variabile di tipo riferimento ad oggetto, quello che passate è il riferimento!

Il passaggio parametri per valore con tipi di dato primitivi è immediato da capire ma porta ad eventuali errori. Vediamone uno tipico:

class Scambia {
  public static void main(String[] args) {
    int primoNumero = 10, secondoNumero = 20;

    scambiaNumeriInteri(primoNumero, secondoNumero);
    System.out.println("primoNumero = " + primoNumero +
       " e secondoNumero = " + secondoNumero);
  }

  static void scambiaNumeriInteri(int a, int b) {
    int temp;

    temp = a;
    a = b;
    b = temp;

    // a e b sono variabili temporanee che qui smettono di esistere.
  }
}

Quando il metodo di classe scambiaNumeriInteri è stato invocato, sono state create sullo stack due nuove variabili intere, a e b, in cui sono stati copiati primoNumero e secondoNumero. Al termine dell'esecuzione del metodo, le variabili vengono rimosse dallo stack e nessuna modifica si propaga nel metodo main.

main()

main() è un metodo static che Viene invocato direttamente dalla JVM. DEVE essere specificato come public static void.

Ancora sui metodi: overload

Abbiamo utilizzato:

System.out.println(3.0)
System.out.println("Benvenuto")
System.out.println(5)

Quella che abbiamo sfruttato senza saperlo è la possibilità che Java offre di "sovraccaricare" (to overload) un metodo. In pratica costruiamo metodi differenti con lo stesso nome, ma con una diversa lista di parametri formali. Il compilatore sceglie il metodo che corrisponde meglio ai parametri attuali scelti dall'utente allorquando utilizza il metodo. La scelta del metodo tra quelli disponibili avviene dunque durante la fase di compilazione (NON durante l'esecuzione!).

L'overload rende più leggibile il codice. Vediamo un esempio, un metodo che calcola il maggiore di due numeri:

public static int massimo(int a, int b) {
  int c = ( a >= b ? a : b );
  return c;
}

public static double massimo(double a, double b) {
  double c = ( a >= b ? a : b );
  return c;
}

A secondo del tipo di parametri formali che passeremo al compilatore, esso sceglierà il metodo corrispondente. Talvolta il compilatore non riesce a scegliere, perché più metodi (o nessuno) si prestano al caso specifico: avremo un errore in compilazione!

Confronto tra stinghe e altre funzioni utili

Una variabile di tipo char può solo contenere una lettera. Per rappresentare una successione di lettere dobbiamo utilizzare una stringa. Una stringa NON è un tipo di dato primitivo e per contenerla serve un oggetto.

Abbiamo detto che gli oggetti risiedono sullo heap. Ad esempio nella seguente dichiarazione, l'oggetto stringa viene creato sullo heap, inoltre:



Ripasso: concatenamento stringhe

Per concatenare due stringhe dobbiamo utilizzare l'operatore +. Rivediamo alcuni esempi.

Esempio: abbiamo le tre variabili altezza, lunghezza e larghezza e vogliamo una produrre una stringa del tipo:

        "Le dimensioni della scatola sono: 13 x 10 x 2 cm (L x W x H)"

Per ottenerla:

        "Le dimensioni della scatola sono: " + lunghezza +
        " x " + larghezza + " x " + altezza + " cm (L x W x H)";

Per costruire una stringa che, stampata, stia su più righe, ad esempio come:

Questa è una stringa
su due righe!

basta inserire la sequenza di escape '\n' nel punto opportuno, cioè:

        "Questa è una stringa\nsu due righe!"

Operare sulle stringhe?

Altri metodi utili della classe String

NOTA BENE: se vogliamo confrontare due stringhe in una espressione logica NON dobbiamo usare l'operatore == ma il metodo equals(). Questo è valido in genere per qualunque oggetto! L'operatore == confronta il riferimento all'oggetto.

Array

Un "array" è una collezione di elementi dello stesso tipo. A ciascun elemento è associata una posizione nell'array: in Java come in C le posizioni all'interno dell'array (gli "indici") partono da 0.

Gli "array" in Java sono degli oggetti. Un esempio, un array di 10 double:

Dichiarazione di un array

Per dichiarare una variabile (riferimento a) array si utilizza la sintassi

double[] movimenti;

Notate la posizione delle parentesi quadre. Per motivi storici (venire incontro alle abitudini dei programmatori C) è possibile anche la sintassi alternativa:

double movimenti[];

E' sconsigliata!

Costruzione dell’array

Per costruire l'array si utilizza la sintassi:

movimenti = new double[10]; 

Che cosa è "new"? È un operatore unario prefisso, con il quale richiediamo alla JVM di creare un oggetto (nello heap). In generale, per creare un array utilizzaremo la sintassi:

new OggettoOTipoPrimitivo[numeroDiElementi]

Le variabili di tipo primitivo contenute nell'array vengono inizializzate ai valori di default indicati nella tabella della lezione scorsa. Vi anticipo che le variabili di tipo riferimento ad oggetto vengono inizializzate a null. Vediamo quale è il processo di dichiarazione e creazione di un array da parte della JVM in questa slide.

Infine ecco un possibile codice che produce l'array della figura vista in precedenza:

class ArrayDiDouble {
  public static void main(String[] args) {
    double[] movimenti = {12.32, 0.3, 28.78, 12.45, 9.01, 1.16, 26.9, 18.04, 11.08, 5.39};

    for(int i=0; i<movimenti.length; i++)
      System.out.println(movimenti[i]);
  }
}

Osservazione: nell'esempio è presentato un metodo di creazione e inizializzazione alternativo che utilizza una sorta di costante letterale per l'array:

double[] movimenti = {12.32, 0.3, 28.78, 12.45, 9.01, 1.16, 26.9, 18.04, 11.08, 5.39};

Il costrutto è equivalente a:

double[] movimenti = new double[10];
movimenti[0] = 12.32;
movimenti[1] = 0.3;
movimenti[2] = 28.78;
movimenti[3] = 12.45;
...

Come in C, gli elementi si manipolano utilizzando l’operatore [ ].

Lunghezza di un array

L’oggetto array contiene una variabile con la lunghezza (cioè il numero di elementi contenuti nell'array: movimenti.length

L'esistenza di proprietà di un oggetto è un concetto comune a tutti gli oggetti. Vengono chiamate in modi diversi: campi o variabili di istanza. Conterranno informazioni che caratterizzeranno gli oggetti stessi. Es: auto: numero di Km percorsi; vestito: taglia, etc.

Vediamo come utilizzare il campo length in questo esempio, che prevede che l'utente inserisca 10 dati:

import javax.swing.*;

class ArrayDiDouble {
  public static void main(String[] args) {
    String stringaInput;
    double[] movimenti = new double[10];

    for(int i=0; i<movimenti.length; i++) {
      stringaInput = JOptionPane.showInputDialog("Inserisci l'elemento numero " + i);
      movimenti[i] = Double.parseDouble(stringaInput);
    }

    for(int i=0; i<movimenti.length; i++)
      System.out.println(movimenti[i]);

    System.exit(0);
  }
}

Lab

Algoritmi da sapere: Massimo di un array

Questo non è un corso di algoritmi, ma di sicuro è necessario imparare bene alcuni algoritmi basilari. Tra questi la ricerca del massimo (o del minimo) di un array. L'idea è semplice:

  1. Il primo elemento dell'array (o l'ultimo se volete "sfogliare" l'array al contrario) è sicuramente maggiore di tutti quelli che abbiamo esaminato in precedenza (quanti ne abbiamo già esaminati? Nessuno, dunque un qualunque numero è sicuramente maggiore di nessun numero). Dunque,
    massimo=elenco[0].
  2. A partire dal secondo elemento dell'array, confrontiamo il nuovo valore esaminato con il massimo trovato fino a quel punto. Se il nuovo elemento è maggiore, questi diventa il nuovo massimo. Dunque, se elenco[i=1]>massimo, allora massimo=elenco[i=1].
  3. Ripetiamo il passo 2, per ogni elemento dell'array
  4. .

Esercizio 1 [Soluzione]

Scrivere un programma che chieda all'utente di inserire tre numeri double. Il programma deve scegliere il maggiore e stamparlo a video.

Esercizio 2 [Soluzione]

Un anno bisestile ha 366 giorni. Un anno è bisestile se è divisibile per 4 (es: 1980), se non è divisibile per 100 (il 1900 non era bisestile), se è divisibile per 400 (il 2000 era bisestile). Il calendario gregoriano è stato introdotto nel 1582 e prima di esso non esistevano gli anni bisestili. Scrivere un programma che chieda all'utente un anno e che risponda indicando se quell'anno sia (stato) bisestile o meno.

Esercizio 3 [Soluzione]

Riscrivere il seguente codice, sostituendo il ciclo for con uno while

int s = 0;
for (int i = 1; i <= 10; i++) s = s + 1;

Esercizio 4 [Soluzione]

Scrivere un programma che stampi a video il seguente pattern:

        1
        22
        333
        4444
        55555
        666666
        7777777
        88888888
        999999999
        

Esercizio 5 [Soluzione]

Scrivere un programma che stampi a video il seguente pattern:

        ########              #
        #######              ##
        ######              ###
        #####              ####
        ####              #####
        ###              ######
        ##              #######
        #              ########
        

Esercizio 6 [Soluzione]

Scrivere un programma che chieda all'utente di inserire 7 numeri double e li metta in un array. Quindi il programma deve indicare all'utente quale era il numero maggiore.

©2008 Roberto Sassi