Forrige kapittel Neste kapittel

Kapittel 8 Mer om klasser og objekter

Innledning

En introduksjon til klasser og objekter ble gitt i kapittel 3. Det ble der skrevet at vi bruker klasser til å definere datatyper for de objektene programmet vårt skal arbeide med, ved at klassene beskriver modeller av objektene. Det ble gitt noen enkle eksempler på dette. Det ble understreket at det er en viktig forskjell mellom primitive datatyper og datatyper definert i form av klasser. Vi har også i mange av programeksemplene i seinere kapitler brukt klasser til å definere objekter som er blitt opprettet av programmet. Flere ganger har vi sett at dette har hatt som en sideeffekt at vi har kunnet bruke programkode om igjen, ved at samme klasse er blitt brukt i flere forskjellige programmer. I dette kapitlet skal vi gå noe nærmere inn på hvordan klasser kan brukes til å definere datatyper i programmer. Dette blir belyst gjennom en del nye programeksempler.

Når vi skal skrive et program for noe, er noe av det første vi tenker på hvilke objekter programmet skal behandle og hvilke egenskaper de har:

Dette danner utgangspunktet for hvilke klasser vi skal definere for programmet og hva klassene skal inneholde av datafelter og metoder.

Eksempel 1

Vi skal definere en datatype for personer. For hver person skal det lagres navn og personnummer. Av operasjoner skal det ikke gjøres annet enn å kunne vise hva som er lagret. En klasse som definerer en slik datatype kan vi skrive på denne måten:

 1 public class Person
 2 {
 3   private String navn;
 4   private int personnr;
 5
 6   //Konstruktør
 7   public Person( String n, int nr )
 8   {
 9     navn = n;
10     personnr = nr;
11   }
12
13   public String getNavn()
14   {
15     return navn;
16   }
17
18   public int getNummer()
19   {
20     return personnr;
21   }
22
23   public String toString()
24   {
25     return "Navn: " + navn + ", personnummer: " + personnr;
26   }
27 }

Du finner denne klassen i fila Person.java. Vær oppmerksom på at klassen ikke utgjør noe kjørbart program, den definerer bare en datatype. Klassen Person inneholder mye av det som er felles for alle klasser som definerer datatyper. Mye av dette er nevnt i tidligere kapitler, men for repetisjonens skyld tar vi en oppsummering. For klassen Person kan vi sette opp følgende liste av egenskaper:

Vi vet at vi oppretter objekter ved å bruke new-operatoren. Bak denne skriver vi navnet på klassen som definerer objektet vi vil opprette, samt aktuelle parametre til klassens konstruktør (i tilfelle konstruktøren har parametre). For å opprette et Person-objekt som representerer en person med navn Siri og personnummer 7 (vi bruker for enkelhets skyld ikke "virkelige" personnumre her), skriver vi derfor:

  Person p1 = new Person( "Siri", 7 );

Virkningen av dette er at det i maskinens RAM blir satt av plass til datafeltene til et objekt av type Person. Datafeltet navn i dette objektet vil få verdien "Siri", og datafeltet personnr i dette objektet vil få verdien 7. new-operatoren vil returnere adressen til det nye objektet. Denne blir tilordnet som verdi til variabelen p1. Vi sier at p1 er en peker eller referanse til objektet, eller at den peker på objektet eller refererer til det. Et objekt blir også kalt en instans av vedkommende klasse. Og klassens datafelter blir kalt instans-variable.

Hvert objekt får tildelt sine egne datafelter. Oppretter vi et nytt objekt, får vi opprettet nye datafelter for objektet, uavhengig av dem som måtte eksistere for eventuelle andre objekter av samme type. Skriver vi for eksempel

  Person p2 = new Person( "Per", 8 );

så har dette ingen konsekvenser for objektet p1 som ble opprettet ovenfor.

Merk deg forskjellen mellom å definere en klasse og opprette et objekt av vedkommende klasse, det vil si instansiere klassen.

En klasses toString-metode har den spesielle egenskapen at den vil bli kalt opp automatisk dersom et objekt blir skjøtt sammen med en tekststreng ved bruk av pluss-operatoren. Skriver vi for eksempel

  "Objektinnhold: " + p1;

så vil java-systemet utføre metodekallet p1.toString().

Arrayer av objekter

Når vi deklarerer en array, spesifiserer vi datatypen for elementene i arrayen. Fra kapittel 7 vet vi at det kan være en hvilken som helst datatype. Datatypen kan vi da også selvsagt selv definere ved hjelp av en klasse. Vi kan for eksempel ha en array av Person-objekter (av den typen vi definerte foran):

  Person[] student = new Person[ 10 ];

Dette innebærer at hver av verdiene student[ i ], i = 0, ..., 9 er av type Person, det vil si at de kan peke på et Person-objekt. Men de vil ikke peke på noe objekt før vi har satt dem til å gjøre det! Når arrayen blir opprettet, vil alle plassene automatisk bli tildelt standardverdien null. Denne indikerer at det ikke blir pekt på noe objekt. Hvert av Person-objektene som vi vil legge inn i arrayen må vi opprette separat. Vi kan for eksempel skrive

  student[ 0 ] = new Person( "Lise", 1 );

På tilsvarende måte må vi gjøre for å opprette og legge inn objekter på de andre plassene i arrayen. Det er da lurt å ha en tellevariabel som holder rede på hvor mange objekter som er lagt inn. Da vet vi samtidig hvor vi skal legge inn neste objekt, se følgende programeksempel. Som nevnt, vil array-verdiene student[ i ] være pekere til de objektene vi har opprettet og lagt inn i arrayen student. Dersom vi for eksempel ønsker å få tak i navnet til Person-objektet som er lagt inn på indeksplass nummer i, kan vi derfor skrive

  String navn = student[ i ].getNavn();

Vær imidlertid oppmerksom på at dersom vi prøver oss på en slik instruksjon for en indeksverdi i der det ikke er lagt inn noe objekt, så vil vi få en feil av type NullPointerException. Programmet vil skrive ut en feilmelding om dette. Grunnen er at student[ i ] da ikke vil peke på noe Person-objekt, men ha verdien null som den fikk da arrayen ble opprettet.

Merknad

De fleste programeksemplene i dette kapitlet forutsetter at du har tilegnet deg stoffet i kapitlet Vindusbaserte programmer.

Programeksempel 1

Vi skal ta for oss et hendelsesbasert program som gjør bruk av en array av Person-objekter av den typen som er definert foran. Programmet bruker objektene til å lagre navn og (fiktive) personnumre for et antall personer. Programmet skal også kunne vise hvilke data som er lagret. Brukeren av programmet skal kunne

Nedenfor kan du se brukergrensesnittet for programmet.

Programmet bruker klassen Person slik den er definert foran. I fila Personregister.java er det definert en klasse Personregister som inneholder en array av Person-objekter med plass til 10 slike objekter, eller rettere sagt pekere til slike objekter. Innholdet i denne fila er gjengitt nedenfor.

Metoden nyPerson mottar via en parameter et (nytt) Person-objekt og setter det inn i arrayen på første ledige plass. Metoden returner true som signal på at innsetting ble foretatt. Dersom arrayen allerede er full når innsetting skal foretas, returnerer metoden false som signal for dette, uten å foreta noe innsetting.

Metoden visPerson mottar som parameter et personnummer. Den leter gjennom arrayen etter et Person-objekt med det mottatte personnummeret. Dersom et slikt finnes, returnerer metoden en String med alle registrerte data for denne personen. Metoden returnerer null-adressen som signal for at det ikke er registrert noen person med det mottatte personnummeret.

Metoden visAlleNavn returnerer en String som inneholder navnene til alle registrerte personer. Det er lagt inn et ny-linje-tegn mellom hvert navn.

 1 public class Personregister
 2 {
 3   private Person[] register;
 4   public static final int KAPASITET = 10;
 5   private int antall;
 6
 7   public Personregister()
 8   {
 9     register = new Person[ KAPASITET ];
10     antall = 0;
11   }
12
13   //Setter inn ny person i registeret i tilfelle det er plass.
14   public boolean nyPerson( Person p )
15   {
16     if ( antall < KAPASITET )
17     {
18       register[ antall ] = p;
19       antall++;
20       return true;
21     }
22     else
23       return false;
24   }
25
26   //Returnerer data om person med gitt nummer.
27   public String visPerson( int nr )
28   {
29     for ( int i = 0; i < antall; i++ )
30       if ( register[ i ].getNummer() == nr )
31         return register[ i ].toString();
32     return null;  //personen var ikke der
33   }
34
35   //Returnerer navneliste over alle registrerte personer.
36   public String visAlleNavn()
37   {
38     String navneliste = "";
39     for ( int i = 0; i < antall; i++ )
40       navneliste += register[ i ].getNavn() + "\n";
41     return navneliste;
42   }
43 }

