Skip to the content.

2.3 Ενθυλάκωση, Πακέτα και Αρθρώματα

© Γιάννης Κωστάρας


<- Δ ->

Ενθυλάκωση (Encapsulation)

Ένα από τα πολλά προβλήματα που υπήρχαν με το διαδικαστικό μοντέλο προγραμματισμού ήταν ότι υπήρχε πρόσβαση χωρίς κάποιον έλεγχο στις καθολικές μεταβλητές, δηλ. οποιοδήποτε τμήμα του προγράμματος μπορούσε να τις αλλάξει. Αυτό καθιστούσε πολύ δύσκολη την αποσφαλμάτωση, όταν εμφανίζονταν bugs που οφείλονταν στη μεταβολή των καθολικών μεταβλητών.

Το αντικειμενοστραφές μοντέλο έφερε περιορισμούς στην πρόσβαση των δεδομένων. Έτσι, πλέον, οι μεταβλητές ορίζονται και χρησιμοποιούνται μόνο από το τμήμα κώδικα που τις χρειάζεται. Επίσης τα δεδομένα ορίζονται πλέον μαζί με τις μεθόδους που τις αλλάζουν, δηλ. μέσα στις κλάσεις, όπως είδαμε στο προηγούμενο μάθημα.

Η ενθυλάκωση:

Πώς επιτυγχάνεται η ενθυλάκωση; Η Java παρέχει κάποιες δεσμευμένες λέξεις για το σκοπό αυτό:

Υπάρχουν δυο ακόμα τρόποι πρόσβασης που θα δούμε στη συνέχεια:

Συνήθως τα δεδομένα χαρακτηρίζονται σχεδόν πάντα ως privateprotected) και οι μέθοδοι που επιτρέπουν πρόσβαση σ’ αυτά τα δεδομένα ως public.

Τα παραπάνω συνοψίζονται στον παρακάτω πίνακα:

Τροποποιητής πρόσβασης Ίδια κλάση Ίδιο πακέτο Υποκλάση Άλλα πακέτα
public X X X X
protected X X X  
package ή τίποτα X X    
private X      

Ας δούμε πώς θα έπρεπε να είχαμε γράψει τη κλάση Car της προηγούμενης διάλεξης, κάνοντας σωστή χρήση της ενθυλάκωσης.

public class Car { // κλάση
  // ιδιότητες/γνωρίσματα
  private String model;
  private int maxSpeed;
  private int ccm;
  private int speed = 0;
  // μέθοδος δημιουργίας αντικειμένων - κατασκευαστής
  public Car(String m, int s, int c) {
    model = m; maxSpeed = s; ccm = c;
  }
  // ενέργειες/μέθοδοι
  public void accelerate() {
     if (speed <= maxSpeed - 10)
        speed+=10;
  }
  public void decelerate() {
     if (speed >= 10)
        speed-=10;
  }  
  public String getModel() {
  	return model;
  }
  public int getMaxSpeed() {
  	return maxSpeed;
  }
  public int getCcm() {
  	return ccm;
  }
  public int getSpeed() {
  	return speed;
  }
}

Πλέον έχουμε ενθυλακώσει τα γνωρίσματα της κλάσης ορίζοντάς τα ως private και την κλάση και τις μεθόδους της ως public πράγμα που σημαίνει ότι είναι προσβάσιμες από οποιαδήποτε άλλη κλάση του προγράμματος.

// Αντικείμενα
jshell> Car audiA3 = new Car("Audi A3", 210, 1595);
audiA3 ==> Car@6d00a15d
jshell> audiA3.speed
|  Error:
|  speed has private access in Car
|  audiA3.speed
|  ^----------^
jshell> audiA3.getSpeed()
$1 ==> 0
jshell> audiA3.accelerate();
jshell> audiA3.getSpeed()
$2 ==> 10

