Notera att detta dokument är en del i ett mer omfattande dokument varför inledningen t.ex. kan te sig något abrupt.

Introduktion till Objektorienterad programmering

Dynamiska Web-sidor: applets

HTML i sig själv ger ingen eller mycket litet möjlighet att skapa dynamiska Web-sidor. Ett sätt att åstadkomma sådana är att skriva s.k. applets, program i programspråket Java som sedan kan exekveras av Web-läsare som "kan" Java.
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.java
varvid 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.

Något om objektorienterad analys och design

Att programmera innebär att lösa ett problem, t.ex. att hantera ett biblioteks böcker eller att visa en figur i en applet. Men varför skall man använda ett objektorienterat angreppssätt? Att svara kortfattat på detta är inte lätt men ett försök skulle kunna vara att den traditionella tekniken - funktions- eller algoritmbaserad - leder till svåröverblickbara, svårutbyggda program.

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 objekt (inte som en samling algoritmer) blir det första steget att bestämma dessa objekt. Detta görs genom att analysera problemet, egentligen utan att man tänker på att programmera i meningen "knappa in kod" eller ens vilket programspråk man skall använda, man beskriver problemet, inte hur man skall lösa det. Nästa steg blir att tänka sig hur programmet kommer att se ut i grova drag (design-fasen), sedan kommer implementationen som bör vara relativt enkel och rättfram om analys och design har gjorts bra, varefter testning av programmet tar vid.

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.

Begrepp och notation i analys och design

Syftet med anlysen är att skapa en modell som visar de viktiga sambanden i ett oftast komplext problem. Det finns flera snarlika metoder för att skapa denna modell. Här kommer vi att utnyttja notationen från en av dessa: Object Modeling Technique (OMT) som delvis ligger till grund för den metod som troligen blir standardnotation i framtiden: Unified Modeling Language (UML).

De viktigaste begreppen i OOP har grafiska motsvarigheter i OMT-notationen. I analysfasen skapar man en figur med hjälp av dessa.

Klass

En klass är en beskrivning (en mall) av någonting. När man skapar ett objekt säger man att klassen instansieras, man skapar en instans. Man kan t.ex. tänka sig klassen Blomma med olika instanser tulpan, ros o.s.v. En rosodlare skulle kanske snarare se klassen Ros som instansieras med olika sorters rosor. Vilka klasser man behöver beror helt på vilket problem man skall lösa.

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 ).

Attribut och metoder

Man kan också rita en klass mer fullständigt, med attribut (datafält) och metoder (de operationer klassens instanser kan utföra) (se figuren till höger). Som synes kan attribut och operationer beskrivas mer eller mindre detaljrikt.

Arv

I OOP används ordet arv för att beskriva ett är en-förhållande. Att en klass B ärver en annan klass, A, betyder alltså att B är ett slags A och att allt som A kan, kan också B. Exempelvis kan man tänka sig en klass Fordon som basklass till Bil, Cykel, etc. Flera nivåer av arv kan förekomma: Fordon, Motorfordon och Bil t.ex.

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).

Association

Ordet association används för att beskriva att två klasser har någon sorts relation. Det kan vara t.ex. lärare-student, bil-bilägare, kurs-tentamen. Relationerna kan vara sådana att varje instans av en klass hör samman med exakt en instans av en annan klass. En instans kan också har en relation till många instanser av en annan 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.

Aggregat

En klass som innehåller instanser av andra klasser som attribut brukar kallas ett aggregat av klasser. Aggregat kan ses som ett specialfall av association men brukar ritas med en speciell aggregatmarkör:

Som framgår av figuren finns det alternativa sätt att visa aggregatsrelationer.

Relationer

För att i figurerna visa vilken relation två objekt har till varandra brukar man använda vanligt språk, t.ex. äger, består av, pratar med. Relationerna skrivs vid de linjer som binder ihop klasserna.

Några ord om metodik i analys och design

I de flesta analys- och designmetoder ingår följande steg: Vissa tekniker har speciella notationer för delar av detta, t.ex. finns i OMT något som kallas den dynamiska modellen som beskriver de tillstånd olika objekt kan befinna sig i. I OMT finns också en funktionell modell som beskriver dataflöden mellan objekt. Båda dessa modeller kan ses som hjälpmedel för de två avslutande punkterna ovan.

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.