Klassene Person og Personregister utgjør til sammen ikke noe fullstendig program. De definerer bare det vi kan kalle datastrukturen til programmet, det vil si objektene og samlingen av objekter som skal brukes til å lagre programmets data. Det som mangler er brukergrensesnittet, det vil si definisjon av det vinduet brukeren vil se på skjermen når programmet kjøres, samt definisjon av de operasjoner som brukeren skal kunne utføre. Når dette skal programmeres, skal vi imidlertid gjøre bruk av de klassene som definerer datastrukturen.

Som nevnt i innledningen til programeksemplet, skulle det lages et hendelsesbasert program. Det vi mangler er derfor en vindusklasse som tar seg av hendelseshåndteringen. Den er definert i fila Persondata.java som er gjengitt nedenfor. (I tillegg må vi ha en driverklasse som inneholder en main-metode.) Vindusobjektet blir også registrert som lytteobjekt for de tre knappene i programvinduet (vist ovenfor) — en knapp for hver av de tre operasjonene som brukeren skulle kunne utføre. For hver knapp er det definert en egen metode som blir kalt opp av actionPerformed-metoden når brukeren klikker på vedkommende knapp.

Merk deg at i tillegg til de objektene som definerer de grafiske skjermkomponentene, så inneholder vindusklassen et Personregister-objekt. Det er dette som skal brukes til å lagre de data som brukeren vil registrere. Objektet må selvsagt opprettes (tilsvarende som alle andre objekter). Det skjer i klassens konstruktør:

    register = new Personregister();

Husk at denne instruksjonen medfører at instruksjonene i Personregister-klassens konstruktør blir utført. Konstruktøren vil opprette en (tom) array med plass til 10 Person-objekter, og den initialiserer tellevariabelen for slike til 0. Når vi i vindusklassen skal skrive kode for å registrere nye personer, eller hente ut data for personer som er registrert, gjør vi bruk av vårt Persregister-objekt, men vi gjør imidlertid ikke direkte bruk av nevnte array. Den har vi heller ikke tilgang til, for den har private aksess i Personregister-klassen. Isteden benytter vi oss av Personregister-klassens public metoder. Disse gjør i sin tur bruk av nevnte array. Prinsipielt trenger vi heller ikke vite hvordan Personregisteret lagrer sine data. Det eneste vi trenger å vite, er navnet på og parametrene til de metodene vi trenger å kalle opp for å lagre data om en ny person eller hente ut lagrede persondata. Vi kan si det på den måten at det er de public metodene til en klasse som utgjør dens grensesnitt med omverdenen.

For å registrere en ny person, er det Personregister-objektets metode nyPerson vi må gjøre kall på. Merk deg imidlertid at denne skal motta som parameter et ferdig opprettet Person-objekt. Dette objektet må vi lese inn data for og opprette i vindusklassen, før vi bruker objektet som aktuell parameter i et kall på Personregister-objektets metode nyPerson. Disse operasjonene skjer i vindusklassens metode nyPerson, se nedenfor. NB! Denne metoden er en annen metode enn Personregister-klassens metode med samme navn!

Vindusklassens metoder visRegister og finnPerson gjør på tilsvarende måte som metoden nyPerson kall på de relevante metoder til vindusklassens Personregister-objekt for å få utført det de skal. Merk deg at det er vindusklassens Personregister-objekt vi bruker for å kommunisere mellom vindusklassen og Personregister-klassen!

Merk deg for øvrig at når vi har et hendelsesbasert program, så er det fornuftig å gi brukeren passende tilbakemeldinger på de operasjoner brukeren forsøker å utføre, slik at brukeren holdes orientert om hva som er utført, eller om eventuelle problemer som har oppstått.

 1 import javax.swing.*;
 2 import java.awt.*;
 3 import java.awt.event.*;
 4
 5 public class Persondata extends JFrame implements ActionListener
 6 {
 7   private JTextField navnefelt, nummerfelt;
 8   private JTextArea utskrift;
 9   private JButton ny, vis, finn;
10   private Personregister register;
11
12   public Persondata()
13   {
14     super( "Persondata" );
15     navnefelt = new JTextField( 20 );
16     nummerfelt = new JTextField( 12 );
17     utskrift = new JTextArea( 10, 25 );
18     utskrift.setEditable( false );
19     ny = new JButton( "Registrer ny person" );
20     ny.addActionListener( this );
21     vis = new JButton( "Vis alle registrerte personer" );
22     vis.addActionListener( this );
23     finn = new JButton( "Finn person med gitt nummer" );
24     finn.addActionListener( this );
25     register = new Personregister();
26     Container c = getContentPane();
27     c.setLayout( new FlowLayout() );
28     c.add( new JLabel( "Navn:" ) );
29     c.add( navnefelt );
30     c.add( new JLabel( "Nr.:" ) );
31     c.add( nummerfelt );
32     c.add( ny );
33     c.add( vis );
34     c.add( finn );
35     c.add( new JScrollPane( utskrift ) );
36   }
37
38   public void nyPerson()
39   {
40     String navn = navnefelt.getText();
41     int nr = Integer.parseInt( nummerfelt.getText() );
42     Person ny = new Person( navn, nr );
43     boolean ok = register.nyPerson( ny );
44     if ( ok )
45       utskrift.setText( navn + " ble registrert." );
46     else
47       utskrift.setText( "Ingen registrering pga. fullt register!" );
48   }
49
50   public void visRegister()
51   {
52     utskrift.setText( "Registrerte personer:\n" );
53     utskrift.append( register.visAlleNavn() );
54   }
55
56   public void finnPerson()
57   {
58     int nr = Integer.parseInt( nummerfelt.getText() );
59     String data = register.visPerson( nr );
60     if ( data != null )
61       utskrift.setText( data );
62     else
63       utskrift.setText( "Finnes ikke i registeret!" );
64   }
65
66   public void actionPerformed( ActionEvent e )
67   {
68     if ( e.getSource() == ny )
69       nyPerson();
70     else if ( e.getSource() == vis )
71       visRegister();
72     else if ( e.getSource() == finn )
73       finnPerson();
74   }
75 }

Driverklasse for programmet kan lastes ned fra fila Personarkiv.java

Merk deg nøye strukturen eller 'arkitekturen' til dette programmet. Den er ganske typisk for hendelsesbaserte programmer som skal foreta registrering, gjenfinning og visning av data for et eller annet. Legg merke til at programmet er bygget opp slik at det skilles mellom datastruktur og brukergrensesnitt. Datastrukturen er definert av klassene Person og Personregister. Disse klassene definerer hvordan programmets data, i dette tilfelle navn og numre for personer, skal lagres og manipuleres med. I disse klassene finnes det ingen instruksjoner for brukerkommunikasjon. Brukergrensesnittet er definert av klassen Persondata. Denne klassen definerer vinduet som vises for brukeren når programmet kjøres. Det er denne klassen som inneholder alle instruksjoner for brukerkommunikasjon. I tillegg kommuniserer klassen med datastrukturen; den legger inn nye data i den og søker etter data med gitt identifikasjon. Kommunikasjonen med datastrukturen foregår på den måten at det ved hjelp av Persondata-klassens objekt av type Personregister gjøres kall på metoder definert i klassen Personregister.

Overloading av konstruktører

På slutten av kapittel 6 ble det skrevet litt om hva som menes med overloading av metoder: metoder kan ha samme navn, men forskjellig parameterliste. På tilsvarende måte kan vi overloade konstruktører, det vil si for én og samme klasse definere flere forskjellige konstruktører. En konstruktør har jo samme navn som klassen den er konstruktør til. Konstruktørene til en klasse skiller seg fra hverandre ved å ha forskjellig parameterliste. På den måten gjør vi det mulig å opprette objekter av klassen på flere forskjellige måter: vi kan velge konstruktør etter hvilke data vi har å gå ut fra. Når vi skal definere alternative konstruktører, kan detaljene utformes på litt forskjellig måte. Vi skal ta for oss et eksempel tilsvarende klassen Time2 som finnes i læreboka Deitel & Deitel. Da trenger vi imidlertid først å vite litt mer om nøkkelordet this.

Nøkkelordet this