Πλέον δεν μπορούμε να καλέσουμε το γνώρισμα speed της κλάσης Car καθώς η πρόσβαση σ’ αυτό έχει οριστεί ως private. Θα πρέπει να χρησιμοποιήσουμε την αντίστοιχη μέθοδο get για να προσπελάσουμε το γνώρισμα. Το γνώρισμα έχει πλέον ενθυλακωθεί στην κλάση και δεν είναι προσβάσιμο πέραν των αντικειμένων της κλάσης.

Είναι καλή τακτική να περιορίζετε όσο γίνεται την πρόσβαση στα γνωρίσματα, τις μεθόδους και τις κλάσεις. Π.χ. αν γνωρίζετε ότι μια μέθοδος θα καλείται μόνο μέσα από την κλάση, τότε καλό είναι να την ορίσετε ως private. Μια κλάση που θα καλείται μόνο από κλάσεις από το ίδιο πακέτο στο οποίο ανήκει, καλό είναι να ορίζεται με τον εξ’ ορισμού τροποποιητή (δηλ. package).

Ο παρακάτω πίνακας δείχνει την αντιστοιχία μεταξύ των τροποποιητών πρόσβασης και του αντίστοιχου συμβόλου της UML:

Τροποποιητής πρόσβασης Συμβολισμός UML
public +
protected #
package ή τίποτα ~
private -

Αν ένα γνώρισμα υπολογίζεται (derived) από άλλα, τότε χρησιμοποιείται το σύμβολο /.

Εσωτερικές (inner) και Εμφωλιασμένες (nested) κλάσεις

Μπορούμε να ενθυλακώσουμε και άλλες κλάσεις μέσα σε κλάσεις. Αυτού του είδους οι κλάσεις λέγονται εσωτερικές (inner) κλάσεις. Διακρίνονται στις ακόλουθες κατηγορίες:

Μια εσωτερική (inner) κλάση είναι μια μη στατική κλάση που δηλώνεται μέσα σε μια άλλη κλάση. Αν η εσωτερική κλάση δηλωθεί ως static, τότε θεωρείται εμφωλιασμένη (nested) κλάση.

class OuterClass {
   private String aField;
   private int anotherField;
   
   class InnerClass {
   	  private int innerField;
   }
}

Για ν’ αρχικοποιήσουμε αντικείμενα μιας εσωτερικής κλάσης χρησιμοποιούμε την ακόλουθη σύνταξη:

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

ή πιο συνοπτικά:

OuterClass.InnerClass innerObject = (new OuterClass()).new InnerClass();

Με άλλα λόγια, μια εσωτερική κλάση σχετίζεται πάντα με ένα στιγμιότυπο της περικλείουσας εξωτερικής κλάσης. Ένα αντικείμενο της εσωτερικής κλάσης υπάρχει/ζει μόνο μέσα στα πλαίσια ενός αντικειμένου της εξωτερικής κλάσης. Για το λόγο αυτό δεν μπορεί να δηλώσει κανένα στατικό μέλος (γνώρισμα ή μέθοδο) ούτε να προσπελάσει τα στατικά μέλη της εξωτερικής κλάσης.

Μια εμφωλιασμένη κλάση ορίζεται, όπως είπαμε, ως στατική.

class OuterClass {
   private String aField;
   private int anotherField;
   
   static class NestedClass {
   	  private int nestedField;
   }
}

Αυτό σημαίνει ότι μπορούμε να δημιουργήσουμε αντικείμενα της εμφωλιασμένης κλάσης χωρίς να δημιουργήσουμε αντικείμενα της εξωτερικής κλάσης.

OuterClass.NestedClass nestedObject = new OuterClass.NestedClass();

Όπως συμβαίνει για όλα τα μέλη μιας κλάσης, έτσι και για τις εσωτερικές/εμφωλιασμένες κλάσεις μπορούμε να καθορίζουμε την εμβέλεια τους ως public, private, protected ή package.

Μια εσωτερική κλάση μπορεί να οριστεί:

και ανάλογα καθορίζεται και η εμβέλειά της.

Για τις εσωτερικές ανώνυμες κλάσεις θα μιλήσουμε στο επόμενο μάθημα.

