Tilstand for oppførselens skyld

Et objekt har både tilstand (attributter) og oppførsel (metoder), og vekten på det ene eller andre aspektet kan variere. Noen ganger er fokuset på oppførsel, hvor tilstanden primært er utformet for å støtte implementasjon av oppførselen. Denne siden presenterer tre kode-eksempler som er ment å illustrere dette.

MinValueHelper - ønsket oppførsel

MinValueHelper-objekter skal kunne ta i mot ett og ett (desimal)tall og hele tiden kunne svare på hva som er hittil minste tall. Nye tall gis inn ved å kalle nextNumber(double) og minste tall kan hentes ut med metoden getMinValue(). Dette er en beskrivelse av oppførselen til objektet, siden det bare er metodene som er nevnt. Det er underforstått at objektene må ha en tilstand, men det er et åpent spørsmål hvilken tilstand som trengs for å støtte implementasjonen av ønsket oppførsel.

MinValueHelper - hvilken tilstand trengs?

For å holde rede på hittil minste tall, trenger en bare å huske nettopp det: hittil minste tall. En trenger ikke huske alle tallene. Det hittil minste tallet kan f.eks. lagres i en variabel vi kaller minValue. (Det er en konvensjon i Java, at dersom en metode heter getEtEllerAnnet og en skal lagre dette EtEllerAnnet, så kaller man variabelen etEllerAnnet.) Hver gang nextNumber kalles, så sjekker vi om det nye tallet er mindre enn hittil minste tall, altså minValue, og i tilfelle så setter vi minValue til det nye tallet.

Her er først versjon av koden (derfor kaller vi den MinValueHelper1):

package stateandbehavior;

public class MinValueHelper1 {

    double minValue;
    
    void nextNumber(double value) {
        if (value < minValue) {
            minValue = value;
        }
    }

    double getMinValue() {
        return minValue;
    }

    public static void main(String[] args) {
        MinValueHelper1 minValueHelper = new MinValueHelper1();
        System.out.println(minValueHelper.getMinValue());
        minValueHelper.nextNumber(3);
        System.out.println(minValueHelper.getMinValue());
        minValueHelper.nextNumber(5);
        System.out.println(minValueHelper.getMinValue());
        minValueHelper.nextNumber(1);
        System.out.println(minValueHelper.getMinValue());
    }
}

Nederst har vi lagt inn en main-metode som kan brukes for å teste MinValueHelper1-klassen. (Denne main-metoden kan forsåvidt ligge i en annen klasse, men det er mest praktisk å legge den her, selv om det egentlig ikke er meningen at denne klassen skal være et kjørbart program i vanlig forstand.). Kjør klassen (altså main-metoden) og se hva som skjer!

Du vil fort oppdage at programmet ikke virker etter hensikten, fordi getMinValue() hele tiden returnerer 0! Hvordan kan det ha seg? Svaret er at minValue automatisk blir initialisert til 0.0, som er default-verdien for double-typen. (default-verdien er den verdien som gis til variabler som ikke bli initialisert når de deklareres, og er false, 0, 0.0 og null for henholdsvis boolean, int, double og objekt-typer). Dermed er denne hele tiden minst, og minValue blir aldri endret.

Vi kan håndtere dette på flere måter, f.eks.

  • Vi kan velge en startverdi for minValue som er størst mulig, slik at første tall er nødt til å bli notert som nytt minste tall. For double-typen er dette Double.MAX_VALUE, så deklarasjonen blir da double minValue = Double.MAX_VALUE.
  • Vi kan velge en spesiell startverdi som tegn på at minValue egentlig ikke er satt ennå. Men i motsetning til objekt-typer som har null-verdien, finnes det ingen gyldig tall-verdi som betyr "ingen verdi", så dette er en dårlig løsning.
  • Vi kan introdusere en egen variabel som sier om minValue er eksplisitt satt. Denne variablen vil starte som false og så settes til true, første gang nextNumber kalles.