For å gjøre kall på en metode definert i en klasse, trenger vi når vi er utenfor klassen et objekt av vedkommende klasse. Ved kallet vil da metoden virke på datafeltene som hører til det objektet vi bruker for å kalle den opp. Nøkkelordet this brukt i en slik metode, vil referere til det objektet som blir brukt ved metodekallet. Tidligere har vi bare brukt nøkkelordet this når vi skulle registrere at et vindusobjekt skulle være lytteobjekt for eksempel for en knapp i vinduet. Dette ble blant annet gjort i vindusklassen Persondata ovenfor. I konstruktøren til denne finner vi blant annet instruksjonene

    ny = new JButton( "Registrer ny person" );
    ny.addActionListener( this );

Når vi kjører programmet, blir det i main-metoden (definert i klassen Personarkiv) opprettet et vindusobjekt av type Persondata. Instruksjonene i klassens konstruktør blir da utført. Nøkkelordet this vil derfor i dette tilfellet referere til Persondata-objektet. Det stemmer jo med det som er sagt tidligere om at vindusobjektet skulle registreres som lytteobjekt.

Til vanlig er det ikke nødvendig inne i en metode å bruke this for å referere til det objektet som ble brukt til å gjøre kall på metoden. Men dersom vi har en metodeparameter eller lokal variabel som har samme navn som et av klassens datafelter, vil denne metodeparameteren eller lokale variabelen overstyre vedkommende datafelt. Dette bør generelt unngås. Men dersom vi likevel gjør det og ønsker å referere til datafeltet med samme navn, må vi prefikse datafeltet med this.

Eksempel

class C
{
  private int n;

  public C( int n )
  {
    this.n = n;  // this.n refererer til datafeltet,
  }              // n refererer til parameteren

  .
  .
  .
}

En annen bruk av nøkkelordet this er i forbindelse med kall på konstruktører. Dersom vi i en konstruktør ønsker å gjøre kall på en annen konstruktør (for samme klasse), kan vi skrive this(). Mellom parentesene () skriver vi da eventuelle aktuelle parametre, i tilfelle vedkommende konstruktør har noen slike. Dette er for øvrig eneste muligheten vi inni en klasse har til å gjøre kall på noen av klassens konstruktører. Utenfor klassen er det bare i forbindelse med bruk av new-operatoren vi har mulighet til å gjøre kall på noen konstruktør. Vi tar for oss et eksempel på this brukt i forbindelse med konstruktører.

Programeksempel 2: Overloadete konstruktører

Klassen Tid nedenfor er en modifikasjon av klassen Time2 i læreboka Deitel & Deitel. Klassen representerer et tidspunkt angitt i timer, minutter og sekunder. Det blir i klassen definert alternative konstruktører slik at vi får opprettet et Tid-objekt uavhengig av hvilke data vi har for vedkommende tidspunkt: Det kan tenkes at vi ikke kjenner verken timeverdi, minuttverdi, eller sekundverdi; kjenner bare timeverdi; kjenner timeverdi og minuttverdi, men ikke sekundverdi; eller kjenner alle tre. Den siste konstruktøren oppretter et Tid-objekt som representerer samme tidspunkt som det Tid-objektet den mottar som parameter. Når en konstruktør mangler verdi for en eller flere av time, minutt eller sekund, blir manglende verdi satt til 0.

I dette eksemplet, der Tid-klassen skal representere et tidspunkt, er det viktig å sikre at time, minutt og sekund får verdier innenfor tillatte grenser, det vil si timer fra 0 til 23, minutter og sekunder fra 0 til 59. Dette blir i klassen sikret ved at datafeltene er beskyttet med private aksess og ved at tildeling av verdier bare kan skje via kall på metoden setTid. I den blir det kontrollert at verdiene er innenfor tillatte grenser. Verdier settes til 0 dersom det gjøres forsøk på å sette en verdi som ikke er tillatt. Ved kontroll på gyldighet og tildeling av verdi brukes operatoren ?:. Denne er forklart i kapittel 4.

Merk deg spesielt konstruktøren med et Tid-objekt som parameter:

  public Tid( Tid t )
  {
    this( t.time, t.minutt, t.sekund );
  }

Det gjøres her direkte aksess på datafeltene til objektet t, selv om disse har private aksess. Dette er mulig å gjøre når vi er inni et annet objekt av samme klasse, ikke ellers.

Legg ellers merke til hvordan klassen DecimalFormat blir brukt til å få ut verdiene med to sifre ved utskrift, slik at det for eksempel skrives 02 istedenfor 2.

 1 import java.text.DecimalFormat;
 2
 3 public class Tid
 4 {
 5   private int time, minutt, sekund;
 6
 7   public Tid()
 8   {
 9     this( 0, 0, 0 );
10   }
11
12   public Tid( int t )
13   {
14     this( t, 0, 0 );
15   }
16
17   public Tid( int t, int m )
18   {
19     this( t, m, 0 );
20   }
21
22   public Tid( int t, int m, int s )
23   {
24     setTid( t, m, s );
25   }
26
27   public Tid( Tid t )
28   {
29     this( t.time, t.minutt, t.sekund );
30   }
31
32   public void setTid( int t, int m, int s )
33   {
34     time = ( t >= 0 && t < 24 ) ? t : 0;
35     minutt = ( m >= 0 && m < 60 ) ? m : 0;
36     sekund = ( s >= 0 && s < 60 ) ? s : 0;
37   }
38
39   public String universaltid()
40   {
41     DecimalFormat toSifre = new DecimalFormat( "00" );
42     return toSifre.format( time ) + ":" +
43             toSifre.format( minutt ) + ":" + toSifre.format( sekund );
44   }
45
46   public String standardtid()
47   {
48     return time + "." + minutt;
49   }
50 }

Klassen Tidstest som er gjengitt nedenfor, og som du finner i fila Tidstest.java, er et lite testprogram for Tid-klassen. Det blir opprettet et objekt ved bruk av hver av de definerte konstruktørene. Dessuten blir det opprettet et objekt med ugyldige initialverdier for time, minutt og sekund. I en dialogboks blir det skrevet ut hvilke tidspunkter de forskjellige objektene er blitt satt til å representere. Bilde av denne dialogboksen følger rett etter programkoden.

 1 import javax.swing.*;
 2
 3 public class Tidstest
 4 {
 5   public static void main( String[] args )
 6   {
 7     Tid t1, t2,t3, t4, t5, t6;
 8     String utskrift;
 9
10     t1 = new Tid();
11     t2 = new Tid( 9 );
12     t3 = new Tid( 9, 51 );
13     t4 = new Tid( 9, 17, 27 );
14     t5 = new Tid( 34, 60, 66 );
15     t6 = new Tid( t4 );
16
17     utskrift = "Konstruert med: " +
18             "\nt1: alle argumenter lik default-verdier" +
19             "\n    " + t1.universaltid() +
20             "\n    " + t1.standardtid();
21     utskrift += "\nt2: time spesifisert, minutt og sekund " + ""
22             + "default-verdier" +
23             "\n    " + t2.universaltid() +
24             "\n    " + t2.standardtid();
25     utskrift +=
26             "\nt3: time og minutt spesifisert, sekund default-verdi" +
27             "\n    " + t3.universaltid() +
28             "\n    " + t3.standardtid();
29     utskrift += "\nt4: time, minutt og sekund spesifisert" +
30             "\n    " + t4.universaltid() +
31             "\n    " + t4.standardtid();
32     utskrift += "\nt5: ugyldige verdier spesifisert for alle tre" +
33             "\n    " + t5.universaltid() +
34             "\n    " + t5.standardtid();
35     utskrift += "\nt6: Tid-objekt t4 spesifisert" +
36             "\n    " + t6.universaltid() +
37             "\n    " + t6.standardtid();
38     JOptionPane.showMessageDialog( null, utskrift,
39               "Demonstrasjon av overloading av konstruktører",
40               JOptionPane.INFORMATION_MESSAGE );
41   }
42 }

Default-konstruktør

Den konstruktøren til en klasse som er uten parametre, blir kalt default-konstruktøren til klassen. Navnet kommer av at dersom du definerer en klasse uten å definere noen konstruktør for klassen, så er det likevel mulig å opprette objekter av klassen ved at du bruker new-operatoren og bak denne skriver klassenavnet etterfulgt av parenteser (). Det som da vil skje er at java-systemet selv vil opprette en parameterløs konstruktør for klassen. Denne konstruktøren vil initialisere alle datafelter til standardverdier. For numeriske datafelt er det 0 (eller 0.0), for logiske datafelt (av type boolean) er det false, for char er det det såkalte null-tegnet '\u0000', for alle datafelt av en eller annen klassetype eller arraytype er det verdien null. NB! Dersom du for en klasse har definert minst én konstruktør med en eller flere parametre, men ikke noen konstruktør uten parameter, så vil heller ikke java-systemet opprette noen default-konstruktør som beskrevet ovenfor.