Ας δούμε ένα παράδειγμα. Ορίζουμε την εσωτερική κλάση Engine μέσα στην κλάση Car.

public class Car {
    // ιδιότητες/γνωρίσματα
    private String model;
    private int maxSpeed;
    private int speed = 0;
    private Engine engine;

    // μέθοδος δημιουργίας αντικειμένων - κατασκευαστής
    public Car(String m, int s, int c, int r, int p) {
        model = m;
        maxSpeed = s;
        this.engine = new Engine(c, r, p);
    }
	
    // μέθοδος δημιουργίας αντικειμένων - κατασκευαστής
    public Car(String m, int s, Engine e) {
        model = m;
        maxSpeed = s;
        this.engine = e;
    }	

    // ενέργειες/μέθοδοι
    public void accelerate() {
        if (speed <= maxSpeed - 10)
            speed += 10;
    }

    public void decelerate() {
        if (speed >= 10)
            speed -= 10;
    }

    public String getModel() {
        return model;
    }

    public int getMaxSpeed() {
        return maxSpeed;
    }

    public int getSpeed() {
        return speed;
    }

    public Engine getEngine() {
        return engine;
    }

    class Engine {
        private int ccm;
        private int rpm;
        private int horsePower;

        public Engine(int ccm, int rpm, int power) {
            this.ccm = ccm;
            this.rpm = rpm;
            this.horsePower = power;
        }

        public int getCcm() {
            return this.ccm;
        }

        public int getRpm() {
            return this.rpm;
        }

        public int getPower() {
            return this.horsePower;
        }
    }
}

jshell> Car car = new Car("Audi A3", 210, 1984, 6000, 170);

Όπως είπαμε, δεν μπορούμε να δηλώσουμε στατικές μεταβλητές ή μεθόδους σε μια εσωτερική κλάση εκτός κι αν πρόκεται για σταθερές πρωτογενούς τύπου ή τύπου συμβολοσειράς. Μια εσωτερική κλάση έχει πρόσβαση σε όλα τα γνωρίσματα και τις μεθόδους της εξωτερικής κλάσης. Μπορούμε να προσπελάσουμε το αντικείμενο this της εξωτερικής κλάσης από την εσωτερική ως: Car.this.

Η παραπάνω κλάση Engine είναι εσωτερική της κλάσης Car. Μια εσωτερική κλάση μπορεί να δηλωθεί και μέσα στο σώμα μιας μεθόδου της εξωτερικής κλάσης.

Η σχέση μεταξύ της εσωτερικής και της εξωτερικής κλάσης είναι μια σχέση συσσωμάτωσης (aggregation) και μάλιστα σύνθετης συσσωμάτωσης (composite aggregation), που σημαίνει ότι όταν η εξωτερική κλάση καταστραφεί, καταστρέφεται μαζί της και η εσωτερική. Π.χ. χωρίς την κλάση Τράπεζα δε δύναται να υπάρχει η κλάση Λογαριασμός (άμα χρεωκοπήσει μια τράπεζα “εξαφανίζονται” κι όλοι οι λογαριασμοί). Η κλάση Car ενσωματώνει την κλάση Engine˙ όταν ένα αντικείμενο της κλάσης Car καταστραφεί, καταστρέφεται και η μηχανή της. Για να είναι η σχέση μεταξύ Car και Engine μια σχέση σύνθετης συσσωμάτωσης (composite aggregation), θα πρέπει η Engine να οριστεί ως private.

Εικόνα 2.3.1 Παράδειγμα σύνθετης συσσωμάτωσης (composite aggregation) στη UML

Αν ορίσουμε την ακόλουθη μέθοδο μέσα στη κλάση Car:

public class Car {
//...

