Paola Magillo, Univestita' di Genova, Corso di Programmazione II per SMID, a.a. 2007-2008.

Lezione 05:

CLASSI ASTRATTE ED INTERFACCE

Premessa utile

Array in Java

Per ogni tipo T (tipo base o classe) esiste tipo T[] degli array di oggetti di tipo T.

Un array e' un oggetto. La dimensione (numero di celle) dell'array viene fissata al momento della sua costruzione. Sintassi particolare:

int[] a = new int[15];
La dimensione di un array e' restituita dall'attributo length:
a.length
Gli indici vanno da 0 a length-1. Gli elementi si accedono con la stessa notazione del C:
for (int i=0; i<a.length; i++) a[i] = i;
Il tentativo di accedere a un elemento di posizione oltre la lunghezza dell'array provoca errore a run-time.

Posso inizializzare tutti gli elementi di un array assieme:

int a[] = {1,2,3,4,5,6,7,8,9,10};

Posso fare array di array (array bidimensionale). Esempio matrice 4x4:

float[][] matrice = new float[4][];
for (float i=0; i<4; i++) matrice[i] = new float[4];
Gli array interni possono avere lunghezze diverse fra loro.

Anche qui posso inizializzare tutti gli elementi insieme:

int[][] a = { {1}, {1,2}, {1,2,3} };

Posso fare array di oggetti. Quando costruisco l'array, gli oggetti contenuti sono nulli, devo costruirli a loro volta o assegnare loro degli oggetti gia' costruiti. Esempio array di rettangoli:

Rectangle a[] = new Rectangle[3];
a[0] = new Rectangle(12,4);
a[1] = new Rectangle(8,13);
a[2] = a[1];

Utilita' matematiche

La classe predefinita Math di Java contiene utilita' matematiche: valori assoluti, radici, funzioni trigonometriche, logaritmi, esponenziali... e generazione di numeri casuali.
Tutte queste sono funzioni di classe (static) e si chiamano per es.:

double radice2 = Math.sqrt(2.0);

Il metodo double random() ritorna un numero casuale tra 0 (incluso) e 1 (escluso).
Per un avere un numero casuale tra 0 e 10 semplicemente lo moltiplico per 10, ecc.

Classi astratte

Classe astratta = non definisce l'implementazione di alcuni metodi, che saranno implementati nelle sottoclassi.

Le sottoclassi di una classe astratta possono essere ancora astratte (se non implementano tutti i metodi) oppure concrete (se li implementano tutti).

Esempio: classe astratta per figura geometrica con area, ha per sottoclassi i vari tipi di figure (rettangoli, triangoli, cerchi...), l'implementazione dell'area dipendera' dal tipo di figura.

abstract class Figure
{
  abstract public double area(); /* da implementare secondo la figura */
}

class Rectangle extends Figure
{
  protected double length;
  protected double width;
  Rectangle(double l, double w) { length = l; width = w; }
  public double area() { return length * width; }
}

class Triangle extends Figure
{
  protected double basis;
  protected double height;
  Triangle(double b, double h) { basis = b; height = h; }
  public double area() { return 0.5 * basis * height; }
}

class Circle extends Figure
{
  protected double radius;
  static final double PI = 3.1416;
  Circle(double r) { radius = r; }
  public double area() { return PI * radius * radius; }
}

Non posso costruire oggetti di classi astratte, ma posso dichiarare variabili di classi astratte e metterci dentro oggetti di sottoclassi concrete.

Esempio: crea un array di figure eterogenee e calcola l'area totale. Ogni figura calcola l'area secondo l'implementazione propria della sua sotto-classe (ved. binding dinamico).

class FigureTest
{
  Figure[] a; // array di figure
  void creaFigure(int num)
  {
    a = new Figure[num];
    int i;
    for (i=0; i<num; i++)
    {
      // double Math.random() ritorna numero casuale tra 0 e 1
      switch ( ((int)(100*Math.random())) % 3 ) // numero casuale 0,1,2
      {
        case 0:
          a[i] = new Rectangle(10.0*Math.random(),10.0*Math.random());
          break;
        case 1:
          a[i] = new Triangle(10.0*Math.random(),10.0*Math.random());
          break;
        case 2:
          a[i] = new Circle(10.0*Math.random());
          break;
      }
    }
  }
  double calcolaArea()
  {
    int i;
    double areaTot = 0.0;
    for (i=0; i<a.length; i++) areaTot += a[i].area();
    return areaTot;
  }
  public static void main(String[] args)
  {
    FigureTest ft = new FigureTest();
    ft.creaFigure(10);
    System.out.println("Area totale = " + ft.calcolaArea());
  }
}

