Javan pakkauksista

Aiemmin Javan lähdekooditiedoston rakennetta kuvaavassa osiossa mainittiin, että tiedoston alussa voi olla muotoa “package pakkauksen_nimi;” oleva pakkausmääritys. Pakkausmääritys ilmaisee, että kaikki kyseisessä tiedostossa määritetyt luokat kuuluvat pakkausmäärityksessä nimettyyn pakkaukseen. Tarkastellaan esimerkiksi seuraavaa yksinkertaista lähdekoodia:

package fi.tuni.programming3;

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello World!");
  }
}

Edellä luokka HelloWorld määritettin kuuluvaksi pakkaukseen fi.tuni.programming3. Tämän efekti on, että luokka HelloWorld onkin oikeastaan tarkemmin ottaen luokka fi.tuni.programming3.HelloWorld. Pakkaus määrittää nimiavaruuden, johon luokka kuuluu (ja tämä mekanismi muistuttaa esim. C++:n nimiavaruuksia). Javassa pakkauksen sisältämiin luokkiin ja rajapintoihin viitataan samanlaisella pistenotaatiolla kuin luokkien jäseniin. Näin ollen toisaalta fi.tuni.programming3.HelloWorld on pakkauksessa fi.tuni.programming3 määritetty luokka HelloWorld, ja toisaalta fi.tuni.programming3.HelloWorld.main on kyseisessä luokassa määritetty main-funktio.

Edellä pakkauksen nimeksi oli määritetty “fi.tuni.programming3”, joka on esimerkki vallitsevasta käytännöstä, että pakkausten nimet pohjautuvat koodin kirjoittajan (taustaorganisaation) internet-osoitteeseen, jonka osat luetellaan käänteisessä järjestyksessä. Tässä pakkauksen nimen pohjana toimi Tampereen yliopiston internet-osoite “tuni.fi”, jonka perään oli lisätty tarkenne programming3 kuvaamaan koodin liittymistä tähän kurssiin. Ns. oikeassa maailmassa esimerkiksi Googlen julkaisemat Java-kirjastot sijaitsevat com.google-alkuisissa pakkauksissa, Microsoftin com.microsoft-alkuisissa, jne.

Pakkausten avulla voidaan välttää keskenään samannimisten luokkien nimien yhteentörmäys. Esimerkiksi Javan omassa luokkakirjastossa on kaksi eri luokkaa, joiden nimi on Date: toinen on pakkauksessa java.sql ja toinen pakkauksessa java.util. Ilman pakkauksia niitä ei voisi käyttää samassa ohjelmassa, koska Java-kääntäjä ei tietäisi, kumpaan luokkaan nimi Date viittaa. Luokkiin kuitenkin voidaan viitata koodissa myös pakkauksen nimen kera, jolloin edellämainittuihin Date-luokkiin voidaan viitata yksiselitteisesti muodoissa java.sql.Date ja java.util.Date. Samaan pakkaukseen ei luonnollisestikaan pidä määrittää kahta keskenään samannimistä luokkaa, koska silloin päädyttäisiin jälleen nimien yhteentörmäykseen.

Pakkaukset vs import

Aiemmin on mainittu, että lähdekooditiedoston alussa olevilla import-lauseilla voidaan tuoda käyttöön muualla määritettyjä luokkia. Tarkemmin ottaen import-lauseiden roolina on mahdollistaa jossain muussa pakkauksessa määritettyyn luokkaan viittaaminen luokan pelkistetyllä nimellä. Jos luokkaan viittaa pakkauksen kera, ei import-lausetta tarvita. Esimerkiksi seuraavat kaksi pakkauksessa java.util määritetyn luokan Arrays sort-funktiota käyttävää koodia ovat toiminnallisesti identtisiä:

import java.util.Arrays;

public class SortedParameters {
  public static void main(String[] args) {
    Arrays.sort(args);
    for(String arg : args) {
      System.out.println(arg);
    }
  }
}
public class SortedParameters {
  public static void main(String[] args) {
    java.util.Arrays.sort(args);
    for(String arg : args) {
      System.out.println(arg);
    }
  }
}