Merknad om set- og get-metoder

Det har tidligere vært nevnt at datafeltene i en klasse bør beskyttes ved å gi dem private aksess. Dermed oppstår også behovet for å ha get- og set-metoder for dem, slik det ble kommentert i kapittel 3, merknad 1. Vår Tid-klasse gir anledning til å belyse dette nærmere: Det er selvsagt viktig at verdiene for time, minutt og sekund holdes innenfor de gyldige verdiene for disse, nemlig 0 til 23 for den første og 0 til 59 for de to andre. Ellers blir dataene meningsløse. Dersom datafeltene ikke hadde hatt private aksess, kunne vi for eksempel i programmet Tidstest ovenfor ha skrevet instruksjoner som

  t1.time = 100;
  t1.minutt = 70;
  t1.sekund = 217;

eller brukt hvilke som helst andre verdier på høyre side av tilordningsinstruksjonene. Tilsvarende hadde vært mulig for de andre Tid-objektene. Når datafeltene har private aksess, er det utenfor Tid-klassen bare ved å bruke Tid-objektenes set-metoder at det er mulig å tilordne datafeltene nye verdier. I disse metodene har vi sørget for å legge inn tester som sikrer at verdiene holdes innenfor tillatte grenser. På tilsvarende måte bør vi selvsagt programmere i andre sammenhenger der det er viktig at dataene holdes innenfor bestemte grenser for å ha mening. Generelt er det som nevnt vanlig å gi datafelter private aksess, slik at de beskyttes mot såkalt uautorisert bruk.

Programeksempel 3

Klassen Tid2 som er gjengitt nedenfor, er lik vår tidligere klasse Tid, men er nå tilføyd get-metoder for datafeltene. Fra før av har den en set-metode som setter verdi på alle de tre datafeltene på én gang.

 1 import java.text.DecimalFormat;
 2
 3 public class Tid2
 4 {
 5   private int time, minutt, sekund;
 6
 7   public Tid2()
 8   {
 9     this( 0, 0, 0 );
10   }
11
12   public Tid2( int t )
13   {
14     this( t, 0, 0 );
15   }
16
17   public Tid2( int t, int m )
18   {
19     this( t, m, 0 );
20   }
21
22   public Tid2( int t, int m, int s )
23   {
24     setTid( t, m, s );
25   }
26
27   public Tid2( Tid2 t )
28   {
29     this( t.time, t.minutt, t.sekund );
30   }
31
32   public void setTid( int t, int m, int s )
33   {
34     time = ( t >= 0 && t < 24 ) ? t : 0;
35     minutt = ( m >= 0 && m < 60 ) ? m : 0;
36     sekund = ( s >= 0 && s < 60 ) ? s : 0;
37   }
38
39   public String universaltid()
40   {
41     DecimalFormat toSifre = new DecimalFormat( "00" );
42     return toSifre.format( time ) + ":" +
43            toSifre.format( minutt ) + ":" + toSifre.format( sekund );
44   }
45
46   public String standardtid()
47   {
48     String tid = time + ".";
49     if ( minutt < 10 )
50       tid += "0";
51     return tid + minutt;
52   }
53
54   public int getSekund()
55   {
56     return sekund;
57   }
58
59   public int getMinutt()
60   {
61     return minutt;
62   }
63
64   public int getTime()
65   {
66     return time;
67   }
68 }

Klassen Klokke som er vist nedenfor skal, som navnet indikerer, representere en klokke. En klokke skal vite tiden, kunne vise den og kunne oppdatere den. Til å representere tiden har vi Tid2-klassen vår. Vi legger derfor inn et Tid2-objekt i Klokke-klassen vår. For å kunne stille klokka, definerer vi set-metoder for både time, minutt og sekund. Metodene gjør kall på Tid2-objektets setTid-metode. De datafeltene som ikke skal endres, blir av setTid-metoden satt til de verdiene som de allerede har. Disse verdiene hentes av de relevante get-metodene.

For å kunne vise hva klokka er, definerer vi vis-metoder. Disse gjør kort og godt kall på Tid2-objektets tilsvarende metoder og returnerer videre det de får returnert av disse metodene.

For å simulere at klokka går et sekund fram, er det definert en metode tikk. Denne henter ut Tid2-objektets nåværende sekundverdi s og øker denne med 1. Men siden vi skal simulere en klokke, må vi sørge for å programmere slik at dersom s på forhånd var lik 59, så skal den nå starte om igjen på 0. Dessuten skal minuttantallet økes med 1. Og dersom minuttantallet da på forhånd var 59, så skal dette starte om igjen på 0 og timeantallet skal økes med 1. Og endelig må vi i så fall sjekke om timeantallet på forhånd var 23. I så fall skal dette starte om igjen på 0. Detaljene i alt dette kan skrives på mange forskjellige måter. Den mest kompakte koden får vi ved å gjøre bruk av javas modulus-operator (rest-operator) %. For når vi skriver

  s = (s + 1) % 60;

for å øke s med 1, så vil resten ved divisjonen (s + 1) / 60 bli (s + 1) så lenge s er mindre enn 59. Dersom s på forhånd er lik 59, så blir (s + 1) lik 60, og da vil resten bli 0, som altså da vil bli tilordnet som den nye verdien til s. Tilsvarende gjelder for koden som er skrevet for å oppdatere minutt og time.


 1 public class Klokke
 2 {
 3   private Tid2 tid = new Tid2();
 4
 5   public void setTime( int h )
 6   {
 7     tid.setTid( h, tid.getMinutt(), tid.getSekund() );
 8   }
 9
10   public void setMinutt( int m )
11   {
12     tid.setTid( tid.getTime(), m, tid.getSekund() );
13   }
14
15   public void setSekund( int s )
16   {
17     tid.setTid( tid.getTime(), tid.getMinutt(), s );
18   }
19
20   public void tikk()
21   {
22     int s = tid.getSekund();
23     int m = tid.getMinutt();
24     int h = tid.getTime();
25     s = (s + 1) % 60;
26     if ( s == 0 )
27     {
28       m = (m + 1) % 60;
29       if ( m == 0)
30         h = (h + 1) % 24;
31     }
32     tid.setTid( h, m, s );
33   }
34
35   public String visUniversaltid()
36   {
37     return tid.universaltid();
38   }
39
40   public String visStandardtid()
41   {
42     return tid.standardtid();
43   }
44 }

Klassene Tid2 og Klokke utgjør ikke til sammen noe kjørbart program. De definerer bare datatyper som kan brukes i et program. Programmet Klokketest er laget for å teste ut de to datatypene. Det oppretter og viser et programvindu som er definert av klassen Klokkevindu. Programvinduet blir som på følgende bilde, der det er skrevet inn verdiene 17, 19 og 25 for henholdsvis time, minutt og sekund.

