MokaByte 61 - Marzo 2002 
Corso introduttivo su Java
II parte: I fondamenti della programmazione in Java
di
Andrea Gini
Questo mese concludiamo l'introduzione ai fondamenti della programmazione. Ricorrendo solo ai costrutti presentati il mese scorso, mostreremo come sia possibile comporre alcuni esempi di complessità crescente. A conclusione di questa panoramica, cercheremo di comprendere quali siano i limiti del sottoinsieme di comandi Java studiati fino ad ora. Per concludere verranno forniti alcuni spunti per esercitazioni

Alcuni programmi di esempio
Utilizzando i costrutti introdotti fino ad ora, proviamo a realizzare un programma che calcoli la somma dei primi 100 numeri interi:

public class SommaNumeri {

  public static void main(String argv[]) {
    int i =0;
    int somma = 0;

    while(i <= 100) {
      somma = somma + i;
      i = i + 1;
    }
    System.out.print("La somma dei primi 100 numeri e' ");
    System.out.println(somma);
  }
}

In questo esempio vengono utilizzate due variabili: 'i' e 'somma'. La prima variabile viene inizializzata a zero, e viene incrementata di un'unità ad ogni iterazione del ciclo. La seconda variabile, anch'essa inizializzata a zero, serve ad accumulare il valore cercato: dopo il primo ciclo essa vale 1, dopo il secondo 3, dopo il terzo 6 e così via. Il ciclo termina non appena i supera il valore 100.
Si vuole ora modificare il programma in modo che calcoli, oltre alla somma dei primi 100 interi, la somma dei primi 50 numeri pari. Un modo per distinguere i numeri pari da quelli dispari è quello di considerare il resto della divisione per due: se è uguale a 0, il numero è pari, in caso contrario è dispari.

public class SommaNumeriPari {

  public static void main(String argv[]) {
    int i =0;
    int somma = 0;
    int sommaPari = 0;
    int resto = 0;

    while(i <= 100) {
      somma = somma + i;
      resto = i % 2;

      if(resto == 0) {
        sommaPari = sommaPari + i;
      }
      i = i + 1;
    }
    System.out.print("La somma dei primi 100 numeri e' ");
    System.out.println(somma);
    System.out.print("La somma dei primi 100 numeri pari e' ");
    System.out.println(sommaPari);
  }
}

Ad ogni ciclo viene calcolato il resto della divisione per due del numero i; se è uguale a zero, il valore di 'i' viene sommato a quello della variabile 'sommaPari':

if(resto == 0) {
  sommaPari = sommaPari + i;
}

Si vuole ora mettere in atto un'ulteriore modifica al programma di esempio, per fare in modo che calcoli, oltre alla somma dei primi 100 interi e dei primi 50 numeri pari, la somma dei primi 50 numeri dispari. Ovviamente dobbiamo inserire una nuova variabile, 'sommaDispari', per accumulare il risultato; quindi dobbiamo aggiungere la clausola 'else' all'istruzione 'if' introdotta precedentemente:

if(resto == 0) {
  sommaPari = sommaPari + i;
}
else {
  sommaDispari = sommaDispari + i;
}

Le due istruzioni verranno eseguite alternativamente: nei cicli pari viene eseguita la prima, nei cicli dispari la seconda. Ecco il codice completo dell'esempio:

public class SommaNumeriPariEDispari {

  public static void main(String argv[]) {
    int i =0;
    int somma = 0;
    int sommaPari = 0;
    int sommaDispari = 0;
    int resto = 0;

    while(i <= 100) {
      somma = somma + i;
      resto = i % 2;

      if(resto == 0) {
        sommaPari = sommaPari + i;
      }
      else {
        sommaDispari = sommaDispari + i;
      }

      i = i + 1;
    }
    System.out.print("La somma dei primi 100 numeri e' ");
    System.out.println(somma);
    System.out.print("La somma dei primi 100 numeri pari e' ");
    System.out.println(sommaPari);
    System.out.print("La somma dei primi 100 numeri dispari e' ");
    System.out.println(sommaDispari);
  }
}

