Detta dokument förutsätter Netscape 4 eller Internet Explorer 4, om du har tidigare versioner klicka här.
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.
Klassen 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 subklassar
Thread
.
Viktiga metoder i 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 exekverarDet 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.
Ingen av metoderna ovan ersätts vanligen, man anropar dem vid behov.
Koden följer här:
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.
update()
:
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 rita om allt som vi gör i dessa exempel.
Programmet har tre klasser: 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 konsumenten försöker hämta en vara visas motsvarande
markering på "konsumentsidan". Lagret visas som en stapel i mitten.
Vi tittar först på valda delar av klassen 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 alltså ingen annan synkroniserad metod startas.
De andra verktygen för synkronisering är metoderna 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.
Observera att metoden wait()
alltså släpper "låset" på objektet
SynchronizerDemo
så att producenten eller konsumenten kan
uppdatera objektet.
Klasserna 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(); } } }
Konsumenten och producenten är båda arvtagare till 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.
Den fullständiga programkoden visar också
hur man kan implementera en
ActionListener
som ser efter vilken knapp som användaren tryckte
på.
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.
Ett roligt (?) exempel (som också visar på skillnaden i effektivitet hos några sorteringsalgoritmer) på multithreading finns här
Javas stream-klasser finns i
java.io
-paketet. Klasserna kan delas upp litet olika
beroende på synsätt: en uppdelning kan vara huruvida man arbetar med tecken
eller bytedata (bilder, ljud, etc): i Java är denna uppdelning tydlig:
...Reader
läser tecken
...Writer
skriver tecken
...InputStream
läser bytedata
...OutputStream
skriver bytedata
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 bytesDessutom 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!).
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ändasMetoder för skrivning:
print( argument ); // skriv argumentet println( argument ); // skriv argumentet, följt av en newlinedä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).
Eftersom operatorn + fungerar för strängar kan man skriva ut flera saker
i samma print
under förutsättning att de objekt man skriver ä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.
Vid läsning måste litet mer jobb till (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 radMan 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.
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 till 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.
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.
De båda instanserna infile
och outfile
kan nu
användas exakt som klasserna i föregående avsnitt.
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 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 numWordsskriver programmet ut
Totala antalet ord: 69 infile: 1 numWords: 6
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.
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.