    public static void main(String[] args) {
        Car.Engine engine = new Car.Engine(1984, 6000, 170);
        Car car = new Car("Audi A3", 210, engine);
    }

ο μεταγλωττιστής θα παραπονεθεί καθώς η Engine δεν μπορεί να κληθεί από μια στατική μέθοδο όπως είναι η main(). Το λάθος διορθώνεται εύκολα αν ορίσουμε την εσωτερική κλάση Engine ως staticEngine γίνεται πλέον εμφωλιασμένη (nested)):

static class Engine {
//...
}

Η Engine πλέον έχει πρόσβαση στα στατικά γνωρίσματα και μεθόδους της Car. Στην περίπτωση αυτή, η Engine μπορεί να “επιβιώσει” και χωρίς την Car, δηλ. η σχέση μεταξύ τους είναι απλής συσσωμάτωσης (aggregation).

jshell> Car car = new Car("Audi A3", 210, new Car.Engine(1984, 6000, 170));
car ==> Car@589838eb

Αν πρέπει να καλέσουμε την Engine από ένα στατικό περιβάλλον όπως η main() τότε η μόνη λύση είναι να την ορίσουμε ως static. Κάθε άλλη λύση, όπως φαίνεται παρακάτω δε δουλεύει:

jshell> Car car = new Car("Audi A3", 210, new Car.Engine(1984, 6000, 170));
|  Error:
|  an enclosing instance that contains Car.Engine is required
|  Car car = new Car("Audi A3", 210, new Car.Engine(1984, 6000, 170));
|    

jshell> Car.Engine engine = new Car.Engine(1984, 6000, 170);
|  Error:
|  an enclosing instance that contains Car.Engine is required
|  Car.Engine engine = new Car.Engine(1984, 6000, 170);
|                      ^-----------------------------^

jshell> Car.Engine engine = car.new Engine(1984, 6000, 170);
|  Error:
|  cannot find symbol
|    symbol:   variable car
|  Car.Engine engine = car.new Engine(1984, 6000, 170);
|                      ^-^

