this
set
- og get
-metoderstatic
: klassevariable og -metoderstatic
klassemedlemmerstatic
importeringEn 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.
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:
private
innebærer at det bare er inni klassen det
er tillatt å referere til datafeltene. På den måten blir de beskyttet mot
det som kalles uautorisert bruk utenfra.get
-metoder som returnerer deres verdier.Person
-objekter.toString
har som standardoppgave å returnere en
tekststreng som inneholder de relevante opplysningene om et objekt av
vedkommende type. Det er vanlig å definere en slik metode for de fleste
klasser.void
), slik
det kreves for metoder. Konstruktørens oppgave er å foreta initialisering.
Den inneholder derfor (vanligvis) parametre med initialverdier til (noen
eller alle av) klassens datafelter.
Instruksjonene i konstruktøren blir utført hver gang vi oppretter et nytt
objekt av vedkommende klassetype ved å bruke new
-operatoren.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()
.
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.
De fleste programeksemplene i dette kapitlet forutsetter at du har tilegnet deg stoffet i kapitlet Vindusbaserte programmer.
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 Personregister
et 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
.
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
.
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
.
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.
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 }
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.
set
- og get
-metoderDet 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.
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 }
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.
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.
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.
static
: klassevariable og -metoderDersom 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.
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
.
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.
static
klassemedlemmerNø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.
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
importeringFra 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 )
.
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.
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.
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() ); } }
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.
Klasser, datafelter, metoder etc. kan tildeles fire forskjellige nivåer av aksess utenfra:
public
aksess: tilgjengelig overalt.protected
aksess: tilgjengelig i klassen selv og i alle
subklasser til denne, samt i alle klasser som tilhører samme pakke.private
aksess: bare tilgjengelig innenfor klassen selv.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).
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.
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.
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.int
-datafeltene
dag, mnd, år
har innført en egen klasse Dato
.
Dette gir større fleksibilitet og klarhet.get
- og set
-metoder
for alle datafeltene. Vurder hvilke datafelter det kan være behov for å endre.Copyright © Kjetil Grønning og Eva Hadler Vihovde, revidert 2014