Classes en objecten

Wat is een klasse ?

Een klasse is een constructie om allerlei dingen te groeperen. Zo zou je potloden tot de klasse der schrijfwaren kunnen rekenen, mensen horen bij de klasse der zoogdieren en panne(n)koeken behoren tot de klasse der etenswaren.

Vaak kun je ook een andere groepering toepassen en meestal horen daar dan andere dingen bij. Zo kun je vers geslepen potloden ook onderbrengen in de klasse der scherpe voorwerpen, mensen in de klasse der zuurstof verbruikende organismen en panne(n)koeken in de klasse der bouwmaterialen, maar dan moeten ze wel een tijdje liggen.

Wat is een Java class ?

Een Java class is een taalconstructie waarmee je groepen van Java-dingen kunt maken. Dat doe je dan niet achteraf door bestaande dingen in een groep te stoppen, maar vantevoren door een klasse te beschrijven en vervolgens dingen te maken die de eigenschappen van die klasse hebben.

Een class doet niks !!!

Een class op zich doet niks. Pas wanneer je objecten gaat maken volgens de class-beschrijving wordt een class nuttig. Je kunt een class zien als een recept voor het maken van objecten. Een recept voor paella kun je niet eten, de paella zelf (het object) wel.

Voorbeeld van een Java class

Laten we beginnen met een eenvoudige class voor het maken van een database met liedjes. Zet de volgende code in een file Song.java

public class Song
{
  public String songTitle;

  public void setTitle(String newTitle)
  {
    songTitle = newTitle;
  }

  public String getTitle()
  {
    return songTitle;
  }

} // Song{}

Hier zie je een class Song{} die één variabele songTitle definieert en twee functies: setTitle() die songTitle een nieuwe waarde kan geven en getTitle() die de huidige waarde opvraagt.

Wat is een Java object ?

Om de class Song{} te gebruiken maken we een hoofd-classe SongExample die een Song-object gaat maken. Zet de volgende code in een file SongExample.java in dezelfde directory als Song.java

public class SongExample
{
  public static void main(String[] args)
  {
    Song mySong = new Song();

    mySong.setTitle("Palomine");
    System.out.println( mySong.getTitle() );
  } // main()

} // SongExample{}

Deze class bevat de main() functie die het hele programma opstart. Binnen main() zie je dat een nieuwe variabele van het type Song gemaakt wordt.

mySong is nu een object van het type Song.

Daarna krijgt mySong een titel en om te bewijzen dat dat gelukt is wordt als laatste de titel weer aan mySong gevraagd en met println() zichtbaar gemaakt.

Wat een beetje raar lijkt is deze regel:

 Song mySong = new Song();
Je ziet dat achter de tweede Song de haakjes () staan die we altijd voor een functie gebruiken, maar Song is toch een class en geen functie ? Hoe dat zit wordt later duidelijk bij het verhaal over constructoren.

Meerdere objecten van hetzelfde type

Een object Song is misschien wel leuk, maar als we er maar één nodig hebben, waarom maken we dan eerst een klasse van songs ? Een klasse beschrijft de eigenschappen van een groep objecten, dus is vooral zinvol als we meerdere objecten willen maken die tot die klasse horen.

In het volgende voorbeeld zie je dat twee objecten gemaakt worden: song1 en song2, beide van het type Song:

public class SongDatabase
{
  public static void main(String[] args)
  {
    Song song1 = new Song();
    Song song2 = new Song();

    song1.setTitle("Palomine");
    song2.setTitle("Baby let's swing");

    System.out.println( song1.getTitle() );
    System.out.println( song2.getTitle() );
  } // main()

} // SongDatabase{}

Constructoren

Bij wat we tot nu toe gezien hebben zijn objecten van het type Song aanvankelijk allemaal precies hetzelfde. Achteraf kan met de member-functie setTitle() een titel gegeven worden waardoor de objecten zich van elkaar gaan onderscheiden.

Dat alle objecten op precies dezelfde manier gemaakt worden komt omdat er impliciet, voor ons verborgen, bij het maken van een object een constructie-functie wordt aangeroepen die het object in een default-toestand brengt. Zo'n functie noemen we een default-constructor.

Je kunt zelf ook constructoren maken, maar realiseer je dat er dan geen default constructor gemaakt wordt. Een default-constructor wordt alleen gemaakt en uitgevoerd als je zelf geen constructoren maakt.

Je kunt zelf een constructor zonder argumenten maken als je wilt dat objecten wel altijd op dezelfde manier, maar op een andere manier dan met de impliciete default constructor gemaakt worden.

Je kunt ook constructoren maken die met parameters de nieuwe objecten bij hun creatie al in een bepaalde toestand brengen.

Een constructor heeft altijd dezelfde naam als de class. Bovendien heeft een constructor geen return type.

We breiden de class Song{} uit met een constructor zonder parameters en een constructor die een String als parameter verwacht.

public class Song
{
  public String songTitle;

  public Song() // constructor
  {
    songTitle = "Unknown";
  }

  public Song(String newTitle) // constructor
  {
    songTitle = newTitle;
  }

  public void setTitle(String newTitle)
  {
    songTitle = newTitle;
  }

  public String getTitle()
  {
    return songTitle;
  }

} // Song{}