jshell> Car car = new Car("Audi A3", 210, car.new Engine(1984, 6000, 170));
|  Exception java.lang.NullPointerException
|        at Objects.requireNonNull (Objects.java:221)
|        at (#5:1)

Στην περίπτωση αυτή, χρειαζόμαστε ένα αντικείμενο τύπου Car για να μπορέσουμε να δημιουργήσουμε ένα αντικείμενο τύπου Engine. Καθώς όμως το αντικείμενο τύπου Car δεν έχει δημιουργηθεί ακόμα (καλούμε τον constructor του), η car.new Engine(...) επιστρέφει NullPointerException. Είναι δηλ. μια περίπτωση ‘chicken and egg’, η μέθοδος κατασκευής της Car χρειάζεται ένα αντικείμενο τύπου Engine για να δημιουργηθεί ενώ ταυτόχρονα η Engine χρειάζεται να υπάρχει ένα αντικείμενο τύπου Car. Μια άλλη πιθανή λύση (αν δε θέλουμε να μετατρέψουμε την Engine σε static) είναι να δημιουργήσουμε ένα αντικείμενο τύπου Car χωρίς μηχανή και στη συνέχεια να προσθέσουμε μια μηχανή με μια μέθοδο π.χ. addEngine(Engine engine) (αφήνεται ως άσκηση για τον αναγνώστη).

Χρησιμοποιήστε στατικές εμφωλιασμένες κλάσεις όταν θέλετε η κατάσταση (state) που αποθηκεύουν να είναι προσβάσιμη κι από άλλες κλάσεις. Ένα παράδειγμα είναι η εμφωλιασμένη κλάση Entry της Map όπως θα δούμε στα μαθήματα της επόμενης εβδομάδας. Διαφορετικά χρησιμοποιήστε εσωτερικές (inner) κλάσεις.

Η σχέση συσσωμάτωσης είναι μια σχέση τύπου “ΈΧΕΙ (HAS-A)” σε αντίθεση με τη σχέση της κληρονομικότητας που θα δούμε στο επόμενο κεφάλαιο που είναι σχέση τύπου “ΕΊΝΑΙ (IS-A)”. Π.χ. η κλάση Car “έχει μια” Engine.

Βιβλιοθήκες (packages)

Οι τροποποιητές πρόσβασης είναι ένας τρόπος περιορισμού της πρόσβασης στις κλάσεις. Ένας άλλος είναι οι βιβλιοθήκες (packages). Μπορούμε να ομαδοποιήσουμε τις κλάσεις σε βιβλιοθήκες.

Π.χ. utils\Util.java

package utils;
public class Util {
   static double circumference(double radius) { ... }
   private static void helper() { ... }
   public static area(double radius) { ... }
}

Αρκεί να χρησιμοποιήσουμε την λέξη-κλειδί package μαζί με το όνομα της βιβλιοθήκης, στην αρχή κάθε αρχείου, το οποίο θέλουμε να εντάξουμε στην βιβλιοθήκη. Καλό είναι να αποθηκεύσουμε τα αρχεία κλάσεων της βιβλιοθήκης σε έναν αντίστοιχο φάκελο του λειτουργικού συστήματος. Έτσι, η παραπάνω κλάση Util θα πρέπει να αποθηκευθεί σε ένα φάκελο utils. Αν δεν ορίσουμε package τότε η κλάση μας ανήκει στο εξ’ ορισμού (default) package.

Τα ονόματα των βιβλιοθηκών ακολουθούν συνήθως την ονοματολογία των πεδίων διαδικτύου (internet domains) τα οποία θεωρούνται μοναδικά, π.χ.

gr.mycompany.app                // αποθηκεύεται στη διαδρομή φακέλων gr/mycompany/app
gr.mycompany.app.model
gr.mycompany.app.view
gr.mycompany.app.controller
gr.mycompany.app.utils

Ο εξ’ ορισμού τροποποιητής πρόσβασης επιτρέπει σε κλάσεις της βιβλιοθήκης να έχουν πρόσβαση σε άλλες κλάσεις της ίδιας βιβλιοθήκης. Π.χ.

package utils;
class Helper {
   void prettyPrint(String... s) { ... }
}
private class Secret {

}

Η κλάση Helper μπορεί να έχει πρόσβαση στις μεθόδους circumference() και area() της κλάσης Util αλλά όχι και στη helper() η οποία είναι private. Αντίστοιχα η Util μπορεί να προσπελάσει την κλάση Helper και τη μέθοδό της prettyPrint() αλλά όχι τη Secret.

Αν η κλάση που θέλουμε να χρησιμοποιήσουμε ανήκει σε κάποια άλλη βιβλιοθήκη, τότε μπορούμε να την εισάγουμε με την εντολή import:

import utils.*;
...
Util.circumference(12.0);   // Error: έχει πρόσβαση package
Util.area(12.0);   // OK: έχει πρόσβαση public

διαφορετικά αν δεν χρησιμοποιήσουμε την import:

utils.Util.area(12.0);  			

Ο βασικός λόγος της εισαγωγής βιβλιοθηκών στην Java, μέσα στον πηγαίο κώδικα, οφείλεται στην ανάγκη διαχείρισης της ονοματολογίας (name spaces) για την αποφυγή συγκρούσεων (name clashes). Π.χ. τι θα συνέβαινε αν ορίζαμε δυο κλάσεις Util ή υπήρχε μια κλάση Util μέσα σε μια εξωτερική βιβλιοθήκη (jar) που χρησιμοποιούμε στο πρόγραμμά μας;

Με τη χρήση στατικής εισαγωγής (static import) μπορούμε να κάνουμε χρήση των στατικών πεδίων μιας κλάσης, χωρίς να χρησιμοποιούμε το όνομα της κλάσης στην οποία ανήκουν. Μπορούμε να εισάγουμε τα στατικά μέλη μιας κλάσης χρησιμοποιώντας την import static. Π.χ.

double radius = Math.sin(Math.PI * theta);

μπορεί να γραφτεί:

import static java.lang.Math.*;
double radius = sin(PI * theta);

Ο διερμηνέας εντοπίζει τις κλάσεις μιας βιβλιοθήκης ως εξής:

Από την γραμμή εντολών θα πρέπει να ορίσουμε το CLASSPATH στη γραμμή εκτέλεσης:

java -cp .:util MyClass.class

Για εξοικονόμηση χώρου, οι βιβλιοθήκες προγραμμάτων Java αποθηκεύονται σε αρχεία τύπου .jar (Java Archive). Αν θέλουμε να χρησιμοποιήσουμε μια εξωτερική βιβλιοθήκη θα πρέπει να την προσθέσουμε στο CLASSPATH, π.χ. στην παρακάτω γραμμή εκτέλεσης προσθέσαμε την εξωτερική βιβλιοθήκη log4j.jar που είναι βιβλιοθήκη αρχείων καταγραφής όπως θα δούμε σε επόμενα μαθήματα:

java -cp .:log4j.jar MyClass.class

Η UML διαθέτει τα διαγράμματα πακέτων (package diagrams). Στην παρακάτω Εικόνα 2.3.βλέπουμε ένα διάγραμμα βιβλιοθηκών με 4 βιβλιοθήκες καθώς και τις συσχετίσεις (dependencies) μεταξύ τους.

Εικόνα 2.3.2 Παράδειγμα διαγράμματος βιβλιοθηκών στη UML

Αρθρώματα (modules)

Προηγουμένως είδαμε πώς μπορούμε να περιορίσουμε την πρόσβαση σε άλλες κλάσεις χρησιμοποιώντας τα πακέτα ή/και τους τροποποιητές πρόσβασης.

Ας υποθέσουμε ότι έχουμε τις εξής κλάσεις:

package app.foo;
public class Foo {

}

package app.foo;
public class Bar {

}

package app.bla;
class Bla {

}

Θα θέλαμε η κλάση Foo να έχει πρόσβαση στην κλάση Bla αλλά όχι η κλάση Bar. Ο μόνος τρόπος είναι ν’ αλλάξουμε την κλάση Bla από πρόσβαση package σε public. Έτσι θα μπορεί η κλάση Foo να έχει πρόσβαση στην Bla αλλά τότε όμως και η Bar θα έχει πρόσβαση στην Bla. Πώς μπορούμε να το αποτρέψουμε αυτό; Δεν μπορούμε.

Από την έκδοση 9 και μετά, η γλώσσα εισήγαγε άλλο ένα επίπεδο ενθυλάκωσης, τα αρθρώματα (modules). Με τ’αρθρώματα η γλώσσα επιτρέπει τη δημιουργία προγραμμάτων με αρθρωτή αρχιτεκτονική (modular architecture), δηλ. συστημάτων με τα εξής χαρακτηριστικά:

Ένα άρθρωμα έχει ένα όνομα (π.χ. java.base), ομαδοποιεί σχετικό κώδικα και πόρους και περιγράφεται με έναν περιγραφέα αρθρώματος (module descriptor). Ο περιγραφέας αρθρώματος είναι ένα αρχείο module-info.java (στο αρχικό πακέτο).

Εικόνα 2.3.3 Περιγραφέας αρθρώματος (module descriptor)

Στην παραπάνω εικόνα, δημιουργώντας το αρχείο module-info.java στον αρχικό κατάλογο του προγράμματος AnagramGame, το μετατρέπουμε σε άρθρωμα. Το όνομά του είναι com.toy.anagrams. Το άρθρωμα εκθέτει ένα από τα πακέτα του, το com.toy.anagrams.util. Άλλα αρθρώματα μπορούν να προσπελάσουν μόνο αυτό το πακέτο του αρθρώματος. Επίσης, για να μπορεί να μεταγλωττιστεί και να εκτελεστεί το πρόγραμμα εξαρτάται από δυο ακόμα αρθρώματα τα java.logging και java.desktop.

Με την ευκαιρία της υποστήριξης αρθρωμάτων, ολόκληρο το JDK από την έκδοση 9 και μετά έχει τμηματοποιηθεί σε 19 αρθρώματα.

Εικόνα 2.3.4 Το JDK 9 τμηματοποιημένο σε αρθρώματα

Το άρθρωμα java.base είναι το βασικό άρθρωμα που εισάγεται εξ’ ορισμού σε όλα τα προγράμματα Java. Εκθέτει διάφορες βασικές βιβλιοθήκες όπως java.lang και java.util. Από εκεί και πέρα μπορείτε να προσθέσετε μόνο τα αρθρώματα που χρειάζεστε για την εφαρμογή σας. Π.χ. αν χτίζετε μια εφαρμογή desktop, προσθέστε μια εξάρτηση στο άρθρωμα java.desktop κλπ.

Πέρα από τα αρθρώματα java (28 αρθρώματα) υπάρχουν και αρθρώματα jdk (43 αρθρώματα) και javafx (7 αρθρώματα). Συνολικά το JDK έχει τμηματοποιηθεί σε 78 αρθρώματα (στην έκδοση 16 έχουν μειωθεί στα 67) τα οποία χωρίζονται σε δυο κατηγορίες: standard και non-standard. Standard είναι εκείνα των οποίων οι προδιαγραφές καθορίζονται από το Java Community Process (JCP) και τα ονόματά τους ξεκινούν με java. . Όλα τα υπόλοιπα αρθρώματα αποτελούν απλά μέρος του JDK και η ονομασία τους ξεκινά με jdk. ή javafx.:

java.activation
java.base
java.compiler
java.corba
java.datatransfer
java.desktop
java.instrument
java.jnlp
java.logging
java.management
java.management.rmi
java.naming
java.prefs
java.rmi
java.scripting
java.se
java.se.ee
java.security.jgss
java.security.sasl
java.smartcardio
java.sql
java.sql.rowset
java.transaction
java.xml
java.xml.bind
java.xml.crypto
java.xml.ws
java.xml.ws.annotation
javafx.base
javafx.controls
javafx.fxml
javafx.graphics
javafx.media
javafx.swing
javafx.web
jdk.accessibility
jdk.attach
jdk.charsets
jdk.compiler
jdk.crypto.cryptoki
jdk.crypto.ec
jdk.dynalink
jdk.editpad
jdk.hotspot.agent
jdk.httpserver
jdk.incubator.httpclient
jdk.jartool
jdk.javadoc
jdk.jcmd
jdk.jconsole
jdk.jdeps
jdk.jdi
jdk.jdwp.agent
jdk.jfr
jdk.jlink
jdk.jshell
jdk.jsobject
jdk.jstatd
jdk.localedata
jdk.management
jdk.management.agent
jdk.management.cmm
jdk.management.jfr
jdk.management.resource
jdk.naming.dns
jdk.naming.rmi
jdk.net
jdk.pack
jdk.packager.services
jdk.policytool
jdk.rmic
jdk.scripting.nashorn
jdk.sctp
jdk.security.auth
jdk.security.jgss
jdk.snmp
jdk.xml.dom
jdk.zipfs

Στις εφαρμογές σας θα χρησιμοποιήσετε τα 19 αρθρώματα που ξεκινούν με java. Τα υπόλοιπα αποτελούν το ίδιο το JDK. Π.χ. το jshell που μάθαμε στα μαθήματα της πρώτης εβδομάδας, βρίσκεται στο άρθρωμα jdk.jshell.

To Apache NetBeans υποστηρίζει modules.

Εικόνα 2.3.5 Δημιουργία αρθρώματος με το NetBeans

Εικόνα 2.3.6 Γραφική αναπαράσταση εξαρτήσεων από το NetBeans

module AnagramGame {
	requires java.logging;
	requires java.desktop;
	exports com.toy.anagrams.lib;
}

Στο παραπάνω παράδειγμα βλέπουμε ότι το άρθρωμά μας AnagramGame διαθέτει εξαρτήσεις σε δυο αρθρώματα της Java και εξάγει ένα πακέτο της προς διάθεση σε άλλα αρθρώματα. Εξάγοντας ένα πακέτο σημαίνει απλά ότι άλλα πακέτα έχουν πρόσβαση στα publicpackage) τμήματα (κλάσεις, μεθόδους, υπο-πακέτα κλπ.) του πακέτου. Δηλαδή, οι κανόνες πρόσβασης σε επίπεδο κλάσης, πακέτου κλπ. συνεχίζουν να ισχύουν.