Ett litet exempel

Ett litet exempel får avsluta detta avsnitt. Tanken är att visa, dels fördelarna med objektorientering jämfört med funktionsbaserad programmering, dels också hur en liten objektmodell kan te sig grafiskt. Bli inte orolig om du inte förstår syntaxen i exemplet, detaljerna i Java kommer senare.

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.

Fullständigt program

För att göra exemplet fullständigt finns programmet att titta på i filerna Bli inte orolig om du inte förstår programmet, detaljerna kommer att klarna så småningom (den applet du ser är inte exakt denna, en teknik som kallas dubbelbuffring används för att reducera flimret).

Implemenation

Hur implementeras arv, aggregat och association i Java? Som exemplet visar används 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.

Sammanfattning

Grundläggande Java

Varför Java?

Java har flera fördelar jämfört med de flesta andra programspråk: Ett Java-program kan exekveras på alla datorer som har en Java interpretator och ett Java run-time system. Tillsammans kallas dessa delar JVM (Java Virtual Machine).

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.

Grundläggande datatyper

Det allra mest i Java är objekt. Enda undantaget är de enkla numeriska typerna, tecknen och den logiska typen 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:


Datatype 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.

Klasser och objekt

Ett Java-program består av objekt - instanser av klasser - som interagerar. Objekt har begrändsad livstid: de skapas, används och ändras, samt dör slutligen. Java innehåller konstruktioner för att skapa och använda objekt, däremot ingenting för att ta bort dem - Java sköter detta automatiskt.

Vad innehåller ett objekt?

En klass definierar ett objekts utseende, hur den ser ut för klassens användare, d.v.s. vi programmerare. Ett litet exempel:
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.

Hur man skapar ett objekt (en instans av en klass)

Förutom de inbyggda typerna har Java endast en datatyp: referens. Referens är en generell typ, vars variabler endera har värdet 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.

Arrayer

Arrayer är samlingar av objekt. De är referenstyper och följer reglerna för dessa. Arrayer skapas med:
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
//

Generella arrayer

I Java finns också en mer generell arraytyp för lagring av instanser av 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.

Metoder

Konstruktor

När man skapar ett objekt av en klass med 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.

Metoder - och något om attribut

En klass som består av endast en konstruktor är inte användbar till speciellt mycket mer än en 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.

Operatorer

Java har i stort sett samma aritmetiska och logiska operatorer som C/C++, klicka här för en genomgång.

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)
=      - tilldelning
Exempel 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 tilldelning
Uttrycket 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

Strängar

I Java finns en speciell klass för att hantera strängar: 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 = 22
Observera 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).

Typklasser för de inbyggda typerna

Ibland vill man kunna hantera variabler av de grundläggande typerna som objekt: man kan ha behov att placera numeriska värden i ett 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    Short
Alla 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.

Konvertering av instanser av egna klasser till strängrepresentation

Som vi sett konverteras tal automatiskt med 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.

Satser

Liksom i C/C++ används { och } för att dela in koden i block. På de ställen man kan skriva en Java-sats kan man skriva flera satser genom att omge dem med {}.

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.

if..else

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) {}".

for

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.

while och do/while

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

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

Nyckelorden 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-program - applikationer och applets

I Java finns ingenting som heter huvudprogram, subrutiner eller funktioner: allting är klasser och Java-program är en samling klasser som kommunicerar med varandra.

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:


Dessa metoder anropas automatiskt av Web-läsaren.

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.java
En 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.

Program som applikation och applet

Ibland vill man ha möjlighet att köra program som både applikation och applet. Detta går bra och standardlösningen är att skriva en applet med en main-metod (ingenting hindrar detta, Web-läsaren bryr sig inte om denna metod) som skapar och öppnar ett fönster. Vår välkomstapplet utbyggd så den kan användas både som applet och applikation:
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.

Sidospår: parametrar till applets

En applet refereras på en HTML-sida med hjälp av formatteringskommandot <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.

Arv

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

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

