Statiske metoder og variabler


Kodeordet static

Kodeordet static innebærer at en feltvariabel eller en metode ikke tilhører objektene i klassen, men i stedet tilhører selve klassen. Denne forskjellen kan fremstå som relativt subtil i begynnelsen, men den har viktige konsekvenser for hva man har tilgang til fra hvor.

TL;DR:

  • Unngå alltid å bruke static for feltvariabler (med noen få unntak, f. eks. konstanter).
  • Bruk gjerne static for metoder hvor all relevant input kommer i form av argumenter, og det ikke er behov for å ha tilgang til instansvariabler.

Det er veldig fort gjort å skrive dårlig kode med lav modularitet som er et mareritt å teste og vedlikeholde dersom man bruker kodeordet static for feltvariabler som ikke er konstanter.

Både variabler og metoder kan være statiske, eller de kan ikke være det. Feltvariabler som er statiske kalles for klassevariabeler. Feltvariabler som ikke er det kalles for instansvariabler. Metoder som er statiske kalles for klassemetoder, og må kalles på klassen. Metoder som ikke er det kalles for instansmetoder, og må kalles på et objekt.
Ordbok: statiske og ikke-statiske metoder og feltvariabler.
 

Statisk kontekst: dersom metoden som kjøres akkurat nå er en klassemetode (altså er static) befinner vi oss i en statisk kontekst. I en statisk kontekst har man ikke tilgang til variabelen this.

Under er et eksempel på klasse med både statiske og ikke-statiske variabler og metoder:

/* 
 * Merk: dette eksempelet ser fornuftig ut ved første øyekast, men er IKKE til
 * etterfølgelse. Les «Hvorfor man alltid unngår klassevariabler» -seksjonen
 * under eksempelet for å forstå hvorfor.
 */
 
class Person {
  // Statisk: klassevariabeler, klassemetoder
  static int numberOfPersons = 0;

  static void incrementNumberOfPersons() {
    Person.numberOfPersons++;
  }

  // Ikke-statisk: instansvariabler, konstruktører, instansmetoder
  String name;

  Person(String name) {
    this.setName(name);
    Person.incrementNumberOfPersons();
  }

  void setName(String name) {
    if (name == null) {
      throw new IllegalArgumentException("Name cannot be null");
    }
    this.name = name;
  }
}

public class Main {
  public static void main(String[] args) {
    Person p1 = new Person("Adam");
    Person p2 = new Person("Eva");
    System.out.println(Person.numberOfPersons); // 2
  }
}

Hvorfor man alltid unngår klassevariabler

Statiske variabler er som globale variabler: de er felles for «alle». I eksempelet over er numberOfPersons en statisk variabel. Den er tilgjengelig fra alle steder i klassen Person, både fra statisk og ikke-statisk kontekst. Det kan være fristende å bruke statiske variabler for å holde på informasjon som er felles for alle objektene i en klasse, slik som over. Men det kan likevel skape problemer. For eksempel dersom vi skal teste klassen vår:

@Test
void testName() {
  Person p1 = new Person("Adam");
  assertEquals("Adam", p1.name);
}

@Test
void testNumberOfPersons() {
  Person p1 = new Person("Adam");
  Person p2 = new Person("Eva");
  assertEquals(2, Person.numberOfPersons); // feiler
}

Her vil testen testNumberOfPersons fungere fint så lenge den kjøres enkeltvis; men den samme testen vil feile dersom vi kjører alle testene samlet. Dette er fordi klassevariabler som numberOfPersons ikke nullstilles på en naturlig måte mellom testene. Det kunne man forsåvidt fikset ved å skrive litt kode som nullstiller verdien først; men det er dårlig stil og vanskelig å vedlikeholde at testskriveren må tenke på dette. Endringer i statiske variabler gjør altså at person A kan skrive en ny og urelatert test (e.g. testName) som gjør at en gammel test skrevet av person B som alltid har virket tidligere (e.g. testNumberOfPersons) plutselig feiler uten at koden den tester er endret i det hele tatt!

Statiske variabler som endrer seg er som globale variabler: ondskapsfulle. De bør unngås for enhver pris, og er et mareritt å teste systematisk eller feilsøke med. De ødelegger dessuten modularitet og gjør det vanskelig å endre deler av koden senere. Så selv om du ikke vil erfare hvor grotesk denne ondskapen egentlig er før du jobber med et mye større prosjekt enn vi gjør i INF101, er rådet utvetydig: unngå statiske variabler (som ikke er konstanter)!

Hva skal man da gjøre i stedet for å oppnå det samme, spør du? Svar: lag nye klasser som tar hånd om felles -variabler og opprettelsen av nye objekter. I eksempelet under vil alle Person -objekter laget av samme PersonFactory -objekt dele på felles SharedVariables -objekt med tilhørende instansvariabler:

class SharedVariables {
  int numberOfPersons = 0;
}

class PersonFactory {
  SharedVariables sharedVariables = new SharedVariables();

  Person newPerson(String name) {
    return new Person(name, this.sharedVariables);
  }
}

class Person {
  String name;
  SharedVariables sharedVariables;

