Innehåll, föreläsning 3 (Objektorienterad programmering i Java)

Föreläsning 3: Objektorienterad programmering i Java

Arv

Arv är det kanske viktigaste begreppet i objektorienterad programmering: med arv kan en klass erhålla en annan klass' egenskaper (attribut och metoder), lägga till egna klass-specifika egenskaper, samt ersätta egenskaper (metoder) som ändras. Vi börjar med en något förändrad version av exemplet Shape tidigare:
public class Shape
{
    
    public Shape( int ix, int iy, int isize, boolean ifill)
    { x =  ix; y = iy; fill = ifill; }

    protected int x;
    protected int y;
    protected boolean fill;
    public void moveRel( int dx, int dy )
    { x += dx; y += dy; }
    public void print() 
    { System.out.println("pos " + x "," + y); }
}

public class Circle extends Shape
{
    public Circle( int ix, int iy, int r, boolean ifill)
    {
        super(ix, iy, r, ifill);
        radius = r;
    }
    protected int radius;
    public void print() 
    { System.out.println("Cirkel, pos " + x "," + y); }    
}
Klassen Shape definierar en position samt huruvida figurens innanmäte skall fyllas eller ej, dess arvinge Circle innehåller dessutom cirkelns storlek. Man kan säga att klassen Circle utökar (extends) Shape och nyckelordet extends används också för att deklerara en arvinge. Attributen i Shape deklareras protected vilket innebär att dess arvingar men inte andra kan komma åt dem.

Vid arv kedjas konstruktorerna, d.v.s. Shapes konstruktor anropas vid konstruktion av Circle-objekt. Om man vill anropa en annan konstruktor än en utan argument måste detta göras explicit: referensen super betyder en referens till den del av klassen som utgörs av superklassen (jämför this). Precis som this kan super användas för att kvalificera attribut och metoder: detta kan ibland vara nödvändigt, t.ex. om ett attribut i superklassen har samma namn som ett i subklassen.

I exemplet ersätts metoden print() i subklassen. Detta innbär att Circle-referenser och Shape-referenser beter sig olika vid anrop av denna metod. Eftersom Shapes print() ju gör en del av det som Circles gör skulle man kanske vilja använda den. Men om vi skriver print() i Circles print() får vi ett rekursivt anrop. Lösningen heter super:

   public void print()  // i klassen Circle
    { System.out.print("Cirkel,"); super.print(); }    
För att riktigt inse styrkan i arv skall vi nu utvidga exemplet med en klass och en metod för att rita figuren. Dessutom införs begreppet abstrakt klass: inga instanser av klassen Shape kommer någonsin att skapas, en figur är en cirkel eller en rektangel, aldrig bara en figur. Nyckelordet abstract i en klassdeklaration innebär att inga instanser av klassen kan skapas. Även metoder kan deklareras abstract:
abstract public class Shape
{
    
    public Shape( int ix, int iy, int isize, boolean ifill)
    { x =  ix; y = iy; fill = ifill; }

    protected int x;
    protected int y;
    protected boolean fill;
    public void moveRel( int dx, int dy )
    { x += dx; y += dy; }
    abstract public void draw(); // Notera ; i stället för { .. }
}

public class Circle extends Shape
{
    public Circle( int ix, int iy, int r, boolean ifill)
    {
        super(ix, iy, ifill);
        radius = r;
    }
    public void draw() { /* do the drawing */ }
    public void setSize( int r ) { radius = r; }
    protected int radius;
}

public class Rectangle extends Shape
{
    public Rectangle( int ix, int iy, int w, int h, boolean ifill)
    {
        super(ix, iy, ifill);
        width = w; height = h;
    }
    public void draw() { /* do the drawing */ }
    public void setSize( int w, int h ) { width = w; height = h; }
    protected int width;
    protected int height;

}
Själva draw()-metoderna visas inte här men finns t.ex. i det tidigare exemplet. Antag nu att vi har en massa figurer vi vill rita. Ett sätt att göra detta vore att ha en array med cirklar och en med rektanglar (och en med trianglar, en med generella polygoner, etc). Men med hjälp av vår arvshierarki kan vi skapa instanser av subklasser och lagra i referenser till superklassen! Den regel som styr hur man får lagra referenser säger just detta: en variabel får referera till instanser av sin "deklarerade" klass och till instanser av alla subklasser. Och vid anrop av en metod väljs inte superklassens metod utan subklassens ersättning om en sådan finns. I fallet abstrakta metoder är ju för övrigt detta beteende nödvändigt. Ett kodavsnitt som utnyttjar detta - som kallas polymorfi kan se ut så här:
Shape[] shape = new Shape[10];
shape[0] = new Circle( 100, 100, 50, false );
shape[1] = new Rectangle( 200, 100, 50, 100, true );
.
.
.
for (int i=0;i < shape.length; i++)
  shape[i].draw();
