Arto Wikla 2011. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

4 Taulukkotekniikkaa: etsimistä, järjestämistä, matriiseja, olioita...

(Muutettu viimeksi 6.10.2011, sivu perustettu 28.9.2010. Arto Wikla)

Muistelua ja kertausta

Kurssilla opittiin jo kauan sitten, miksi taulukot ovat hyödyllisiä ja käyttökelpoisia: yksi muuttuja tarkoittaa kokonaista "lokerikkoa", jossa kukin "lokero" käyttäytyy kuin yksittäinen muuttuja. Ja oleelliseksi osoittautui, että taulukkoa voidaan indeksoida dynaamisesti indeksin arvoa muuttelemalla:

String[] nimet = new String[3];

nimet[0] = "Aku";
nimet[1] = "Iines";
nimet[2] = "Hessu";

for (int indeksi=0; indeksi <  nimet.length; ++indeksi)
  System.out.println(nimet[indeksi]);

Edellä opittiin myös näppärä "for-each"-toisto, jota voi käyttää, ellei taulukon alkioiden arvoja muuteta eikä tarvita tietoa indekseistä:

String[] nimet = new String[3];

nimet[0] = "Aku";
nimet[1] = "Iines";
nimet[2] = "Hessu";

for (String alkio: nimet)
  System.out.println(alkio);

Tässä luvussa opitaan lisää taulukoiden ominaisuuksia, käyttöä ja erityisesti muutamia keskeisiä ohjelmointitekniikoita: etsimistä ja järjestämistä.

Taulukko on olio

Javassa alkeistyyppejä ovat vain numeeriset tyypit (int, double, char, ...) ja totuusarvotyyppi (boolean). Kaikki muut tyypit ovat viittaustyyppejä. Javassa kaikki viittaustyypin arvot ovat olioita, luokan ilmentymiä. Ja kaikki oliot "roikkuvat aina langan päässä", eli kun muuttujan arvona on olio, muuttujan arvona on tarkemmin sanoen aina viite olioon.

Taulukko ei ole poikkeus: Javassa taulukko on olio ja siis taulukkomuuttuja viittaa taulukko-olioon.

Tarkastellaan aluksi pientä yksinkertaista esimerkkiä ja selvennetään käsitteitä:

int[] luku = new int[10];

[Huom: Javassa on valitettavasti mahdollista määritellä samaa tarkoittaen myös int luku[] = new int[10]; Tämä onneton kirjoitustyyli on kotoisin eräistä Javan edeltäjistä. Koska edellinen tapa on paljon selkeämpi, kurssilla käytetään ainoastaan sitä!]

Taulukot ovat siis Javassa olioita: Ne luodaan konstruktorilla, niitä käsitellään viitteinä, ..., parametrina välitettyä taulukkoa voi muuttaa, metodi voi palauttaa arvonaan viitteen taulukkoon, ...

Sääntöjä:

  1. Taulukon indeksit ovat aina int-tyyppisiä.
  2. Taulukon ensimmäinen indeksi on aina 0.
  3. Taulukon koko on aina konstruktorin kutsussa annettu kokonaislukuarvo, ja siis suurin käytössä oleva indeksi on koko-1, koska ensimmäinen indeksi on aina 0. Taulukkokonstruktorille saa antaa myös muuttujien arvosta riippuvan koon. Koon ei siis tarvitse olla ohjelmaan kirjoitettu vakio.
  4. Taulukon käyttäjä on vastuussa siitä, että indeksinä ei käytetä koskaan arvoa joka ei ole välillä [0,koko-1]. Jos käytetään, ohjelman suoritus keskeytyy virheeseen ArrayIndexOutOfBounds.
  5. Taulukon alkioilla on niiden tyypin määräämät oletusalkuarvot (esim. 0, 0.0, false, null).
  6. Taulukko-olion koko ei voi koskaan muuttua. Mutta taulukkomuuttuja voi saada arvokseen viitteen uuteen – erikokoiseenkin – taulukko-olioon.
  7. Taulukon koon saa selville ilmauksella .length. Jos muuttujan luku arvona on 10-alkionen taulukko-olio, luku.length on 10.

Monenlaisia taulukoita