Nu kunnen we objecten direct bij het maken al een titel geven.

public class SongDatabase
{
  public static void main(String[] args)
  {
    Song song3 = new Song("Don't ask");
    Song song4 = new Song("Sube");

    System.out.println( song3.getTitle() );
    System.out.println( song4.getTitle() );

  } // main()

} // SongDatabase{}

Je ziet dat nu de haakjes ineens wel nuttig zijn omdat het maken van een nieuw object ineens heel veel lijkt op het aanroepen van een functie met parameters.

O nee, niet weer arrays...

Nee, je kunt opgelucht ademhalen. Geen array maar een ArrayList deze keer. Een voorbeeld met een array staat wel bij de voorbeelden, maar een Arraylist is veel handiger voor het maken van een liedjes-database omdat je makkelijk elementen kunt toevoegen, weghalen, de lijst sorteren en je hoeft niet vantevoren aan te geven hoeveel elementen je erin wilt zetten.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Collections;

public class SongDatabaseArrayList
{
  public static void main(String[] args)
  {
    // Create an ArrayList for containing Song objects
    ArrayList songList = new ArrayList();

    songList.add( new Song("Palomine") );
    songList.add( new Song("Baby let's swing") );
    songList.add( new Song("Sube") );
    songList.add( new Song("Horizons") );

    for(int i=0; i < songList.size(); i++)
    {
      System.out.println(i + ": " + songList.get(i).getTitle() );
    }

  } // main()

} // SongDatabaseArrayList{}

Voor gevorderden volgt hier een betere manier om de inhoud van een ArrayList te bekijken. Je maakt een iterator, dat is een soort aanwijsstok waarmee je door de ArrayList heen loopt en telkens een volgend element aanwijst.


    Iterator songIterator = songList.iterator();

    while(songIterator.hasNext())
    {
      System.out.println( songIterator.next().getTitle() );
    }

ArrayList functies

import java.util.ArrayList;
ArrayList mijnArrayList = new ArrayList();
add
get
remove

Waarom die get/set functies ?

We kunnen de variabelen songTitle van de Song-objecten gewoon veranderen en lezen. Waar hebben we dan die get- en set-functies voor nodig ?

Net als bij het aanroepen van member-functies kun je variabelen van de class lezen en schrijven. De algemene vorm is <class>.<variabele>

public class SongYear
{
  public int releaseYear;

  public int setYear(int newYear)
  {
    if(newYear < 1900 || newYear > 2009)
    {
      System.out.println("Year out of range");
      return -1;
    }
    releaseYear = newYear;
    return 0;
  }

  public int getYear()
  {
    return releaseYear;
  }

} // SongYear{}

In de class SongYear kun je het jaar bijhouden waarin in song uitkwam. Stel dat we niet willen dat het jaar in de toekomst ligt of voor 1900, dan kunnen we dat in de functie setYear beveiligen. Maar als je dan buiten die functie om rechtstreeks het jaar op een ongeldige waarde kunt zetten heb je daar weinig aan:

public class WrongSongDatabase
{
  public static void main(String[] args)
  {
    SongYear mySong = new SongYear();

    mySong.setYear(2002);
    System.out.println( mySong.getYear() );

    mySong.setYear(-10);
    System.out.println( mySong.getYear() );

    mySong.releaseYear = -10;
    System.out.println( mySong.releaseYear );

  } // main()

} // WrongSongDatabase{}

Om dit op te lossen kun je releaseYear 'private' maken in plaats van 'public'.

private int releaseYear;

Op die manier kunnen alleen functies die bij de class horen de variabele lezen of veranderen en kun je er niet rechtstreeks bij. Het volgende is dan niet meer mogelijk en leidt tot een fout bij het compileren van je programma:

mySong.releaseYear = -10;
System.out.println( mySong.releaseYear );

Afgeleide classes, a.k.a. subclasses

Voorgebakken classes gebruiken

Bij Java krijg je al een heleboel classes die je zo kunt gebruiken. Je hoeft dus niet alles zelf te maken. Er zijn complete bibliotheken (libraries) met classes voor het lezen van het toetsenbord, lezen van de muis, maken van knopjes en nagaan of de gebruiker daarop drukt, maken van pop-up window en menu's, lezen en schrijven van files, datastructuren, communicatie en nog veel meer.

Om die classes te gebruiken moet je meestal een library importeren, bijvoorbeeld zo:

import java.util.*;

Zelf libraries maken

Dat kan ook :-P

Class Path

Dit is een lijst met directories waarin .class files gezocht worden. Uitleg volgt later.

Java interfaces

Een interface is een soort class, maar alleen bedoeld om subclasses van af te leiden en niet om objecten mee te maken. Een interface schrijft precies voor welke functionaliteit objecten moeten hebben, maar zegt niet hoe dat dan moet werken.

Als je bij een class denkt aan een recept voor paella, dan zou je bij een interface kunnen denken aan een recept voor spaanse visgerechten. Het is te abstract om de gerechten echt te maken want je weet niet of het gaat over paella of inktvisringetjes of gebakken zeebaars. maar je weet wel dat er vis in verwerkt is en dat alles gebakken is in olijfolie.

Van zo'n interface kun je classes afleiden die de eigenschappen van de interface delen, maar elk op hun eigen manier implementeren.