Cicli while nidificati
All'interno di un ciclo while possiamo inserire qualunque istruzione, compreso un altro ciclo while: in questo caso il ciclo più interno verrà rieseguito per intero ad ogni passo del ciclo più esterno. Il programma seguente calcola la tabellina del 10, e la stampa sullo schermo:

public class Tabelline {

  public static void main(String argv[]) {
    int i = 0;
    int j = 0;

    while(i <= 10) {
      while(j <= 10) {
        int prodotto = i*j;
        System.out.print(prodotto);
        System.out.print("\t");
        j = j + 1;
      }
      System.out.println();
      i = i + 1;
      j = 0;
    }
  }
}

Il ciclo più esterno ha il compito di stampare le righe: ad ogni passo, esso avvia un altro ciclo in modo da stampare un valore per ogni colonna. Tra un valore e l'altro viene stampato un carattere di tabulazione, in modo da ottenere una formattazione precisa dei dati.

L'uso dei cicli nidificati è una tecnica di programmazione importantissima, che presenta delle ovvie difficoltà a chi si avvicina alla programmazione per la prima volta. Il tema verrà ripreso e sviluppato nei prossimi capitoli: per il momento è sufficiente aver chiara la possibilità di comporre qualunque tipo di combinazione di istruzioni, ricorrendo addirittura a strutture nidificate.

 

Quanti programmi posso scrivere con le nozioni appena apprese?
I costrutti descritti fino ad ora sono solo una piccola parte di quelli offerti dal linguaggio Java, tuttavia essi permettono di realizzare un gran numero di programmi. Dopo aver preso confidenza con gli esempi, si può tentare di realizzare qualcosa di diverso: con un po' di fantasia è possibile individuare migliaia di possibilità. Ci si può domandare quali siano i limiti di questo mini linguaggio, così semplice e primitivo. Quanti programmi è possibile scrivere? Che tipo di applicazioni si possono realizzare? La risposta, per certi versi sconcertante, è che questo piccolo linguaggio permette di realizzare qualunque programma si desideri, compreso Word, Excel, un compilatore Java o il vostro sistema operativo preferito.

Nel 1966 due matematici italiani, Corrado Bohm e Giuseppe Jacopini, dimostrarono formalmente che qualunque programma per calcolatore poteva essere riscritto usando un linguaggio dotato solamente di variabili intere, assegnamento, ciclo while e costrutto condizionale, un linguaggio del tutto equivalente a quello appena descritto. Il teorema di Bohm Jacopini fu un argomento decisivo nel dibattito sull'abolizione del salto incondizionato "goto", un costrutto presente nei linguaggi di allora che rendeva il codice estremamente difficile da capire.

Un interpretazione superficiale del teorema di Bhom Jacopini sembrerebbe suggerire l'inutilità di ulteriori costrutti nei linguaggi di programmazione. Tale affermazione equivarrebbe ad asserire l'inutilità dell'automobile, dal momento che è possibile raggiungere qualunque destinazione a piedi. Per quanto questo linguaggio sia tecnicamente sufficiente per realizzare programmi anche molto complessi, è chiaro che oltre un certo livello, l'impresa andrebbe al di la delle capacità umane.

L'importanza di questo asserto risiede nel fatto che esso definisce in modo chiaro e rigoroso un insieme di nozioni che costituiscono, a tutti gli effetti, la base di qualunque linguaggio di programmazione. Nei prossimi capitoli affronteremo lo studio di costrutti più avanzati: tipi di dati numerici, vettori, stringhe, metodi, classi ed oggetti. Ogni nuovo strumento permetterà di realizzare programmi più brevi e più comprensibili di quelli che si possono scrivere con il semplice linguaggio descritto in questo capitolo. La possibilità di creare strumenti nuovi attraverso un uso sapiente di quelli esistenti è senza dubbio uno degli aspetti più affascinanti della scienza del calcolo: questo processo induttivo è la chiave principale del rapido sviluppo dell'informatica.

