Helsingin yliopisto / Tietojenkäsittelytieteen laitos
Copyright © 2005 Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

5.3 Yleisiä tietorakenteita, geneerisyyttä ja näppärät luokat: Vector<E> ja Hashtable<K,V>

(Muutettu viimeksi 12.12.2005)

Luku esittelee lyhyesti luokan Object, vähän uutta geneerisyyttä sekä uudistetut tietorakenneluokat Vector<E> ja Hashtable<K,V>. (Tämä luku on vanha versio! Uudistettu luku 5.3 esittelee "uudenaikaisemmat" vastaavat luokat ArrayList ja Hashmap.)

Vector on vaihtelevan kokoinen taulukko, jonne voi tallettaa mitä tahansa Javan olioita. Hashtable on rakenne, jolla voi luoda ns. assosiaatiolistoja: olioon voidaan liittää jokin toinen olio ja ensimmäisellä assosioda toinen, esimerkiksi "avaimella voidaan hakea arvoa".

Uudistetuissa luokissa Vector<E> ja Hashtable<K,V> on käytetty ns. geneerisyyttä, käännösaikaista tyyppiparametrointia. Tähän hyödylliseen ja voimakkaaseen Javan uutuuteen ei peruskursseilla ole kovin paljon aikaa käytettävissä. Esimerkit kuitenkin antavat mallia välineiden käyttöön omissa ohjelmissa.

Luokka Object

Luokka Object on ainoa Javan luokka, jolla ei ole yliluokkaa. Kaikki muut luokat ovat sen välillisiä tai välittömiä aliluokkia.

Luokassa Object on joitakin metodeita, jotka siis periytyvät kaikille muille luokille. Vanha tuttu on toString(), joka on tapana aliluokissa korvata (override) sopivasti. Object-luokan toString() muodostaa oliosta kuin oliosta tekstiesityksen, joka kertoo olion luokan ja heksadesimaaliluvun, joka antaa tietoa olion sijainnista tulkin tietorakenteissa, esim. Pikkuvarasto@80cb94a.

Object-tyyppiselle muuttujalle voi sijoittaa minkä tahansa olioarvon, mutta tällöin tuolle oliolle voi soveltaa vain Object-luokan metodeita. Jos näitä on polymorfisesti korvattu aliluokissa (ks. luku 4.4), valitaan kuitenkin olion oman luokan metodi.

Kun Object-tyyppisen muuttujan arvona on jonkin aliluokan ilmentymä ja tuolle oliolle halutaan soveltaa metodia, jota Object-luokasta ei löydy, olioarvo on muunnettava explisiittisellä tyyppimuunnoksella olion oman luokkan tyyppiin, esim:

    Object otus = new Pikkuvarasto();
    // ...
    otus.vieVarastoon(3.14);                  // väärin!
    ((Pikkuvarasto)otus).vieVarastoon(3.14);  // sallittu
    //...
    
    otus = "BÖÖ!";
    int i;
    i = otus.length();           // väärin!
    i = ((String)otus).length(); // sallittu
Huom: Komponenttiin viittaus (".") sitoo tiukemmin kuin eksplisiittinen tyyppimuunnos, siksi nuo sulkeet tyyppimuunnoksen ympärillä ovat välttämättömät, ks. "Operaatioiden sitovuus".

Olion luokkaa voi kysellä instanceof-operaatiolla:

    Object otus = new Pikkuvarasto(); 
    // ...
    if (otus  instanceof  Pikkuvarasto)
      ((Pikkuvarasto)otus).vieVarastoon(3.14);
    else if (otus  instanceof String)
      i = ((String)otus).length();

Vähän geneerisyydestä, hyvin vähän...

Ennen geneeristen tyyppien käyttömahdollisuutta yleisten tietorakenteiden käyttäminen oli vähän vaivalloista, koska alkiot täytyi itse muuntaa sopivan tyyppisiksi sekä rakenteeseen vietäessä, että sieltä otettaessa - aivan samaan tapaan kuin yllä meneteltiin Object-tyyppisen muuttujan kohdalla:
    ((Pikkuvarasto)otus).vieVarastoon(3.14);
    double määrä = ((Pikkuvarasto)otus).otaVarastosta(30);

Uudessa Javassa tyyppiin voidaan liittää käännösaikaisia tyyppiparametretreja:

public class GenTyyppi<T> {

  private T kenttä;
    
  public GenTyyppi() { }

  public GenTyyppi(T kenttä) {       
    this.kenttä = kenttä;
  }
     
  public void printtaa() {
    System.out.println(kenttä);
  }
}