  Person(String name, SharedVariables sharedVariables) {
    this.setName(name);
    this.sharedVariables = sharedVariables;
    this.sharedVariables.numberOfPersons++;
  }

  void setName(String name) {
    if (name == null) {
      throw new IllegalArgumentException("Name cannot be null");
    }
    this.name = name;
  }
}

public class Main {
  public static void main(String[] args) {
    PersonFactory factory = new PersonFactory();
    Person p1 = factory.newPerson("Adam");
    Person p2 = factory.newPerson("Eva");
    System.out.println(factory.sharedVariables.numberOfPersons); // 2
  }
}

God bruk av klassevariabler: konstanter

Den eneste fornuftig bruken av klassevariabler som finnes på INF101-nivå er såkalte konstanter. Dette er variabler som aldri endrer seg, for eksempel \(\pi\) eller standardverdier. Slike variabler pleier vi å navngi med store bokstaver, og vi benytter oss i tillegg av kodeordet final for å understreke at verdien ikke kan endre seg.

class Person {
  static final String DEFAULT_NAME = "Kari";

  String name = Person.DEFAULT_NAME;

  Person(String name) {
    this.name = name;
  }

  Person() {
  }
}

public class Main {
  public static void main(String[] args) {
    Person p1 = new Person("Ola");
    Person p2 = new Person();
    System.out.println(p1.name); // Ola
    System.out.println(p2.name); // Kari
  }
}

Kvalifisering av metoder og variabler

Når man som nybegynner blir kjent med statiske og ikke-statiske metoder, er det lurt å alltid spesifisere hva man kaller metoden : for eksempel, kall en instansmetode med this.metode() eller objekt.metode(), og kall en klassemetode med Klassenavn.metode() i stedet for å kun bruke metode(). Dette kalles å kvalifisere navnet til metoden.

På samme måte kan det være lurt å referere til instansvariabler med this.variabel eller objekt.variabel, og referere til klassevariabler med Klassenavn.variabel – slik klargjør man for seg selv og andre hvorvidt man jobber i en statisk kontekst eller ikke.

Etter hvert som man føler seg komfortabel, kan man eventuelt benytte seg av Java sine snarveier og droppe kvalifiseringene this. og Klassenavn. før metoder og variabler for å gjøre koden mer kompakt.

Organisering av innholdet i en klasse

Det finnes ulike motstridende stilguider i Java, og stil-guiden til Google spesifiserer til og med at det ikke finnes én riktig rekkefølge å organisere innholdet i en klasse på. Det er likevel verdt å nevne at en vanlig rekkefølge er å gruppere alt med statisk kontekst først, og deretter alt med ikke-statisk kontekst.

Statisk

  1. Klassevariabler
  2. Klassemetoder

Ikke-statisk

  1. Instansvariabler
  2. Konstruktører
  3. Andre instansmetoder

«Non-static method or variable cannot be referenced from a static context»

Som nykommer til Java er dette en av de vanligste feilmeldingene man ser, og ofte er kodeeditorer ivrige med forslag til hva man kan gjøre for å fikse feilen; noen forslage er gode, andre forslag er ikke like gode.

Problemet feilmeldingen beskriver er at kode som kjøres i en statisk kontekst forsøker å kalle en metode eller bruke en variabel som ikke er statisk. Feilen kan forekomme hvis man blander statiske og ikke-statiske ting i samme klasse. For eksempel:

public class Main {
  int x = 0;

  public static void main(String[] args) {
    System.out.println(x); // Non-static variable 'x' cannot be
  }                        // refereced from a static context
}

Her er x en instansvariabel; denne variabelen eksisterer derfor kun i objekter av klassen Main. Men det er ikke opprettet noen Main-objekter; det er kun klassen Main som eksisterer. Metoden main er en statisk metode, og kjøres i kontekst av klassen Main.

Problemet kan løses på én av to måter; enten

// Løsning 1: gjøre variabelen static
public class Main {
  static int x = 0;

  public static void main(String[] args) {
    System.out.println(x);
  }
}

// Unngå denne løsningen,
// den bruker klassevariabler
// Løsning 2: opprett et Main-objekt
public class Main {
  int x = 0;

  public static void main(String[] args) {
    Main m = new Main();
    System.out.println(m.x);
  }
}
// Løsningen bevarer x som en instans-
// variabel, og er sannsynligvis bedre

Løsningen du velger kommer an på hva situasjonen er; men husk på at det styrende prinsippet er at man bør unngå klassevariabler såfremt de ikke er konstanter. Dermed blir «riktig» løsning ofte å finne ut hvilket objekt du ønsker å se på variablene til, eventuelt opprette et slikt objekt om du ikke har et fra før.

Dersom feilmeldingen gjelder en metode, sjekk om metoden kan være statisk:

Dersom en metode kan være statisk, er det ofte lurt at den er statisk, siden statiske metoder kan brukes både i statiske og ikke-statiske kontekster. Dersom det ikke lar seg gjøre at en metode er statisk uten å også gjøre en variabel statisk, er det viktigere å ta hensyn til variabelen – da er metoden avhengig av instansvariabler, og du bør ikke gjøre metoden statisk.