Seuraavat aika keinotekoiset ohjelmanpätkät antavat esimerkkejä monenlaisista taulukkomuuttujien määrittelyistä, taulukko-olioiden konstruoinnista ja taulukoiden käytöstä:

    // int-taulukoita: "kokonailukutaulukkoA" jne.

    int[] kA;                 // taulukkomuuttuja, EI vielä taulukkoa!
    int[] kB = new int[4];   // muuttuja ja taulukko-olio
    int[] kC = {5, -23, 12, 31, 7}; // olio alkiot luetellen, luvallista vain
                                    // muuttujan määrittelyn yhteydessä

    for (int i=0; i<kB.length; ++i) // indeksoiden läpikäynti
       kB[i] = 2 * (i+1);

    for (int alkio: kB)             // "for-each"
       System.out.println(alkio);

    kA = kC;             // KOPIOIDAAN VIITE

    kA[1] = 123456;      // MYÖS kC[1] MUUTTUU!
    System.out.println(kA[1] + " " + kC[1]);

    kC = new int[3];  // luodaan uusi taulukko kC:n arvoksi
    kC[1] = -98;
    System.out.println(kA[1] + " " + kC[1]);

Tulostus:

2
4
6
8
123456 123456
123456 -98

    // double-taulukoita:  "doubletaulukkoA" jne.

    double[] dA = new double[5];
    double[] dB = {3.14, 7.21, 0.0, 9.1}; // siis double[4]!

    dA[2] = dB[0] + dB[3];

    // boolean-taulukoita:  "totuusarvotaulukkoA" jne.

    boolean[] tA = new boolean[4];
    boolean[] tB = {true, true, false};  // siis boolean[3]!

    tA[1] = tB[0] || tB[2];
    if (tA[1]) 
      System.out.println("Tosi on!");

Tulostus:

Tosi on!

    // char-taulukoita:  "merkkitaulukkoA" jne.

    char[] mA = new char[5];
    char[] mB = {'q', 'w', 'e'};         // siis char[3]!

    mB[1] = 'u';
    System.out.println("Merkkialkion oletusalkuarvo on" + mA[2] + ".");

Tulostus:

Merkkialkion oletusalkuarvo on.

    // String-taulukoita:  "merkkijonotaulukkoA" jne.

    String[] jA = new String[3];
    String[] jB = {"kissa", "hiiri"};     // siis String[2]!

    jA[0] = jB[1];
    jA[2] = jB[0];
    jA[1] = "koira";
    for (String alkio : jA)
      System.out.println(alkio);
    jB[1] = jA[1];

Tulostus:

hiiri
koira
kissa

Etsiminen taulukosta - peräkkäishaku

Tähän tutustuttiin jo aiemmin, mutta kertaus ei ole pahasta!

Tyypillinen ohjelmointitehtävä on sen selvittäminen, onko taulukossa jotakin kysyttyä alkiota. Jos taulukko ei ole järjestyksessä, ns. peräkkäishaku on normaali menettely.

Laaditaan metodi, joka saa parametrinaan int[]-tyyppisen arvon eli kokonaislukutaulukon ja selvittää, löytyykö taulukosta toisena parametrina annettua kokonaislukua. Jos kysytty arvo löytyy, metodi palauttaa haettavan luvun indeksin arvonaan (jos haettavia on taulukossa monta, palautetaan niistä ensimmäisen indeksi). Jos kysyttyä ei löydy, metodi palauttaa arvon -1, joka ei koskaan voi kelvata taulukon indeksiksi:

public class EsimerkkiPerakkaishausta {  // Peräkkäishaku int-taulukosta
                                         //            halutaan indeksi

//-------------------------------------------------------------------------
  private static int hae(int[] taulu, int haettava) {  // int-metodi!
    for (int i=0; i<taulu.length; ++i) 
      if (taulu[i] == haettava)
        return i; // tärppäsi, lopetetaan samantien
    // jos tähän päästiin, ei löytynyt!
    return -1;
  }
//-------------------------------------------------------------------------

  public static void main(String[] args) { // esittelypääohjelma
    int[] a = {40, 20, 50, 10, 30};

    System.out.println(hae(a, 10)); // tulostaa 3
    System.out.println(hae(a, 20)); // tulostaa 1
    System.out.println(hae(a, 30)); // tulostaa 4
    System.out.println(hae(a, 40)); // tulostaa 0
    System.out.println(hae(a, 50)); // tulostaa 2
    System.out.println(hae(a, 60)); // tulostaa -1
  }
}

Jos riittää pelkkä tieto, löytyykö kysytty, peräkkäishaku voidaan ohjelmoida myös totuusarvoisena metodina:

public class Esimerkki2Perakkaishausta {  // Peräkkäishaku int-taulukosta
                                          //     halutaan vain tieto löytyykö

//-------------------------------------------------------------------------
  private static boolean hae(int[] taulu, int haettava) {  // boolean-metodi!
    for (int alkio: taulu) 
      if (alkio == haettava)
        return true; // tärppäsi, lopetetaan samantien
    // jos tähän päästiin, ei löytynyt!
    return false;
  }
//-------------------------------------------------------------------------

  public static void main(String[] args) { // esittelypääohjelma
    int[] a = {40, 20, 50, 10, 30};

    System.out.println(hae(a, 10));   // tulostaa true

    if (hae(a, 20))
      System.out.println("Löytyy!");  // tulostaa Löytyy!
    else
      System.out.println("Ei Löydy!");

    System.out.println(hae(a, 60));   // tulostaa false
  }
}

Etsiminen taulukosta - binäärihaku

Jos taulukko on järjestyksessä, ns. binäärihaku on erittäin tehokas väline arvon hakemiseen taulukosta.

public class EsimerkkiBinaariHausta {  // Binäärihaku järjestetystä int-taulukosta

//-------------------------------------------------------------------------
  private static int binHae(int[] taulu, int haettava) {
    int vasen = 0;
    int oikea = taulu.length-1;
    int keski;
    while (vasen <= oikea) {
      keski = (vasen+oikea)/2;

      if (haettava == taulu[keski])
        return keski;     // löytyi ja lopetetaan!

      if (haettava < taulu[keski])
        oikea = keski-1;
      else
        vasen = keski+1;
    }
    // jos tähän päästiin, hakualue tuli tyhjäksi eikä löytynyt!
    return -1;
  }
//-------------------------------------------------------------------------

  public static void main(String[] args) { // esittelypääohjelma

    int[] a = {10, 20, 30, 40, 50};

    System.out.println(binHae(a, 5));   // tulostus -1 
    System.out.println(binHae(a, 10));  // tulostus 0
    System.out.println(binHae(a, 20));  // tulostus 1
    System.out.println(binHae(a, 30));  // tulostus 2
    System.out.println(binHae(a, 35));  // tulostus -1
    System.out.println(binHae(a, 40));  // tulostus 3
    System.out.println(binHae(a, 50));  // tulostus 4
    System.out.println(binHae(a, 60));  // tulostus -1
  }
}

Ohjelmointitekniikkaa: taulukon järjestäminen

Taulukon alkioiden järjestäminen on myös usein tarvittu toimenpide. Järjestämisalgoritmeja on monenlaisia, tehokkaita ja vähemmän tehokkaita.

Yksi melko tehoton, mutta helposti ymmärrettävä järjestämisalgoritmi on ns. yksinkertainen vaihtojärjestäminen (nousevaan järjestykseen):

public class VaihtoJarj { // Yksinkertainen vaihtojärjestäminen

//-------------------------------------------------------------------------
   private static void vaihtoJarjesta(int[] taulu) {
     for (int i=0; i < taulu.length-1; ++i) 
       for (int j=i+1; j < taulu.length; ++j)
         if (taulu[i] > taulu[j]) { // onko järjestys väärä?
            int apu = taulu[i];     // jos on, vaihdetaan!
            taulu[i] = taulu[j];
            taulu[j] = apu;
         }
   }
//-------------------------------------------------------------------------

  public static void main(String[] args) { // esittelypääohjelma
    int[] a = {40, 20, 50, 10, 30};

    for (int i=0; i<a.length; ++i)
       System.out.print(a[i]+" ");
    System.out.println();

    vaihtoJarjesta(a);

    for (int i=0; i<a.length; ++i)
       System.out.print(a[i]+" ");
    System.out.println();
  }
}
Tulostus:
40 20 50 10 30
10 20 30 40 50

Yksinkertaisen vaihtojärjestämisen (vaikkapa nousevaan järjestykseen) idea on seuraava:

Toinen vieläkin tehottomampi järjestämismenetelmä on ns. kuplajärjestäminen (bubble sort). Sen idean voisi kuvailla seuraavasti:

Kuplajärjestäminen voidaan ohjelmoida vaikka seuraavasti:

private static void kuplaJarjesta(int[] taulu) {
  for (int i=taulu.length; i > 0; --i) 
    for (int j=0; j < i-1; ++j)
      if (taulu[j] > taulu[j+1]) { // onko järjestys väärä?
         int apu = taulu[j];       // jos on, vaihdetaan!
         taulu[j] = taulu[j+1];
         taulu[j+1] = apu;
      }
}

Edellisiä selvästi parempi järjestämisalgoritmi on ns. lisäysjärjestäminen (insertion sort):

private static void lisaysJarjesta(int[] taulu) {
  for (int i=1; i<taulu.length; ++i) {
    int apu = taulu[i];
    int j = i;
    while (j > 0 && taulu[j-1] > apu) {
      taulu[j] = taulu[j-1]; 
      --j;
    }
    taulu[j] = apu;
  }
}

Järjestämisalgoritmien (kuten muidenkin algoritmien!) tehokkuutta voidaan analysoida matemaattisesti: kaikki edellä esitetyt järjestämisalgoritmit kuuluvat ns. luokkaan O(n2). Se tarkoittaa suurinpiirtein, että "jos järjestettävän taulukon koko kaksinkertaistetaan, algoritmin suoritusaika nelinkertaistuu". Parhaat ns. "yleiset" (=vertailuun perustuvat) järjestämisalgoritmit "toimivat ajassa O(n*log n)".

Toisaalta suoritusaikoja voidaan myös kokeellisesti mitata. Pieni vertailu antoi seuraavia suoritusaikoja (3.10.2011) kun järjestettiin 100000-alkoinen int-taulukko (ohjelma VertaileJarjAlgoritmeja.java). Ajat riippuvat tietysti käytettävästä tietokoneesta, mutta suhteet säilynevät. Esimerkin ajat ovat millisekunteina.

                arvottu          valmiiksi      käänteisessä
                alkujärjestys    järjestetty    järj. oleva
                                 taulukko       taulukko
              
 vaihtojärj.     23947 ms.        12512 ms.      11476 ms.

 kuplajärj.      26046 ms.        13199 ms.      11836 ms.

 lisäysjärj.      1923 ms.            4 ms.       3847 ms.

Kuten taulukosta näkee, lisäysjärjestäminen on muita paljon nopeampi, vaikka se kuuluukin samaan "tehokkuusluokkaan" toisten kanssa. Ja erityisesti melkein järjestyksessä olevien taulukoiden järjestämisessä se on ylivoimainen.

Huom: Koska yllä annettu ohjelma VertaileJarjAlgoritmeja.java ei välttämättä toimi kunnolla kaikissa Windows-järjestelmissä, käytettävissä on myös versio, joka mittaa ajat nanosekunnin tarkkuudella: VertaileJarjAlgoritmejaB.java. Tämän version pitäisi Windowsissakin antaa vertailukelpoisia aikoja.

Kokeillaanpa sitäkin:

                arvottu            valmiiksi         käänteisessä
                alkujärjestys      järjestetty       järj. oleva
                                   taulukko          taulukko
              
 vaihtojärj.     23715.334903 ms.  12525.66967  ms.  11508.570413 ms.

 kuplajärj.      25989.121225 ms.  13181.302456 ms.  11857.060521 ms.

 lisäysjärj.      1919.676356 ms.      3.474728 ms.  3836.819151 ms.

Tällainen tarkkuus ei toki olisi tässä tarpeen...

Huom: Tällaisessa mittaamisessa on omat ongelmansa: Jos tietokone on vaikkapa kytkettynä verkkoon, ennakoimattomat tietoliikennetapahtumat voivat vääristää mittaustuloksia. Suorituskyvyn mittaaminen onkin tärkeä tietojenkäsittelytieteen osa-alue. Se ei ole helppoa!

Huom: (6.10.) Yksi kurssin opiskelija (DN) antoi mainion linkkivinkin: järjestämisalgoritmien visualisoija Sorting Visualizer. Pidemmälle ehtineet, huomatkaa että tuonne voi yksinkertaisella kielellä ohjelmoida myös omia järjestämisalgoritmeja visualisoitaviksi! Tässä esimerkkinä yksinkertainen vaihtojärjestäminen tuon sivun ohjelmointikielellä CoffeeScriptillä:

for i in [0...VA.length - 1]
  for j in [i + 1...VA.length]
    if VA.gt(i,j)
      VA.swap(i,j)

String-olioita taulukossa