Ja ilmentymiä voidaan luoda antamalla konstruktorille jokin tyyppi parametriksi:
public class GenEsim {

  public static void main(String[] args) {

    GenTyyppi<String> x  = new GenTyyppi<String>();
    GenTyyppi<String> xx = new GenTyyppi<String>("BÖÖ");

    x.printtaa();
    xx.printtaa();

    GenTyyppi<Pikkuvarasto> y  = new GenTyyppi<Pikkuvarasto>();
    GenTyyppi<Pikkuvarasto> yy = new GenTyyppi<Pikkuvarasto>(
                                    new Pikkuvarasto(10, "Mehu"));
    y.printtaa();
    yy.printtaa();
  }
}
Ohjelma tulostaa:
null
BÖÖ
null
(Mehu: 10.0)

Koska jo kääntäjä tietää todellisen tyyppiparametrin, se osaa generoida itse tarvittavat tyyppimuunnokset! Alkeistyypit (esim. int, double, boolean, ...) eivät kuitenkaan kelpaa todelliseksi tyyppiparametriksi, vaan niiden sijalla on käytettävä vastaavia luokkia, ns. "käärepaperiluokkia" ("wrapper") Integer, Double, Boolean, ...

Näiden "käärepaperiluokkin" käyttäminen on tehty helpoksi ns. "autoboxing"-menettelyn avulla. Koska kääntäjä tietää tietorakenteeseen vietyjen alkeistyyppisten arvojen tyypin, se osaa automaattisesti generoida tarvittavat tyyppimuunnokset tietoja rakenteeseen vietäessä ja niitä sieltä otettaessa. Esimerkki nähdään Vector-luokan esittelyn lopussa.

Tällä peruskurssilla geneerisyydestä opetellaan vain geneeristen luokkien Vector<E> ja Hashtable<K,V> käyttöä. Omien geneeristen luokkien ohjelmointi on melko monitahoista ja vaatii tietoa enemmän kuin kurssille mahtuu. Pieni esimerkki erääseen harjoitustehtävään liittyen saattaa silti olla mielenkiintoista luettavaa: KokeileGenVertTaulukkoa.java.

[Lisätietoa Javan uudesta geneerisyydestä löytyy tietenkin Sunin sivuilta sekä erityisesti Angelika Langerin erinomaisilta sivuilta "Java Generics FAQs - Frequently Asked Questions". Uusi geneerisyys ei ole välttämättä vielä lopullisessa asussaan; Langer toteaa eräässä tiedotteessaan: "My conclusion from looking into the issues is: refrain from overloading, unless you are sure you fully understand overload resolution in Java, which is a mystery even to me". Jos geneerisyyden erikoisasiantuntijallakin on ongelmia ymmärtää metodien kuormittamisen ja geneerisyyden suhdetta, on luultavaa, että jossakin on vielä vikaa...]

Vector<E>

Luokka Vector<E> määrittelee näennäisesti "vaihtelevanmittaiset" taulukot, joiden komponenttityyppi annetaan Vector-oliota luotaessa. Komponentit on tavallisen taulukon tapaan numeroitu indeksein nollasta alkaen. Komponenttien lisääminen ja poistaminen muuttaa lisäys- ja poistokohtaa seuraavien komponenttien indeksejä.

Joitakin Vector-luokan välineitä:

Esimerkki: Ohjelma lukee ensin joukon lukuja. Sitten lukuja voi poistaa. Lopuksi ohjelma tulostaa jäljelle jääneiden lukujen summan:

import java.util.*;

public class VectorEsim {

  private static Scanner lukija = new Scanner(System.in);

  public static void main(String[] args) {

    Vector<Integer> taulu = new Vector<Integer>();
    int luku;

    System.out.println("Syötä lukuja, nolla lopettaa.");    
    luku = lukija.nextInt();
    while (luku != 0) {
      taulu.addElement(luku);
      luku  = lukija.nextInt();
    }

    System.out.println(taulu);

    System.out.println("Syötä poistettavia lukuja, nolla lopettaa.");
    luku = lukija.nextInt();
    while (luku != 0) {
      boolean oli = taulu.removeElement(luku);
      if (oli)
        System.out.println(taulu);
      else 
        System.out.println("Ei löydy");
      luku = lukija.nextInt();
    }
 
    System.out.println(taulu);
    System.out.println("Alkioiden summa = " + summa(taulu));
  }

  private static int summa(Vector<Integer> tau) {
    int summa = 0;
    for (int i=0; i < tau.size(); ++i)
        summa += tau.elementAt(i);
    return summa;
  }
}
Ilman edellä jo mainostettua "autoboxingia" esimerkiksi lauseet
     taulu.addElement(luku);
     ...
     summa += tau.elementAt(i);