Γενικά ισχύουν οι ακόλουθες λέξεις-κλειδιά σ’ ένα άρθρωμα:

Εικόνα 2.3.7 Απαγορεύονται οι κυκλικές εξαρτήσεις μεταξύ αρθρωμάτων

Στη UML τα αρθρώματα μπορούν να αναπαρασταθούν ως υποσυστήματα (subsystems). Στο παράδειγμα της παρακάτω εικόνας βλέπουμε πώς διάφορα υποσυστήματα (αρθρώματα) εξαρτώνται το ένα από το άλλο αλλά και πώς εμφωλιάζουν (nest) άλλα πακέτα ή υποσυστήματα.

Εικόνα 2.3.8 Παράδειγμα διαγράμματος αρθρωμάτων στη UML

Δημιουργία αυτόνομων προγραμμάτων Java

Μέχρι την έκδοση 8 της γλώσσας, κάποιος χρειαζόταν να έχει εγκατεστημένο στο σύστημά του το Java Runtime Environment (JRE) ώστε να μπορεί να εκτελεί προγράμματα Java. Το JRE είναι ένα υποσύνολο του Java Development Kit (JDK) που είναι αναγκαίο για την ανάπτυξη προγραμμάτων Java.

Από την έκδοση 9 και μετά, με την εισαγωγή των αρθρωμάτων, ο προγραμματιστής μπορεί να δημιουργήσει το δικό του JRE μαζί με την εφαρμογή του, το οποίο περιέχει μόνο τα αρθρώματα που απαιτούνται για να εκτελεστεί η εφαρμογή.