Vindusklassen Klokkevindu er gjengitt nedenfor. Den inneholder et objekt av type Klokke (som i sin tur inneholder et objekt av type Tid2). Klokka blir initialisert til 00:00:00, det vil si midnatt. Brukeren kan skrive inn verdier (og trykke Retur-tast) for å sette ønskede verdier på time, minutt og sekund. Innstilt tid blir vist i format av universaltid i et eget tekstfelt og som standartid i et annet tekstfelt. For å simulere at klokka går ett sekund fram, må brukeren i dette programmet klikke på en knapp. For øvrig kunne dette ha blitt programmert til å skje automatisk ved bruk av maskinens systemklokke. Legg merke til hvordan sekund, minutt og time endrer seg når gammel verdi er 59 for minutt og sekund, 23 for time.

 1 import javax.swing.*;
 2 import java.awt.*;
 3 import java.awt.event.*;
 4
 5 public class Klokkevindu extends JFrame implements ActionListener
 6 {
 7   private Klokke ur;
 8   private JTextField sek, min, time, universaltid, standardtid;
 9   private JButton klokketikk;
10
11   public Klokkevindu()
12   {
13     super( "Javaklokke" );
14     ur = new Klokke();
15     sek = new JTextField( 2 );
16     min = new JTextField( 2 );
17     time = new JTextField( 2 );
18     universaltid = new JTextField( 6 );
19     universaltid.setEditable( false );
20     standardtid = new JTextField( 6 );
21     standardtid.setEditable( false );
22     klokketikk = new JButton( "1 sekund fram" );
23     sek.addActionListener( this );
24     min.addActionListener( this );
25     time.addActionListener( this );
26     klokketikk.addActionListener( this );
27     Container c = getContentPane();
28     c.setLayout( new FlowLayout() );
29     c.add( new JLabel( "Sett time:" ) );
30     c.add( time );
31     c.add( new JLabel( "Sett minutt:" ) );
32     c.add( min );
33     c.add( new JLabel( "Sett sekund:" ) );
34     c.add( sek );
35     c.add( new JLabel( "Universaltid " ) );
36     c.add( universaltid );
37     c.add( new JLabel( "Standardtid" ) );
38     c.add( standardtid );
39     c.add( klokketikk );
40     visTid();
41   }
42
43   public void visTid()
44   {
45     universaltid.setText( ur.visUniversaltid() );
46     standardtid.setText( ur.visStandardtid() );
47   }
48
49   public void actionPerformed( ActionEvent e )
50   {
51     if ( e.getSource() == time )
52     {
53       int t = Integer.parseInt( time.getText() );
54       ur.setTime( t );
55       time.setText( "" );
56       visTid();
57     }
58     else if ( e.getSource() == min )
59     {
60       int m = Integer.parseInt( min.getText() );
61       ur.setMinutt( m );
62       min.setText( "" );
63       visTid();
64     }
65     else if ( e.getSource() == sek )
66     {
67       int s = Integer.parseInt( sek.getText() );
68       ur.setSekund( s );
69       sek.setText( "" );
70       visTid();
71     }
72     else if ( e.getSource() == klokketikk )
73     {
74       ur.tikk();
75       visTid();
76     }
77   }
78 }

Sammensetning eller komposisjon: objekter som datafelter i klasser

Datafeltene i en klasse kan være av en hvilken som helst datatype. Svært ofte er de av en eller annen klassetype, det vil si at verdiene er objekter (eller egentlig pekere til objekter). Klassen som definerer vedkommende datatype kan enten være en som finnes i javas klassebibliotek eller det kan være en vi har definert selv. I klassen Klokke ovenfor har vi et datafelt av type Tid2, som vi har definert selv. I klassen Klokkevindu har vi et datafelt av type Klokke, som vi også har definert selv, og vi har datafelter av type JTextField og JButton. Disse typene er definert i klassebiblioteket.

Når vi skal skrive et program for noe, er det å definere datastruktur for de objektene som programmet skal behandle en viktig del av programmeringsarbeidet. En fornuftig datastruktur gjør det lettere å forstå programmet både for oss selv mens vi holder på å utvikle det, og for eventuelle andre som skal sette seg inn i det. Dessuten vil du komme til å oppleve at dersom du tar fram igjen et program noen uker eller måneder etter at du skrev det, så er det meste glemt. Da vil det gå mye lettere å sette seg inn i programmet igjen dersom det har en fornuftig datastruktur og det dessuten er brukt selvforklarende navn på variable, datatyper, etc. En fornuftig datastruktur kan dessuten gjøre programmet både mer effektivt og mer fleksibelt for seinere oppdateringer og endringer.

Programeksempel 4

Som et lite eksempel på hvordan vi kan designe en datastruktur, tar vi for oss oppgaven med å definere en datatype for en ansatt i en eller annen virksomhet. Det skal registreres navn og lønnstrinn, samt fødselsdato og dato for tiltredelse. (Et liknende eksempel finnes i læreboka Deitel & Deitel.) Når det gjelder lønn, har virksomheten et system med lønnstrinn, der hvert lønnstrinn tilsvarer en bestemt årslønn. Det er altså bare lønnstrinnet programmet skal registrere. Men det skal sikre at ingen har lavere lønnstrinn enn 30, og ingen har høyere lønnstrinn enn 80. Lønnstrinnet må kunne endres, men det skal ikke være tillatt å sette det ned til et lavere trinn.

Den mest nærliggende tanke er å definere en klasse Ansatt som har datafelter for navn og lønnstrinn, samt dag, måned og år for de nevnte dataoer. Men for å få mer fleksibilitet og muligheter for gjenbruk, kan det her være fornuftig å definere en egen datatype for dato. Den kan jo også komme til nytte i andre sammenhenger. En mulig datastruktur kan derfor skisseres slik:

class Dato
{
  private int dag, mnd, år;
  .
  .
  .
}

class Ansatt
{
  private String navn;
  private int lønnstrinn;
  private Dato født, tiltrådt;
  .
  .
  .
}

Klassene må ha hver sin konstruktør som foretar initialisering av datafeltene. Vi kan vel gå ut fra at det for en ansatt ikke er aktuelt å endre verken navn, fødselsdato eller tiltredelsesdato. Lønnstrinnet må derimot kunne endres innenfor de restriksjoner som er nevnt ovenfor. Til dette trenger vi en set-metode. For datoer skulle det ikke være behov for noen set-metoder, for dersom vi endrer en dato, er det ikke lenger samme datoen. Da er det mer naturlig å opprette et nytt Dato-objekt isteden. Ved opprettelsen av en dato bør vi legge inn test på at det blir registrert verdier som representerer en gyldig dato. Begge klassene må vi utstyre med et passende sett av metoder som kan brukes til å lese ut registrerte data, eventuelt formatert på forskjellige måter. Kode for de to klassene er gjengitt nedenfor og finnes i filene Dato.java og Ansatt.java. Dato-klassen er utstyrt med static-metoden okDato som gir en test på om gitte verdier for dag, måned og år representerer en gyldig dato. Denne metoden bør da brukes for å sjekke om verdiene kan være konstruktørparametre for et Dato-objekt. For ansatte blir lønnstrinnet initialisert til minimumsverdien. Etterpå kan det endres ved bruk av set-metoden.

 1 public class Dato
 2 {
 3   private int dag, måned, år;
 4
 5   public Dato( int d, int m, int å )
 6   {
 7     dag = d;
 8     måned = m;
 9     år = å;
10   }
11
12   public String toString()
13   {
14     return dag + ". " + månedsnavn( måned ) + " " + år;
15   }
16
17   public static boolean skuddår( int år )
18   {
19     if ( (år % 4 == 0) && (år % 100 != 0) || (år % 400 == 0) )
20       return true;
21     else
22       return false;
23   }
24
25
26   public static int månedsdager( int måned, int år )
27   {
28     int[] dager = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
29     int antDager = dager[ måned - 1 ];
30     if ( måned == 2 && skuddår( år ) )
31       antDager++;
32     return antDager;
33   }
34
35
36   public static boolean okDato( int dag, int mnd, int år )
37   {
38     if ( mnd < 1 || mnd > 12 )
39       return false;
40     else
41     {
42       int antdg = månedsdager( mnd, år );
43       if ( dag > 0 && dag <= antdg )
44         return true;
45       else
46         return false;
47     }
48   }
49
50   public static String månedsnavn( int mnd )
51   {
52     String[] navn = { "januar", "februar", "mars", "april", "mai",
53       "juni", "juli", "august", "september", "oktober", "november",
54       "desember" };
55     if ( mnd > 0 && mnd < 13 )
56       return navn[ mnd - 1 ];
57     else
58       return "ukjent måned";
59   }
60
61   public boolean sammeDato(Dato d)
62   {
63     return dag == d.dag && måned == d.måned && år == d.år;
64   }
65
66   public int getDag()
67   {
68     return dag;
69   }
70
71   public int getMåned()
72   {
73     return måned;
74   }
75
76   public int getÅr()
77   {
78     return år;
79   }
80 }



 1 public class Ansatt
 2 {
 3   private String navn;
 4   private Dato født, tiltrådt;
 5   private int lønnstrinn;
 6   private final int MIN = 30, MAX = 80;
 7
 8   public Ansatt( String n, Dato f, Dato t )
 9   {
10     navn = n;
11     født = f;
12     tiltrådt = t;
13     lønnstrinn = MIN;
14   }
15
16   public String getNavn()
17   {
18     return navn;
19   }
20
21   public int getLønnstrinn()
22   {
23     return lønnstrinn;
24   }
25
26   public void setLønnstrinn( int trinn )
27   {
28     if ( trinn > lønnstrinn && trinn <= MAX )
29       lønnstrinn = trinn;
30     else if ( trinn > MAX )
31       lønnstrinn = MAX;
32   }
33
34   public String toString()
35   {
36     return navn + ", født: " +
37           født.toString() + ", tiltrådt: " +
38           tiltrådt.toString();
39   }
40 }