Esercizi

  1. Margherita Elettronica
    Ogni margherita ha un numero di petali che varia casualmente tra 30 e 50. E' risaputo che la presenza o l'assenza di un petalo può avere delle grosse conseguenze sulle nostre aspirazioni sentimentali.
    Creare un programma MargheritaElettronica, con una variabile "numeroPetali" calcolata con la seguente formula:

    int numeroCasuale = (int)(21 * Math.random());
    int numeroPetali = 30 + numeroCasuale;


    Il programma, mentre conta ad uno ad uno i petali, stampa alternativamente le scritte "m'ama" e "non m'ama".
    Importante: prima di eseguire il programma, pensare intensamente alla persona amata.

  2. Shining
    A voler essere precisi, le scritte che compaiono sui fogli nel film Shining sono state realizzate seguendo pattern compositivi differenti dal semplice incolonnamento. Si chiede di costruire, utilizzando in modo corretto i cicli while, un programma che stampi su schermo una sequenza di frasi con il seguente schema:

    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca
    Il mattino ha l'oro in bocca


    Suggerimento: ricorrere a due cicli while. Ad ogni passo del ciclo più esterno si esegua un ciclo while di questo tipo.

    numeroSpazi = 0;
    while(numeroSpazi < maxSpazi) {
    System.out.print(" ");
    numeroSpazi++;
    }

    Ad ogni iterazione, il ciclo più esterno incrementa la variabile 'maxSpazi': se il suo valore è maggiore di 10, la riporta a zero.

  3. Shining 2
    Dopo aver rivisto il film Shining, costruire programmi che riproducano alcuni dei pattern presenti nel film.

  4. Sistema Operativo
    Riprodurre il sistema operativo preferito, utilizzando solo i costrutti presenti in questo capitolo.

 

Invito all'approfondimento
Rapidità, precisione e capacità di eseguire in modo instancabile operazioni ripetitive sono qualità importantissime per un calcolatore. Se presenti in un essere umano, tali caratteristiche tendono a conferire una connotazione maniacale. Il cinema ricorre spesso a questa associazione, come nel già citato Shinig o in Rain Man, dove un Dustin Hoffmann da Oscar fornisce una straordinaria rappresentazione delle eccezionali capacità di calcolo di alcuni soggetti autistici, abbinata ad una desolante incapacità di relazione sociale.

Curiosamente il mondo del cinema tende a rappresentare con tinte fosche anche lo scenario inverso: per divenire davvero pericolosa, una macchina deve prima assumere qualità umane. La voce suadente di HAL 9000, protagonista indiscusso del colossale 2001 Odissea nello spazio, non serve soltanto a comunicare rotte astrali e guasti agli impianti, ma anche a mentire, per mettere in pratica un diabolico piano congegnato grazie alla sofisticata euristica che lo accomuna ad un essere senziente. Matrix e la saga di Terminator sono due esempi più recenti di film realizzati non solo per sbancare al botteghino, ma anche per indurre a riflettere sul desiderio di creare macchine dotate di una quale forma di intelligenza.

Dove si situa, ai giorni nostri, il confine reale tra intelligenza umana ed intelligenza artificiale? Un computer capace di fornire una diagnosi attraverso l'analisi dei sintomi di un paziente può essere definita "esperto", grazie alla sua capacità di confrontare in pochi attimi il caso in esame con milioni di altri simili, memorizzati all'interno di un archivio elettronico. Ma un simile "sistema esperto" non sarà mai in grado di cogliere le sottili sfumature nella voce di un paziente o di percepire le sue emozioni: la capacità di cogliere questi parametri, così difficili da quantificare e da classificare, è la qualità umana che può distinguere un buon medico da un medico straordinario.

Ma se mai un giorno qualcuno dovesse riuscire a dotare una macchina di coscienza e volontà, speriamo solo si ricordi di plasmare la sua creazione secondo le tre leggi della robotica di Asimov, la prima delle quali recita "Un robot non può arrecare danno agli esseri umani, nè permettere che degli esseri umani ricevano danno a causa del suo mancato intervento"

 

Conclusioni
Questo mese abbiamo consolidato le nozioni apprese il mese scorso. Attraverso l'analisi di esempi di complessità crescente, abbiamo cominciato a farci un'idea di come ragiona una macchina. Il mese prossimo approfondiremo il tema dei tipi di dati primitivi, ed introdurremo i vettori, o array, uno dei più importanti strumenti per la gestione della memoria.

 

Esempi allegati
Gli esempi completi qui descritti posson essere scaricati tramite il seguente link

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it