Notera att detta dokument är en del i ett mer omfattande dokument varför inledningen t.ex. kan te sig något abrupt.
import java.applet.*; import java.awt.*; public class Welcome extends Applet { public void paint( Graphics g) { g.drawString("Welcome to the World of Internet Programming", 100, 100); } };Detta program sparas i filen Welcome.java som kompileras med kommandot
javac Welcome.javavarvid en fil (med suffixet .class) i s.k. byte-kod skapas. Denna fil anropas sedan i sin tur från en HTML-sida:
<APPLET CODE="Welcome.class" WIDTH=500 HEIGHT=120> </APPLET>som resulterar i detta:
Nu finns det inte mycket dynamik i texten men Java är speciellt skapat för och därför mycket lämplgt för att generera grafik och användargränssnitt. Mer om det senare.
Det som händer när en Web-läsare hittar ordet APPLET är att Java-programmet (eller snarare den bytekod som genereras av javac) laddas ned till klientmaskinen, d.v.s. den maskin som surfaren sitter vid. Detta kan verka farligt men en applet har mycket begränsade befogenheter och kan inte komma åt klientdatorn (surfarens dator) eller dess disk.
Java är ett objektorienterat programspråk. Vad detta innebär beskrivs i det följande.
Som vi har sett är Java ett objektorienterat språk. Vad innebär detta för
själva processen "att skapa ett program"? Svaret är att eftersom OOP ser
program som en samling av
Sammanfattningsvis: Objektorientering är att arbeta med modeller och att ställa problemområdet i centrum. I ett objektorienterat synsätt delas programmering in i fyra faser: analys, design, implementation och testning. Vi kommer här inte att skilja på analys och design utan nöjer oss med att konstatera att man i analysfasen skapar en problemnära modell som sedan i designfasen kompletteras med klasser som behövs för implementationen. Man bör hålla isär de två men i praktiken växlar man ofta mellan nivåerna: en analys följs av en design som ger upphov till förändringar i analysen, etc.
De viktigaste begreppen i OOP har grafiska motsvarigheter i OMT-notationen. I analysfasen skapar man en figur med hjälp av dessa.
Man kan se en klass som ett record eller en datapost med operationer, d.v.s. funktioner som kan påverka klassen eller besvara frågor om den. Funktionerna kallas metoder och klassens data kallas dess attribut. En viktig del av OOP är att inga andra än klassens metoder kan ändra på data i klassens instanser.
En klass ritas som en rektangel (till vänster ).
I det inledande exempelprogrammet är Applet
en klass som
finns i Java
och Welcome
är en sorts
Applet
.
Arv visas med en triangel med den toppiga sidan uppåt mot basklassen:
Här har vi en superklass (basklass) som innehåller det som är gemensamt för de båda subklasserna, både attribut och metoder.
Det kan finnas klasser som inte är möjliga att instansiera, s.k. abstrakta klasser. En sådan klass tjänar endast som basklass till andra klasser och kan innehålla abstrakta metoder, d.v.s. metoder som inte implementeras i klassen men i de subklasser som kan instansieras (senare i detta avsnitt visar ett exempel på en abstrakt klass).
En association skall vara en fast relation, inte tillfällig. Tillfälliga relationer kan ofta bättre beskrivas som operationer (metoder).
En linje utan siffra eller ring betyder att multipliciteten är exakt 1, en fylld ring betyder många (noll eller fler), en ofylld ring betyder en eller ingen och en eventuell siffra anger alla tillåtna multiplicitetsvärden.
I figuren är associationen mellan Klass_1 och Klass_4 sådan att till varje Klass_1 skall en eller flera Klass_4 vara associerade. Varje instans av Klass_5 är associerad till 1,2,3,4 eller 8 instanser av Klass_2. Varje instans av Klass_3 är associerad till en eller ingen Klass_6, medan varje instans av Klass_6 har många Klass_3-objekt knutna till sig. Klass_3 skulle t.ex. kunna vara personer och Klass_6 yrke (om man nu antar att ingen i dessa arbetslöshetstider är ofin nog att ha flera jobb).
I OMT-notationen används olika sorters cirklar för att poängtera hur många instanser av en klass som kan relateras till en klass. Cirklar kan finnas i båda ändar av relationen. Om man, vilket man kan göra, anger multipliciteten med siffror är cirklarna strängt taget onödiga. I UML har man också tagit bort dem och anger 0.. i stället för fylld cirkel, 0-1 i stället för tom cirkel, o.s.v.
Som framgår av figuren finns det alternativa sätt att visa aggregatsrelationer.
Steg ett i analysfasen är att leta efter lämpliga objekt t.ex. genom att göra en lista med alla substantiv i problembeskrivningen (den traditionella algoritmbaserade programmeringstekniken utgår från verben). Först kan man tillåta sig att vara okritisk och ta med "allting", men innan man är klar måste man rensa bort ovidkommande saker och kanske göra de enklaste klasserna till attribut. Regeln är ungefär att de presumtiva klasser som inte behövs utanför en annan klass kan ses som attribut till denna andra klass.
Modellen byggs sedan ut genom att man letar efter associationer och arv. Exakt hur man går till väga är svårt att beskriva i ord. Men det är viktigt att man verkligen hittar arvsrelationer (tänk efter om en klass B är en sorts A) och associationer. Eftersom allting i Java är klasser (det finns inga "vanliga" funktioner) blir det ju inget program om man inte lyckas få sina klasser att hänga ihop.
Sista delen i analysfasen är att förfina sin design genom att lägga till operationer på klasserna. Här, liksom tidigare, är scenarier ett hjälpmedel
Att skapa en objektmodell är oftast ett iterativt arbete, d.v.s ett senare steg kan påverka tidigare modellen som då måste anpassas.
Programmet implementerar en del av ett ritprogram. För att inte det hela ska bli alltför oöverskådligt har inte själva den interaktiva konstruktionen av figurer tagits med utan appletens init-metod skapar ett antal figurer som användaran kan flytta omkring, dels en och en (om man klickar med musen nära centrum av en figur och med musknappen nedtryckt sedan flyttar markören), dels också alla samtidigt (om man klickar någon annanstans). Appleten följer här:
I objektmodellen är Shape
-klasserna mer detaljerade än de övriga
(det finns ju ingen mening i att lägga in Javas hela klasshierarki ovanför
Applet t.ex. - vi anser Applet vara superklass):
Notationen {abstract} kan användas för att särskilt markera att operationen draw är abstrakt och alltså måste implementeras i subklasserna.
De två klasserna MouseAdapter
och
MouseMotionAdapter
hanterar händelserna musklick och
musförflyttning (mer om hur detta går till
senare).
Hur skulle nu Shape
och dess subklasser implementeras i ett icke
objektorienterat språk? Det troligaste är något i stil med (i ett tänkt
Java-liknande språk där klasserna är enkla records som i Pascal eller C):
class Shape { int typeOfShape; int x,y,size; boolean fill; } Shape shape[MAXNUMOFSHAPES]; for (int i=0; i < numOfShapes; i++) draw( shape[i] ); draw ( Shape shape ) { switch (shape.typeOfShape) { case SQUARE: drawSquare( x, y, size, fill); case TRIANGLE: drawTriangle( x, y, size, fill); case CIRCLE: drawCircle( x, y, size, fill); } }Principen för hur det ser ut i Java:
class Shape { int x,y,size; boolean fill; draw(); } class Triangle extends Shape { draw() { rita triangeln; } } class Square extends Shape { draw() { rita kvadraten; } } class Circle extends Shape { draw() { rita cirkeln; } } Shape shape[MAXNUMOFSHAPES]; for (int i=0; i < numOfShapes; i++) shape[i].draw();Punkt-notationen används för metoder: a.b() innebär alltså att metoden b anropas för instansen a och vi behöver alltså inte skicka med a som argument till b.
De båda kodsnuttarna ovan är inte fullständiga och kan kanske tyckas likvärdiga
vid första anblicken. Men anta nu att man får ändrade förutsättningar, t.ex.
att man vill kunna rita rektangler eller ellipser. I den funktionsbaserade
lösningen krävs nu relativt omfattande ändringar: alla funktioner som
utför något - i exemplet endast draw()
men många fler funktioner
finns sannolikt - måste ändras genom att nya case-satser läggs in. Den
objektorientade lösningen däremot tillåter att man lägger in kod för de
nya figurerna utan någon ändring alls av existerande kod:
Man kan lägga till nya figurer (rektanglar, ellipser,
månghörningar, etc) utan att någonting i det redan skrivna behöver
ändras eller ens kompileras om.
Lägg märke till att alla typer av figurer (som är subklasser till
Shape
) kan lagras i variabler av typen Shape
.
Olika figurer kan sedan "bete sig" olika när en metod anropas - detta kallas
polymorfi och är en mycket viktig del av objektorienterad
programmering. Programmeraren uppnår denna polymorfi genom att
ersätta (eng. override) metoder i basklassen:
man skriver helt enkelt en ny metod med samma signatur: samma namn
och samma parametrar. Abstrakta metoder måste "ersättas" i
subklasserna eftersom de inte är implementerade i basklassen.
En annan mycket stor fördel med OOP-lösningen är att man kan ändra den interna
representationen av formerna utan att program som använder
Shape
-klasserna påverkas. Detta kallas inkapsling
(eng. encapsulation) av data.
extends
för arv. Aggregat åstadkoms helt enkelt genom
att en klass innehåller variabler som refererar instanser av en annan klass, i
en array om multipliciteten är större än 1. Referenser används också för
associationer. I koden ser man alltså ingen skillnad mellan associationer och
aggregat, mer än att aggregat-delarna instansieras av klassen själv medan
referenser till associationer normalt skickas i en metod - att
OOPExempel
själv skapar Shape
-objekten är ju en
följd av att själva konstrukörsobjektet inte finns med.
java.awt
för att skapa grafiska användargränssnitt. Klassen
Object
i paketet java.lang
är basklass för
samtliga Java-klasser.
java.net
, för
nätverksprogrammering. Detta innehåller klasser för att skriva klient-
och server-applikationer.
Det faktum att Java är interpreterat innebär att program i Java inte är lika snabba som t.ex. program i C, men dagens applikationer är nästan alla interaktiva och/eller nätverksbaserade och befinner sig mestadels i väntetillstånd. Dessutom finns till många system numera s.k. just-in-time (förkortningen JIT avser just detta) kompilatorer som kompilerar bytekoden till maskinkod just innan ett program körs.
boolean
. Till skillnad
från de flesta andra språk definierar Java exakt storleken på alla inbyggda
typer. Alla Java-variabler har default-värden, d.v.s. värden som variabler
som ej har getts ett värde får.De inbyggda typerna är, med angivande av storlek i bit och default-värde:
char
är alltså inte 8 bits utan varje
tecken är en 16-bit Unicode kod. För oss programmerare är
detta ingenting vi normalt behöver tänka på, dels för att klassen
String
gömmer stränghanteringen, dels också för att de 256
värden som kan representeras i en byte är identiska i ASCII-koden och
Unicode-koden.
class Circle { public int r; public int x; public int y; public move( int dx, int dy ) { x += dx; y += dy; } }Denna klass har attributen
r
, x
och
y
samt metoden move()
.
Nyckelordet public
anger att klassens användare kan komma åt
allting. Datafält och metoder koms åt med hjälp av punktnotation: t.ex.
circle.r
eller circle.move(-10,-10)
om
circle
är ett objekt av klassen Circle
.
null
(default) som betyder att variabeln inte refererar något
eller också refererar variabeln en
klassinstans. Eftersom Java är ett s.k. hårt typkontrollerat språk kan en
viss variabel bara referera instanser av en viss klass (se dock avsnittet
Arv för en mer nyanserad bild av verkligheten)Skillnaden mellan de inbyggda typerna och referenser är att de förra hanteras by value, d.v.s. en kopia av variabeln överförs vid metodanrop, medan referenserna hanteras by reference, d.v.s endast adressen överförs.
I Java räcker det inte med att deklarera en variabel för att skapa ett objekt, man måste explicit skapa objektet med new:
Circle a; // skapar tom referens a = new Circle(); // skapar ett Circle-objekt Circle b = new Circle(); // skapar både referens och objekt.En tom referens har värdet
null
som alltså betyder att variabeln
inte refererar någonting. Ordet null
är reserverat i Java.
Objekt av klassen String
kan skapas genom uttryck som
String s = "Detta är en sträng";Man kan alltså (och bör så ofta som möjligt) ge variabler värden direkt vid deklarationen. Detta gäller både de inbyggda typerna och klassinstanser. Något som skiljer Java från de flesta programspråk (men inte C++) är att variabler kan deklareras "var som helst", d.v.s. exekverande satser och deklarationer kan blandas.
Variabler existerar bara i det block de deklareras, d.v.s. från
deklarationstillfället till nästa }
:
{ Circle a(10); { Circle b(20); b.draw(); } // a finns här, men inte b: a.draw(); - ok b.draw(); - fel }I Java allokeras minne för alla objekt dynamiskt. Till skillnad från t.ex. C++ och C behöver man inte återlämna minne, Java har automatisk s.k. "garbage collection", d.v.s. ett objekt som inte längre behövs (inte har någon variabel som refererar till sig) tas automatiskt bort.
int[] intBuf = new int[1024]; Circle[] circles = new Circle[10]; double[] doubleTabl = { 1.0, 1.5, 2.0, 3.0, 6.0 }; int[][] twoDArray = new int[8][8];Arrayers element initieras med respektive typs defaultvärde, i fallet objekt alltså med
null
, d.v.s. inga Circle-objekt skapas i
exemplet, endast 10 st. tomma referenser. Man kan också som i exemplets
doubleTable
ge arrayens element värden direkt i deklarationen.Observera att i Java, till skillnad från C++, kan man direkt dynamiskt allokera flerdimensionella arrayer. Java implementerar multidimensionella arrayer som arrayer av arrayer varför man, liksom i C++ och C, kan skapa ickerektangulära arrayer:
int[][] strange2Darray = { {5,6}, {3}, {3,4,5} }; double[][] triangular = new double[10][]; // triangular array: x for (int i=0;i < 10; i++) // xx double[i] = new double[i+1]; // xxx ...Man kommer åt elementen i en array på vanligt sätt:
int i = strange2Darray[0][1]; int j = intBuf[123];Som i C och C++ är index för första elementet 0. Java kontrollerar alltid att index håller sig i tillåtet område, annars "kastas ett undantag", se Undantag.
Varje array har ett fält, length
som talar om hur många element
den innehåller:
int[] a = new int[10]; int[][] b = new int[10][5]; int[][][] c = new int[5][6][4]; // // a.length = 10 // b.length = 10 // c.length = 5 // c[3].length = 6 // c[3][3].length = 4 //
Object
:
Vector
. En Vector
har ingen
bestämd storlek utan växer efter behov. Eftersom s.k. operatoröverlagring
(möjligheten att överlagra operatorer som +, -, etc.) ej finns i Java kan
man inte använda []-syntaxen för elementåtkomst i en Vector
.
I stället finns en mängd metoder, t.ex. elementAt
för
manipulering av enskilda element.
new
skriver man något
som ser ut som ett metodanrop:
Circle = new Circle(); Circle = new Circle( 10, 100, 150):Det är också vad som sker, systemet anropar en s.k. konstruktor, en metod som initierar objektet enligt parametrarna. Observera att i Java kan man inte skriva t.ex.
Circle = new Circle;
som man kan i C++,
uttrycket efter new
måste alltid se ut som ett metodanrop.
Normalt skriver man en eller flera konstruktorer men gör man det inte skapar
Java en s.k. default-konstruktor utan argument som inte gör någonting. Man
kan skriva flera konstruktorer med olika parameteruppsättning, s.k.
överlagring (eng. overload, observera skillnaden mellan
overload och override).
Exempel på en klass med konstruktor:
class Circle { public Circle( int ir, int ix, int iy ) { r = ir; x = ix; y = iy; } private int r; private int x; private int y; }Här har attributen deklarerats
private
vilket beyder att endast
klassens metoder kommer åt dem.
Om man skriver en metod som heter void finalize()
kommer denna
att anropas när objektet inte längre refereras men innan objektets minne
återlämnas. Oftast behöver man ingen sådan metod men ibland kan man behöva
återlämna någon systemresurs som inte Javas skräphanterare tar hand om.
struct
i C: man kan initiera attribut (och läsa
och ändra dem om de är deklarerade public
). Låt oss titta litet
mer på en klass som beskriver en rektangel:
class Rectangle { public Rectangle( int ix, int iy, int iw, int ih) { x = ix; y = iy; w = iw; h = ih; } public Rectangle( int ix, int iy, int side ) { x = ix; y = iy; w = side; h = side; } public int area() { return w * h; } private int x; private int y; private int w; private int h; }Här ser vi exempel på överlagrade konstruktorer och dessutom en metod som beräknar rektangelns yta. Metoden
area()
är en s.k.
instansmetod, en metod som anropas med en klassinstans genom
punktnotation:
Rectangle r = new Rectangle( 500, 500, 200, 300); int ytan = r.area();I en metod kan man referera "sig själv" med
this
. Man skulle
t.ex. kunna skriva instansmetoden area()
så här:
public int area() { return this.w * this.h; }Nu gör man naturligtvis inte så men det finns tillfällen när
this
verkligen behövs, nämligen när man behöver skicka en referens till sig
själv till en metod i en annan klass. Man kan också använda this
för att anropa en annan konstruktor, t.ex:
public Rectangle( int ix, int iy, int side ) { this(ix,it,side,side); }De flesta metoder är instansmetoder, men det finns också s.k. statiska metoder (eller klassmetoder). Antag t.ex. att man vill kunna räkna ut ytan på en rektangel utan att skapa ett objekt. Då är en klassmetod ett alternativ:
public static area( int w, int h ) { return w*h;}Det är nyckelordet
static
som anger att metoden är en klassmetod.
Anledningen till att man skriver area som klassmetod i klassen
Rectangle
är att endast denna klass bör veta hur en rektangels
yta beräknas (inkapsling inte bara av data utan också av metoder).
Även attribut kan vara statiska, i vårt exempel kan man tänka sig att man vill flytta alla rektanglar likadant (translatera dem). Ett sätt att implementera den möjligheten är att deklarera en par klassattribut:
private static int dx=0; private static int dy=0;Som synes kan man också ge initialvärden till statiska attibut. Dessa initieras när klassen laddas. Initialvärden kan också ges till vanliga instansattribut: dessa initieras när instansen skapas.
Liksom i fallet med konstruktorer kan man överlagra metoder, d.v.s.
skriva flera metoder med samma namn men med olika parameteruppsättningar, t.ex.
kan man i fallet Rectangle
tänka sig en klassmetod som
beräknar en kvadrats yta:
public static area( int side) { return side*side; }Eftersom Java inte har funktioner vanlig mening är statiska metoder vanliga: de kan ju nästan ses som vanliga globala funktioner. En del klasser har bara statiska metoder, t.ex. klassen
Math
. En del av dess
deklaration och exempel på användning:
class Math { public static final double E = ...; // konstanten e (2.718...) public static final double PI = ...; // konstanten pi (3.142...) public static double sin(double a) { .. } public static double cos(double a) { .. } public static double sqrt(double a) { .. } public static double exp(double a) { .. } public static double pow(double a, double b) { .. } } double a = Math.sqrt( 1.2); double cirkelyta = 2 * Math.PI * radius;Ännu en modifierare till attribut,
final
, dyker upp i Math. Detta
är Javas konstantdeklaration, E
och PI
får inte
ändras.
De operatorer som opererar på referenser (objekt) är (alla utom '+' opererar på samtliga typer av objekt):
(type) - cast operator, se vidare nedan i avsnittet om arv. + - med String-objekt som operander - strängsammanslagning == - likhet (de båda operanderna refererar samma objekt) != - ej likhet (de båda operanderna refererar ej samma objekt) = - tilldelningExempel på användning:
Shape a = new Circle(); Circle b = (Circle) a; String b = "Kalle " + "Anka"; Circle c; if (a==b) c = a; // likhet och tilldelning if (a!=b) c = new Circle(); // olikhet och tilldelningUttrycket
c=a
i exemplet betyder att c kommer att referera samma
objekt som a (eller inget objekt alls om a har värdet null
).
Om man verkligen vill kopiera objektet kan man använda metoden
clone()
. Metoden equals()
används för att ta reda
på om två objekt är lika:
if (a.equals(b)) c = a.clone(); // riktig likhet och riktig tilldelning
String
. Genom att representationen av strängar göms här
behöver man som programmerare inte bry sig om hur tecken lagras (d.v.s. att
Unicode används i stället för ASCII). Några exempel på användning visar
bäst klassens metoder:
// // Användning av klassen String. // String a = "detta är en sträng "; int i = 324; a = a + i; // a = "detta är en sträng 324" String b = a.substring(9, 18); // b = "en sträng" String c = a.substring(9); // c = "en sträng 324" (till slutet) char d = a.charAt(0); // d = 'd' int p1 = a.indexOf('e'); // p1 = 1 (första 'e') int p2 = a.indexOf("ng"); // p2 = 16 (första "ng") int l = a.length(); // l = 22Observera att metoden
substring(startindex,endindex)
returnerar
en sträng som börjar i startindex
och slutar i
endindex-1
.
Klassen String
har metoder för konvertering av tal till sträng.
Metoderna är överlagringar av varandra och heter
public static String valueOf(typ)
där typ är en av de inbyggda typerna.
Strängar av klassen String
får inte ändras. Om man
vill kunna ändra i en sträng kan man använda klassen
StringBuffer
i stället:
// // Användning av klassen StringBuffer // StringBuffer s = new StringBuffer("detta är en sträng "); int i = 324; a.append ( i); // a = "detta är en sträng 324" a.setCharAt( 0, 'D'); // a = "Detta är en sträng 324" a.insert(9, "inte "); // a = "Detta är inte en sträng 324"Metoderna
append()
och insert()
är överlagrade för
alla typer: String.valueOf()
används för konvertering till sträng
(valueOf()
i sin tur använder toString()
om
argumentet är en instans av en klass).
Vector
-objekt eller man vill kunna utföra en operation på en
double.För detta ändamål finns i Java till varje inbyggd typ en motsvarande typklass:
TYP TYPKLASS boolean Boolean byte Byte char Character double Double float Float int Integer long Long short ShortAlla typklasser har följande konstruktorer och metoder (där Typklass är en typklass och typ själva typen):
Typklass( typ v); // ex: Integer a = new Integer(12); Typklass( String s); // ex: Double d = new Double("123.45"); String toString(); // konvertera till sträng, ex: String a = a.toString(); typ typValue(); // returnerar värdet, ex int i = a.intValue(); boolean equals(typ v);// sant om de två objekten har samma värde.Typklasserna innehåller alltså metoder för konvertering mellan tal och strängar. Metoden
toString()
anropas automatiskt i uttryck som
t.ex.
Integer a = new Integer(10); String s = "Value of a is " + a; System.out.println( "Value of a is " + a );Utskrift till terminalfönstret av strängar görs med anrop till
System.out.println
med en sträng som parameter. Faktum är att
även vanliga tal, som inte lagras i en instans av en typklass, konverteras på
samma sätt och med samma metod.
toString()
. Om man
skriver egna klasser bör man skriva toString
(egentligen
överlagra eftersom metoden finns i Object
) så får man samma
uppförande som de inbyggda typerna har.
Javas satskonsruktioner liknar mycket C++. I detta avsnitt visas med exempel samtliga konstruktioner som finns i språket utom de som har med undantag att göra.
int i=0; . if (i > 0) i++; else i--;Skillnaden mellan Java och C/C++ är att villkorsuttrycket i Java måste vara av typen
boolean
. Värdena 0
och
null
t.ex. har inte värdet falskt utan man måste skriva
"if (anObject!=null) {}
" och "if (a!=0) {}
".
int i; for (i = 0; i < 10; i++) System.out.println( i ); for (int j=5; j >= 0; j--) {} for (int i=0, j=100; i < 100; i++, j-=5) {}Exemplen visar de vanligaste formerna av
for
-loopen. Observera att
man kan göra flera saker i initierings- och inkrementeringsdelarna av loopen g
enom att använda komma.Viktigt att veta är att variabler som deklareras i initieringsdelen existerar bara inne i loopen, inte efteråt.
int i = 10; while (i > 0) { Shape a = firstShape(i); if (a != null) do { a.do_it(); a = nextShape(a); } while (a != null); i--; }Dessa konstruktioner ser ut som i de flesta programspråk. Viktigt att komma ihåg är att vi måste testa explicit mot 0 och
null
i
villkorsutrrycken.
switch ( i ) { case 1: i += 123; break; case 3: i += 456; break; case 5: i += 789; break; default: i = -1; break; }Variabler av typerna
byte
, char
, short
,
int
och long
är tillåtna som
case
-lablar. Notera att break
är nödvändigt för
att inte fortsätta exekveringen med nästa case
.
break
och continue
används på samma
sätt som i C/C++ med tillägget att man kan hoppa flera nivåer med hjälp av
etiketterade break
och continue
. Man bör vara
sparsam med användandet av dessa båda satser eftersom de ofta leder till
svårläst och svårunderhållen kod; inga exempel förutom i switch
ges.
Java är ett generellt programspråk som kan användas också för applikationer
utanför Internet. Skillnaden mellan en applet och en applikation ligger i
hur och av vem programmen startas: en applet startas av en Web-läsare
medan en applikation startas i en speciell metod som heter
main
. Flera av de klasser som tillsammans utgör en applikation
kan innehålla main-metoder: programmet startas i den main som finns i den
klass som anges på kommandoraden.
En applet å andra sidan administreras och körs av Web-läsaren. Programmeraren
skapar en applet genom att skriva en subklass till Applet
och
ersätta en eller flera av följande metoder:
Mer om hur man använder metoderna kommer i avdelningen om
trådar. Metoden paint()
är inte
appletspecifik utan används av alla GUI-komponenter.
Oavsett om man har skrivit en applet eller en applikation kompileras programmet med javac:
javac myclass.javaEn applikation startas sedan genom att man ger kommandot
java
följt av namnet på den klass man vill starta i (en klass som har en
main
-metod):
java myclass.class
medan appleten startas av Web-läsaren på det sätt vi tidigare visat.
import java.applet.*; import java.awt.*; public class WelcomeApplication extends Applet { public static void main( String[] args ) { WelcomeApplication wapp = new WelcomeApplication(); Frame frame = new Frame("Welcome"); frame.add(wapp); frame.setSize( 500, 200); wapp.init(); wapp.start(); frame.show(); } public void paint( Graphics g) { g.drawString("Welcome to the World of Internet Programming", 100, 100); } };Metoden
main()
skapar en instans av appleten,
ett fönster (en Frame
)
att visa den i, placerar appleten i fönstret (metoden add
),
definierar fönsterstorleken, anropar init
och start
(som Web-läsaren annars skulle ha gjort), samt visar fönstret.
Lägg märke till att main()
är en
klassmetod (static
) som inte
returnerar någonting. Argumentet är en array av strängar - de argument som
gavs på kommandoraden efter klassens namn. Eftersom ingenting kan returneras
från main kan man inte returnera ett värde till systemet den vägen: använd
System.exit(n)
där n är det du vill returnera om du
måste meddela systemet något.
Applikationen ovan saknar någonting viktigt: den går inte att avsluta! Nästa avsnitt handlar om AWT (Abstract Windowing Toolkit) och hur man tar hand om användarens önskemål.
<APPLET>
:
<APPLET CODE="filnamn" WIDTH=pixelbredd HEIGHT=pixelhöjd CODEBASE=appletdirectory ALIGN=sidplacering> <PARAM NAME=parameter VALUE=värde> </APPLET>(dessa är de vanligaste kommandona, det finns fler)
Man måste alltid ange CODE
, WIDTH
och
HEIGHT
, resten kan utelämnas. CODEBASE
skall vara
den katalog där appleten ligger, inte sjålva appleten. Om
CODEBASE
utelämnas används den katalog där det refererande
dokumentet finns.
Nyckelordet PARAM
kan förekomma många gånger, där varje
förekomst svarar mot en parameternamn med värde. Parametrarna kan läsas av
appleten, se exemplet Scribble
). Metoden
getParameter( String par )
returnerar värdet av parametern
par
i form av en sträng.
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, 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 cirkels 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. Shape
s
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 basklassen 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 Shape
s
print()
ju gör en del av det som Circle
s gör
skulle man kanske vilja använda den. Men om vi skriver print()
i Circle
s 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 basklassens 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 operatorm
instanceof
,
dels också en s.k. cast. 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. basklassers finalize
anropas inte
automatiskt.
Som tidigare påpekats är klassen Object
basklass til samtliga
Java-klasser - detta utan att man behöver ange det med extends
.
I basklassen 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
Object
s 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 basklassens
typ (som kan vara en referens till en subklass) måste skrivas med ett argument
av klassen Object
. Operatorn instanceof
returnerar sant om dess första operand är en instans av den andra operanden.
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 ); } }
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.
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:
java.applet
klasser för applets
java.awt
klasser för grafik och användargränssnitt.
java.awt.event
klasser för händelsehantering
java.awt.image
klasser för bilder
java.io
klasser för läsning/skrivning
java.lang
klasser för själva Java
java.math
klasser för matematiska funktioner
java.net
klasser för nätverksprogrammering
java.util
klasser med en del vanliga datatyper
java.lang
inte behöver importeras explicit,
paketet finns alltid tillgängligt.
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
Klassen
Undantagshanteringen använder de tre nyckelorden
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
(OBS! dessa klasser håller på att ersättas av en ny uppsättning GUI-klasser
(
Dessa klasser kan sedan delas in i olika grupper:
Ett användargränssnitt består av en behållare (
Exakt hur de olika komponenterna placeras i sin behållare bestäms av
en s.k. layout-hanterare: klasserna som har namn som slutar
på
Klassen
Interaktivt styrda program kan inte skrivas enligt det traditionella
sekventiella mönstret: initiera, kör algoritmen, avsluta. I stället
använder man s.k. händelsestyrd programmering. Händelser är sådant som
användaren gör, t.ex. klickar med musen, tar bort ett fönster. I AWT genererar
alla sådana händelser ett händelseobjekt som sedan tas om hand av en s.k.
lyssnare (
Som Java-programmerare behöver man oftast inte hantera händelseobjekten.
Några undantg:
I klassen
Eftersom en lyssnare är ett gränssnitt måste man implementera alla metoder
även om man bara är intresserad av en av dem. Vi skall snart se att det
finns klasser, s.k. adaptorer, som förenklar detta åt oss.
Exemplet efter de korta beskrivningarna visar hur man använder
layout-hanterarna).
Observera att endast tryckningar på knappar i
Koden och litet mer information om de olika hanterarna finns
här.
Program får rita endast på order från systemet; annars kunde konstigheter
som att man ritar i icke-existerande fönster inträffa. Ritning fungerar
därför så att en komponent begär att få bli ritad genom att anropa
metoden
En fix komponent (som
Grafik kan man rita i flera av AWT-komponenterna men oftast ritar man i
instanser av
Appleten som följer ritar de figurer som klassen
Koden finns här.
Man kan också skapa egna färger med konstruktorn:
För att minska skrivarbetet finns några s.k. adaptorer, tomma
implementationer av lyssnargränssitten, definierade. I stället för att
implementera ett gränssnitt kan man skapa en subklass till en adaptor:
Flera av de exempel vi sett har nästlade eller
inre klasser, d.v.s.klasser som helt definieras inne i
andra klasser. Ett exempel är de två adaptorerna i
En inre klass är ett specialfall av en nästlad klass, nämligen en
icke statisk nästlad klass. Inre klasser kan komma åt den omgivande
klassens metoder och attribut. En instans av en inre klass kan finnas och
fungera bara inuti en instans av sin omgivande klass.
Klassen
Viktiga metoder i
Ingen av metoderna ovan ersätts vanligen, man anropar dem vid behov.
Koden följer här:
Koden finns här
Ytterligare ett exempel på dubbelbuffring är det tidigare
exemplet där koden ser ut så
här med dubbelbuffring. I större
applikationer kan man uppnå mycket genom att rita om bara den del av appleten
som ändrats i stället för att som vi gör i dessa exempel där allt ritas om.
Programmet har tre klasser:
Vi tittar först på valda delar av klassen
De andra verktygen för synkronisering är metoderna
Observera att metoden
Klasserna
Konsumenten och producenten är båda arvtagare till
Den fullständiga programkoden visar också
hur man kan implementera en
Ett roligt (?) exempel (som också visar på skillnaden i
effektivitet hos några sorteringsalgoritmer) på multithreading finns
här
Javas stream-klasser finns i
Metoder för skrivning:
Eftersom operatorn + fungerar för strängar kan man skriva ut flera saker
i samma
Vid läsning måste litet mer jobb till (
Java är inte speciellt bra på denna typ av I/O; förklaringen ligger i att
moderna program använder grafiska användargränssnitt och därför inte
behöver ha tillgång ett fullständigt utbyggt bibliotek för textmässig läsning
och skrivning. Det är ju faktiskt också så att program som inte har råd att
krascha inte kan läsa in tal på annat sätt än genom egen parsning och för
detta finns stöd i Java.
De båda instanserna
Ett exempel får avsluta detta avsnitt. Programmet nedan läser en textfil
och räknar, dels totala antalet ord, dels också hur många gånger ett antal
ord förekommer. Resultatet skrivs till skärmfönstret. Programmer använder
en
Mer om hur man använder strömmar, och koordination mellan asynkron input och
output, kommer i avsnittet om nätverksprogrammering. I filkopieringsprogrammet
ovan läses troligen 4096 bytes varje gång (utom den sista) men
om filen ligger på en annan dator någonstans i världen kan man inte vara säker
på att så är fallet.
Throwable
är basklass 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. 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 instanser av samma superklass
räcker det att tala om att metoden kastar superklassen.finally
-block som alltid exekveras:
try
{
}
catch (Exception e)
{
}
finally
{
// exekveras alltid även om undantag kastats.
}
Java AWT och händelsehantering
En nästan fullständig objektmodell av de klasser - Abstract Windowing
Toolkit (AWT) - som utgör Javas byggstenar för att bygga grafiska
användargränssnitt (s.k. GUI - Graphical User Interface) ser ut så här:
Swing
) - principerna behålls i denna standard som mest
ändrar utseende och namn på existerande komponenter och lägger till en mängd
nya. Men plugga alltså inte in detaljerna i AWT eftersom de kommer att förlora
i betydelse).
Övriga klasser kan ses som huvudsakligen abstrakta eller sådana som används
internt av andra klasser.Graphics, Color, Cursor, Font, Image
Point, Dimension, Polygon, Rectangle
Component, MenuComponent
Button, Label, Choice, Checkbox
TextArea, TextField
MenuBar, Menu, PopupMenu
Window, Frame, Panel, ScrollPane
Canvas
List, Dialog, FileDialog
Scrollbar
...Layout
.
Frame
i
applikationer, Panel
i applets - Applet
är en
subklass till Panel
) som andra komponenter placeras i.
Eftersom dessa komponenter i sin tur kan vara behållare (Panel
s)
kan man bygga hur komplicerade gränssnitt som helst....Layout
. Man placerar alltså normalt inte komponenterna på
fasta positioner (även om detta också är möjligt) eftersom man vill att
utformningen skall vara så oberoende som möjligt av fönstersystem. Varje
behållare har en layout-hanterare kopplad till sig. Alla layout-hanterarna
implementerar gränssnittet LayoutManager
. Metoden
setLayout(LayoutManager)
i Container
används för
att koppla en layout-hanterare till en behållare - om man skulle vilja placera
ut komponenterna själv skall man anropa setLayout
med argumentet
null
.
Behållare och komponenter
Alla komponenter utom fönster (Window
och Dialog
)
måste placeras i en behållare för att synas. Man placeras en
komponent genom att anropa någon av behållarens add
-metoder.
Vilken metod man bör använda styrs av vilken layout-hanterare man valt.Component
har metoder för
Varje komponent har sitt eget koordinatsystem där x ökar till höger och y
nedåt.
Notera att positionen normalt inte anges på detta sätt, man överlåter
ansvaret på en layout-hanterare. Positionen anges i behållarens
koordinatsystem.
paint(Graphics), update(Graphics), repaint()
setForeground(Color), setBackground(Color),
Color getForeground, Color getBackground()
setFont(Font), Font getFont()
setSize(int w, int h), Dimension getSize()
setLocation(int x, int y), Point getLocation()
Händelsehantering
OBS! detta avsnitt beskriver händelsehanteringsmodellen som finns i JDK 1.1
som skiljer sig en del från den modell som JDK 1.0 hade.Listener
). Klasser som har med händelser att göra
finns i paketet java.awt.event
(AWTEvent
ligger i java.awt
). De viktigaste händelserna:
Den andra viktiga begreppet för handelsehantering är lyssnarna: de är
gränssnitt, inte klasser. Klasser som behöver få reda på att händelser
inträffar implementerar således en lyssnare. En händelsehanterare kan vara
en instans av vilken klass som helst. Tre steg kan urskiljas i hanteringen
av händelser.
InputEvent
: int getModifiers()
ger musknapp,
shift m.m.
MouseEvent
: int getX(), int getY()
ger
pekarposition.
KeyEvent
: char getKeyChar()
ger tecknet.
ItemEvent
: Object getItem()
ger objektet som
genererade händelsen
Observera att händelsehanteraren och komponenten (knappen) är helt oberoende
av varandra, allt man gör är att man talar om för knappen att man vill veta
när den trycks in.
public class MyEventHandler implements ActionListener {
...
}
MyEventHandler
måste skapas och den
komponent vars händelser man är intresserad av måste informeras om lyssnaren:
MyEventHandler oneEventHandler = new MyEventHandler();
Button b = new Button();
b.addActionListener( oneEventHandler );
MyEventHandler
:
public void actionPerformed( ActionEvent event )
{
... gör vad som ska göras
}
Component
finns metoder för att hantera (lägga till
och ta bort) lyssnare:
Olika subklasser lägger till sina egna lyssnare:
addFocusListener(FocusListener),
removeFocusListener(KeyListener)
addKeyListener(KeyListener), removeKeyListener(KeyListener)
addMouseListener(MouseListener),
removeMouseListener(MouseListener)
addMouseMotionListener(MouseMotionListener),
removeMouseMotionListener(MouseMotionListener)
För fullständighetens skull följer här en uppräkning av några av de viktigaste
metoderna i de olika lyssnarna och när de anropas.
Button, MenuItem, TextField: addActionListener(ActionListener),
removeActionListener(ActionListener)
Choice, Checkbox, List: addItemListener(ItemListener),
removeItemListener(ItemListener)
TextComponent: addTextListener(TextListener),
removeTextListener(TextListener)
Window: addWindowListener(WindowListener),
removeWindowListener(WindowListener)
ActionListener
actionPerformed(ActionEvent)
-
en aktion (knapptryck, menyval) inträffar
FocusListener
focusGained(FocusEvent)
- komponenten får fokus
focusLost(FocusEvent)
- komponenten förlorar fokus
ItemListener
itemStateChanged(ItemEvent)
-
status har ändrats (från vald till ej vald ellervice versa)
KeyListener
keyPressed(KeyEvent), keyReleased(KeyEvent)
- en tangent trycks
ned eller släpps.
keyTyped
- en tecken har skrivits
MouseListener
mouseClicked(MouseEvent), mousePressed(MouseEvent),
mouseReleased(MouseEvent)
mouseExited(MouseEvent), mouseEntered(MouseEvent)
- muspekaren
kommer in i eller lämnar en komponent
TextListener
textValueChanged
- texten har ändrats
WindowListener
windowActivated(WindowEvent), windowClosed(WindowEvent),
windowClosing(WindowEvent), windowDeactivated(WindowEvent),
windowDeiconified(WindowEvent), windowIconified(WindowEvent),
windowOpened(WindowEvent)
Layout-hanterare
Exakt hur
komponenterna i ett användargränssnitt läggs ut bestäms
av en s.k. layout-hanterare. Vilken man väljer beror på vad man vill uppnå.
Tänk också på att att AWT möjliggör hierarkiska GUIs: man kan t.ex. ha ett
fönster med FlowLayout
där alla eller några av komponenterna är
Panel
er, var och en med sin egen layout-hanterare. Alla behållare
har en metod setLayout( LayoutManager )
som används för att
koppla en hanterare till behållaren (LayoutManager
är ett
interface
som implementeras av layout-hanterarna.FlowLayout
FlowLayout
(default för Panel
) placerar komponenterna
i en rad efter varandra eller i flera rader om utrymmet tar slut. Vid
konstruktionen av FlowLayout
- objektet kan man välja om
komponenterna skall centreras, vänsterjusteras eller högerjusteras i det
tillgängliga utrymmet.GridLayout
GridLayout
placerar komponenterna i ett rutnät där alla rutor
är lika stora. Man kan specifiera antalet rader och kolumner samt avståndet
mellan rutorna.
BorderLayout
BorderLayout
(default för Frame
) delar in
behållaren i fem områden: öster, väster, norr, söder och mitten. I varje
område kan en komponent placeras. Mittenområdet växer mest om behällaren
ändrar storlek.
CardLayout
CardLayout
är annorlunda än de övriga på så sätt att endast
en komponent syns samtidigt. Komponenterna ses som kort i en kortlek.
GridBagLayout
GridBagLayout
är en generalisering av GridLayout
där
de olika komponenterna placeras i rutnät med olikstora rutor: man använder
instanser av GridBagConstraints
för att specificera hur. Denna
klass är inte speciellt lättanvänd eller intuitiv, men eftersom den existerar
nämns den in alla fall här.Exempel
Denna applet använder GridLayout
med 2x2 rutor med en
Panel
i varje ruta. Dessa i sin tur använder olika typer
av layout-hanterare.Card
-Panel
en tas om hand av programmet.
Att rita grafik
När en applikation eller applet ska ritas upp - t.ex. när fönstret första
gången visar sig på skärmen eller blir synligt efter att ha varit skymt -
börjar systemet i huvudfönstret och letar sig rekursivt nedåt i hierarkin.
Alla komponenter ritas upp under resans gång.repaint()
. AWT begär sedan i sin tur att komponenten
ska rita sig genom att anropa komponentens update()
-metod.
Denna metod i sin tur rensar sin rityta och anropar paint()
.Button
) implementerar alltså hela sin
grafik i paint()
-metoden. Detta gäller de flesta komponenter
men om någonting ändras i bilden kan alltså komponenten själv anropa
repaint()
för att få möjlighet att rita om sig. I
animeringar
ersätter man också ofta update()
med en egen variant. De två
metoderna paint()
och update()
har ett argument:
en instans av ett objekt av klassen Graphics
som kan ses som
den ritmiljö man ritar i. Klassen har metoder för att rita linjer, cirklar,
ellipser, skriva text, m.m. Dessutom finns information om hur stor del av
fönstret som behöver ritas om.Canvas
eller Applet
eller (oftare
ändå) subklasser till dessa. Det enklaste sättet att rita grafik är att helt
enkelt skriva ritinstruktionerna i paint()
.Graphics
definierar. Det finns också metoder för att rita bilder.
Färger
Instanser av klassen
java.awt.Color
definierar vilken färg med
ritar med. Klassen innehåller dels några konstanter för de vanligaste färgerna,
dels också några metoder för att manipulera en färg:
brighter()
, darker()
.
Color( int redvalue, int greenvalue, int bluevalue);
//Exempel, skapa en gråröd färg:
Color mycolor = new Color( 200, 100, 100);
//Exempel, använd röd färg:
g.setColor( Color.red ); // Använd den fördefinierade konstanten
där färgvärdena skall ligga i intervallet 0 till 255 där 255 har högst
intensitet.
Typsnitt
Klassen
java.awt.Font
inkapslar typsnittshantering i Java-grafik.
Des enda konstruktor är:
Font(String name, int style,int size)
// Exempel:
Font myfont = new Font( "Times", Font.BOLD, 12 );
style
skall vara en av Font.BOLD, Font.ITALIC
eller
Font.PLAIN
. Den första parametern anger typsnittsfamilj i form av
en sträng: vilka typsnitt som finns är plattformsberoende.
Adaptorer, inre och anonyma klasser
Lyssnargränssnitten kräver att alla metoder implementeras, oavsett hur
många man egentligen använder. Om man använder bara någon enstaka metod
måste en mängd tomma metoder skrivas, vilket inte bara är onödigt utan också
leder till svårlästa program.
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class Scribble extends Applet
{
public int x;
public int y;
MyMouseAdapter mma;
MyMouseMotionAdapter mmma;
private Color stringToColor ( String colstring )
{
if (colstring.equals("red")) return Color.red;
else if (colstring.equals("green")) return Color.green;
else if (colstring.equals("blue") )return Color.blue;
else if (colstring.equals("yellow")) return Color.yellow;
else return Color.white;
}
public void init()
{
setBackground( stringToColor( getParameter("colour")) );
mma = new MyMouseAdapter();
mmma = new MyMouseMotionAdapter();
addMouseListener( mma );
addMouseMotionListener( mmma );
}
public class MyMouseAdapter extends MouseAdapter
{
public void mousePressed( MouseEvent e )
{ x = e.getX(); y = e.getY(); }
}
public class MyMouseMotionAdapter extends MouseMotionAdapter
{
public void mouseDragged( MouseEvent e )
{
Graphics g = getGraphics();
g.drawLine( x, y, e.getX(), e.getY() );
x = e.getX(); y = e.getY();
}
}
}
Scribble
.
Eftersom en Javaklass inte kan ärva mer än en klass kan man inte ärva
både Applet
och en adaptor utan man får ta till en
inre klass.Anonyma klasser
Klasserna MyMouseAdapter
och MyMouseMotionAdapter
i exemplet Scribble
har som enda uppgift att implementera
händelsehanteringen. Själva namnen är ointressanta och faktum är att Java
tillåter icke namngivna inre klasser, s.k. anonyma klasser. Vi skulle
kunna förenkla Scribble
:
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class ScribbleAnonymous extends Applet
{
public int x;
public int y;
public void init()
{
setBackground( Color.yellow );
addMouseListener(
new MouseAdapter()
{
public void mousePressed( MouseEvent e )
{ x = e.getX(); y = e.getY(); }
}
);
addMouseMotionListener(
new MouseMotionAdapter()
{
public void mouseDragged( MouseEvent e )
{
Graphics g = getGraphics();
g.drawLine(x,y,e.getX(),e.getY());
x = e.getX(); y = e.getY();
}
}
);
}
}
Ett överdrivet användande av inre och speciellt anonyma klasser gör koden
klart oläsbar varför man bör vara sparsam med dem: anonyma klasser bör
ha en eller högst två metoder och vara av typen "klass som gör en enda sak"
(som adaptorerna).
Animering, trådar och strömmar
Klassen Thread
De applets vi hittils tittat på har alla varit passiva i den meningen att de
inte gjort någonting av sig själva. Vissa applikationer, t.ex. animeringar,
arbetar annorlunda; de lever sitt eget liv och tar egna initiativ. För att den
typen av applikationer skall kunna leva tillsammans med andra är det mycket
viktigt att de inte tar all datorkraft i anspråk. I Java finns stöd för s.k.
trådar, alltså möjlighet att exekvera någonting - inom ett program eller en
applet - separat i sitt eget kontrollflöde. Man kan t.ex. inte låta metoden
paint()
sköta en lång animering som består i att visa bild efter
bild eftersom datorn under tiden inte kan göra annat. Alla små animeringar
som finns på Web-sidor skulle då hindra oss från att göra något annat medan
de pågår.En tråds liv
Förutom att placera trådar i de olika tillstånden måste man som programmerare
ibland också synkronisera olika trådar som arbetar på samma data.
Thread
implementerar trådar. Dess vanligaste konstruktorer
är:
Thread()
Thread(Runnable)
Runnable
är ett gränssnitt med en enda metod, run()
som alltså måste implementeras, endera i en subklass till Thread
eller i en annan klass som då ges som första argument till den andra
konstruktorn. När en tråd startas anropas run()
automatiskt.
Eftersom Thread
implementerar (en tom) run()
ska man inte skriva implements Runnable
om man subklasser
Thread
.Thread
är
static void sleep(long millis) - söver ner den tråd som exekverar
static void sleep(long millis , int nanos) - söver ner den tråd som exekverar
void start() - startar exekveringen av tråden
void final stop() - stoppar exekveringen av tråden
void final suspend() - stoppar tillfälligt en tråd
void final resume() - startar om en tråd som stoppats med suspend()
static yield() - stoppar tillfälligt den tråd som nu exekverar
Det kan se konstigt ut med statiska metoder men om de anropas i en tråds metod
arbetar de ju på den tråden eftersom det är den som exekverar.Animering
Vårt första exempel med trådar är en enkel animering. Appleten nedan visar
en text som skyms av en oval som ändrar storlek.
import java.applet.*;
import java.awt.*;
public class WelcomeAnimation extends Applet implements Runnable
{
static final int sleepingTime = 100;
static final String msg =
"Welcome to the World of Internet Programming";
private Font font;
private int size = 100;
private boolean shrinking = true;
private Thread animator = null;
public void init()
{
super.init();
font = new Font("Times", Font.BOLD, 12 );
setBackground( Color.white );
}
public void start() // Appletens start(), *ej* trådens
{
animator = new Thread( this ); // Skapa en tråd
animator.start();
}
public void stop() // Appletens stop(), *ej* trådens
{
animator.stop();
animator = null;
}
public void paint( Graphics g)
{
g.setColor(Color.red);
g.fillOval(230-size*3/2, 97-size/2, size*3, size);
if (shrinking)
{ size -= 2; if (size == 0) shrinking = false; }
else
{ size += 2; if (size == 100) shrinking = true; }
g.drawString( msg, 100, 100);
}
public void run()
{
while (true)
{
repaint();
try { Thread.sleep(sleepingTime); }
catch (InterruptedException e) { System.exit(1); }
}
}
public void destroy()
{
if ( animator != null )
{
animator.stop();
animator = null; // För att hjälpa skräphanteraren ...
}
}
};
Om man söver ner en tråd bör man alltid fånga eventuella avbrottsundantag
på det sätt som görs i koden.
Dubbelbuffring
Appleten i föregående avsnitt kanske (det beror på web-läsare, dator, andra
appliktioner, etc ) flimrar på ett obehagligt sätt. Det finns
flera tekniker att minska flimret och en vanlig teknik är s.k. dubbelbuffring,
d.v.s man ritar sin bild i ett "fönster" i minnet och överför sedan hela
bilden till skärmen. I Java är det lätt att ordna detta genom att ersätta
metoden update()
:Synkroniseringg
Olika trådar arbetar ibland på samma data. För att detta skall fungera
krävs att trådarna synkroniseras så att inte en av trådarna läser data som
inte finns t.ex. Appleten här visar en producent-konsument situation med ett
lager med plats för 20 varor av något slag.
SynchronizerDemo
,
Producer
och Consumer
varav de båda senare exekverar
i varsin tråd och alltså asynkront placerar varor i lagret respektive tar
bort dem. För att göra det hela litet mer intressant finns en slumpmässig
försening i klasserna. Dessutom kan man suspendera och starta om både
producent och konsument. När lagret är fullt och producenten vill lägga in en
vara (men alltå måste vänta) visas en röd markering på "producentsidan"
och om lagret är tomt och producenten försöker hämta en vara visas motsvarande
markering på "konsumentsidan". Lagret visas som en stapel i mitten.SynchronizerDemo
(de
delar som har med den grafiska presentationen utelämnas):
public class SynchronizerDemo extends Applet
{
final int bufferSize = 20;
int inStore = 0;
private Producer producer = null;
private Consumer consumer = null;
public void init()
{
consumer = new Consumer( this );
producer = new Producer( this );
}
synchronized void get()
{
if (inStore == 0)
{
setEmpty( true );
try { wait(); }
catch ( InterruptedException e) {}
}
if ( (inStore--) == bufferSize ) // Om bufferten var full *innan*
notify(); // vi tog ett element meddelar vi att nu finns det plats
}
synchronized void put()
{
if (inStore == bufferSize)
{
try { wait(); }
catch ( InterruptedException e) {}
}
if ( (inStore++) == 0) // Om bufferten var tom *innan* vi lade dit ett
notify(); // element meddelar vi att nu finns element att hämta
}
public void destroy()
{
if ( producer != null ) { producer.stop(); producer = null; }
if ( consumer != null ) { consumer.stop(); consumer = null; }
}
}
Man använder kvalificeraren synchronized
för att synkronisera
metoder: alla metoder som inte får exekvera samtidigt deklareras
synchronized
. Under tiden som en synkroniserad metod exekverar
kan ingen annan synkroniserad metod startas.wait()
och
notify()
. Man anropar wait()
i en synkroniserad
metod för att låta en annan tråd exekvera så att skälet till att man väntar
kan upphävas. notify()
är motsatsen till wait()
:
man väcker upp en väntande tråd efter att något skett med objektet, i vårt
fall att lagret inte längre är tomt respektive att det inte längre är
fullt. Det objekt som väcks är det som har egenrätt på
SynchronizerDemo
-instansen just nu, d.v.s. det objekt som
startade en synkroniserad metod.wait()
alltså släpper "låset" på objektet
SynchronizerDemo
så att producenten eller konsumenten kan
uppdatera objektet.Producer
och Consumer
(även de något
förenklade genom att grafiken tagits bort):
class Producer extends Thread
{
SynchronizerDemo demo;
long sleepingTime = 500;
Producer( SynchronizerDemo d )
{
demo = d;
start();
}
public void run()
{
while (true)
{
try { if (Math.random()>0.9) sleep(sleepingTime); }
catch ( InterruptedException e) {}
demo.put();
}
}
}
class Consumer extends Thread
{
SynchronizerDemo demo;
long sleepingTime = 500;
Consumer( SynchronizerDemo d)
{
demo = d;
start();
}
public void run()
{
while (true)
{
try { if (Math.random()>0.9) sleep(sleepingTime); }
catch ( InterruptedException e) {}
demo.get();
}
}
}
Thread
och
har alltså redan ärvt gränssnittet Runnable
genom
Thread
.
Det kanske mest intressanta med klasserna är att ingendera innehåller några
konstigheter: all synkronisering sker i klassen SynchronizerDemo
.
Allt Producer
och Consumer
gör är att producera och
konsumera så fort de kan.
ActionListener
som ser efter vilken knapp som användaren tryckte
på
Multithreading
Multithreading är att flera trådar kan exekvera samtidigt i ett program. Vi
har redan i exemplet SynchronizerDemo
sett att två trådar kan
exekvera samtidigt och till och med synkroniserat dem. Med multithreading
avses oftast att flera trådar arbetar asynkront: t.ex. att en tråd utför
något tungt i bakgrunden medan en annan tråd håller igång användargränssnittet.
Strömmar
De flesta program måste kunna läsa och/eller skriva data. Det kan gälla
information på en disk, någonstans på nätet, i minnet eller någon annanstans.
Java har ett gemensamt gränssnitt för all sådan dataöverföring:
strömmar (eng: streams). Algoritmer och
metoder för att läsa eller skriva data ser exakt likadana ut oavsett varifrån
man läser eller var man skriver.java.io
-paketet. Klasserna kan delas upp litet olika
beroende på synsätt: en uppdelning kan vara i om man arbetar med tecken eller
bytedata (bilder, ljud, etc): i Java är denna uppdelning tydlig:
En annan indelningsgrund är huruvida klasserna bara läser/skriver data eller
om klassen också på något sätt bearbetar det som läses/skrives: se vidare
här.
...Reader
läser tecken
...Writer
skriver tecken
...InputStream
läser bytedata
...OutputStream
skriver bytedata
Grundklasserna
De fyra abstrakta superklasserna Reader
, Writer
,
InputStream
och OutputStream
har liknande
gränssnitt för enkel läsning/skrivning:
Reader: (läser tecken eller arrayer av tecken)
int read(); // läser ett tecken och returnerar det i en int
int read( char buf[] ); // läser högst buf.length tecken, returnerar antalet
InputStream: (läser bytes eller arrayer av bytes)
int read(); // läser ett tecken och returnerar det i en int
int read( byte buf[]);// läser högst buf.length bytes, returnerar antalet
Writer: (skriver tecken eller arrayer av tecken)
int write( int c); // skriver ett tecken (c)
int write( char buf[] ); // skriver en array (buf.length st.) av tecken
OutputStream: (skriver bytes eller arrayer av bytes)
int write( int c); // skriver en byte
int write( byte buf[] ); // skriver en array (buf.length) av bytes
Desstom finns för samtliga klasser en metod som läser eller skriver till en
specificerad (med offset och längd) del av buf
. Läsmetoderna
väntar tills data finns tillgängligt eller end-of-file påträffas. Observera
att man alltså inte kan vara säker på att hela arrayen läses (data behöver ju
inte finnas än!).
Skrivning till skärmfönster och läsning från tangentbord
Strömmar används också vid enkel terminalbaserad läsning/skrivning. Instanser
av de två klasserna PrintStream
och
InputStream
används. I klassen System
(en av de
klasser som innehåller bara klassmetoder och därför inte kan instansieras)
finns de tre strömmarna in, out
och err
. De två
första används för normal läsning och skrivning medan err
används för att rapportera fel. Alla tre är alltid öppna och redo att
användas
print( argument ); // skriv argumentet
println( argument ); // skriv argumentet, följt av en newline
där argumentet kan vara vad som helst som har en strängrepresentation (alla
inbyggda typer, strängar och alla objekt som är instanser av klasser med
en toString
-metod).
print
under förutsättning att man börjar med en sträng
och de objekt som följer är instanser av klasser med en
toString
-metod. I Java har man ingen kontroll över hur många
siffror som skrivs ut vid utmatning av flyttal t.ex.
InputStream
innehåller
endast de byte-baserade inläsningsmetoderna). Man gör då så att man
skapar en InputStreamReader
t.ex:
InputStreamReader = new InputStreamReader( System.in );
Denna klass fungerar som en "översättare" från byte-data till textdata.
I praktiken brukar man också använda en BufferedReader
för att öka effektiviteten.
BufferedReader = new BufferedReader( new InputStreamReader( System.in ));
Man måste gå "omvägen" över InputStreamReader
eftersom
BufferedReader
s konstruktor ska ha en instans av
Reader
eller någon arvinge till denna.
BufferedReader
och InputStreamReader
innehåller båda
de metoder för läsning som beskrevs ovan plus:
String readLine() - läs en rad
Man kan inte direkt läsa in instanser av de inbyggda typerna. För att läsa
vanliga tal t.ex. kan man använda någon av klasserna
StringTokenizer
eller
StreamTokenizer
, som delar upp en sträng respektive det som
läses från en ström i instanser av String
och sedan använda
de metoder som beskrevs
ovan i avsnittet om strängar.
Textfiler
Textfiler hanteras analogt med ovanstående:
InputStreamReader
ersätts av FileReader
(filnamnet
anges i konstruktorn) och PrintStream
av FileWriter
(också med filnamn som konstruktorargument). För att få tillgäng till
metoder för metoder för textformattering kan sedan klassen
PrintWriter
användas:
BufferedReader infile = new BufferedReader( new FileReader("filnamn.ny") );
PrintWriter outfile = new PrintWriter( new FileWriter("filnamn.old") );
Eftersom instanserna av FileReader
och FileWriter
inte används förutom i konstruktorn finns ingen anledning att namnge dem
utan de skapas direkt vid konstruktionen av sina "wrapper"-klasser.infile
och outfile
kan nu
användas exakt som klasserna i föregående avsnitt.StreamTokenizer
för att dela upp text i ord:
import java.io.*;
class Grep
{
public static void main( String[] args ) throws IOException
{
StreamTokenizer infile =
new StreamTokenizer( new FileReader( args[0] ) );
int numWordsToLookFor = args.length;
int[] numWords = new int [ numWordsToLookFor ];
for (int i=0; i < numWordsToLookFor; i++)
numWords[i] = 0;
while ( infile.nextToken() != StreamTokenizer.TT_EOF )
if (infile.ttype == infile.TT_WORD )
{
numWords[0]++; // räknar totala antalet ord
for (int i=1; i < numWordsToLookFor; i++)
if ( infile.sval.equals( args[i] ))
numWords[i]++;
}
System.out.println( "Totala antalet ord: " + numWords[0] );
for (int i=1; i<numWordsToLookFor; i++)
System.out.println( args[i] + ": " + numWords[i] );
}
}
Klassen StreamTokenizer
beskrivs närmare
här. Programmet ovan öppnar den fil som ges som första argument till
programmet, lägger en StreamTokenizer
som ett lager runt
filobjektet, samt räkner hur många gånger de ord som ges som extra
argument förekommer i filen. Om man ger kommandot
java Grep Grep.java infile numWords
skriver programmet ut
Totala antalet ord: 69
infile: 1
numWords: 6
Binärfiler
För binärfiler (byte-data) finns som sagt en uppsättning klasser. Här visas
det enkast tänkbara exemplet: filkopiering.
import java.io.*;
public class FileCopy
{
public static final int BUFSIZE = 4096;
public static void copy( String sourceName, String destinationName )
throws IOException
{
FileInputStream source = null;
FileOutputStream destination = null;
try
{
source = new FileInputStream( sourceName );
destination = new FileOutputStream( destinationName );
byte[] buffer = new byte[BUFSIZE];
int numBytesRead;
while ( ( numBytesRead = source.read(buffer)) != -1)
destination.write( buffer, 0, numBytesRead);
}
finally // vi stänger alltid filerna
{
if (source != null)
{ try { source.close(); } catch (IOException e) {} }
if (destination != null)
{ try { destination.close(); } catch (IOException e) {} }
}
}
public static void main( String[] args ) throws IOException
{
copy ( args[0], args[1] );
}
}
Liksom i tidigare exempel med strömmar saknas felhantering m.m. ovan. Java
tillhandahåller en klass, File
, som instansieras med ett filnamn.
Instansen kan sedan användas för att ta reda på allehanda information om filen.