For å lage et lite testprogram som bruker de to klassene våre, definerer vi en klasse Stab som inneholder en array av Ansatt-objekter. I Stab-klassen ville det vært naturlig å definere metoder for å administrere samlingen av Ansatt-objekter, på tilsvarende måte som vi gjorde i et tidligere program der klassen Personregister inneholdt en array av Person-objekter. Klassen Stab kunne da vært brukt i et interaktivt program som utvekslet data med en bruker. Men for å redusere størrelsen på eksemplet, lar vi her konstruktøren til Stab-klassen opprette Dato- og Ansatt-objekter som blir lagt inn i Ansatt-arrayen. Siden datoer blir hardkodet med gyldige verdier, er det ikke gjort bruk av testmetoden okDato, slik det burde gjøres i tilfelle dataene ble lest inn fra en bruker. Vi definerer en toString-metode som returnerer arrayens innhold.

 1 public class Stab
 2 {
 3   private Ansatt[] staben = new Ansatt[ 2 ];
 4
 5   public Stab()
 6   {
 7     String ansatt1 = "Synnøve Solbakken";
 8     Dato f1 = new Dato( 1, 1, 1990 );
 9     Dato t1 = new Dato( 20, 8, 2010 );
10     staben[ 0 ] = new Ansatt( ansatt1, f1, t1 );
11
12     String ansatt2 = "Sidsel Sidserk";
13     Dato f2 = new Dato( 17, 5, 1986 );
14     Dato t2 = new Dato( 1, 3, 2008 );
15     staben[ 1 ] = new Ansatt( ansatt2, f2, t2 );
16   }
17
18   public String toString()
19   {
20     String output = "";
21     for ( int i = 0; i < staben.length; i++ )
22       if ( staben[ i ] != null )
23         output += staben[ i ] + "\n";
24     return output;
25   }
26 }

Klassen Stabtest inneholder applikasjonens main-metode. Den oppretter et Stab-objekt og viser dets innhold i en dialogboks.

 1 import javax.swing.*;
 2
 3 public class Stabtest
 4 {
 5   public static void main( String[] args )
 6   {
 7     Stab s = new Stab();
 8     JOptionPane.showMessageDialog( null, "Ansatte\n" + s.toString() );
 9     System.exit( 0 );
10   }
11 }

Nedenfor er vist den dialogboksen som kommer opp når programmet kjøres.

Programeksempel 5

Som et annet eksempel på objekter som datafelter i klasser, samt også eksempel på gjenbruk av programkode, skal vi nå ta for oss en modifisert utgave av personregisterprogrammet i Programeksempel 1 tidligere i dette kapitlet. Istedenfor å registrere navn og personnummer for personene, skal vi registrere navn og fødselsdato. Som datatype for datoregistreringen skal vi bruke vår klasse Dato fra Programeksempel 4. For ikke å blande sammen den nye datatypen for personer med den vi brukte tidligere, blir den nye klassen kalt Person2. Kildekoden finnes i fila Person2.java. Den har følgende innhold:

 1 public class Person2
 2 {
 3   private String navn;
 4   private Dato født;
 5
 6   //Konstruktør
 7   public Person2( String n, Dato f )
 8   {
 9     navn = n;
10     født = f;
11   }
12
13   public String getNavn()
14   {
15     return navn;
16   }
17
18   public Dato getDato()
19   {
20     return født;
21   }
22
23   public String toString()
24   {
25     return "Navn: " + navn + "\tFødt: " + født;
26   }
27 }

Legg her merke til at i toString-metoden vil det bli automatisk kall på Dato-klassens toString-metode. For øvrig er det selvsagt slik at når vi endrer navnet til en klasse, må vi også endre navnet på fila som klassen ligger i (forutsatt at klassen er public), samt endre navn på klassens konstruktør(er).

I klassen som definerer personregisteret, må vi gjøre to endringer: arrayen skal nå inneholde Person2-objekter istedenfor Person-objekter. Metoden visPerson skal returnere data om en person på grunnlag av personens fødselsdato, istedenfor på grunnlag av personnummeret. Siden det kan være flere personer som har samme fødselsdato, har vi valgt å skrive metoden slik at den returnerer data om alle registrerte personer med samme fødselsdato.

For variasjonens skyld, skal vi også gjøre et par endringer til. Istedenfor å ha en tellevariabel for antall personer som er lagt inn i registeret, skal vi legge inn nye personer på første ledige plass i arrayen. Vi skal dessuten tilføye en operasjon for å fjerne en gitt person fra registeret, slik at det kan oppstå ledige plasser i arrayen innimellom de som er opptatt. Første ledige plass ved innsetting kan derfor være innimellom plasser som er opptatt.

Når det gjelder operasjon for å fjerne en person, så skal vi i dette programmet gå ut fra at en person blir entydig identifisert ut fra sitt navn og sin fødselsdato. Iallfall så programmerer vi fjerningsoperasjonen slik at den fjerner første forekomst av person med mottatt navn og fødselsdato. Programmet skal gi tilbakemelding til brukeren på om person ble fjernet eller ikke.

Fjerningsoperasjonen programmerer vi på den måten at vi i personregisterklassen legger inn en metode fjernPerson(String navn, Dato d) som prøver å fjerne første forekomst av person med mottatt navn og dato. Metoden returnerer true eller false som tilbakemelding på om fjerning ble foretatt eller ikke. I vindusklassen må vi ha en tilsvarende metode som leser inn navn og dato, og som kaller opp metoden i personregisterklassen. For denne metoden må det tilordnes en ekstra knapp i vinduet, og knappen må tilordnes lytteobjekt.

På grunn av disse endringene, har vi valgt å skifte navn også på denne personregisterklassen, til Personregister2. Det nye innholdet finnes i fila Personregister2.java. Det er gjengitt nedenfor.

 1 public class Personregister2
 2 {
 3   private Person2[] register;
 4   public static final int KAPASITET = 10;

 5
 6   public Personregister2()
 7   {
 8     register = new Person2[KAPASITET];
 9   }
10
11   //Setter inn ny person i registeret i tilfelle det er plass.
12   public boolean nyPerson(Person2 p)
13   {
14     for (int i = 0; i < register.length; i++)
15     {
16       if (register[i] == null)
17       {
18         register[i] = p;
19         return true;
20       }
21     }
22     return false;
23   }
24
25   //Returnerer data om personer med gitt fødselsdato.
26   public String visPerson(Dato d)
27   {
28     String personer = "";
29     for (int i = 0; i < register.length; i++)
30       if (register[i] != null && d.sammeDato(register[i].getDato()))
31         personer += register[i].toString() + "\n";
32     if (personer.length() > 0)
33       return personer;
34     else
35       return null;  //det var ingen person med denne fødselsdato
36   }
37
38   //Sletter første forekomst av person med mottatt navn
39   //og fødselsdato. Returverdi indikerer om fjerning ble foretatt.
40   public boolean fjernPerson(String navn, Dato d)
41   {
42     for (int i = 0; i < register.length; i++)
43     {
44       Person2 p = register[i];
45       if (p != null)
46       {
47         if (d.sammeDato(p.getDato()) && navn.equals(p.getNavn()))
48         {
49           register[i] = null;
50           return true;
51         }
52       }
53     }
54     return false;
55   }
56
57   //Returnerer navneliste over alle registrerte personer.
58   public String visAlleNavn()
59   {
60     String navneliste = "";
61     for (int i = 0; i < register.length; i++)
62       if (register[i] != null)
63         navneliste += register[i] + "\n";
64     return navneliste;
65   }
66 }

I vindusklassen må datafeltet av type Personregister byttes til å være av type Personregister2. Dessuten må et par tekster i vinduet endres til å passe med at det nå registreres fødselsdato istedenfor personnummer. Ved innlesing av datoer bør det sjekkes at datoen er gyldig. Funksjonalitet for innlesing av gyldige datoer og opprettelse av Person-objekter ligger i metodene lesDato og nyPerson som er gjengitt nedenfor. Videre må det tilføyes funksjonalitet for fjerningsoperasjonen, som allerede nevnt ovenfor.