Her velger vi den siste løsningen (selv om den første er enklest), fordi den illustrerer bruken av logiske verdier. Her er koden:

package stateandbehavior;

class MinValueHelper2 {

    double minValue;
    boolean isMinValueSet = false;
    
    void nextNumber(double value) {
        if (! isMinValueSet) {
            minValue = value;
            isMinValueSet = true;
        } else if (value < minValue) {
            minValue = value;
        }
    }

    double getMinValue() {
        return minValue;
    }
    
    public static void main(String[] args) {
        MinValueHelper2 minValueHelper = new MinValueHelper2();
        System.out.println(minValueHelper);
        minValueHelper.nextNumber(3);
        System.out.println(minValueHelper);
        minValueHelper.nextNumber(5);
        System.out.println(minValueHelper);
        minValueHelper.nextNumber(1);
        System.out.println(minValueHelper);
    }
}

Kjør testen og se at det fungerer bedre nå! Det er egentlig fortsatt et aspekt vi burde se på, nemlig om getMinValue() skal håndtere tilfellet hvor isMinValueSet er false og returnere en egen verdi da, f.eks. Double.MAX_VALUE eller utløst unntak. Hva synes du?

AverageHelper - ønsket oppførsel

AverageHelper-objekter skal som MinValueHelper-objekter, kunne ta i mot ett og ett (desimal)tall. Spørsmålet vi ønsker svar på denne ganger er gjennomsnittet av alle tallene som hittil er gitt inn med nextNumber-metoden, og til det har vi metoden getAverage(). Gjennomsnittet er som kjent summen av alle tallene delt på antall tall. Dersom ingen tall er gitt inn, så sier vi at getAverage() skal returnere 0.0.

AverageHelper - hvilken tilstand trengs?

I dette tilfellet er det ikke naturlig å lagre tallet vi ønsker å spørre om direkte, altså ha en average-variabel. I stedet lagrer vi den løpende summen og antall tall som hittil er gitt inn. Gjennomsnittet regner vi ut hver gang det spørres om.

Her er koden:

package stateandbehavior;

class AverageHelper {

    double currentSum = 0.0;
    int numberCount = 0;

    @Override
    public String toString() {
        return "AverageHelper [currentSum=" + currentSum
                + ", numberCount=" + numberCount + ", getAverage()=" + getAverage() + "]";
    }

    void nextNumber(double value) {
        currentSum = currentSum + value;
        numberCount = numberCount + 1;
    }

    double getAverage() {
        if (numberCount == 0) {
            return 0.0;
        }
        return currentSum / numberCount;
    }
    
    public static void main(String[] args) {
        AverageHelper averageHelper = new AverageHelper();
        System.out.println(averageHelper);
        averageHelper.nextNumber(3);
        System.out.println(averageHelper);
        averageHelper.nextNumber(5);
        System.out.println(averageHelper);
        averageHelper.nextNumber(1);
        System.out.println(averageHelper);
    }
}

nextNumber-koden er enklere, siden vi ikke trenger noen test på om det er første gang den er kalt. getAverage-metoden må imidlertid ha en test for å unngå å dele på 0.

Forøvrig har vi lagt til en viktig metode, nemlig toString() og endret litt på main-metoden som brukes til testing. toString() er en forhåndsdefinert metode (derfor står det @Override over, siden vi egentlig redefinerer den) som brukes når vi ønsker å gjøre om (innholdet i) et objekt til en String, f.eks. for utskrift ved testing. Vi kan bruke den selv, men som regel brukes den implisitt når vi som her kaller System.out.println eller brukes + med en String-verdi til venstre og et objekt til høyre. Les mer om toString() her: toString()-metoden.

toString()-metoden brukes vanligvis til å vise frem vesentlig intern tilstand, og her har vi valgt å legge inn verdiene til de to variablene og resultatet av å kalle getAverage(). Når vi i main-metoden kaller System.out.println(averageHelper) så vil toString()-metoden bli kalt og vi får full innsikt i hvordan både tilstanden og svaret fra getAverage() endres.