Taulukko on siis itse olio: se luodaan new-ilmauksella, viite siihen voi olla muuttujan arvona, viite taulukkoon voidaan välittää parametrina, metodi voi palauttaa arvonaan viitteen taulukkoon, ...

Myös taulukon komponentit voivat olla olioita eli tarkemmin sanottuna viitteitä olioihin!

String-olioita sisältävä taulukkomuuttuja voidaan määritellä:

String[] taulu;

Metodin muuttujana taululla ei ole mitään alkuarvoa, luokassa määriteltynä kenttänä sillä on alkuarvo null.

Erityisesti on siis syytä huomata, että määrittely String[] taulu; EI siis luo taulukko-oliota, se vain nimeää muuttujan, joka voidaan asettaa viittaamaan taulukko-olioon. Taulukko-olio on siis luotava erikseen:

String[] taulu = new String[5]; 

asettaa muuttujan taulu alkuarvoksi viisialkioisen String-taulukko-olion. Ja tässä on syytä huomata erityisesti, että määrittely new String[5] luo taulukko-olion, jonka alkioilla EI ole arvonaan vielä mitään String-olioita. String-taulukon alkioilla on vain oletusalkuarvo null, viite "ei mihinkään olioon"!

Lause

String[] taulu = {"kissa", "hiiri", "koira"};

asettaa muuttujan taulu alkuarvoksi kolmialkioisen String-taulukko-olion, jonka alkioilla on luetellut alkuarvot.

Huom: Pääohjelmametodin pakollinen parametri

String[] args

on siis String-taulukko. Sen alkuarvoksi tulevat automaattisesti ohjelman käynnistyskomentoa seuraavat välilyönnein toisistaan erotetut merkkijonot, ns. komentoriviparametrit. Jos ohjelma Ohjelma käynnistetään komennolla

java Ohjelma omena appelsiini luumu

pääohjelman args-parametri saa alkuarvokseen kolmialkioisen String-taulukon, jonka alkioilla on arvot "omena", "appelsiini" ja "luumu".

Esimerkkejä String-taulukoista:

public class StringTauluKokeilu {
  public static void main(String[] args) {

    if (args.length == 0)
      System.out.println("Ei komentoriviparametreja.");
    else
      for (String yksiParametreista: args)       // komentoriviparametrit
        System.out.println(yksiParametreista);

    String[] ekaTaulu = new String[4];      // alkioiden oletusalkuarvo on null
    for (String yksiAlkioista: ekaTaulu)
       System.out.println(yksiAlkioista);

    for (int i=0; i < ekaTaulu.length; ++i)
      ekaTaulu[i] = "alkio numero " + i;   // viedään sisältöä taulukkoon 
    for (String yksiAlkioista: ekaTaulu)
       System.out.println(yksiAlkioista);

    String[] tokaTaulu = {"kissa", "hiiri", "koira"};  // annetaan alkuarvot
    for (String yksiAlkioista: tokaTaulu)       
       System.out.println(yksiAlkioista);
  }
}

Kun tämä ohjelma käynnistetään komennolla:

java StringTauluKokeilu  omena appelsiini   luumu

saadaan tulostus:

omena
appelsiini
luumu
null
null
null
null
alkio numero 0
alkio numero 1
alkio numero 2
alkio numero 3
kissa
hiiri
koira

String-taulukon järjestämisestä

Kuten jo aiemmin on opittu, String-arvoja toki voi vertailla operaatioilla == ja !=. Tällaiset vertailut eivät kuitenkaan tutki merkkijonojen sisällön samuutta vaan vastaavat kysymykseen "ovatko oliot samat". Erisuuruusvertailut sen sijaan eivät ole lainkaan käytettävissä String-arvoille eivatkä itse asiassa millekään muillekaan viittaustyyppisille arvoille (eli olioille).

Aiemmin jo opittiin tärkeä String vertailujen väline:

Tällä välineellä String-taulukon voi järjestää "aakkosjärjestykseen" (itse asiassa hienosti sanottuna merkkikoodien mukaiseen positionaaliseen järjestykseen...) :

public class StringienVaihtoJarj { // Yksinkertainen vaihtojärjestäminen String-taulukolle

//-------------------------------------------------------------------------
   private static void vaihtoJarjesta(String[] taulu) {
     for (int i=0; i < taulu.length-1; ++i) 
       for (int j=i+1; j < taulu.length; ++j)
         if (taulu[i].compareTo(taulu[j]) > 0 ) { // onko järjestys väärä?
            String apu = taulu[i];     // jos on, vaihdetaan!
            taulu[i] = taulu[j];
            taulu[j] = apu;
         }
   }
//-------------------------------------------------------------------------