For å markere at det er et nytt program, kaller vi nå klassen Persondata2. Programmets tilhørende driverklasse med main-metode må da selvsagt endres i samsvar med dette. Den nye vindusklassen finnes i fila Persondata2.java og den nye driverklassen finnes i fila Personarkiv2.java

 41   public Dato lesDato()
 42   {
 43     String nr = fnrfelt.getText() ;
 44     int dag = Integer.parseInt(nr.substring(0,2));
 45     int mnd = Integer.parseInt(nr.substring(2,4));
 46     int år = Integer.parseInt(nr.substring(4,8));
 47     if (Dato.okDato(dag, mnd, år))
 48       return new Dato(dag, mnd, år);
 49     else
 50       return null;
 51   }
 52
 53   public void nyPerson()
 54   {
 55     String navn = navnefelt.getText();
 56     Dato f = lesDato();
 57     if ( f == null)
 58     {
 59       utskrift.setText("Ugyldig dato!" +
 60           "\nIngen registrering foretatt.");
 61       return;
 62     }
 63     Person2 ny = new Person2( navn, f );
 64     boolean ok = register.nyPerson( ny );
 65     if ( ok )
 66       utskrift.setText(navn + " ble registrert.");
 67     else
 68       utskrift.setText("Ingen registrering pga. fullt register!");
 69   }

Det nye vinduet er vist nedenfor.

Modifikatoren static: klassevariable og -metoder

Dersom vi bruker modifikatoren static på et datafelt eller en metode i en klasse, innebærer det at datafeltet eller metoden vil tilhøre selve klassen istedenfor å bli knyttet til instanser (objekter) av klassen. Dessuten vil de eksistere uavhengig av om det er opprettet noe objekt av klassen eller ikke. På grunn av dette er det også slik at en static metode bare kan gjøre aksess på datafelter som er static. I tillegg kan den selvsagt ha sine egne lokale variable.

En ikke-static metode kan aksessere både static og ikke-static datafelter.

For å gjøre kall på en static metode utenfor klassen der den er definert, prefikser vi med klassenavnet.

Eksempler

I klassebiblioteket er det mange static-metoder som vi tidligere har gjort kall på, slik som

  Integer.parseInt( ... );
  Double.parseDouble( ... );
  JOptionPane.showMessageDialog( ... );
  Math.random();

Det er imidlertid også mulig å kalle opp en static metode ved hjelp av et klasseobjekt. Men for å markere at det dreier seg om en static metode, bør vi heller prefikse med klassenavnet. Metoder som er private og static er det lite aktuelt å ha. De kan bare aksesseres innenfor klassen, og siden de tilhører klassen selv og ikke noe bestemt objekt, betyr det at deres oppgave er å være hjelpemetoder for andre static-metoder. Det kan vel forekomme at vi har slike, men sannsynligvis ikke så ofte.

Når det gjelder static datafelter, er aksessmuligheten avhengig av om de er public eller private. Datafelter som er public og static, kan utenfor klassen, slik som metoder, aksesseres ved å prefikse med klassenavnet. Dette er mest aktuelt for konstanter, det vil si datafelter som i tillegg er final.

Eksempel

  Math.PI
  JOptionPane.PLAIN_MESSAGE

Datafelter som er private og static, kan utenfor klassen bare aksesseres av klassens public metoder, uavhengig av om disse er static eller ikke.

Hensikten med static klassemedlemmer

Nøkkelordet static bruker vi på slikt som skal være felles for alle klasseobjekter, eller på noe som alle klasseobjekter skal ha tilgang til og som ikke er knyttet til noe bestemt objekt.

Eksempel: Automatisk generering av medlemsnumre

Vi tenker oss at vi skal definere en datatype for medlemmer i en tenkt forening (musikkorps, fotballag, rideklubb eller liknende). Vi ønsker at medlemmene automatisk skal tildeles medlemsnummer fra 1 og oppover etter hvert som de blir registrert. For å få til dette, kan vi programmere slik:

class Medlem
{
  private String navn, adresse;
  private int medlemsnummer;
  private static int nestenummer = 1;  //startverdi

  //konstruktør:
  public Medlem( String n, String a )
  {
    navn = n;
    adresse = a;
    medlemsnummer = nestenummer++;
  }

  //her følger resten av klasseinnholdet
}

For å illustrere virkningen, skriver vi nå:

class Test
{
  Medlem første = new Medlem( "Siri", "Oslo" ); //får medlemsnummer lik 1
  Medlem andre = new Medlem( "Tone", "Bærum" ); //får medlemsnummer lik 2
}

Poenget er her at datafeltet nestenummer tilhører selve klassedefinisjonen og har en verdi som er tilgjengelig for alle objektene som vi oppretter. Hver gang vi oppretter et nytt objekt, brukes nåværende verdi som medlemsnummer for det nye objektet. Deretter økes nåværende verdi med 1.

static importering

Fra tidligere kjenner vi til hvordan vi til en java-fil kan importere klasser fra en pakke. Navngitte klasser importerer vi ved at vi skriver klassenavnet sammen med pakken den ligger i, slik som for eksempel

import javax.swing.JOptionPane;

For å importere alle klasser vi måtte behøve fra en pakke skriver vi en stjerne istedenfor et konkret klassenavn, som i eksemplet

import java.awt.*;

Kompilatoren vil da plukke ut de klassene den behøver fra vedkommende pakke. Vi risikerer ikke å importere masse klasser som vi ikke har bruk for.

import-instruksjoner må stå forrest i fila. Hensikten med importeringen er at vi da i vedkommende java-fil kan referere direkte til klasser vi har importert, slik at vi slipper å prefikse med pakkenavn når vi skal bruke dem i programmet. Med de import-instruksjonene som står ovenfor, kan vi for eksempel skrive

  JOptionPane.showInputDialog("Fornavn");

istedenfor

  javax.swing.JOptionPane.showInputDialog("Fornavn");

Og vi kan skrive

  Container c = getContentPane();

istedenfor

  java.awt.Container c = getContentPane();

På tilsvarende måte er det mulig å importere static klassemedlemmer fra en klasse, enten navngitte slike, eller alle vi måtte ha bruk for fra en klasse. Også slike import-instruksjoner må stå forrest i java-filer. Skriver vi for eksempel

import static java.lang.Integer.MAX_VALUE;

så kan vi i fila skrive

  int minimum = MAX_VALUE;

istedenfor

  int minimum = Integer.MAX_VALUE;

Skriver vi for eksempel

import static java.lang.Math.*;

så kan vi i fila skrive for eksempel PI istedenfor Math.PI og sqrt( 17.34 ) istedenfor Math.sqrt( 17.34 ).

Pakker