Variante: calcola l'area totale solo dei rettangoli. Ogni oggetto conosce la sua classe...

  double calcolaArea()
  {
    int i;
    double areaTot = 0.0;
    for (i=0; i<a.length; i++)
    {
      if (a[i].getClass().getName().compareTo("Rectangle") == 0) 
      areaTot += a[i].area();
    }
    return areaTot;
  }

Gerarchie di ereditarieta'

La relazione di super-classe / sotto-classe genera una gerarchia che (se ereditarieta' e' singola) si puo' rappresentare con un albero.

Esempio:

Diagramma di classi

Disegno...

Interfacce

Interfaccia = simile a una classe, ma dichiara solo metodi senza implementarli e puo' dichiarare solo costanti (parola chiave "final"), non variabili.

Per es. la classe astratta Figure potevo anche scriverla come interfaccia:

interface Figure
{
  public double area();
}

class Rectangle implements Figure
{
  ...come prima...
}

...eccetera per Triangle e Circle...

Interfacce hanno loro gerarchia di super-interfacce e sotto-interfacce, parallela e indipendente rispetto alla gerarchia di super-classi e sotto-classi.

Una classe (sotto-classe) estende un'altra classe (super-classe) aggiungendo attributi e metodi e re-implementando metodi ereditati. In Java parola chiave "extends".
Un'interfaccia (sotto-interfaccia) estende un'altra interfaccia aggiungendo costanti o dichiarazioni di metodi non implementati. In Java parola chiave "extends".
Una classe implementa un'interfaccia implementando i metodi che questa dichiarava; puo' anche aggiungere attributi e metodi a piacere. In Java parola chiave "implements".