  public static void main(String[] args) { // esittelypääohjelma
    String[] a = {"omena", "appelsiini", "luumu", "kissa", "hiiri", "koira"};

    for (int i=0; i<a.length; ++i)
       System.out.print(a[i]+" ");
    System.out.println();

    vaihtoJarjesta(a);

    for (int i=0; i<a.length; ++i)
       System.out.print(a[i]+" ");
    System.out.println();
  }
}

Tulostus:

omena appelsiini luumu kissa hiiri koira 
appelsiini hiiri kissa koira luumu omena

Huomaa miten vähän esimerkkiin tuli muutoksia int-versioon verrattuna! Jatkokurssilla nähdään, miten voidaan ohjelmoida esimerkiksi juuri järjestämisalgoritmeja, jotka osaavat järjestää mitä vain tietyn ehdon täyttäviä viittaustyyppisiä arvoja.

String-oliot ja char-taulukot

Merkkijonot eli String-oliot ovat kerran synnyttyään muuttumattomia.

Muuttuvien merkkijonojen "merkkipeliin" Javassa on tarjolla luokat StringBuilder ja StringBuffer. Ne jätetään kuitenkin tämän kurssin ulkopuolelle. Javan APIsta löytyy tarvittaessa tietoa.

Koska on kuitenkin hyödyllistä oppia tekemään asiat "omin käsin", tällä kurssilla tutustutaan merkkitaulukon käyttämiseen merkkijonojen käsittelyssä:

Määrittelyt

char[] jono1 = new char[10];
char[] jono2  = {'k','i','s','s','a'};

asettavat muuttujan jono1 arvoksi 10-alkioisen merkkitaulukon ja muuttujan jono2 arvoksi 5-alkioisen merkkitaulukon. Jälkimmäisen alkioille annetaan alkuarvot, edellisen alkiot saavat oletusalkuarvon (merkkityypin oletusalkuarvo taulukossa ja luokan kenttänä on ns. null-merkki '\u0000').

Nyt voidaan ohjelmoida vaikkapa:

   for (char alkio : jono2)   // for-each!
      System.out.print(alkio);
   System.out.println();

   for (int i=0; i<jono1.length; ++i)
      jono1[i] = '*';

   for (int i=0; i<jono2.length; ++i)
     jono1[9-i] = jono2[i];

   for (char alkio : jono1)   // for-each!
     System.out.print(alkio);
   System.out.println(); 

Lauseet tulostavat:

kissa
*****assik
String-olioita voi muuntaa char[]-taulukoiksi String-luokan metodilla:
public char[] toCharArray()

Merkkitaulukosta puolestaan voi luoda String-olion konstruktorilla:

public String(char[] value)

Laaditaan esimerkkiohjelma, joka lukee merkkijonon ja tulostaa sen sellaisena, jossa kaikki 'a'-merkit on korvattu merkillä 'ä' ja 'A'-merkit on korvattu merkillä 'Ä' (Aeta.java):

import java.util.Scanner;
public class Aeta {
  private static Scanner lukija = new Scanner(System.in);
  public static void main(String[] args) {

    System.out.println("Anna merkkijono");
    String jono = lukija.nextLine();

    char[] mtaulu = jono.toCharArray();

    for (int i=0; i<mtaulu.length; ++i)
      if (mtaulu[i] == 'a')
        mtaulu[i] = 'ä';
      else if (mtaulu[i] == 'A')
        mtaulu[i] = 'Ä';

    jono = new String(mtaulu);

    System.out.println("Merkkijono muunnettuna:");
    System.out.println(jono);
  }
}

Huom: Merkkijonotaulukko (char[]-olio) on mahdollista tulostaa sellaisenaankin, koska println-metodi on kuormitettu myös char-talukkoparametrille:

System.out.println(mtaulu);

Matriisit - taulukko-olioita taulukossa

Matriisi on tietojen esitys muodossa rivit-sarakkeet, esimerkiksi:

 
     _0:__1:__2:
 0: | 5   2   3 |
 1: |_9__-1___4_|

Tässä matriisissa on kaksi riviä ja kolme saraketta.

Toisinaan matriisia kutsutaan myös kaksiulotteiseksi taulukoksi. Myös esimerkiksi kolmiulotteinen taulukko on mahdollinen, samoin neliulotteinen...

