Indhold:
Forstå og definere interfaces
Polymorfi og anvendelse af interfaces
Standardbibliotekets brug af interfaces
Forudsættes af kapitel 13, Hændelser, kapitel 17, Flertrådet programmering, kapitel 18, Serialisering, kapitel 19, RMI og kapitel 21, Avancerede klasser.
Forudsætter kapitel 5, Nedarvning.
I
et eksempel anvendes appletter, beskrevet i kapitel 10, Appletter.
I generel sprogbrug er et interface (da.: snitflade) en form for grænseflade, som man gør noget gennem. F.eks. er en grafisk brugergrænseflade de vinduer med knapper, indtastningsfelter og kontroller, som brugeren har til interaktion med programmet.
Vi minder om, at en klasse er definitionen af en type objekter. Her kunne man opdele i
Grænsefladen - hvordan
objekterne kan bruges udefra.
Dette udgøres af navnene1
på metoderne, der kan ses udefra.
Implementationen - hvordan objekterne
virker indeni.
Dette udgøres af variabler og programkoden
i metodekroppene.
Et 'interface' svarer til punkt 1): En definition af, hvordan objekter bruges udefra. Man kan sige, at et interface er en "halv" klasse.
Et interface er en samling navne på metoder (uden krop)
Et interface kan implementeres af en klasse - det vil sige, at klassen definerer alle interfacets metoder sammen med programkoden, der beskriver, hvad der skal ske, når metoderne kaldes.
Lad os definere et interface kaldet Tegnbar, der beskriver noget, der kan tegnes.
import java.awt.*; public interface Tegnbar { public void sætPosition(int x, int y); public void tegn(Graphics g); }
I stedet for "class" erklæres et interface med "interface".
Metoder i et interface har ingen krop, alle metodeerklæringerne følges af et semikolon.
Der kan ikke oprettes objekter ud fra et interface. Det kan opfattes som en "tom skal", der skal "fyldes ud" af en rigtig klasse, der implementerer metoderne (definerer kroppene).
Man ser, at tegnbare objekter:
har en metode til at sætte positionen på skærmen
har en metode til at tegne objektet.
I UML-notation (tegningen til højre) er Tegnbar-interfacet tegnet med kursiv. Alle metoderne er abstrakte (= ikke implementerede) og er derfor også tegnet kursivt.
Lad os nu definere en klasse, der implementerer Tegnbar-interfacet.
En klasse kan erklære, at den implementerer et interface og så skal den definere alle metoderne i interfacet og give dem en metodekrop
Vi skal altså definere alle interfacets metoder sammen med programkoden, der beskriver, hvad der skal ske, når metoderne kaldes.
import java.awt.*; public class Stjerne implements Tegnbar { private int posX, posY; public void sætPosition(int x, int y) // kræves af interfacet Tegnbar { posX = x; posY = y; } public void tegn(Graphics g) // kræves af interfacet Tegnbar { g.drawString("*",posX,posY); } }
Her har klassen Stjerne "udfyldt skallen" for Tegnbar ved at skrive "implements Tegnbar" og definere sætPosition()- og tegn()-metoderne (vi har også variabler til at huske x og y).
Man kan erklære variabler af en interface-type. Disse kan referere til alle slags objekter, der implementerer interfacet2. Herunder erklærer vi en variabel af type Tegnbar og sætter den til at referere til et Stjerne-objekt.
Tegnbar t; t = new Stjerne(); // Lovligt, Stjerne implementerer Tegnbar
Stjerne-objekter er også af type Tegnbar. Ligesom ved nedarvning siger man, at der er relationen Stjerne er-en Tegnbar og at t er polymorf, da den kan referere til alle slags Tegnbare objekter.
Man kan ikke oprette objekter ud fra et interface (der er en "skal" og intet siger om, hvordan metoderne er implementerede - hvordan skulle objektet reagere, hvis de blev kaldt?).
t = new Tegnbar(); // FEJL! Tegnbar er ikke en klasse
Lad os udvide (arve fra) Terning og få den til at implementere Tegnbar-interfacet:
import java.awt.*; public class GrafiskTerning extends Terning implements Tegnbar { int x, y; public void sætPosition(int x, int y) { this.x = x; this.y = y; } private void ci(Graphics g, int i, int j) { g.fillOval(x+1+10*i,y+1+10*j,8,8); // Tegn fyldt cirkel } public void tegn(Graphics g) { int ø = værdi; g.drawRect(x,y,30,30); // Tegn kant if (ø==1) ci(g,1,1); // Tegn 1-6 øjne else if (ø==2) { ci(g,0,0); ci(g,2,2); } else if (ø==3) { ci(g,0,0); ci(g,1,1); ci(g,2,2); } else if (ø==4) { ci(g,0,0); ci(g,0,2); ci(g,2,0); ci(g,2,2); } else if (ø==5) { ci(g,0,0); ci(g,0,2); ci(g,2,0); ci(g,2,2); ci(g,1,1); } else {ci(g,0,0); ci(g,0,1); ci(g,0,2); ci(g,2,0); ci(g,2,1); ci(g,2,2); } } }
For at gøre koden kort har tegn() en hjælpemetode ci(), der tegner en cirkel for et øje.
Bemærk:
Man kan godt have flere metoder end specificeret i interfacet (i dette tilfælde ci()).
GrafiskTerning er en Tegnbar og samtidig en Terning. Der kan kun arves fra én klasse, men samtidigt kan der godt implementeres et interface (faktisk også flere).
Lad os gøre det samme med et raflebæger. For variationens skyld lader vi bægeret altid have den samme position ved at lade sætPosition()'s krop være tom.
import java.awt.*; public class GrafiskRaflebaeger extends Raflebaeger implements Tegnbar { public GrafiskRaflebaeger() { super(0); } public void sætPosition(int x, int y) { // tom metodekrop } public void tegn(Graphics g) { g.drawOval(80,20,90,54); g.drawLine(150,115,170,50); g.drawLine(100,115,80,50); g.drawArc(100,100,50,30,180,180); } }
Kunne vi have udeladt sætPosition()-metoden, der alligevel ikke gør noget? Nej, vi har lovet at implementere begge metoder, om det så blot er med en tom krop, idet vi skrev "implements Tegnbar".
En hvilken som helst klasse kan gøres til at være Tegnbar. Herunder udvider vi standardklassen Rectangle til at være Tegnbar:
import java.awt.*; public class Rektangel extends Rectangle implements Tegnbar { public Rektangel(int x1, int y1, int width1, int height1) { super(y1,x1,width1,height1); } public void sætPosition(int x1, int y1) { x = x1; y = y1; } public void tegn(Graphics g) { g.drawRect(x,y,width,height); } }
Lad os nu lave et vindue, der viser nogle tegnbare objekter:
import java.awt.*; import java.util.*; public class TegnbareObjekter extends Frame { ArrayList<Tegnbar> tegnbare = new ArrayList<Tegnbar>(); GrafiskRaflebaeger bæger = new GrafiskRaflebaeger(); public void sætPositioner() { for (Tegnbar t : tegnbare) { int x = (int) (Math.random()*200); int y = (int) (Math.random()*200); t.sætPosition(x,y); } } public TegnbareObjekter() { Stjerne s = new Stjerne(); tegnbare.add(s); tegnbare.add( new Rektangel(10,10,30,30) ); tegnbare.add( new Rektangel(15,15,20,20) ); GrafiskTerning t; t = new GrafiskTerning(); bæger.tilføj(t); tegnbare.add(t); t = new GrafiskTerning(); bæger.tilføj(t); tegnbare.add(t); tegnbare.add(bæger); sætPositioner(); } public void paint(Graphics g) { for (Tegnbar t : tegnbare) { t.tegn(g); } sætPositioner(); repaint(5000); // gentegn efter 5 sekunder } }
public class VisTegnbareObjekter { public static void main(String[] arg) { TegnbareObjekter vindue = new TegnbareObjekter(); vindue.setSize(300,300); vindue.setVisible(true); } }
Programmet holder styr på objekterne i tegnbare-listen. Da stjerner, rektangler, terningerne og raflebægeret alle er Tegnbare, kan de behandles ens, hvad angår tegning og positionering.
Det er meget kraftfuldt, at man kan erklære variabler af en interface-type. Disse kan referere til alle mulige slags objekter, der implementerer interfacet. Herefter kan vi f.eks. løbe en liste igennem og arbejde på objekterne i den, selvom de er af vidt forskellig type.
Dette så vi i paint()-metoden i TegnbareObjekter-klassen:
for (Tegnbar t : tegnbare) { t.tegn(g); }
Et interface som Tegnbar kan bruges til at etablere en fællesnævner mellem vidt forskellige objekter, som derefter kan behandles ens. Dette kaldes polymorfi (græsk: "mange former").
Fællesnævneren - nemlig at de alle implementerer det samme interface - tillader os at arbejde med objekter uden at kende deres præcise type. Dette kan i mange tilfælde være en fordel, når vi arbejder med objekter, hvor vi ikke kender (eller ikke interesserer os for) den eksakte type.
Interfaces bliver brugt i vid udstrækning i standardbibliotekerne og mange steder benyttes polymorfi til at gøre det muligt at lade systemet arbejde på programmørens egne klasser.
I det følgende vil vi se nogle eksempler på, at implementationen af et interface fra standardbiblioteket gør, at vores klasser passer ind i systemet på forskellig måde.
Hvis et objekt implementerer Comparator-interfacet, skal det definere metoden:
public int compare(Object obj1, Object obj2)
Denne metode skal sammenligne obj1 og obj2 og afgøre om obj1 kommer før obj2 eller omvendt. For eksempel kunne vi lave en klasse, der kan sammenligne Terning-objekter ud fra antallet af øjne, de viser:
import java.util.*;
public class TerningComparator implements Comparator
{
public int compare(Object obj1, Object obj2) // kræves af Comparator
{
Terning t1 = (Terning) obj1;
Terning t2 = (Terning) obj2;
if (t1.værdi == t2.værdi) return 0; // obj1 og obj2 har samme plads
if (t1.værdi > t2.værdi) return 1; // obj1 kommer efter obj2
else return -1; // obj1 kommer før obj2
}
}
En Comparator giver standardbiblioteket mulighed for at sammenligne nogle objekter og sortere dem i forhold til hinanden. Sortering kan bl.a. ske ved at kalde metoden Collections.sort() med en liste af objekter og en Comparator:
import java.util.*; public class BenytTerningComparator { public static void main(String[] arg) { ArrayList<Terning> liste = new ArrayList<Terning>(); liste.add( new Terning()); liste.add( new Terning()); liste.add( new Terning()); liste.add( new Terning()); liste.add( new Terning()); System.out.println("før sortering: "+liste); Comparator sammenligner = new TerningComparator(); Collections.sort(liste, sammenligner ); System.out.println("efter sortering: "+liste); } }
før sortering: [6, 3, 4, 2, 4] efter sortering: [2, 3, 4, 4, 6]
Metoden sort() vil løbe listen igennem og sammenligne elementerne ved kalde compare() på Comparator-objektet for at sortere dem i rækkefølge.
Systemkaldet Collections.sort() sorterer en liste af elementer ved hjælp af en sammenligner (Comparator).
Systemet
finder rækkefølgen af elementerne ved at kalde
compare() på sammenligneren
til at afgøre om de
enkelte elementer kommer før eller efter hinanden.
Hvis man vil bruge flere tråde (processer, der kører samtidigt i baggrunden) i sit program, kan dette opnås ved at implementere interfacet Runnable og definere metoden run(). Derefter opretter man et tråd-objekt med new Thread(objektDerImplementererRunnable). Når tråden startes (med trådobjekt.start()), vil det begynde en parallel udførelse af run()-metoden i objektDerImplementererRunnable.
Dette vil blive behandlet i kapitel 17, Flertrådet programmering.
Når man programmerer grafiske brugergrænseflader, kan det være nyttigt at kunne få at vide, når der er sket en hændelse, f.eks. at musen er klikket et sted.
Dette sker ved, at man definerer et objekt (lytteren), der implementerer MouseListener-interfacet. Den har forskellige metoder, f.eks. mouseClicked(), der er beregnet på et museklik.
Lytteren skal registreres i en grafisk komponent, f.eks. en knap eller et vindue. Det gøres ved at kalde komponentens addMouseListener()-metode med en reference til lytteren. Derefter vil, hver gang brugeren klikker på komponenten, lytterens mouseClicked() blive kaldt.
Analogt findes lyttere til tastatur, musebevægelser, tekstfelter, kontroller osv. I kapitel 13 om grafiske brugergrænseflader og hændelser er disse ting beskrevet nærmere.
Lav klassen Hus, der skal implementere Tegnbar. Føj den til TegnbareObjekter og prøv, om det virker.
Prøv at tilføje et ikke-'Tegnbar't objekt (f.eks. en streng eller et Point-objekt) til tegnbare-listen. Hvad sker der så? Hvilken fejlmeddelelse kommer der?
Lav tre implementationer af Comparator, der sorterer strenge hhv. alfabetisk, omvendt alfabetisk og alfabetisk efter andet tegn i strengene. Lav en liste (ArrayList) med ti strenge og test din sortering med Collections.sort(liste, Comparator-objekt).
Kig på matador-spillet afsnit 5.3. Ændr Felt til at være et interface.
1Egentlig signaturen, dvs. metodenavn og antal og type af parametre.
2Det vil sige alle objekter, hvis klasse implementerer interfacet.
3public=tilgængelig for alle, static=klassevariabel, final=konstant; umulig at ændre.