Kuten edellä mainittiin, import koskee nimenomaan eri pakkauksissa määritettyjä luokkia. Kooditiedostosta voi viitata sen kanssa samassa pakkauksessa määritettyihin muihin luokkiin suoraan pelkällä luokan nimellä tarvitsematta import-lausetta.

Toinen motiivi pakkausten käyttöön on koodin organisointi niin, että samantapaisiin tehtäviin liittyvät luokat on määritetty keskenään samaan pakkaukseen. Tämä osaltaan helpottaa koodin ylläpitoa suurissa ohjelmistoissa, joissa voi olla satoja tai tuhansiakin luokkia. Pakkausten käyttö itse asiassa suoranaisesti pakottaakin organisoimaan tiedostot hierarkkiseen hakemistorakenteeseen: Java tulkitsee pakkauksen nimen pisteillä erotelluksi alihakemistopoluksi, joka kertoo, missä alihakemistossa kyseisen pakkauksen tiedostot sijaitsevat. Esimerkiksi ylempänä esitetty pakkaukseen fi.tuni.programming3 määritetty luokka HelloWord pitäisi sijoittaa alihakemistoon “fi/tuni/programming3” (tai Windowsissa “fi\tuni\programming3”). Ohjelmaa ajettaessa Java-virtuaalikoneelle tulisi antaa luokan nimi pakkauksen kera eli ajo tehtäisiin tapaan java fi.tuni.programming3.HelloWorld. Tällöin virtuaalikone aloittaisi suorituksen luokkatiedostosta fi/tuni/programming3/HelloWorld.class (tai Windowsissa fi\tuni\programming3\HelloWorld.class).

Kun käytetään aiemmin mainittua periaatetta, että pakkaukset nimetään käänteisesti internet-osoitteen mukaan, tulee koodi organisoitua hakemistorakenteeseen, jossa esimerkiksi kaikki saman organisaation koodit ovat keskenään samassa alihakemistossa.

Maven ja pakkaukset

Myös Maven-projekteissa on mahdollista (ja suotavaa) käyttää pakkauksia. Samaan tapaan kuin edellä kooditiedoston alkuun laitetaan pakkausmääritys, tässä esimerkissä package fi.tuni.programming3;. Kooditiedostot myös organisoidaan pakkauksia vastaavaan kansiorakenteeseen. Edellä esitetty esimerkkiluokka tulisi Maven-projektissa siis sijoittaa polkuun src/main/java/fi/tuni/programming3/HelloWorld.java.

Pakkauksien käyttö edellyttää myös muutamia muutoksia projektitiedostoon pom.xml. HelloWorld-luokalle tehdyn Maven-projektin projektitiedosto voisi näyttää esimerkiksi tältä:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fi.tuni</groupId>
  <artifactId>programming3</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>com.jolira</groupId>
        <artifactId>onejar-maven-plugin</artifactId>
        <version>1.4.4</version>
        <executions>
          <execution>
            <configuration>
              <mainClass>fi.tuni.programming3.HelloWorld</mainClass>
              <onejarVersion>0.97</onejarVersion>
              <attachToBuild>true</attachToBuild>
            </configuration>
            <goals>
              <goal>one-jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Mavenilla hallituissa Java-projekteissa on vakiintunut käytäntö se, että groupId ja artifactId muodostavat yhdessä projektin Java-pakkauksen nimen. Jos tavoitteena on esimerkiksi sijoittaa projektin tiedostot fi.tuni.programming3-nimiseen pakkaukseen, annetaan groupId -elementin arvoksi fi.tuni ja artifactId-elementin arvoksi programming3. Mavenia tuntevat ohjelmoijat olettavat, että tätä käytännettä noudatetaan. Jatka siksi perinnettä itsekin, vaikka pakkaus ja elementti voivat olla teknisellä tasolla eri nimisiä.

Huomaa myös, että onejar-pluginin mainClass-elementissä on annettava pääluokka pakkauksen kanssa, eli tässä esimerkissä elementin sisältö on fi.tuni.programming3.HelloWorld.

Tämän viikon ja seuraavien viikkojen tehtävissä kooditiedostot tulee aina sijoittaa annettuun pakkaukseen, jotta tehtävät menevät tarkistimesta läpi.