Tips: Hvis du synes det er kjedelig å skrive en toString()-metode, så bruk Eclipse sin funksjon for å skrive den for deg! Velg Source->Generate toString...-funksjonen fra hovedmenyen, kryss av for variablene du ønsker å har med i String-en og Eclipse gjør resten.

 

AverageWithoutDuplicatesHelper - ønsket oppførsel

AverageWithoutDuplicatesHelper-objekter er som AverageHelper-objekter, med én viktig forskjell: Tall som allerede er gitt inn, skal ikke regnes med, verken i summen eller antallet. Poenget med dette kravet er å illustrere hvordan en liten endring i kravene til oppførsel kan gjøre problemet vesentlig vanskeligere.

AverageHelper - hvilken tilstand trengs?

Vi ser fort at vi her må holde rede på alle tallene som allerede er gitt inn, og da må vi finne en egnet type for en ny variabel. Vi trenger en (forhåndsdefinert) klasse for å håndtere samlinger av tall, med metoder for å 1) sjekke om et tall allerede finnes i samlingen og 2) legge til et nytt tall. Java har flere klasser som duger, bl.a. ArrayList og HashSet:

  • ArrayList er en klasse for generelle lister, som holder rede på rekkefølge og indeks for en samling elementer. Vi trenger ikke disse egenskapene her, kun å kunne slå opp og evt. legge til.
  • HashSet er en klasse for å holde rede på matematiske mengder, med fokus på å kunne slå opp og legge til elementer spesielt effektivt. Dette er egentlig akkurat det vi trenger (selv om vi ikke nødvendigvis er opptatt av effektivitet akkurat nå).

Vi innfører en ny variabel kalt uniqueValues av typen HashSet og initialiserer den til et tomt sett: HashSet<Double> uniqueValues = new HashSet<Double>(); Notasjonen HashSet<Double> angir at vi ønsker et HashSet som bare skal inneholder Double-objekter. Dermed vil Java kunne sjekke at vi ikke legger inn feil type verdier. Vi er forøvrig nødt til å bruke Double og ikke double, fordi slike generelle klasser bare håndterer objekt-typer og Double er objekt-typen til verdi-typen double. (Ved bruk av ArrayList måte vi gjort tilsvarende, nemlig brukt ArrayList<Double>.) Les mer om det her: Collection-rammeverket.

Det nye i koden forøvrig er at vi i nextNumber-metoden kun endrer summen og antall tall hvis vi ikke har registrert dette tallet før. Dette sjekkes med HashSet sin contains-metode. Så passer vi på å legge det nye tallet til tall-mengden vår:

package stateandbehavior;

import java.util.HashSet;

public class AverageWithoutDuplicatesHelper {

    double currentSum = 0.0;
    int numberCount = 0;
    HashSet<Double> uniqueValues = new HashSet<Double>();

    @Override
    public String toString() {
        return "AverageWithoutDuplicatesHelper [currentSum=" + currentSum
                + ", numberCount=" + numberCount + ", getAverage()=" + getAverage() + "]";
    }

    void nextNumber(double value) {
        if (! uniqueValues.contains(value)) {
            currentSum = currentSum + value;
            numberCount = numberCount + 1;
            uniqueValues.add(value);
        }
    }
    double getAverage() {
        if (numberCount == 0) {
            return 0.0;
        }
        return currentSum / numberCount;
    }
    
    public static void main(String[] args) {
        AverageWithoutDuplicatesHelper averageHelper = new AverageWithoutDuplicatesHelper();
        System.out.println(averageHelper);
        averageHelper.nextNumber(3);
        System.out.println(averageHelper);
        averageHelper.nextNumber(5);
        System.out.println(averageHelper);
        averageHelper.nextNumber(1);
        System.out.println(averageHelper);
    }
}