Ας δούμε πώς μπορούμε να δημιουργήσουμε μια αυτόνομη εφαρμογή AnagramGame. Κάντε δεξί κλικ στο Project και επιλέξτε Properties, Build --> Packaging και επιλέξτε Create JLINK distribution όπως φαίνεται στην ακόλουθη εικόνα.

Εικόνα 2.3.9 ‘Πακετάρισμα’ μιας εφαρμογής Java με τη βοήθεια του jlink

Όταν χτίσετε (build) το έργο σας, στο φάκελο dist δημιουργείται μαζί με το anagrams.jar μια ιεραρχία dist/jlink/AnagramGame:

bin/
conf/
include/
legal/
lib/
man/
release

η οποία περιλαμβάνει και το δικό σας (custom) JRE που απαιτείται για να εκτελέσετε το πρόγραμμά σας. Εκτελέστε το δίνοντας (ανάλογα με την πλατφόρμα σας):

dist/jlink/AnagramGame/bin/AnagramGame

Μπορείτε πλέον να συμπιέσετε το φάκελο dist και να μετονομάσετε το συμπιεσμένο αρχείο ως π.χ. AnagramGame.zip και να το διανέμετε.

Μπορείτε επίσης να δημιουργήσετε προγράμματα εγκατάστασης για τις διάφορες πλατφόρμες. Εμφανίστε πάλι το παράθυρο Properties και επιλέξτε Build --> Deployments και επιλέξτε Enable Native Packaging Actions in Project Menu και OK. Πλέον, όταν κάνετε δεξί κλικ στο έργο AnagramGame εμφανίζεται ένα νέο μενού Package as που σας επιτρέπει να δημιουργήσετε π.χ. installers για τις διάφορες πλατφόρμες.

Πηγές

  1. Apache NetBeans
  2. Baeldung, “Guide to jlink”
  3. Jenkov Nested Classes

<- Δ ->