Javassa oikeastaan ei ole kaksiuluotteisia taulukoita, mutta koska Javan taulukoiden komponenttityyppi voi olla mikä tahansa Javan tyyppi, myös taulukkotyyppi voi olla taulukon komponenttina! Näin saadaan luotua rakenteita jotka muistuttavat moniulotteisia taulukoita. Tässä luvussa tutustutaan vain "kaksiulotteisiin" taulukoihin, matriiseihin. Ja niitä kutsutaan jatkossa kaksiulotteisiksi ilman sitaatteja.

Määrittely

int[][] matriisiA;

tarkoittaa, että muuttujan matriisiA tyyppi on int[]-taulukoista muodostuva taulukko. Muuttujan arvoksi siis voi asettaa viitteen tällaiseen olioon. Vielä sillä ei ole arvonaan tällaista viitettä.

Kun kirjoitetaan

int[][] matriisiA = new int[2][3];

muuttujan määrittelyn yhteydessä samalla luodaan muuttujan arvoksi kaksialkioinen taulukko, jonka kummatkin alkiot ovat viitteitä kolmialkoisiin int-taulukoihin. Tai sama toisin ajatellen: muuttujan arvoksi asetetaan 2*3 int-matriisi. Matriisin int-alkioiden alkuarvo on 0.

Matriisi voidaan myös luetella alkioiden alkuarvoina:

int[][] matriisiB = { {5, 2, -3}, {9, -1, 4} };

Huomaa miten tässä luetellaan kaksi yksiulotteista int-taulukkoa!

Myös "taulukoiden taulukkoa" indeksoidaan. Kun indeksoidaan yhdellä indeksillä

matriisiB[0]

tarkoitetaan taulukkomuuttujan ensimmäistä int[]-taulukkoa:

{5, 2, -3}

Kun tuosta halutaan valita vaikkapa toinen alkio, kirjoitetaan:

matriisiB[0][1]

Tällaista kahdella indeksillä indeksoitua matriisin alkiota käytetään sitten aivan kuin mitä tahansa int-muuttujaa:

matriisiB[0][1] = 66;
matriisiB[2][1] = matriisiB[0][1] - 3;
...

Kaksiulotteisen taulukon alkiot voidaan läpikäydä for-lauseen sisältämällä for-lauseella seuraavaan tapaan:

for (int rivi=0; rivi<matriisi.length; ++rivi)
  for (int sarake=0; sarake<matriisi[rivi].length; ++sarake) 
     // alialgoritmi alkion matriisi[rivi][sarake] käsittelyyn
     ...

Jos toistettavassa alialgoritmissa ei tarvita tietoa alkioiden indekseistä (riveistä ja sarakkeista) eikä käsittelyjärjestyksestä, läpikäynti voidaan ohjelmoida käyttäen "for-each"-toiston toistamaa "for-each"-toistoa:

for (int[] rivi : matriisi)
  for (int alkio : rivi)
    // alialgoritmi yhden alkion arvon käsittelyyn
    ...

Laaditaan esimerkkisovellus, joka lukee 2*3-matriisin ja tulostaa sen 7:llä kerrottuna (so. jokainen alkio kerrotaan 7:llä). Lopuksi ohjelma laskee ja tulostaa "7:llä kerrotun" matriisin alkioiden summan.

import java.util.Scanner;
public class Matriisi7 {
  private static Scanner lukija = new Scanner(System.in);
  public static void main(String[] args) {

    int[][] matriisi = new int[2][3];

    // luetaan arvot:
    for (int rivi=0; rivi<matriisi.length; ++rivi)
      for (int sarake=0; sarake<matriisi[rivi].length; ++sarake) {
        System.out.println("Anna alkio "+rivi+", "+sarake);
        matriisi[rivi][sarake] = lukija.nextInt();
      }

    // kerrotaan alkiot 7:llä:
    for (int rivi=0; rivi<matriisi.length; ++rivi)
      for (int sarake=0; sarake<matriisi[rivi].length; ++sarake)
        matriisi[rivi][sarake] *= 7;  // operaatio '*='kertoo!

    // tulostetaan matriisi:
    System.out.println("Seitsemällä kerrottuna:");
    for (int rivi=0; rivi<matriisi.length; ++rivi) {
      for (int sarake=0; sarake<matriisi[rivi].length; ++sarake)
        System.out.print(matriisi[rivi][sarake]+"\t");
      System.out.println();
    }

    // lasketaan alkioiden summa:
    int summa = 0;
    for (int[] rivi : matriisi)   // nyt tarvitaan vain alkioiden arvot!
      for (int alkio : rivi)
        summa = summa + alkio;
    System.out.println("Alkioiden summa on " + summa + ".");
  }
}