joutuisi kirjoittamaan paljon kömpelömmin:
     taulu.addElement(new Integer(luku));
     ...
     summa += ((Integer)(tau.elementAt(i))).intValue();


Hashtable<K,V>

Luokan Hashtable ilmentymä on rakenne, jolla K-tyyppisistä arvoista, avaimista ("key"), voi assosioida V-tyyppisiä arvoja ("value"). "Avaimella siis haetaan avaimeen liitettyä arvo".

Joku voisi sanoa, että Hashtable-olio on kuin taulukko, jota "indeksoidaan" K-tyyppisillä arvoilla, ei kokonaisluvuilla..

Luokan Vector tapaan Hashtable on aina "riittävän" suuri. Avaimena käytettävien arvojen luokan K on toteutettava hashCode- ja equals-metodit (tästä ei tässä enempää; mm. String, Integer, Double, ..., kelpaavat tyyppiparametria K vastaavaksi todelliseksi tyypiksi).

Joitakin Hashtable-luokan välineitä:

Esimerkki: "Kielenkäännösohjelma". Ohjelma opiskelee ensin sanapareja "sana alkukielelellä" - "sana käännettynä toiselle kielelle". Sitten ohjelma tarjoaa sanojen käännöspalvelun:
import java.util.*;

public class HashtableEsim {
  private static Scanner lukija = new Scanner(System.in);

  public static void main(String[] args) {

    Hashtable<String,String> taulu = new Hashtable<String,String>();
    String sana1, sana2;

    do {
      System.out.print("Sana alkukielellä? (tyhjä lopettaa) ");
      sana1 = lukija.nextLine();
      if (sana1.equals("")) 
        break;

      if (taulu.containsKey(sana1)) {
        System.out.println("Sana \"" + sana1 + "\" löytyy jo!");
        System.out.println("Vanha käännös " + "\"" +taulu.get(sana1) + 
                           "\" jää voimaan!");
        continue; // uusi alkukielinen sana
      }

      System.out.print("Sana käännettynä?  (tyhjä lopettaa) ");
      sana2 = lukija.nextLine();
      if (sana2.equals("")) 
        break;  // syöttö loppui!

      // nyt molemmat sanat ovat ei-tyhjiä ja ensimmäinen on uusi

      taulu.put(sana1, sana2);

    } while (true); // loppuu jomman kumman sanan ollessa ""

    System.out.println(taulu);

    do {
      System.out.print("Minkä sanan käännöksen haluat tietää? " +
                       "(tyhjä sana lopettaa) "); 
      sana1 = lukija.nextLine();
      if (sana1.equals(""))
        break;  // kysely loppui!
      
      if (!taulu.containsKey(sana1))
        System.out.println("Sanan \"" + sana1 +"\" käännös on tuntematon!");
      else
        System.out.println("Sanan \"" + sana1 + "\" käännös on \"" + 
                           taulu.get(sana1) +"\"");

    } while (true); // loppuu tyhjään sanaan
  }
}

Esimerkki Vector<E>-olioista Hashtable<K,V>-rakenteessa

Geneerisiä rakenteita voidaan tietenkin myös yhdistellä ihan samaan tapaan kuin kurssilla aiemmin on tehty vaikkapa olioiden taulukoita ja olioita, joiden sisällä on taulukoita. On siis mahdollista esimerkiksi luoda Vector<E>-oliota, joiden komponentit ovat Hashtable<K,V>-oliota. Ja päinvastoin.

Tässä pikku esimerkissä Hashtable<K,V>-olioon talletetaan avaimiksi sanoja ja assosioitaviksi arvoiksi sanojen luetteloita Vector<E>-olioina:

import java.util.*;

public class HVEsimerkki {

  public static void main(String[] args) {

    Hashtable<String, Vector<String>> seuraajat =
        new Hashtable<String,Vector<String>>();

    Vector<String> sanoja = new Vector<String>();

    sanoja.addElement("kissa");
    sanoja.addElement("hiiri");
    sanoja.addElement("hevonen");

    seuraajat.put("eläimiä", sanoja);

    Vector<String> lukuja = new Vector<String>();
    lukuja.addElement("134");
    lukuja.addElement("-23");
    lukuja.addElement("9871");

    seuraajat.put("lukuja", lukuja);

    System.out.println(seuraajat);
  }
}


Takaisin luvun 5 sisällysluetteloon.
Java and all Java-based marks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the U.S. and other countries. University of Helsinki is independent of Sun Microsystems, Inc.