Klasser (og interface'er) som logisk hører sammen, uten at de nødvendigvis tilhører samme klassehierarki, kan det være ønskelig å gruppere sammen, slik at de til sammen utgjør en slags enhet. Dette kan vi få til ved å definere dem til å tilhøre samme pakke, engelsk: package, som også er et av nøkkelordene i java. Som eksempler på dette har vi klassene i javas klassebibliotek. De er gruppert sammen til pakker: java.lang, java.awt, java.awt.event, javax.swing, og så videre. En fullstendig oversikt finnes på adressen http://download.oracle.com/javase/8/docs/api/. Vi vet at når vi i et program skal bruke noen av klassene som ligger i en pakke, så må vi importere dem ved å skrive en import-instruksjon.

En pakke svarer til en filkatalog (filmappe) eller et hierarki av filkataloger, avhengig av om pakkens navn består av ett eller flere ledd.

Når vi oppretter programmer på den måten vi hittil har gjort i TextPad, så vil de av javasystemet bli lagt inn i en såkalt navnløs pakke. Bruk av den navnløse pakken har vært stort sett greit hittil. Men etter hvert vil den inneholde svært mange filer og kan bli uoversiktlig. Vi vil oppleve behov for å gruppere filene våre på en eller annen måte. Bruk av pakker er en grei måte å gjøre det på. For pakkenavn er det vanlig å bruke bare små bokstaver. Se for øvrig anbefalinger om pakkenavn nedenfor. Vi skal ta for oss et konkret eksempel etter at vi har sett på noen generelle regler og anbefalinger som gjelder for pakker.

Regler og anbefalinger for pakker

Filene som tilhører en pakke må plasseres i en egen filkatalog (filmappe) med samme navn som pakken, i den forstand at dersom pakkenavnet består av flere ledd, så må leddene svare til en filsti. Dersom for eksempel pakkenavnet er prog.minpakke, og en fil Minfil.java skal tilhøre denne pakken, så må denne fila ligge på en filsti som ender opp med /prog/minpakke/Minfil.java.

Aller først i hver av filene som skal tilhøre pakken, må vi skrive

package prog.minpakke;  //dvs. navnet du vil ha på pakken
//eventuelle import-instruksjoner må skrives etterpå

public class ....
.
.
.

Dersom vi ikke skriver public foran class, kan klassen bare brukes av andre klasser i den samme pakken.

Før en pakke kan tas i bruk, må filene i den kompileres! Når vi bruker TextPad, vil klassefilene da havne i samme filkatalog som kildefilene ligger i, og som altså må ha en filsti som er i samsvar med pakkenavnet.

Programeksempel 6

Vi skal omarbeide programmet i Programeksempel 4 på den måten at de to klassene Dato og Ansatt legges i en pakke personale som blir importert til det programmet som skal bruke klassene. Forrest i javafilene for disse klassene må vi da skrive pakkedeklarasjon:

package personale;

public class Dato
{
  < Klassens innhold som før. >
}
package personale;

public class Ansatt
{
  < Klassens innhold som før. >
}

Det er viktig at vi lagrer filene på riktig sted! De må ligge i en underkatalog med navn personale i forhold til det programmet som skal importere og bruke klassene! Når vi har plassert klassene med tilhørende pakkedeklarasjon i riktig filkatalog, må vi huske på å kompilere klassene. Da er de ferdig til bruk. I TextPad virker det på den måten at når vi refererer til klassene, som vi gjør i klassen Stab nedenfor, og så kompilerer denne, så vil også de to klassene i pakken personale automatisk bli kompilert på riktig måte.

Filene for klassene Stab og Stabtest kan vi nå skrive på følgende måte:

import personale.*;

public class Stab
{
  < Klassens innhold som før. >
}

I driverklassen Stabtest trenger vi ikke gjøre noen endringer:

import javax.swing.*;

public class Stabtest
{
  public static void main( String[] args )
  {
    Stab s = new Stab();
    JOptionPane.showMessageDialog( null, "Ansatte\n" + s.toString() );
  }
}

Merknad om pakkenavn

Når et program skal bruke klasser fra flere forskjellige pakker, ved å importere dem, er det av stor betydning å sikre entydighet ved at klasser og pakker har unike navn. For å oppnå dette, er det innført noen anbefalinger for pakkenavn:

Første del av navnet: domene-navn på internett i motsatt rekkefølge. Dersom for eksempel domene-navnet er iu.hio.no, så er første del av pakkenavnet anbefalt å være no.hio.iu. Deretter står en fritt, men som alltid ellers bør en selvsagt bruke selvforklarende navn.

Aksessibilitet

Klasser, datafelter, metoder etc. kan tildeles fire forskjellige nivåer av aksess utenfra:

Generelt gjelder selvsagt at dersom et datafelt eller en metode skal aksesseres utenfor klassen der datafeltet eller metoden er deklarert, så må det gjøres ved å bruke et objekt av vedkommende klassetype, eller klassenavnet, dersom det dreier seg om noe som er static.

Hvilket nivå av aksess som skal velges, er avhengig av behov for beskyttelse og av hvilken rolle vedkommende komponent skal ha i et program. Generelt bør datafelter ha private aksess; i enkelte tilfeller protected eller pakkeaksess, men svært sjelden public aksess (unntatt konstanter).

MVC

Bokstavene mvc er forkortelse for de tre engelske ordene model-view-controller. Ordkombinasjonen betegner en bestemt arkitektur for oppbygging av programkomponenter og programmer. Kort sagt går den ut på å skille datalagring og behandling av data fra visuell representasjon av data, visning av data og brukerkommunikasjon.

I MVC-arkitekturen er det modellen som utgjør det vi kan kalle programmets kjerne. Det er den som lagrer programmets data og inneholder det som gjerne blir kalt programlogikken, det vil si foretar den behandlingen av data som dreier seg om mer enn å sende data fra ett sted til et annet. Visningsdelen tar for seg output; den gir et visuelt bilde av modellens tilstand. Controlleren er input-delen; den formidler kommandoer fra brukeren til modellen. Som en følge av en slik formidling kan det være behov for at modellen signaliserer til visningen at den må oppdatere seg for å gjenspeile modellens nye tilstand.

Fordelen med et slikt opplegg er først og fremst at vi oppnår større fleksibilitet og bedre muligheter for gjenbruk av programkode. Data som lagres av samme modell kan for eksempel vises på flere forskjellige måter. Vi får dessuten programmer som blir mer oversiktlige og lettere å vedlikeholde. Vi kan for eksempel gjøre endringer i visningen uten at modellen blir berørt, eller omvendt.

I de fleste programeksemplene vi har tatt for oss etter at vi begynte å lage vindusbaserte programmer, har vi brukt varianter av denne arkitekturen. Ser vi tilbake på Programeksempel 1 foran i kapitlet, så har vi der klassen Personregister som utgjør modellen. Vindusklassen Persondata er en kombinert visning og controller, siden klassen også tjener som lytteobjekt for bruker-input. Klassen kommuniserer med modellen.

Programeksempel 2 inneholder ingen brukerkommunikasjon. I Programeksempel 3 er det klassene Tid2 og Klokke som til sammen utgjør modellen, mens Klokkevindu er kombinert visning og kontroller.

I Programeksempel 4 er det heller ingen brukerkommunikasjon, men det har vi i Programeksempel 5 der det er klassene Person2 og Personregister2 som definerer modellen, mens Persondata2 er kombinert visning og controller.

Felles for alle disse eksemplene er at vi har hatt en egen klasse som har definert selve programvinduet for visning av data og brukerkommunikasjon, mens det har vært andre klasser som har tatt for seg lagring av data og behandling av data. Vindusklassen har kommunisert med modellen. Siden programmer som er bygget opp etter MVC-arkitekturen har en del åpenbare fordeler, som nevnt ovenfor, er det sterkt å anbefale at arkitekturen blir brukt i alle tilfelle der det ligger til rette for det. En annen fordel med dette er at det bidrar til bedre kommunikasjon programmerere imellom; andre som leser programkoden vår vil forvente seg en oppbygging av programmet etter denne arkitekturen, og når vi selv skal sette oss inn i programmer som andre har laget, så vil vi også forvente oss en oppbygging etter denne arkitekturen. Dermed blir tid spart og risikoen for feil og misforståelser blir redusert.

MVC-arkitekturen er konsekvent gjennomført i programmeringen av Javas grafiske vinduskomponenter, de såkalte swing-komponentene. For de enkle komponentene som vi har brukt hittil og som blir brukt videre i kompendiet Introduksjon til programmering, trenger vi ikke å ha kjennskap til dette for å kunne bruke komponentene. Men for mer avanserte komponenter er dette nødvendig. Slike komponenter blir til dels brukt i kompendiet Kompendium for kurset Programutvikling som er en videreføring av dette. I notatet MVC-arkitektur er det en mer utfyllende beskrivelse av MVC-arkitekturen med henvisning til Javas swing-komponenter.

Noen generelle anbefalinger om klassedesign

Hva som vil være et fornuftig opplegg for definisjon av klasser for objekter som et program skal behandle, vil variere fra tilfelle til tilfelle. Men det finnes noen anbefalinger som stort sett bør følges for å være i samsvar med det som regnes som god objektorientert programmering. De følgende rådene, i litt forkortet form, er hentet fra boka Core Java av Cay S. Horstmann og Gary Cornell.

  1. La data alltid ha private aksess. I tillegg til den beskyttelsen det sikrer, gir det oss muligheten til å endre hvordan dataene blir representert, uten at det får konsekvenser for dem som skal bruke klassen vår. (De må bruke klassens public metoder.) Dessuten vil det bli lettere å oppdage programfeil.
  2. Ha for vane å initialisere data. Lokale variable blir ikke automatisk initialisert. Det blir datafelter, men det er ikke alltid at default-verdiene passer til situasjonen.
  3. Bruk ikke for mange datafelter av grunnleggende datatype. Eksempel: Programeksempel 4 ovenfor der vi istedenfor de tre int-datafeltene dag, mnd, år har innført en egen klasse Dato. Dette gir større fleksibilitet og klarhet.
  4. Det er ikke nødvendig å lage get- og set-metoder for alle datafeltene. Vurder hvilke datafelter det kan være behov for å endre.
  5. Bruk en standardform for klasseoppsett. Med dette menes at du bør være konsekvent med å liste opp klasseinnhold (konstanter, datafelter, metoder av forskjellig aksesstype, ...) i en bestemt rekkefølge. Dette gjør det enklere å finne fram i klassene.
  6. La ikke en klasse ha for mange oppgaver som den har "ansvar for". Del heller opp i flere klasser. Hva klassen har "ansvar for" gjenspeiler seg i hvor mange metoder den har. Hva som er fornuftig oppdeling er imidlertid sterkt avhengig av situasjonen.
  7. Bruk selvforklarende navn både på klasser og metoder.

Forrige kapittel Neste kapittel

Copyright © Kjetil Grønning og Eva Hadler Vihovde, revidert 2014