Olio taulukossa: omien olioiden taulukko

Myös omia tyyppejä - eli omia luokkia - voidaan käyttää taulukon komponenttityyppinä! Esimerkiksi määrittely:

Varasto[] varastot = new Varasto[4];

määrittelee Varasto[]-tyyppisen muuttujan ja asettaa sen arvoksi nelialkioisen taulukon, jonka kukin alkio on tyypiltään Varasto. Ensimmäistäkään Varasto-oliota ei vielä synny. Oliotaulukon alkioiden oletusalkuarvo on null.

Varasto-olioita voidaan sitten luoda erikseen tyyliin:

varastot[0] = new Varasto(100.0);
varastot[1] = new Varasto(100.0, 20.2);
varastot[2] = new Varasto(1027.8);
varastot[3] = new Varasto(1027.8, 100.8);

Tämän jälkeen taulukon alkioita - noita luotuja Varasto-olioita - voidaan käyttää aivan samaan tapaan kuin aiemmin:

varastot[0].lisaaVarastoon(30.0);
System.out.println(varastot[0].getSaldo());
varastot[0].otaVarastosta(7.0);
System.out.println(varastot[0]);

varastot[2] = varastot[0];
System.out.println(varastot[2]);
// ...

Taulukkomuuttujan määrittelyn yhteydessä voidaan myös omien olioiden tapauksessa luoda taulukko-olio yksinkertaisesti luettelemalla alkuarvot:

Varasto[] varastot = {new Varasto(100.0), new Varasto(100.0, 20.2),
                      new Varasto(1027.8), new Varasto(1027.8, 100.8)};

Taulukko oliossa: taulukko kenttänä

Kuten muitakin tyyppejä myös taulukkotyyppiä voi käyttää luokan kenttänä, ilmentymämuuttujana.

Toteutetaan esimerkkinä luokka Vakioveikkausrivi veikkausrivin esittämiseen. API:

Toteutus:

public class Vakioveikkausrivi {

  // privaatti tietorakenne:
  private char[] rivi;

  // konstruktori:

  public Vakioveikkausrivi() {
    this.rivi = new char[13];
    for (int i=0; i<rivi.length; ++i)
      rivi[i] = '-';
  }

  // getteri:  // kutsuja vastaa oikeasta kohteen valinnasta: 1-13

  public char getArvaus(int moneskoKohde) {
    return rivi[moneskoKohde -1];
  }

  // setterit: // kutsuja vastaa oikeasta kohteen valinnasta: 1-13
               // jos ei 1, x, X tai 2, ei muutoksia

  public void  setArvaus(int moneskoKohde, char arvaus) {
    if (arvaus=='1' || arvaus=='x' || arvaus=='X' || arvaus=='2')
      rivi[moneskoKohde -1] = arvaus;
  }
    // kuormitetaan: 1 ja 2 voidaan antaa myös int-arvona:

  public void setArvaus(int moneskoKohde, int arvaus) {
    if (arvaus==1)
      rivi[moneskoKohde -1] = '1';
    else if (arvaus==2)
      rivi[moneskoKohde -1] = '2';
  }

  public String toString() {
    return new String(rivi);
  }
}

Käyttöesimerkki:

public class VakioveikkausrivinKokeilu  {
  public static void main(String[] args) {

    Vakioveikkausrivi v = new Vakioveikkausrivi();
    System.out.println("Tyhjä rivi: " +v);

    v.setArvaus(1, '1');
    v.setArvaus(2, 'x');
    v.setArvaus(3, '2');
    v.setArvaus(4, 1);
    v.setArvaus(5, 'X');
    v.setArvaus(6, 2);
    System.out.println("6 asetettu: " +v);

    v.setArvaus(13, '1');
    v.setArvaus(12, 'x');
    v.setArvaus(11, '2');
    System.out.println("loppuun 3:  " +v);
  }
}

Esimerkkiohjelman tulostus:

Tyhjä rivi: -------------
6 asetettu: 1x21X2-------
loppuun 3:  1x21X2----2x1