public class Circle extends Shape
{
    public Circle( int ix, int iy, int r, boolean ifill)
    {
        super(ix, iy, 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. Shapes konstruktor anropas vid konstruktion av Circle-objekt. Om man vill anropa en annan konstruktor än en utan argument måste detta göras explicit: referensen super betyder en referens till den del av klassen som utgörs av superklassen (jämför this). Precis som this kan super användas för att kvalificera attribut och metoder: detta kan ibland vara nödvändigt, t.ex. om ett attribut i 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 Shapes print() ju gör en del av det som Circles gör skulle man kanske vilja använda den. Men om vi skriver print() i Circles print() får vi ett rekursivt anrop. Lösningen heter super:

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

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

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

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

}
Själva draw()-metoderna visas inte här men finns t.ex. i det tidigare exemplet. Antag nu att vi har en massa figurer vi vill rita. Ett sätt att göra detta vore att ha en array med cirklar och en med rektanglar (och en med trianglar, en med generella polygoner, etc). Men med hjälp av vår arvshierarki kan vi skapa instanser av subklasser och lagra i referenser till superklassen! Den regel som styr hur man får lagra referenser säger just detta: en variabel får referera till instanser av sin "deklarerade" klass och till instanser av alla subklasser. Och vid anrop av en metod väljs inte 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 Objects equals() returnerar sant endast om samma objekt refereras av de två variablerna. Man bör ersätta detta med en metod som kontrollerar om alla relevanta attribut är lika.
class Circle
{
    public Circle( int ix, int iy, int isize )
    {  x = ix; y = iy; size = isize;  }

    public boolean equals( Object obj )
    { 
       if (obj instanceof Circle)
          {
             Circle c = (Circle) obj;
             return (c.x==x) && (c.y==y) && (c.size==size);
          }
       else return false;
    }
}
Observera att equals för att fungera med variabler av 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.

Gränssnitt

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

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

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

}

Paket och skyddsnivåer

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

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

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

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

Några viktiga paket

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

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

Viktiga paket:

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

Undantag

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

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

Klassen 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.

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

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

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

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

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

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:

(OBS! dessa klasser håller på att ersättas av en ny uppsättning GUI-klasser (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).

Dessa klasser kan sedan delas in i olika grupper:

Övriga klasser kan ses som huvudsakligen abstrakta eller sådana som används internt av andra klasser.

Ett användargränssnitt består av en behållare (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 (Panels) kan man bygga hur komplicerade gränssnitt som helst.

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å ...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.

Klassen 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.

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.

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 (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:

Som Java-programmerare behöver man oftast inte hantera händelseobjekten. Några undantg:

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.
  1. Man måste tala om att klassen implementerar ett lyssnargränssnitt:
    public class MyEventHandler implements ActionListener {
    ...
    }
    
  2. En instans av klassen 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 );
    
  3. Kod för att ta hand om händelsen måste skrivas i MyEventHandler:
    public void actionPerformed( ActionEvent event )
    {
     ... gör vad som ska göras
    }
    
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.

I klassen Component finns metoder för att hantera (lägga till och ta bort) lyssnare:

Olika subklasser lägger till sina egna lyssnare: 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.

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)

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.

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 Paneler, 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.

Exemplet efter de korta beskrivningarna visar hur man använder 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.

Observera att endast tryckningar på knappar i Card-Panelen tas om hand av programmet.

Koden och litet mer information om de olika hanterarna finns här.

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.

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 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().

En fix komponent (som 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.

Grafik kan man rita i flera av AWT-komponenterna men oftast ritar man i instanser av Canvas eller Applet eller (oftare ändå) subklasser till dessa. Det enklaste sättet att rita grafik är att helt enkelt skriva ritinstruktionerna i paint().

Appleten som följer ritar de figurer som klassen Graphics definierar. Det finns också metoder för att rita bilder.

Koden finns här.

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().

Man kan också skapa egna färger med konstruktorn:

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.

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:

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();
	}
    }

}


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 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.

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.

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.

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 subklasser 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 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.

Ingen av metoderna ovan ersätts vanligen, man anropar dem vid behov.

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.

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.

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():

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.

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.

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 producenten 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 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å

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.

Ett roligt (?) exempel (som också visar på skillnaden i effektivitet hos några sorteringsalgoritmer) på multithreading finns här

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.

Javas stream-klasser finns i 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.

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

Metoder för skrivning:

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).

Eftersom operatorn + fungerar för strängar kan man skriva ut flera saker i samma 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.

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 BufferedReaders 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.

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.

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.

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 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.

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.