Men hur ska man kunna använda metoden setSize() som ju har olika argumentuppsättning i de två fallen? Så långt möjligt bör man undvika att hamna i lägen där man måste veta exakt vilken subklass en referens avser men ibland kan det bli nödvändigt:
// Antag att alla cirklar ska ges storleken 12
for (int i=0;i < shape.length; i++)
  {
    if ( shape[i] instanceof Circle)
      {
        Circle c = (Circle) shape[i]; // cast shape[i] to Circle.
        c.setSize( 12 );
      }
  }
Här ser vi två nya konstruktioner, dels operatorn instanceof som returnerar sant om dess första operand är en instans av den andra operanden, dels också en s.k. cast, d.v.s. man konverterar en variabel från en typ till en annan. Java kontrollerar att typkonverteringen kan göras: om man använder uttrycket Circle c = (Circle) shape[i]; när shape[i] är en Rectangle kastas ett undantag. En varning: använd instanceof och casts så litet som möjligt: det är fullt möjligt att helt ta bort fördelarna med OOP genom flitigt uttnyttjade av dessa "finesser"!

Precis som attribut kan också metoder och klasser deklareras final: en final-deklarerad klass kan inte ärvas och en final-deklarerad metod kan inte ersättas i subklasser. Metoder som är deklarerade private eller static blir också final.

Metoden finalize() används ju för att städa upp efter sig och kan ses som motsatsen till konstruktorn. Till skillnad från den "kedjas" inte finalize(), d.v.s. superklassens finalize anropas inte automatiskt.

Som tidigare påpekats är klassen Object superklass til samtliga Java-klasser - detta utan att man behöver ange det med extends. I superklassen finns några metoder som kan vara bra att ersätta i de klasser man skriver själv:

public boolean equals( Object obj);
protected Object clone();
public String toString();
toString() har tidigare diskuterats. Metoden equals() avgör om två objekt är lika. Vad detta betyder kan diskuteras men Objects equals() returnerar sant endast om samma objekt refereras av de två variablerna. Man bör ersätta detta med en metod som kontrollerar om alla relevanta attribut är lika.
class Circle
{
    public Circle( int ix, int iy, int isize )
    {  x = ix; y = iy; size = isize;  }

    public boolean equals( Object obj )
    { 
       if (obj instanceof Circle)
          {
             Circle c = (Circle) obj;
             return (c.x==x) && (c.y==y) && (c.size==size);
          }
       else return false;
    }
}
Observera att equals() för att fungera med variabler av superklassens typ (som kan vara en referens till en subklass) måste skrivas med ett argument av klassen Object.

Gränssnitt

I vissa språk kan man ärva flera klasser. Java tillåter inte detta men något liknande kan åstadkommas med hjälp av en annan Java-mekanism: interface. Ett (en?) interface är en klass som innehåller endast abstrakta metoder. En klass implementerar ett interface. Ett alternativt sätt att implementera uppritning av de olika figurerna i det tidigare exemplet vore att ta bort den abstrakta metoden draw i Shape och använda interface:
interface Drawable 
{
  public void draw( Graphics g);
}

class Circle extends Shape implements Drawable
{
    public Circle( int ix, int iy, int isize, boolean ifill)
    {  super(ix, iy, isize, ifill);  }

    public void draw( Graphics g )
    {
	if (fill) g.fillOval(x-size,y-size,size*2,size*2);
	else      g.drawOval(x-size,y-size,size*2,size*2);
    }
}
Ett annat exempel på användning av interface är metoden clone i Object. För att en klass skall kunna "klonas" krävs att den implementerar (det helt tomma!) gränssnittet Cloneable:
class Circle implements Cloneable
{
    public Circle( int ix, int iy, int isize )
    {  x = ix; y = iy; size = isize;  }
 
    public Object clone() 
    { return new Circle( ix, iy, isize ); }

}

Paket och skyddsnivåer

I Java används paket (packages) för att hålla samman klasser som hör ihop på något sätt. T.ex. finns paketet java.net för nätverksprogrammering. Man kan skapa egna paket genom att ange nyckelordet package i klassdeklarationen. Alla klasser utan detta buntas ihop i ett default-paket. Detta har viss betydelse för åtkomst av attribut och metoder som vi ska se. Exempel på ett egendefinierat paket:
package MyShapes;