In Java una classe puo estendere una sola super-classe (ereditarieta' singola) ma puo' implementare un numero arbitrario di interfacce.

Esempio

Interfaccia "figura" dice che oggetti devono saper calcolare l'area. Interfaccia "scalabile" dice che oggetti devono sapersi allargare o restringere. Classe "rettangolo" le implementa entrambe.

interface Figure
{
  public double area();
}

interface Scalable
{ 
  public void enlarge();
  public void shrink();
}

class Rectangle implements Figure, Scalable
{
  protected double length;
  protected double width;
  Rectangle(double l, double w) { length = l; width = w; }
  public double area() { return length * width; }
  public void enlarge() { length*=2.0; width*=2.0; }
  public void shrink() { length*=0.5; width*=0.5; }
}

Interfaccia stabilisce un "contratto" che le classi devono rispettare.

Le interfacce sono ortogonali alla gerarchia delle classi. Classi non correlate fra loro per ereditarieta' possono implementare la stessa interfaccia.

Posso dichiarare variabili di tipo interfaccia e metterci dentro oggetti di qualunque classe implementi quella interfaccia.

Liste di oggetti generici

Abbiamo visto che la classe Vagone e' l'elemento base per implementare una lista di interi (ved. lezione 4). Sostanzialmente, il treno (ved. lezione 3) e' una lista di interi.

Per definire una lista di oggetti generici basta sostituire, nel vagone, int con Object. Object in Java e' super-classe comune implicita di tutte le classi che non sono sotto-classi di altre. Transitivamente e' super-classe indiretta di tutte le classi Java. Percio' ogni oggetto di qualsiasi classe puo' essere visto come Object.

Allora in una lista posso mettere oggetti qualsiasi, anche di classi eterogenee.

class VagoneGenerico /* come Vagone ma Object invece che int */
{
  Object oggetto;
  VagoneGenerico prossimo;
  VagoneGenerico(Object o) { oggetto = o; prossimo = null; }
  void aggancia(VagoneGenerico vag) { prossimo = vag; }
  void print() { ... }
}

La lista e' come il treno: aggancia una sequenza di vagoni (ciascuno contenente un oggetto).

Differenza tra la classe ObjectList e la classe Treno:

class ObjectList
{
  protected VagoneGenerico primo;
  public void aggiungiInCima(Object o)
  {
    VagoneGenerico aux = new VagoneGenerico(o);
    /* creo nuovo vagone con l'oggetto assegnato */
    aux.aggancia(primo); 
    /* aggancio il primo vagone dietro al nuovo */
    primo = aux;
    /* il nuovo diventa il primo vagone */
  }
  public boolean vuota() { return (primo == null); }
  void print() { ... }
  public Object oggettoPerNumero(int n) // n parte da 0
  {
    VagoneGenerico aux = primo;
    int i = 0;
    while ( (aux!=null) && (i<n) ) 
    {  aux = aux.prossimo;
       i++;
    }
    if (aux!=null) return aux.oggetto;
    else return null;
  }
}

Nel mettere in lista un oggetto, questo viene automaticamente convertito a Object. Esempio due oggetti di classi Class1 e Class2:

Class1 o1;
Class2 o2;
ObjectList l = new ObjectList();
l.aggiungiInCima(o1);
l.aggiungiInCima(o2);

Nel prelevare oggetti dalla lista, attenzione che vengono restituiti come oggetti della classe generica Object, non come oggetti della loro propria classe.

Object o = l.oggettoPerNumero(1); /* giusto */
Class1 c = l.oggettoPerNumero(1); /* sbagliato */
Se so che l'oggetto prelevato e' di classe Class1 (per es. ho messo in lista solo oggetti di class1), posso fare una conversione forzata (cast):
Class1 c = (Class1)l.oggettoPerNumero(2);
Altrimenti devo prima guardare di che classe e' (con getClass()...).

Per poter mettere in lista valori di tipi base (int, boolean...), esistono classi wrapper (= impacchettatrici) che corrispondono ai tipi base. I loro valori sono i valori base visti come oggetti.
Es. per tipo base int la classe wrapper Integer:

Analogo per classi Boolean, Float, Double, Character...

Trasformo il valore base in oggetto della corrispondente classe wrapper e lo metto in lista.

Invece che lista di oggetti generici (classe Object) posso fare lista di oggetti di una particolare classe (astratta o concreta) o di una interfaccia.
Se e' una classe potro' mettere in lista oggetti di quella classe o di sue sottoclassi.
Se e' di un'interfaccia mettere in lista oggetti di qualsiasi classe implementi quella interfaccia.

Esempio: lista di Figure, posso metterci di classi Rectangle, Triangle, Circle.

Eccezioni

Meccanismo per gestire errori a run-time.
Un'eccezione viene sollevata (thrown) da programma quando si verifica una situazione che il programmatore considera essere di errore.
Esempio: tento di assegnare lunghezza o larghezza negativa a un rettangolo.

L'eccezione puo' essere catturata e gestita, sempre da programma, compiendo azioni di ricovero dell'errore.

Un metodo deve dichiarare quali eccezioni puo' sollevare.

Un metodo che nella sua implementazione usa un altro metodo che puo' sollevare una eccezione, deve fare una delle seguenti cose:

Le eccezioni sono oggetti, istanze di classi che estendono (direttamente o indirettamente) la super-classe Exception.

Classe rettangolo con eccezioni

class RectangleExcp
{
  private int length, width;

  public int getLength() { return length; }
  public int getWidth() { return width; }

  public void setLength (int l) throws NegativeRectangleException
  {
    if (l>=0) length = l;
    else throw new NegativeRectangleException("Negative length");
  }

  public void setWidth (int w) throws NegativeRectangleException
  {
    if (w>=0) width = w;
    else throw new NegativeRectangleException("Negative width");
  }

  RectangleExcp(int l, int w) throws NegativeRectangleException
  {
    setLength(l); setWidth(w);
  }

  RectangleExcp()  {  length = width = 0;  }

  int area() { return length * width; }

  void print()
  { System.out.println("Rectangle " + length + " x " + width); }

  public static void main(String[] args)
  {
    /* legge da command line due numeri interi per le 
       dimensioni del rettangolo */
    int input_l = 0;
    int input_w = 0;
    if (args.length!=2)
    {
      System.out.println("Necessari due interi come argomenti.");
      System.out.println("Per default: " + input_l + " " + input_w);
    }
    else
    {
      input_l = Integer.parseInt(args[0]);
      input_w = Integer.parseInt(args[1]);
      System.out.println("Argomenti: " + input_l + " " + input_w);
    }
    RectangleExcp r = new RectangleExcp();
    try
    {
      r.setLength(input_l); r.setWidth(input_w);
      r.print();
      int a = r.area();
      System.out.println("Area = " + a);
    }
    catch (NegativeRectangleException excp)
    {
      System.out.println("Errore... " + excp.getMessage());
      excp.printStackTrace();
      r.print();
    }
  }

  class NegativeRectangleException extends Exception
  {
    NegativeRectangleException(String msg) { super(msg); }
  };

}
CLasse NegativeRectangleException e' classe interna. Dichiarata dentro un'altra classe (qui RectangleExcp), non puo' essere usata al di fuori di quest'ultima se non con la notazione RectangleExcp.NegativeRectangleException.