public class Shape 
{
...
}
Genom att placera package MyShapes; i alla filer som beskriver figurer skapar man ett eget paket.

Alla attribut och metoder har en (oftast explicit angiven) skyddsnivå som talar om vem som kommer åt den. Om man inte anger någon nivå får variabeln eller metoden s.k. default-nivå, vilket innebär åtkomst för alla metoder i samma paket men ingen annan, t.ex. inte subklasser i andra paket. Om man anger private kommer endast klassens egna metoder åt fältet, public ger fullständig åtkomst för vem som helst. Däremellan finns protected som ger alla i samma paket plus alla subklasser åtkomst och private protected som ger endast subklasser (oavsett paket) åtkomst.

Vilken nivå man skall använda avgörs från fall till fall och beror på hur fältet (attributen eller metoden) skall användas.

Några viktiga paket

I Java används import för att göra klasser och/eller paket tillgängliga för vår kod. Ett par exempel:

import java.awt.*;
import java.awt.Button;
Den första raden ger oss tillgång till alla klasser i java.awt och ser till så att vi kan referera till dem med bara deras namn, t.ex. Label, den andra raden gör samma sak men bara med klassen Button. Alternativt, om vi inte använder import kan vi ge hela namnet, t.ex. java.awt.Label direkt i koden.

Viktiga paket:

Observera att java.lang inte behöver importeras explicit, paketet finns alltid tillgängligt.

Undantag

Ett program kan råka ut för oväntade händelser och fel av olika slag som man måste ta hand om. I traditionell programmering blir koden ofta nedtyngd av olika felkontroller - vissa program ägnar hälften av koden åt att kontrollera status från anropade funktioner. I Java används en genomtänkt modell för felhantering: undantag (exceptions). Detta innebär att när fel inträffar genereras ett s.k. undantagsobjekt (en exception) som tas om hand av den metod som bäst kan hantera felet: man säger att man kastar ett undantag som sedan fångas av den som kan göra något vettigt med det.

Undantag som ej fångas skickas vidare till anropande metod. Ett undantag måste fångas av någon metod, annars hamnar det till slut i main()-metoden varvid Java skriver ett felmeddelande och avslutar programmet.

Klassen Throwable är superklass till Error och Exception som alla undantag ärver från. Man kan enklast säga att undantag av typen Error är fatala fel som man knappast skall försöka rädda. De mer intressanta undantagen finns alltså i Exception. Eftersom undantag är Object kan de definiera data och metoder som fångaren kan använda. När man genererar undantag kan man använda sig av de fördefinierade eller man kan definiera egna.

Undantagshanteringen använder de tre nyckelorden throw, catch och try:

// Först ett egendefinerat undantag:
class SquareRootOfNegativeValue extends Exception
{
    public SquareRootOfNegativeValue() { super(); }
    public SquareRootOfNegativeValue(String s) { super(s); }
}

class TestSqrt
{
    private double value;
    public TestSqrt( double a ) { value = a; }
    public double getSqrt()  throws  SquareRootOfNegativeValue
    {
	if (value >= 0) return Math.sqrt( value );
	else throw new SquareRootOfNegativeValue();
    }
    public static void main( String[] args )
    {
	try // koden inom detta block kontrolleras
	    {
		Double a = Double.valueOf( args[0] );
		TestSqrt ts = new TestSqrt( a.doubleValue() );
		ts.getSqrt();
	    }
	catch (SquareRootOfNegativeValue e) 
	    { System.out.println("Negative value");}
	catch (NumberFormatException e)  //  kan kastas av Double.valueOf
	    { System.out.println("Couldn't parse the number"); }
	catch (ArrayIndexOutOfBoundsException e ) // kan kastas av args[0]
	    { System.out.println("No argument"); }
    }
}
Java kräver att en metod som kan generera ett undantag måste endera fånga det eller tala om att undantaget kastas vidare med throws:
int getElement( int[] arr, int index ) throws IndexOutOfBoundsException 
{
  return arr[index];
}
Undantag som är subklasser till Error eller RuntimeException behöver ej deklareras med throws (i stort sett all kod kan generera dessa undantag). Observera att om en metod kan kasta flera undantag som alla är subklasser till samma superklass räcker det att tala om att metoden kastar superklassen.

Man kan vilja vara säker på att en metod exekverar en del kod oavsett om try-blocket exekveras färdigt eller ej. Sådan kod kan placeras i ett finally-block som alltid exekveras:

try 
{
 
}
catch (Exception e)
{
}
finally
{ 
// exekveras alltid även om undantag kastats.
}