Εισαγωγή στην Java - κεφ. 7

...ασύγχρονα μαθήματα γλώσσας Java

Εισαγωγή στην Java - κεφ. 7

Δημοσίευσηαπό alkismavridis » 19 Ιουν 2012, 22:47

Προηγούμενο: Κλάσεις, Αντικείμενα και Μέθοδοι
Επόμενο: Πίνακες - Πέρασμα ορισμάτων

Σύνδεση προγραμμάτων -
Σχέσεις μεταξύ Αντικειμένων και Κλάσεων


Σκοπός αυτού του κεφαλαίου είναι η κατανόηση του μηχανισμού με τον οποίο η Java μας επιτρέπει να συνδέουμε τα προγράμματά μας με άλλα προγράμματα Java, καθώς και να κατανοήσουμε τις σχέσεις μεταξύ κλάσεων και αντικειμένων σε μία τέτοια περίπτωση. Θα δούμε το μηχανισμό με τον οποίο η Java εντοπίζει κλάσεις στον υπολογιστή μας, και θα μάθουμε τη χρήση των λέξεων-κλειδί import, package, public, protected, private, extends και super.

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

1.1 Χρήση πολλαπλών εκτελέσιμων/bytecode
Μπαίνουμε σε ένα πολύ ενδιαφέρον κεφάλαιο του προγραμματισμού! Ας φανταστούμε ότι έχουμε φτιάξει ένα πρόγραμμα (μία κλάση) που δημιουργεί ένα αντικείμενο και κάνει κάποια χρήσιμα πράγματα (μέσω πχ των μεθόδων του). Μπορούμε λοιπόν να εκτελέσουμε το πρόγραμμά μας (εκτελέσιμο ή bytecode), και έτσι εκμεταλλευόμαστε το δημιούργημά μας. Ωραία ως εδώ.
Ας πούμε τώρα ότι φτιάχνουμε ένα νέο πρόγραμμα, και για κάποιο λόγο θέλουμε να χρησιμοποιήσουμε κάποια από τα εργαλεία που φτιάξαμε στο προηγούμενο. Πως μπορεί να γίνει αυτό; Μπορεί να σκεφτείτε: σιγά το πράγμα! πάμε στον παλιό κώδικα, και κάνουμε «αντιγραφή - επικόλληση» τις μεθόδους, τους constructor και οτιδήποτε άλλο χρειάζεται στον καινούριο κώδικα. Compile και όλα καλά! Έ δάσκαλε;
Απάντηση: Όχι!! Γιατί εκτός από το ότι κάτι τέτοιο θα ήταν τελείως άκομψο και χρονοβόρο (άντε τώρα αν δύο πεδία ή μέθοδοι τυχαίνει να έχουν ίδιο όνομα, να μετονομάζεις σε όλο τον κώδικα για να τα ξεχωρίσεις!), ο constructor θα πάψει να λειτουργεί στην καινούρια κλάση γιατί... το όνομά της δε θα συμπίπτει πια με το δικό του, όπως πρέπει.

Ευτυχώς η Java (και πολλές άλλες γλώσσες) μας δίνουν τη δυνατότητα να «ενσωματώσουμε» στα προγράμματά μας εργαλεία από άλλα! Αυτός ο μηχανισμός έχει δύο διαφορετικές εκδοχές: είναι δηλαδή τελείως διαφορετικός για εκτελέσιμα απ' ότι για bytecode, οπότε διαχωρίστε στο μυαλό σας τις δύο αυτές περιπτώσεις.

Επειδή οι μηχανισμοί που θα περιγράψουμε παρακάτω ίσως να διαφέρουν ανάλογα με τον compiler, σας προτρέπω να ξεσκονίσετε το manual του compiler που χρησιμοποιείτε, και να βρείτε τις αντίστοιχες παραγράφους.

Για να το κάνετε αυτό πατήστε στο τερματικό "man " και το όνομα του compiler σας, πχ
Κώδικας: Επιλογή όλων
man gcj
.

1.2 Πακέτα
Αρχικά θα πρέπει να γνωρίσουμε την έννοια του πακέτου. Θα καταπιαστούμε με αρκετές λεπτομέρειες που αφορούν τα πακέτα, χωρίς ακόμα να είναι εμφανής ο σκοπός τους. Μη σας αποθαρρύνει αυτό: στο τέλος θα ανταμειφθείτε!
Πακέτο είναι απλώς μία συλλογή από κλάσεις. Στη συλλογή αυτή δίνουμε ένα ορισμένο όνομα (που είναι το όνομα του πακέτου). Στο όνομα αυτό μην βάζετε τελείες '.', γιατί αυτές όπως θα δούμε στη συνέχεια έχουν ένα ιδιαίτερο ρόλο!

Σύμβαση: Στα ονόματα των πακέτων δε χρησιμοποιούμε κεφαλαία γράμματα.

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

Κάθε κλάση μπορεί να ανήκει μόνο σε ένα πακέτο. Για να «βάλουμε» την κλάση μας σε κάποιο πακέτο γράφουμε στον κώδικα πριν από οποιαδήποτε άλλη εντολή, την
Μορφοποιημένος Κώδικας: Επιλογή όλων
package όνομα;
πχ
package myclasses;


1.3 Σχέση των πακέτων με το σύστημα αρχείων για τα bytecode
Έχουμε πει ότι όταν φτιάχνουμε bytecode, ο κώδικας της κλάσης πρέπει να βρίσκεται σε αρχείο με τίτλο ίδιο με τον τίτλο της και την κατάληξη .java. Αντίστοιχα, η ίδια η κλάση θα βρίσκεται σε αρχείο με κατάληξη .class
Αν μετονομάσετε το πρώτο, απλά δε θα γίνει compile, ενώ αν μετονομάσετε το δεύτερο, το jre δε θα τρέξει το bytecode. Υπάρχουν δηλαδή εμφανείς περιορισμοί όσον αφορά τις ονομασίες των αρχείων μας. (κατά την παραγωγή εκτελέσιμων αυτοί οι περιορισμοί δεν υπάρχουν)
Αν τώρα η κλάση μας οριστεί να βρίσκεται μέσα σε κάποιο πακέτο, εμφανίζεται ένας ακόμη περιορισμός: το .class αρχείο πρέπει να βρίσκεται σε ένα φάκελο που θα έχει το όνομα του πακέτου.
Ας πούμε ότι φτιάχνουμε μία κλάση με όνομα Linux και την έχουμε βάλει στο πακέτο με όνομα freeos.
Το .class αρχείο θα έχει τίτλο Linux.class, και θα πρέπει να βρίσκεται μέσα σε ένα φάκελο με όνομα freeos! Στην περίπτωση που μία κλάση ανήκει σε ένα πακέτο, επηρεάζεται το όνομά της (όπως το βλέπει η java).
Στο παράδειγμά μας ας πούμε, το πλήρες όνομα της κλάσης θα είναι πλέον όχι σκέτο Linux, αλλά freeos.Linux ! Με άλλα λόγια:

Το πλήρες όνομα μιας κλάσης που βρίσκεται σε ένα πακέτο είναι το όνομα του πακέτου, τελεία, και το όνομα της κλάσης!



1.4 Εκτέλεση bytecode που βρίσκεται σε πακέτο
Για να τρέξουμε ένα bytecode που βρίσκεται μέσα σε πακέτο κάνουμε τα εξής:

1.Γράφουμε τον κώδικα και κάνουμε compile κατά τα γνωστά (μην ξεχάσετε την εντολή package στην αρχή).
2.Φτιάχνουμε ένα φάκελο με το όνομα του πακέτου ως τίτλο (αν δεν υπάρχει ήδη), και βάζουμε μέσα το .class αρχείο. (εννοείται ότι μπορούμε να έχουμε δημιουργήσει εξ αρχής την φάκελο αυτό, και να έχουμε βάλει μέσα τον κώδικα...)
3. Πάμε με το τερματικό μας έξω από τον φάκελο αυτό εκτελούμε την εντολή java ή gij με το πλήρες όνομα της κλάσης μας, όπως περιγράφεται παραπάνω.

Δηλαδή ας υποθέσουμε ότι έχουμε το παρακάτω απλό προγραμματάκι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο AClass.java
package apack;
public class AClass {

int afield;

public AClass (int i) { //ο όρος public θα εξηγηθεί μετά...
this.afield = i;
//αυτή η εντολή είναι ισοδύναμη με την afield=i;
}//AClass

public void info() {//μία απλή μέθοδος
System.out.print("Είμαι ένα αντικείμενο της κλάσης AClass. Έχω afield ίσο με "+afield+".\n");
}//info

public static void main(String[] arg) {
System.out.print("Είμαι η main της κλάσης AClass, του πακέτου apack.\n");
}//main

}//class


Ας δούμε πως θα τρέξουμε αυτό το πρόγραμμα:
Κώδικας: Επιλογή όλων
1. Κάνουμε compile
gcj -C AClass.java

2. Δημιουργούμε ένα φάκελο με όνομα apack, αν δεν υπάρχει ήδη και μετακινούμε το .class αρχείο που μόλις φτιάξαμε. Αυτό γίνεται είτε με το ποντίκι (δημιουργία φακέλου, και αποκοπή-επικόλληση) είτε μέσω τερματικού με τις δύο παρακάτω:
mkdir apack
mv AClass.class apack

Όπως είπαμε πριν, μπορούμε να έχουμε δημιουργήσει από πριν αυτό τον φάκελο, και όλη η «δουλειά» να έχει γίνει σε αυτόν

3. Δεν μπαίνουμε στον φάκελο apack. Από τον πατρικό φάκελο του apack πατάμε:
Κώδικας: Επιλογή όλων
java apack.AClass
Έφτιαξα ένα αντικείμενο AClass με afield 11


Αυτό είναι, όπως βλέπετε το πρόγραμμα έτρεξε! Συνοψίζοντας θα λέγαμε ότι από τη σκοπιά του συστήματος αρχείων, το bytecode βρίσκεται στον φάκελο [.../.../]apack, και έχει όνομα AClass. Από τη σκοπιά του jre όμως, το bytecode βρίσκεται στον προηγούμενο (πατρικό) φάκελο, δηλαδή στον [.../.../] και έχει όνομα apack.AClass
Γι αυτό άλλωστε, για να το εκτελέσουμε, πάμε σε αυτόν τον φάκελο και πατάμε "java apack.AClass"


1.5 Υποπακέτα
Όπως φαίνεται από τα παραπάνω, υπάρχει κάποια σχέση μεταξύ φακέλων στο σύστημα αρχείων μας και των πακέτων της java: οι κλάσεις συμβολίζουν τα αρχεία, και οι φάκελοι τα πακέτα. Και όπως ένας φάκελος περιέχει αρχεία και άλλους φακέλους, έτσι και τα πακέτα μπορούν να περιέχουν κλάσεις, αλλά και άλλα πακέτα!
Ας πούμε λοιπόν ότι θέλουμε να φτιάξουμε ένα υποπακέτο με όνομα subpack μέσα στο πακέτο apack που φτιάξαμε πρίν. Η ερώτηση βασικά είναι πως θα «βάλουμε» μία κλάση στο υποπακέτο μας.
Απάντηση: απλώς θα γράψουμε την εντολή package στη δήλωση της κλάσης μας, με το πλήρες όνομα του πακέτου μας, όπως θα γράφαμε τη διαδρομή ενός αρχείου στο σύστημα αρχείων μας (αρχίζοντας από τον πατρικό φάκελο του πακέτου μας). Εδώ όμως αντί για το σύμβολο / που χρησιμοποιούμε στο linux για τη διαδρομή αρχείων, θα χρησιμοποιούμε την τελεία. Υπό αυτή τη σκοπιά η εντολή
Κώδικας: Επιλογή όλων
package apack.subpack;
δηλώνει ότι η κλάση μας θα βρίσκεται σε ένα πακέτο subpack, το οποίο με τη σειρά του θα βρίσκεται στο apack.
Γι αυτό άλλωστε σας είπα πριν ότι στο όνομα του πακέτου δεν χρησιμοποιούμε τελείες: για τον ίδιο λόγο που στο όνομα ενός φακέλου δε χρησιμοποιούμε το '/'. Αν ο compiler δει στην εντολή package μία τελεία, θα καταλάβει ότι πρόκειται για υποπακέτο!
Ας φτιάξουμε μία άλλη απλή κλάση, την Sub1 η οποία θα βρίσκεται στο υποπακέτο subpack...

Μορφοποιημένος Κώδικας: Επιλογή όλων
//Αρχείο Sub1.java
package apack.subpack;

public class Sub1 {
String s;
public Sub1(String st) { //επίσης. Αγνοείστε προς το παρών τον όρο public. Όλα θα γίνουν ξεκάθαρα σε λίγο...
s=st;
}//constructor

public void message() { //μία απλή μέθοδος
System.out.print("Είμαι ένα αντικείμενο της κλάσης Sub1, με s = "+ s +"\n");
}//message

public static void main(String[] args) {
System.out.print("Είμαι η main της κλάσης Sub1 από το πακέτο subpack!\n");
}//main
}//class


Με την ίδια λογική όπως και πριν, το πλήρες όνομα της κλάσης μας θα είναι η διαδρομή της μέσα στα πακέτα, ακολουθούμενη από το όνομά της. Στην περίπτωσή μας δηλαδή apack.subpack.Sub1
Για να τρέξουμε το bytecode που θα δημιουργηθεί, πρέπει το .class αρχείο να βρίσκεται σε ένα φάκελο με όνομα subpack, ο οποίος με τη σειρά του θα βρίσκεται στον φάκελο apack. Πάμε στον πατρικό φάκελο του apack όπως πριν και γράφουμε
Κώδικας: Επιλογή όλων
java apack.subpack.Sub1
ή
gij apack.subpack.Sub1


1.6 Σύνδεση με άλλα προγράμματα σε bytecode
Σε ένα οποιοδήποτε πρόγραμμα μπορούμε να κάνουμε αναφορές σε πεδία, μεθόδους και constructors από άλλα Java bytecode. Έτσι μπορούμε να εκμεταλλευτούμε εργαλεία από bytecode που έχουμε φτιάξει στο παρελθόν, ή που έχει φτιάξει κάποιος άλλος!
Το πρώτο βήμα για να κάνουμε κάτι τέτοιο είναι να καταλάβουμε τον τρόπο με τον οποίο το ή ο compiler θα ψάξει για τα άλλα bytecode. Και αυτό γιατί όπως καταλαβαίνετε, το jre/compiler δε θα «σαρώσει» ολόκληρο το σύστημα αρχείων μας για να τα βρει: κάτι τέτοιο θα ήταν υπερβολικά αργό, και θα είχε πολλά άλλα πρακτικά προβλήματα. Θα πρέπει να υπάρχει λοιπόν κάποιος τρόπος να λέμε στο jre (ή τον compiler) πιο «στοχευμένα» που να ψάξει.
Και ακριβώς εδώ κολλάει ο ρόλος των πακέτων, και όλοι περιορισμοί που έχουμε κάνει περί ονομασίας φακέλων αλλά και .java και .class αρχείων. Γράφοντας το πλήρες όνομα μίας κλάσης λέμε στο jre/compiler να ψάξει στα τρία εξής σημεία για να τη βρει:

1. Σε έναν φάκελο που «εκ φύσεώς του» ψάχνει για να βρεί κλάσεις. Συνήθως βρίσκεται στο <jre>/lib/rt.jar, όπου <jre> ο φάκελος που βρίσκεται το jre σας (εμένα πχ βρίσκεται στο /usr/lib/jvm/java-6-openjdk-amd64/jre). Εκεί υπάρχει μία πληθώρα έτοιμων κλάσεων που μας δίνει η Java, όπως η java.lang.String και η java.util.Scanner που έχουμε χρησιμοποιήσει επανειλημμένα.
2. Στον τρέχων φάκελο. Ο τρέχων φάκελος είναι εκείνος από τον οποίο τρέξαμε την εντολή java ή gij.
3. Σε μία άλλη πληθώρα φακέλων που ορίζεται από τη μεταβλητή συστήματος CLASSPATH. Κάθε σύστημα Unix ή Linux έχει αυτή τη μεταβλητή. Ο τρόπος που μπορούμε να τη διαβάσουμε ή να την τροποποιήσουμε είναι ο ίδιος όπως κάθε άλλη μεταβλητή συστήματος: echo CLASSPATH για ανάγνωση, και export CLASSPATH = φάκελος1;φάκελος2; κτλ για τροποποίηση...

Η παραπάνω διαδικασία είναι ίδια και όταν κάνουμε compile μία κλάση, και όταν την εκτελούμε. Αυτό που γίνεται είναι ότι, με αφετηρία τα τρία σημεία που προαναφέραμε, ο compiler ή το jre ψάχνει για τα αρχεία με όνομα το όνομα της κλάσης. Κάθε φορά που το όνομα αναφέρει πακέτο, ψάχνει για ένα φάκελο με τίτλο το όνομα του πακέτου για να ανοίξει! Έτσι εντοπίζονται οι κλάσεις.
Η μόνη διαφορά μεταξύ εκτέλεσης και compile είναι ότι κατά το compile, ο compiler θα ψάξει αρχικά για ένα .class αρχείο για να «συνδέσει». Αν δε το βρεί ψάχνει για ένα .java αρχείο με τον ίδιο τίτλο. Αν το βρει, το κάνει και αυτό compile και έπειτα συνδέει με το bytecode που προκύπτει.
Σε αυτό το σημείο να τονίσω ότι αντίθετα σε ότι συμβαίνει σε άλλες γλώσσες (C/C++), στην java έχοντας μόνο το «πρόγραμμα», δηλαδή το bytecode, μπορούμε να το εκμεταλλευτούμε από άλλα προγράμματα. Δε χρειαζόμαστε ούτε τον κώδικα, ούτε τα λεγόμενα header files ως βοήθεια. Το bytecode είναι αρκετό, περιέχει όλη την πληροφορία!

1.7 Δυνατότητες χρήσης
Ας δούμε τώρα τι μπορούμε να κάνουμε στον κώδικά μας, εφ όσον είμαστε σίγουροι ότι ο compiler θα βρει τις «εξωτερικές» κλάσεις. Με το πλήρες όνομα της κλάσης αυτής μπορούμε:
i. να δηλώσουμε μία μεταβλητή
ii. να καλέσουμε τους constructor
iii. να καλέσουμε static μεθόδους και static πεδία

για τα non static πεδία και μεθόδους χρησιμοποιούμε κατά τα γνωστά το όνομα του αντικειμένου.

Όταν λέμε ότι το πρόγραμμά μας θα χρησιμοποιεί εργαλεία από άλλα προγράμματα, αυτό δε σημαίνει ότι το bytecode μας θα «περιέχει» τις πληροφορίες αυτές (static linking) αλλά ότι θα τις αντλήσει από κάπου αλλού (dynamic linking). Του λέμε απλώς τι να βρει και που.


Στα bytecode δεν υφίσταται static linking, σε αντίθεση με τα εκτελέσιμα όπου και τα δύο είδη μπορούν να υπάρξουν.

Ας δούμε τώρα στην πράξη πως ένα πρόγραμμα αντλεί πληροφορίες από ένα άλλο: θα γράψουμε μία κλάση που θα τη βάλουμε στον πατρικό φάκελο του apack.

Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο PackTest.java
public class PackTest {

static java.util.Scanner sc;
/*δήλωση μεταβλητής. Τη δηλώνουμε static για να μπορούμε να
έχουμε πρόσβαση χωρίς τη δημιουργία αντικειμένου PackTest.*/
static apack.AClass a; //ομοίως
static apack.subpack.Sub1 b;

public static void main(String ar[]) {

sc = new java.util.Scanner(System.in);
System.out.print("Δώσε έναν ακέραιο και ένα μικρό κείμενο\n");

int i = sc.nextInt(); //όπως βλέπετε, δε χρειαζόμαστε πια κανένα java.util.. απλώς το όνομα του αντικειμένου!
String str = sc.next();

a = new apack.AClass(i);
b = new apack.subpack.Sub1(str);

a.info();
b.message();

//με την ίδια λογική μπορούμε να καλέσουμε την μέθοδο main των κλάσεων αυτών:
apack.AClass.main(null);
apack.subpack.Sub1.main(null);

}//main
}//class


Κάντε compile και τρέξτε το παραπάνω: θα διαπιστώσετε ότι θα βρει τις κλάσεις apack.AClass και apack.subpack.Sub1, και θα χρησιμοποιήσει μέσα από αυτές τους constructor και τις μεθόδους τους. Η σύνδεση επετέγφθη!

1.8 Η εντολή import
Υπάρχει η δυνατότητα, αντί να αναφερόμαστε σε μία κλάση με το πλήρες όνομά της, να γράφουμε μόνο τον τίτλο της, αρκεί πρώτα να το έχουμε «πει» στον compiler, για να ξέρει και εκείνος για ποια κλάση μιλάμε. Πώς γίνεται αυτό;
Με την εντολή import! Την εντολή αυτή τη γράφουμε έξω από τη δήλωση της κλάσης, και ακολουθείται από ένα όνομα: το πλήρες όνομα της κλάσης που θέλουμε να χρησιμοποιήσουμε. Από εκεί και πέρα μπορούμε να αναφερόμαστε στην κλάση αυτή μόνο με τον τίτλο της. Μπορούμε να κάνουμε import όσες κλάσεις θέλουμε.
Θα τροποποιήσουμε λοιπόν το παραπάνω πρόγραμμα, κάνοντας import δύο από τις τρεις παραπάνω κλάσεις που εμφανίζονται (την java.util.Scanner και την apack.subpack.Sub1). Ας το δούμε:

Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο PackTest.java
import java.util.Scanner;
import apack.subpack.Sub1;

public class PackTest {

static Scanner sc; //δείτε, το πρόθεμα java.util δε χρειάζεται πια!
static apack.AClass a; //εδώ χρειάζεται πρόθεμα, γιατί δεν κάναμε import την AClass
static Sub1 b;

public static void main(String ar[]) {

sc = new Scanner(System.in);
System.out.print("Δώσε έναν ακέραιο και ένα μικρό κείμενο\n");

int i = sc.nextInt();
String str = sc.next();
System.out.print("\n");

a = new apack.AClass(i);
b = new Sub1(str);

a.info();
b.message();

apack.AClass.main(null);
Sub1.main(null);
}//main
}//class


Βλέπουμε ότι οι «οροσειρές» των ονομάτων με τις τελείες περιορίστηκαν κάπως.

Υπάρχει όμως και μία άλλη μορφή της εντολής import: μπορούμε να κάνουμε import ένα ολόκληρο πακέτο! Αυτό είναι πολύ χρήσιμο όταν θέλουμε να κάνουμε import πολλές κλάσεις που βρίσκονται σε κάποιο πακέτο, και δεν υπάρχει κανένας λόγος να τις κάνουμε μία-μία. Αυτό γίνεται με το σύμβολο του αστερίσκου, το '*'
Παράδειγμα, για να κάνουμε import όλες τις κλάσεις του πακέτου java.util, γράφουμε
Κώδικας: Επιλογή όλων
import java.util.*;

Μετά από αυτό, μπορούμε να χρησιμοποιούμε οποιαδήποτε κλάση του πακέτου java.util. Ο compiler θα τα συνδέσει με το ανάλογο .class αρχείο. Τις κλάσεις του πακέτου που δε θα χρησιμοποιήσουμε, ο compiler είναι αρκετά έξυπνος για να τις αγνοήσει.
Για να δούμε πως λειτουργεί αυτό, θα φτιάξουμε μία πανομοιότυπη κλάση με την AClass, την BClass και θα τη βάλουμε δίπλα στην AClass, δηλαδή στο πακέτο apack. Ας το δούμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο BClass.java
package apack;
public class BClass {

int afield;

public BClass (int i) {
this.afield = i;
}//BClass

public void info() {
System.out.print("Είμαι ένα αντικείμενο της κλάσης BClass. Έχω afield ίσο με "+afield+".\n");
}//info

public static void main(String[] arg) {
System.out.print("Είμαι η main της κλάσης BClass, του πακέτου apack.\n");
}//main

}//class


Ας κάνουμε τώρα το πρόγραμμα PackTest να καλεί και την BClass. Θα κάνουμε χρήση της εντολής import για ολόκληρο το πακέτο apack:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο PackTest.java
import java.util.Scanner;
import apack.*;
import apack.subpack.Sub1;

public class PackTest {

static Scanner sc;
static AClass a;
static BClass b;
static Sub1 sb;

public static void main(String ar[]) {

sc = new Scanner(System.in);
System.out.print("Δώσε έναν ακέραιο και ένα μικρό κείμενο\n");

int i = sc.nextInt();
String str = sc.next();
System.out.print("\n");

a = new AClass(i);
b = new BClass(i);
sb = new Sub1(str);

a.info();
b.info();
sb.message();

AClass.main(null);
BClass.main(null);
Sub1.main(null);
}//main
}//class


Όπως βλέπετε, μπορούμε να αναφερόμαστε και στην AClass και στην BClass με το τίτλο τους μόνο, μιας και τις κάναμε import και τις δύο με την εντολή
Κώδικας: Επιλογή όλων
import apack.*;


Σημαντικό: Όταν κάνουμε import ένα πακέτο, δε σημαίνει ότι κάνουμε και τα υποπακέτα του!

Αυτό στην περίπτωσή μας σημαίνει ότι το πακέτο subpack δεν γίνεται import με την εντολή import apack.*; και γι αυτό πρέπει να κάνουμε ξεχωριστά import οτιδήποτε θέλουμε από αυτό. Αν σβήσετε την εντολή import apack.subpack.Sub1; ο compiler θα σας πετάξει error.

Τέλος να σχολιάσουμε την ειδική περίπτωση όπου κάνουμε import δύο κλάσεις με τον ίδιο τίτλο που βρίσκονται σε διαφορετικά πακέτα.
Ας πάρουμε ένα παράδειγμα από την «πραγματική ζωή». Ανάμεσα στη θάλασσα έτοιμων αντικειμένων που μας δίνει η Java είναι και τα java.util.Timer, και javax.swing.Timer που όπως βλέπετε έχουν διαφορετικά πλήρη ονόματα, αλλά ίδιο τίτλο.
Τι θα γίνει λοιπόν στην περίπτωση που κάνουμε import και τα δύο (είτε ως ξεχωριστές κλάσεις είτε ολόκληρα τα πακέτα τους) και μετά γράψουμε κάπου στο πρόγραμμά μας Timer; Ποιό Timer από τα δύο;
Σε αυτή την περίπτωση ο compiler θα μας παραπονεθεί (και με το δίκιο του) ότι δεν μπορεί να καταλάβει ποιο Timer εννοούμε, και άρα πρέπει να του γράψουμε το πλήρες όνομα της κλάσης για να ξεχωρίσει. Σε μία τέτοια περίπτωση λοιπόν η εντολή import δε θα μας γλιτώσει από το επιπλέον γράψιμο. :eh:

1.9 Σύνδεση προγραμμάτων σε εκτελέσιμα
Εδώ τα πράγματα είναι πολύ διαφορετικά! Ό,τι έχουμε πει περί ονοματολογίας πακέτων και χρήση της εντολής import ισχύουν απαράλλαχτα.
Σε ότι αφορά όμως το μηχανισμό σύνδεσης ενός εκτελέσιμου με άλλα, αυτός είναι πολύ πιο περίπλοκος, αλλά και γενικός. Η διαδικασία ονομάζεται στα αγγλικά linking, και είναι ίδια (ή σχεδόν ίδια) για όλες τις γλώσσες προγραμματισμού.
Μία βαθύτερη κατανόηση των δυνατοτήτων του μηχανισμού αυτού είναι εκτός του σκοπού αυτού του οδηγού (όποιος έχει όρεξη μπορεί να διαβάσει τα manual των ld, gcj και μερικές παραγράφους από αυτό του gcc). Παρ' όλα αυτά θα δώσω δύο μεθόδους με τις οποίες μπορούμε να συνδέουμε τα java εκτελέσιμά μας μεταξύ τους.

i. Όλοι οι κώδικες σε ένα μεγάλο εκτελέσιμο
Αυτή είναι η πιο εύκολη εκδοχή. Απλώς γράφουμε σαν input για τον compiler, όλα τα αρχεία που θα χρειαστεί (όπου κι αν βρίσκονται)! Σε αυτή την περίπτωση ο compiler θα φτιάξει ένα μεγάλο εκτελέσιμο που θα τα περιέχει όλα μέσα του. Πχ στην περίπτωσή μας θα πηγαίναμε στον φάκελο με που βρίσκεται το PackTest.java και θα γράφαμε
Κώδικας: Επιλογή όλων
gcj --main=PackTest -o Program PackTest.java apack/AClass.java apack/BClass.java apack/subpack/Sub1.java

Όταν μία κλάση βρίσκεται εντός κάποιου πακέτου, στο σημείο "main=..." γράφουμε το πλήρες όνομα της κλάσης.



ii. Ο κάθε κώδικας σε ξεχωριστά αρχεία
Εδώ πρέπει να ακολουθήσουμε τους περιορισμούς όπως και για τα bytecode. Το κάθε .class αρχείο πρέπει να βρίσκεται σε συγκεκριμένο φάκελο, ανάλογα με το πακέτο στο οποίο βρίσκεται η κλάση μας.
Πάμε σε κάθε ένα από τα «βοηθητικά αρχεία» που θα χρειαστεί να συνδέσουμε το τελικό μας πρόγραμμα (στην περίπτωσή μας είναι τα AClass.java, BClass.java και Sub1.java) και γράφουμε τα εξής:
Κώδικας: Επιλογή όλων
gcj -shared -fPIC -o ένα_όνομα όνομα_αρχείου.java

στην περίπτωσή μας δηλαδή
Κώδικας: Επιλογή όλων
alkis@Alkis:~/Programs/java/Tutorial$ cd apack/
alkis@Alkis:~/Programs/java/Tutorial/apack$ gcj -shared -fPIC -o L1.o AClass.java
alkis@Alkis:~/Programs/java/Tutorial/apack$ gcj -shared -fPIC -o L2.o BClass.java
alkis@Alkis:~/Programs/java/Tutorial/apack$ cd subpack/
alkis@Alkis:~/Programs/java/Tutorial/apack/subpack$ gcj -shared -fPIC -o L3.o Sub1.java
alkis@Alkis:~/Programs/java/Tutorial/apack/subpack$

(τα ονόματα L1.o L2.o και L3.o είναι ενδεικτικά, δώστε ό,τι όνομα προτιμάτε!)
Έπειτα πάμε στο κύριό μας πρόγραμμα και γράφουμε ό,τι θα γράφαμε κανονικά, προσθέτωντας στο τέλος μία λίστα από -l: ακολοθούμενο από τη διαδρομή προς τα .o αρχεία που δημιουργήσαμε πιο πρίν (χωρίς κενό μετά το ':' ). Δηλαδή στην περίπτωσή μας:
Κώδικας: Επιλογή όλων
alkis@Alkis:~/Programs/java/Tutorial/apack/subpack$ cd ../..
alkis@Alkis:~/Programs/java/Tutorial$ gcj --main="PackTest" -o PR PackTest.java -l:apack/L1.o -l:apack/L2.o -l:apack/subpack/L3.o

Με αυτό τον τρόπο φτιάχνουμε ένα αρχείο το οποίο έχει «συνδεθεί» με τα άλλα, χωρίς όμως να τα περιέχει όπως συνέβη στην πρώτη περίπτωση.

1.10 Τελευταία σχόλια
i. Το ότι μπορεί μία κλάση να χρησιμοποιεί εργαλεία από άλλες, μπορούμε να το δούμε και αντίστροφα: μία κλάση που γράφουμε τώρα, ίσως να την γράφουμε όχι με σκοπό κάποιος να την «τρέξει» απ' ευθείας, αλλά να χρησιμοποιήσει τις μεθόδους , τους constructors της κτλ από ένα άλλο, «μεγαλύτερης κλίμακας» πρόγραμμα. Υπό αυτή την οπτική γωνία, μπορούμε να συμπεράνουμε ότι δε χρειάζεται οπωσδήποτε μία κλάση να έχει κάποια μέθοδο main!!!
Ας πάρουμε παράδειγμα την κλάση Scanner. Τι είναι αυτή η κλάση;
Τίποτα άλλο από ένα αντικείμενο που έφτιαξε κάποιος καλός άνθρωπος για να το παίρνουμε έτοιμο εμείς και να κάνουμε τη δουλειά μας.
Αν βλέπαμε ας πούμε τον κώδικα την κλάσης Scanner, πιστεύετε ότι θα υπήρχε κάποια μέθοδος main μέσα; Ώστε να γράψετε πχ "java Scanner" για να «τρέξετε» την κλάση Scanner; Για να γίνει τι;
Η απάντηση είναι προφανώς όχι. Η κλάση αυτή φτιάχτηκε μόνο... για να την καλούμε από αλλού!
Καταλαβαίνουμε λοιπόν ότι είμαστε ελεύθεροι να μην φτιάχνουμε μία main σε κάθε κλάση μας. Ο compiler δε θα παραπονεθεί καθόλου, και θα δημιουργήσει κανονικά ένα .class αρχείο.
Αν τώρα εμείς είμαστε ξεροκέφαλοι, και πάμε να «τρέξουμε» αυτό το bytecode πατώντας "java ...", τότε το jre θα παραπονεθεί ότι δεν υπάρχει μέθοδος main να ξεκινήσει. Ο σκοπός μίας τέτοιας κλάσης θα είναι να τη χρησιμοποιήσουμε από αλλού.
main λοιπόν θα φτιάχνουμε μόνο στις κλάσεις τις οποίες θα καλεί απ ευθείας το jre.

ii. Ο διορατικός αναγνώστης ίσως κάνει το εξής εύλογο ερώτημα: ωραία μας τα λές εώς τώρα, αλλά εγώ τόσο καιρό χρησιμοποιούσα κλάσεις όπως η String, ή η System, χωρίς να χρειάζεται να κάνω τίποτα import! Πως γίνεται αυτό; Που βρήκε ο compiler τις κλάσεις αυτές και «έκανε τη δουλειά»;
Απάντηση: ο compiler κάνει αυτόματα import ένα πακέτο, το οποίο ονομάζεται java.lang και έχει πολλές, και πολύ χρήσιμες κλάσεις (όπως οι δύο που αναφέραμε). Γι αυτό δε χρειάζεται ποτέ να κάνουμε import τις κλάσεις αυτές. :D

Επίσης αυτόματα import γίνεται το «τρέχων» πακέτο, δηλαδή το πακέτο που ανήκει η κλάση της οποίας γράφουμε τον κώδικα.
Πχ μέσα στον κώδικα της BClass (από τις κλάσεις που φτιάξαμε παραπάνω) θα μπορούσαμε να αναφερόμαστε στις υπόλοιπες κλάσεις του πακέτου apack (δηλαδή στην AClass) χωρίς να κάνουμε τίποτα import.


2.1 Προσβασιμότητα σε μέλη μία κλάσεις
Μπαίνουμε τώρα στη δεύτερη ενότητα του κεφαλαίου αυτού: στο τι μπορούμε να κάνουμε αφού συνδέσουμε την κλάση μας με άλλες.
Όπως είδαμε παραπάνω, μπορούμε να αναφερόμαστε σε πεδία, constructor και μεθόδους άλλων κλάσεων. Με την ίσια λογική, την κλάση που φτιάχνουμε τώρα, μπορεί κάποιος να τη χρησιμοποιήσει στη συνέχεια από κάποια άλλη, ας την πούμε «εξωτερική κλάση».
Εδώ γεννάται το εξής ερώτημα: μπορούμε να φτιάξουμε πεδία, constructor ή μεθόδους που να μην μπορούν να χρησιμοποιηθούν από όλες τις εξωτερικές κλάσεις; Δηλαδή να ορίσουμε στην κλάση μας, τι θα είναι προσβάσιμο και τι όχι;
Το γιατί να κάνουμε κάτι τέτοιο μπορεί να σας φαίνεται αίνιγμα, αλλά -πιστέψτε με- είναι πολύ σημαντικό σε ορισμένες περιπτώσεις.

Η απάντηση είναι φυσικά ένα μεγάλο Ναι.
Υπάρχει ο μηχανισμός εκείνος που λέει ότι το τάδε πεδίο/constructor/μέθοδος μπορεί να είναι προσβάσιμο με τον τάδε τρόπο. Υπάρχουν τέσσερις τέτοιοι «τρόποι», τέσσερις δηλαδή κατηγορίες ως προς την προσβασιμότητα. Σε ποια από τις τέσσερις ανήκει το κάθε ένα καθορίζεται κατά τη δήλωσή τους ως εξής:

i. Με χρήση της λέξης-κλειδί public
Αν δηλώσουμε ένα πεδίο ως public, αυτό είναι προσβάσιμο για ανάγνωση/τροποποίηση από οποιαδήποτε άλλη κλάση.
Ομοίως, ένας constructor ή μία μέθοδος που είναι δηλωμένη ως public μπορεί να καλείται από οποιαδήποτε άλλη κλάση.

ii. Με χρήση της λέξης-κλειδί protected
Δηλώνοντας ένα πεδίο ως protected, αυτό είναι προσβάσιμο (ανάγνωση/τροποποίηση) από όλες τις κλάσεις που ανήκουν στο ίδιο πακέτο με αυτήν που γράφουμε. Από κλάσεις άλλων πακέτων το πεδίο είναι αόρατο! Υπάρχει μεν στη μνήμη του υπολογιστή μας για να επιτελέσει το σκοπό του, αλλά αυτός που (από κάποια άλλη κλάση εκτός πακέτου) θα αναφερθεί σε αυτό στον κώδικά του, θα λάβει error από τον compiler του.
Με την ίδια λογική, ένας constructor ή μία μέθοδος που δηλώνεται ως protected, μπορεί να καλεστεί μόνο από κάποια κλάση στο ίδιο πακέτο με την «τρέχουσα».

iii. Χωρίς καμία λέξη-κλειδί
Αυτό που κάνουμε πιο συχνά, δηλαδή να μη χρησιμοποιούμε καμία ειδική λέξη-κλειδί για να δηλώσουμε τα μέλη της κλάσης μας. Αυτή η κατηγορία ονομάζεται package-private, και προς το παρόν φανταστείτε την να έχει ακριβώς τις ιδιότητες της περίπτωσης ii, της protected. H διαφορά θα γίνει εμφανείς σε επόμενες παραγράφους.

iv. Με χρήση της λέξης-κλειδί private
Φτάσαμε στην πιο αυστηρή κατηγορία. Σε ένα πεδίο που ορίζεται ως private, μπορούμε να έχουμε πρόσβαση μόνο μέσα από την κλάση αυτή. Καμία «εξωτερική» κλάση δεν μπορεί να το ανιχνεύσει.
Ομοίως, ένας private constructor ή μέθοδος μπορεί να καλεστεί μόνο εντός της κλάσης αυτής.

Αν πάμε (από μία εξωτερική κλάση) να «αγγίξουμε» ένα μέλος που δεν έχουμε δικαίωμα όπως καθορίζεται από τα παραπάνω, ο compiler θα μας βγάλει error ότι δεν μπορεί να έχει πρόσβαση στο συγκεκριμένο μέλος.

Ας δούμε μερικά παραδείγματα δήλωσης:
Μορφοποιημένος Κώδικας: Επιλογή όλων
public int i;
public Scanner sc1, sc2;
public static boolean b1 = true;
public void myMethod(int j) { ...
public AClass() { ...

protected static final double pi = 3.14159;
protected static int aMethod() { ...
protected MyClass() { ...

char ch;
String method() { ...
BClass() { ... //constructor

private int secret= 2362;
private static AClass obj;
aMethodeThatReturnsAClass() { ...
private Sub1() { ...


Όταν προγραμματίζουμε, προσπαθούμε να κάνουμε τα μέλη της κλάσης μας «όσο πιο private» γίνεται. Κάτι θα είναι public μόνο αν δεν επηρεάζει σημαντικά τη λειτουργία της κλάσης. Μία πολύ καλή τακτική είναι να έχουμε τα «ευαίσθητα» πεδία μίας κλάσης private (ή το πολύ protected), και να έχουμε public μεθόδους για να διαβάσουμε ή να τροποποιήσουμε το πεδίο.
Ας δούμε ένα παράδειγμα. Περίπτωση 1:
Μορφοποιημένος Κώδικας: Επιλογή όλων
public double pleura = 5;
public double square = 25;

Κάποιος έξω από την κλάση μπορεί να τροποποιήσει όπως θέλει το pleura η το square: δεν μπορούμε ούτε να ελέγξουμε τον τρόπο με τον οποίο θα το κάνει, ούτε να κάνουμε το πρόγραμμα να «αντιδρά» σε μία τέτοια τροποποίηση.

Περίπτωση 2:
Μορφοποιημένος Κώδικας: Επιλογή όλων
private double pleura = 5;
private double square = 25;

public void setPleura(double d) {

if(d<0) {
System.out.print("Δε γίνεται να υπάρχει αρνητική πλευρά\n");
return;
}//if

pleura = d;
square = d*d;
}//setPleura

public double getPleura() {
return pleura;
}//getPleura

public double getSquare() {
return square;
}//getSquare

Εδώ αυτός που θα χρησιμοποιήσει την κλάση μας δε θα μπορεί να «αγγίξει» απ ευθείας τις μεταβλητές pleura και square. Οι μέθοδοι όμως setPleura, getPleura και getSquare μπορούν (αφού είναι στην ίδια κλάση με τις length και square). Ο χρήστης της κλάσης μας μπορεί να καλεί τις μεθόδους αυτές για να διαβάσει ή να τροποποιήσει την length.
Τη square μπορεί μόνο να τη διαβάσει μέσω της μεθόδου getSquare.

Τι καταφέραμε: πρώτον να μη μπορεί κάποιος εκτός κλάσης να δίνει οποιαδήποτε τιμή στην pleura, αλλά μόνο θετικές τιμές. Μπορούμε έτσι να έχουμε το κεφάλι μας ήσυχο, σε τυχών μαθηματικές πράξεις ότι κρατάμε στα χέρια μας ένα θετικό αριθμό (πως θα ήμασταν σίγουροι για κάτι τέτοιο αν η μεταβλητή ήταν public; Ο χρήστης της κλάσης μας θα μπορούσε να δώσει ό,τι θέλει!)
Δεύτερον η αλλαγή της pleura έξω απ την κλάση μπορεί να γίνει μόνο καλώντας την μέθοδο setLength. Άρα ταυτόχρονα με την αλλαγή της μεταβλητής θα εκτελεστούν και όλες οι υπόλοιπες εντολές της μεθόδου! Άρα κάνουμε το πρόγραμμά μας ικανό να αντιδρά με έναν ορισμένο τρόπο (εδώ αλλάζει το square ώστε να ταιριάζει με το καινούριο pleura). Μπορούμε λοιπόν να είμαστε σίγουροι ότι η μεταβλητή square είναι μονίμως «ενημερωμένη».

Ας δούμε ένα ολοκληρωμένο παράδειγμα:
Γράψτε τις δύο παρακάτω κλάσεις:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο Cube.java

public class Cube {
private double pleura, embado, ogos;

public Cube(double d) {
pleura = d;
embado = 6*d*d;
ogos = d*d*d;
}//constructor

public void setPleura(double d) {
pleura = d;
embado = 6*d*d;
ogos = d*d*d;
}//setPleura

public double getPleura() {
return pleura;
}//getPleura

public double getEmbado() {
return embado;
}//getPleura

public double getOgos() {
return ogos;
}//getOgos
}//class

//μόλις φτιάξαμε την πρώτη μας κλάση χωρίς μέθοδο main...


Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο Test.java

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

Cube a = new Cube(3);
java.util.Scanner sc = new java.util.Scanner(System.in);

System.out.print("Τώρα έχεις ένα κύβο με πλευρά "+a.getPleura()+", εμβαδό "+a.getEmbado()+", και όγκο "+a.getOgos()+".\n");

System.out.print("\nΔώσε μία άλλη τιμή για την πλευρά...\n");
a.setPleura(sc.nextDouble());

System.out.print("\nΤώρα έχεις ένα κύβο με πλευρά "+a.getPleura()+", εμβαδό "+a.getEmbado()+", και όγκο "+a.getOgos()+".\n\n");
}//main
}//class


Αυτό που κάναμε με λίγα λόγια είναι ότι, επειδή οι τρεις μεταβλητές pleura, embado, και ogos συνδέονται μεταξύ τους, δεν αφήνουμε το χρήστη να τις «πειράζει» ξεχωριστά, αλλά και τις τρεις μαζί με ελεγχόμενο τρόπο, μέσω της μεθόδου setPleura.

Κλείνοντας, να σημειώσουμε ότι και η ίδια η κλάση έχει διαβαθμίσεις της ως προς την προσβασιμότητα. Όλες τις κλάσεις εώς τώρα τις δηλώναμε ως public. Αυτό σημαίνει ότι η κλάση στο σύνολό της είναι ορατή από παντού.
Η άλλη επιλογή που έχουμε είναι να μη γράψουμε κανένα χαρακτηριστικό: να τη δηλώσουμε δηλαδή ως package-protected. Σε αυτή την περίπτωση η κλάση θα είναι ορατή μόνο από κλάσεις του ίδιου πακέτου. Αν πάμε να αναφερθούμε με οποιονδήποτε τρόπο στην κλάση από άλλες, εκτός πακέτου κλάσεις, θα πάρουμε ένα ωραιότατο σφάλμα από τον compiler.


2.2 Κληρονομικότητα
Θα γνωρίσουμε τώρα την πιο σημαντική ίσως έννοια του αντικειμενοστραφούς προγραμματισμού: την έννοια της κληρονομικότητας. Οι μεγάλοι δάσκαλοι του προγραμματισμού μπορούν να γράφουν βιβλία ολόκληρα για την κληρονομικότητα. Ειδικά στη C++ υπάρχει τεράστια φιλολογία γύρω από το θέμα.

Στη Java το θέμα είναι αρκετά πιο απλά δομημένο. Η γενική ιδέα της κληρονομικότητας είναι μία κλάση που επεκτείνει μία άλλη. Αυτό σημαίνει με απλά λόγια ότι η νέα κλάση θα περιέχει ό,τι και η παλιά, συν κάτι καινούριο! Θα περιέχει όλους τους constructor, όλες τις μεθόδους και τα πεδία της παλιάς, συν ό,τι άλλο δηλώσουμε.
Μπορούμε να φτιάχνουμε κλάσεις που επεκτείνουν έτοιμες κλάσεις που μας δίνει η Java, ή και άλλες που φτιάξαμε εμείς (βασικά το μόνο που διαχωρίζει τις έτοιμες κλάσεις από αυτές που φτιάχνουμε μόνοι μας είναι ότι οι πρώτες βρίσκονται σε μία ειδική τοποθεσία που καθορίζει ο compiler ή το jre, στην πραγματικότητα τίποτα το ουσιαστικό, είναι ισάξιες).

Επί του πρακτικού τώρα. Για να πούμε στον compiler ότι η κλάση μας επεκτείνει κάποια άλλη, κατά τη δήλωση της γράφουμε τη λέξη-κλειδί extends ακολουθούμενη από το όνομα της κλάσης που θα επεκτείνουμε. πχ:
Μορφοποιημένος Κώδικας: Επιλογή όλων
public class MyClass extends OtherClass {
...


Για να ανακαλύψουμε πως λειτουργεί η κληρονομικότητα, θα κατασκευάσουμε δύο κλάσεις, την Mother, και την Daughter η οποία θα επεκτείνει τη Mother. Η Mother ονομάζεται υπερκλάση (superclass) της Daughter, ενώ η Daughter υποκλάση (subclass) της Mother.
Αρχικά οι δύο αυτές κλάσεις θα είναι πολύ απλές, και σιγά-σιγά θα τις εμπλουτίζουμε για να βλέπουμε όλο και περισσότερες δυνατότητες της κληρονομικότητας. Ας αρχίσουμε λοιπόν!

Μορφοποιημένος Κώδικας: Επιλογή όλων
//Αρχείο Mother.java
public class Mother {

public int year;

public Mother(int n) {
this.year=n;
System.out.print("Μόλις καλέσατε τον constructor Mother(...int...) με όρισμα "+n+"!\n");
}//constructor

public void method1() {
System.out.print("Είμαι η method1 της κλάσης Mother!\nΤο year μου είναι "+year+"\n");
}//method1
}//class


Μορφοποιημένος Κώδικας: Επιλογή όλων
//Αρχείο Daughter.java
public class Daughter extends Mother {

//Από τη στιγμή που η κλάση Daughter κάνει extend τη Mother, περιέχει ήδη το πεδία year. Θα δημιουργήσουμε άλλο ένα, το add;
public int add = 30;

public Daughter(int n) {
super(n);// θα το σχολιάσουμε σε λίγο
System.out.print("Μόλις καλέσατε τον constructor Daughter(...int...) με όρισμα "+n+"\n");
}//constructor

//Επίσης έχει τη μέθοδο της κλάσης Mother. Θα δημιουργήσουμε μία ακόμα.
public void method2() {
System.out.print("Είμαι η method2 της κλάσης Daughter!\nΤο year μου είναι "+year+", και το add μου είναι "+add+"\n");
}//method2
}//class


Παρατηρήστε ότι πουθενά στην κλάση Mother δε γίνεται αναφορά στην Daughter. Στην πραγματικότητα η Mother δεν ξέρει καν την ύπαρξη της Daughter! Και δε χρειάζεται κι όλας. Όταν δηλαδή φτιάχνουμε μία κλάση, δε χρειάζεται να έχουμε στο μυαλό μας ότι στο μέλλον ίσως την επεκτείνουμε!


Ας υλοποιήσουμε τώρα τα παραπάνω με ένα δοκιμαστικό πρόγραμμα:

Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο MDTest.java
public class MDTest {
public static void main(String[] arg) {

//αρχικά θα φτιάξουμε δύο αντικείμενα...
System.out.print("Δημιουργία αντικειμένου Mother:\n");
Mother m = new Mother(1956);
System.out.print("\n\nΔημιουργία αντικειμένου Daughter:\n");
Daughter d = new Daughter(1980);
System.out.print("\n\n");

//βλέπουμε τις ιδιότητες του Mother.
System.out.print("\n\nm.method1: ");
m.method1(); //καλούμε κατά τα γνωστά τη μέθοδο method1
System.out.print("m.year= "+m.year+" "+", m.year*2= "+m.year*2+"\n");// γενικώς κάνουμε ό,τι θέλουμε με τα πεδία του m...

//και τώρα ας παίξουμε λίγο με το αντικείμενο Daughter..
System.out.print("\n\nd.method2: ");
d.method2(); //καλούμε τη μέθοδό του...
System.out.print("d.add= "+d.add+", d.year= "+d.year+"\n");
/*Παρατηρήστε κάτι στην παραπάνω: αν και δεν φαίνεται πουθενά στην κλάση Daughter
το πεδίο year, ένα αντικείμενο Daughter θα το έχει και αυτό! Το κληρονομεί από την Mother.*/

System.out.print("d.method1: ");
d.method1(); /*ομοίως: παρόλο που η μέθοδος method1 δεν φαίνεται να υπάρχει στην κλάση Daughter, το αντικείμενο Daughter
μπορεί να τη χρησιμοποιεί!*/
d.method2();
}//main
}//class


Όπως μπορούμε να διαπιστώσουμε, η κλάση Daughter περιέχει το πεδίο year και την μέθοδο merhod1 της κλάσης Mother.
Αυτό φαίνεται και μέσα στην Daughter, στην μέθοδο method2 αναφερόμαστε απ ευθείας στο πεδίο year, αν και αυτό δε φαίνεται πουθενά, αλλά και στην MDTest καλώντας τη μέθοδο method1, της κλάσης Mother.

Εδώ να σημειώσουμε ότι η θυγατρική κλάση παίρνει και τα static μέλη της μητρικής της. Αν πχ η κλάση Mother είχε μία static μέθοδο, την stat(), οι όροι Mother.stat() και Daughter.stat() θα ήταν εντελώς ισοδύναμοι. Το ίδιο φυσικά θα ίσχυε για ένα static πεδίο.

2.3 Η λέξη κλειδί super
Ας σχολιάσουνε λίγο τη λέξη-κλειδί super που εμφανίζεται στον constructor της Daughter. Για να σας προϊδεάσω θα σας πω ότι η χρήση της super είναι πολύ παρόμοια με αυτήν της this. Τη this την χρησιμοποιούμε για να αναφερθούμε στο συγκεκριμένο αντικείμενο, ενώ τη super για να αναφερθούμε στο αντικείμενο της υπερκλάσης το οποίο κατά κάποιο τρόπο περιέχεται στο αντικείμενό μας.
Με απλά λόγια: Μπορούμε να φανταστούμε ότι το αντικείμενο Daughter περιέχει ένα Mother. Με την super αναφερόμαστε ακριβώς σε αυτό το Mother.
Για να χρησιμοποιήσουμε ένα πεδίο ή μία μέθοδο του αντικειμένου αυτού, μπορούμε κάλλιστα να γράψουμε super.my_field ή super.myMethod(...), όπως δηλαδή κάναμε και με την this.
Η super όμως, όπως και η this άλλωστε, μπορεί να χρησιμοποιηθεί από έναν constructor ο οποίος καλεί έναν άλλο constructor. Η αντίστοιχη μορφή είναι η super(...). Τη χρησιμοποιούμε όπως και τη this(...) που είδαμε στο προηγούμενο κεφάλαιο. Η super όμως, αντί να καλεί κάποιον constructor της ίδιας κλάσης, καλεί κάποιον της μητρικής.

Και αν σκεφτούμε για λίγο πως κατασκευάζεται ένα αντικείμενο που επεκτείνει ένα άλλο, θα καταλάβουμε ότι το να καλέσουμε τον constructor της μητρικής κλάσης είναι απαραίτητο: Από τη στιγμή που το αντικείμενο Daughter «περιέχει» την πληροφορία ενός Mother, για να κατασκευάσουμε ένα Daughter πρέπει πρώτα να κατασκευαστεί ένα Mother, και έπειτα να «τακτοποιηθούν» και τα επιπλέον στοιχεία του Daughter. Και γι αυτό η πρώτη υποχρέωση ενός constructor είναι να εκτελέσει την εντολή super(...), δηλαδή τον constructor της μητρικής κλάσης.
Μέσα στις παρενθέσεις της super μπαίνουν τα ορίσματα του constructor που θα καλεστεί. Αν στην συγκεκριμένη περίπτωση θέλαμε να καλέσουμε κάποιον άλλο constructor της υπερκλάσης, θα γράφαμε την super και στις παρενθέσεις τα ανάλογα ορίσματα.

:!:Αν η πρώτη εντολή του constructor δεν είναι η super(...), ο compiler συμπεραίνει ότι «υπονοήσατε» την super(), και την προσθέτει αυτόματα. Δηλαδή προσθέτει μία κλήση σε constructor της μητρικής κλάσης χωρίς ορίσματα. Αν αυτός ο constructor δεν υπάρχει (δεν υπάρχει υποχρεωτικά ένας constructor χωρίς ορίσματα), τότε ο compiler θα σας πετάξει error. Είναι καλό λοιπόν να υπάρχει η εντολή super(...) ως η πρώτη εντολή κάθε constructor, εκτός αν η κλάση μας δεν επεκτείνει κάποια άλλη οπότε αυτό δε χρειάζεται. Ας δούμε μερικούς έγκυρους constructor:

Μορφοποιημένος Κώδικας: Επιλογή όλων
//class B extends A...
public B() {
super(5); //πρέπει φυσικά να υπάρχει ότι υπάρχει ο A(...int...)
//άλλες εντολές...
}//B

public B(int h) {
super(); //πρέπει να υπάρχει ο A()...
myfield = h;
//κτλ κτλ
}//B

//και ακριβώς την ίδια δουλειά θα κάνει και το:
public B(int h) {
myfield = h;
//κτλ κτλ
}//B

/*μιας και όπως είπαμε πριν, όταν δεν υπάρχει καμία super(...),
ο compiler βάζει αυτόματα την super() χωρίς ορίσματα. Και φυσικά αυτό υπονοεί ότι η A() πρέπει να υπάρχει.*/


Για να τα δούμε αυτά στην πράξη, τροποποιούμε τις παραπάνω κλάσεις μας ως εξής:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//1. Γράφουμε έναν επιπλέον constructor στην κλάση Mother, χωρίς ορίσματα:
public Mother() {
this.year=1965;
System.out.print("Μόλις καλέσατε τον constructor Mother() χωρίς ορίσματα!\n");
}//constructor

/*2. Κάνουμε compile την Mother και εκτελούμε μία φορά το MDTest.class. Από τις πρώτες τρεις γραμμές καταλαβαίνουμε ποιοι constructor χρησιμοποιήθηκαν.
Η πρώτη γραμμή αναφέρεται στη δημιουργία του Mother, ενώ οι δύο επόμενες σε αυτή του Daughter.
Ποιόν constructor φώναξε η εντολή super(n);*/

//3. Πάμε στον constructor της Daughter και αντικαθιστούμε την
super(n);
//με την
super();
//Κάνουμε compile την Daughter και ξαναεκτελούμε το MDTest.class. Ποιοί constructor καλέστηκαν τώρα για τη δημιουργία του Daughter;
//καταλαβαίνετε τώρα τι καλεί η super();

/*4. Ξαναπάνε στον constructor της Daughter και σβήνουμε εντελώς την εντολή super(); Την κάνουμε compile ξανά, και τρέχουμε την MDTest.
Τι παρατηρείτε; Ακόμα και χωρίς καμία super, ο compiler κάλεσε κάποιον
Constructor της υπερκλάσης. Ποιόν;*/

/*5. Πάμε στην Mother και σβήνουμε τον constructor που μόλις φτιάξαμε (αυτόν χωρίς ορίσματα).
Κάνουμε compile την Mother, και μετά προσπαθούμε προσπαθούμε να κάνουμε compile την Daughter.
Τι σφάλμα μας βγάζει ο compiler; Τι έψαξε και δεν βρήκε;*/

/*6. Επαναφέρουμε τον constructor του βήματος 1 στη Mother,
και την εντολή super(n); στον constructor της Daughter, και κάνουμε και τις δύο κλάσεις compile*/



2.4 Προσβασιμότητα στα μέλη της υπερκλάσης
Αν το καλοσκεφτείτε, μία κλάση δεν πρέπει να έχει πρόσβαση σε όλα τα πεδία της υπερ-κλάσης της. Γιατί;

Ας φανταστούμε ότι έχουμε φτιάξει μία κλάση με όνομα Secret που κρατάει ένα private int, το οποίο δε θέλουμε κάποιος να μπορεί να δει ή να αλλάξει όποτε θέλει. Και ας πούμε ότι έχουμε δώσει την κλάση αυτή σε κάποιον να τη χρησιμοποιήσει.
Αυτός θα μπορούσε άνετα να φτιάξει μία δική του κλάση, την Sectet2 extends Secret. Σε αυτή την περίπτωση η έννοια private θα ήταν παρελθόν, αφού η κλάση Secret2 θα μπορούσε να κάνει ό,τι θέλει με τον private int μας, μιας και πλέον «της ανήκει».
Το ίδιο θα γινόταν και με έναν private constructor, ή μέθοδο.

Για αυτόν ακριβώς τον λόγο η Java μας δίνει την δυνατότητα να προστατεύουμε τα μέλη μίας κλάσης ακόμα και... από τα παιδιά της!
Πιο συγκεκριμένα γενικεύουμε τον πίνακα περί προσβασιμότητας που ορίσαμε παραπάνω, λαμβάνοντας υπ' όψιν και την ιδιαίτερη σχέση κλάσης-υποκλάσης:

i. Τα public μέλη είναι προσβάσιμα από παντού.
ii. Τα protected μέλη είναι προσβάσιμα από άλλες κλάσεις του ίδιου πακέτου και από τις υποκλάσεις.
iii. Τα μέλη χωρίς χαρακτηριστικό (package-private) είναι προσβάσιμα μόνο από κλάσεις του ιδίου πακέτου (όχι από τις υποκλάσεις).
iv. Τα private μέλη δεν είναι προσβάσιμα από πουθενά έξω απ' την κλάση.

Δύο σχόλια:
Πρώτον η κληρονομικότητα δεν έχει σχέση με τα πακέτα. Μπορεί κάλλιστα μία κλάση που βρίσκεται σε κάποιο πακέτο κα επεκτείνει μία άλλη κλάση σε τελείως διαφορετικό πακέτο.
Δεύτερον φάνηκε η διαφορά της περίπτωσης ii και iii, δηλαδή της protected και της package-private. Η πρώτη επιτρέπει την πρόσβαση στην υποκλάση, ενώ η δεύτερη όχι. Τώρα αν η υποκλάση τυχαίνει να βρίσκεται και στο ίδιο πακέτο με την «μαμά» της, τότε η πρόσβαση επιτρέπεται.

Για να μην πλατιάσω, δε θα δώσω ανάλογο παράδειγμα. Σας συνιστώ όμως να «παίξετε» με τα μέλη της κλάσης Mother (πχ με το year), κάνοντάς τα private, package-private κτλ, και έπειτα να κάνετε Compile για να δείτε πότε ο compiler θα σας «επιτρέψει» να αγγίξετε τα μέλη αυτά από την Daughter και πότε όχι.
Δοκιμάστε επίσης να βάλετε την κλάση Mother σε άλλο πακέτο από την κλάση Daughter, για να δείτε τη διαφορά μεταξύ package-private και protected (σε μία τέτοια περίπτωση μην ξεχάσετε να κάνετε import την Mother). Η εμπειρία που θα αποκομίσετε από μία τέτοια διαδικασία είναι ισάξια με το να διαβάζετε 80 σελίδες θεωρίας και παραδειγμάτων. :-)


2.5 Final κλάσεις
Έχουμε δει μέχρι τώρα ότι πως μπορούμε να επεκτείνουμε μία οποιαδήποτε κλάση. Ας πούμε όμως ότι φτιάχνουμε μία κλάση που, για κάποιον λόγο δε θέλουμε να επιτρέψουμε σε άλλους να την επεκτείνουν. Μπορούμε;
Η απάντηση είναι φυσικά ναι! Αρκεί να δηλώσουμε ότι η κλάση μας είναι final (με την ίδια λέξη-κλειδί όπως δηλώναμε μεταβλητές που δεν αλλάζουν). Αυτό το καθορίζουμε κατά τη δήλωση της κλάσης. Κάπως έτσι δηλαδή:

Μορφοποιημένος Κώδικας: Επιλογή όλων
public final class MyClass { ...

ή
final class MyClass extends OtherClass { ...
κλτ.

Μη μπερδευτείτε: μία final κλάση μπορεί να επεκτείνει κάποια άλλη. Η ίδια δεν μπορεί να επεκταθεί.
Δοκιμάστε για πείραμα να δηλώσετε την κλάση Mother ως final, και αφού την κάνετε compile, δοκιμάστε να κάνετε compile και την Daughter να δείτε τι θα σας πει ο compiler. ;-)
Έπειτα σβήστε το final από τη δήλωση της Mother και κάντε την compile για να συνεχίσουμε κανονικά.


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

Ας πάρουμε την περίπτωση των πεδίων. Τι θα γίνει για παράδειγμα αν μέσα στην Daughter δηλώσουμε ένα πεδίο με όνομα year (που ήδη υπάρχει στη Mother);
Η απάντηση είναι τίποτα το ιδιαίτερο! Μπορούμε μάλιστα να δηλώσουμε τη «νέα» μεταβλητή year με τελείως διαφορετικό τύπο από την year της Mother (πχ να είναι String). Μπορούμε γενικώς να τη μεταχειριστούμε σαν μία εντελώς ξεχωριστή μεταβλητή, η οποία δεν έχει καμία σχέση με την year της Mother, παρόλο που έχουν το ίδιο όνομα.
Με τον τρόπο αυτό, δημιουργείται το εξής παράδοξο: ένα αντικείμενο Daughter θα περιέχει δύο year. Μία int που κληρονομεί από την Mother, και μία String που είναι καθαρά δική της. Ωραία θα μου πείτε. Αλλά αν κάπου γράψουμε year μέσα στην κλάση, ή mydaughter.year κάπου απ έξω (όπου mydaughter κάποιο αντικείμενο Daughter), ο compiler ποιο απ' τα δύο θα διαλέξει;
Η απάντηση είναι το πιο «πρόσφατο», δηλαδή αυτό της κλάσης Daughter! Με αυτό τον τρόπο έχουμε «κρύψει» το year που έρχεται από τη Mother. Κάθε φορά που θα γράφουμε year, ο compiler θα καταλαβαίνει το άλλο year.
Υπάρχει βέβαια ένας τρόπος να το «ξεθάψουμε» και να αναφερθούμε στο year που προέρχεται απ την Mother. Θα το δούμε σε επόμενη ενότητα.
Κλείνοντας αυτή την παράγραφο οφείλω να σας πω, ότι προσωπικά δε βρίσκω καμία χρησιμότητα στο να «παρακάμψετε» με αυτό τον τρόπο πεδία. Στα καινούρια πεδία θα έδινα καινούρια ονόματα. Για λόγους πληρότητας όμως σας ανέφερα τα παραπάνω για να ξέρετε ότι κάτι τέτοιο γίνεται.

Και τώρα θα πάμε στην πιο ουσιώδη περίπτωση της παράκαμψης μεθόδων. Αυτό συμβαίνει όταν σε μία κλάση δηλώνεται μία μέθοδος με όνομα και λίστα ορισμάτων, ίδια με μιας άλλης μεθόδου που ήδη υπάρχει σε κάποια υπερκλάση της. Αν πχ πηγαίναμε στην Daughter και δηλώναμε μία μέθοδο method1() χωρίς ορίσματα ενώ αυτή υπάρχει ήδη στην Mother. Θα μας αφήσει άραγε ο compiler να κάνουμε κάτι τέτοιο ή θα μας βγάλει error ότι η μέθοδος αυτή ήδη υπάρχει; Η απάντηση είναι ότι θα μας αφήσει... υπό προϋποθέσεις! Και αυτές είναι οι εξής:

Η νέα μέθοδος πρέπει:
i. να έχει ίδιο τύπο επιστροφής με την παλιά,
ii. να συμφωνεί με την παλιά ως προς το αν είναι static ή όχι, και
iii. να είναι «πιο public» από την παλιά.

Με άλλα λόγια δεν μπορούμε να παρακάμψουμε μία μέθοδο που επιστρέφει int με μία άλλη που επιστρέφει ένα Scanner ή οτιδήποτε άλλο. Και δεν μπορούμε να παρακάμψουμε μία static μέθοδο με μία που δεν είναι static, ή το αντίστροφο, και δεν μπορούμε να παρακάμψουμε μία protected μέθοδο με μία private (εδώ όμως μπορούμε να κάνουμε το αντίστροφο!).

Ας το κάνουμε λοιπόν στην πράξη: ας προσθέσουμε μία μέθοδο με όνομα method1 και χωρίς ορίσματα (ακριβώς ίδια δηλαδή με αυτή κλάση Mother):
Μορφοποιημένος Κώδικας: Επιλογή όλων
//μέσα στο Daughter.java
public void method1() {
System.out.print("Είμαι η method1 της κλάσης Daughter. Παρακάμπτω την συνονόματή μου method1 της κλάσης Mother!\n");
}//method1

Κάντε compile, και τρέξτε την MDTest. Παρατηρείστε: ποια από τις δύο method1 καλεί το αντικείμενο Daughter, δηλαδή το d; Θα διαπιστώσετε ότι καλεί τη «δική του» έκδοση της method1. Αυτό ακριβώς σημαίνει παράκαμψη μίας μεθόδου. Η παλιά «πληροφορία» για το τι έκανε η μέθοδος χάνεται, και όποτε αυτή καλεστεί, θα εκτελεστούν οι νέες εντολές.

Μέσα στη θυγατρική κλάση μπορούμε ακόμα να χρησιμοποιήσουμε τις μεθόδους τις παλιάς, ακόμα και αν τις έχουμε παρακάμψει. Αυτό γίνεται με τη χρήση της super. Αν μέσα στην Daughter για παράδειγμα θέλουμε να χρησιμοποιήσουμε τη method1 μέσω ενός αντικειμένου Daughter μπορούμε, γράφοντας super.method1()
Όμως μέχρι εκεί! Από άλλες κλάσεις (πχ από την MDTest) δεν μπορούμε. Αν καλέσουμε με οποιονδήποτε τρόπο τη method() μέσω ενός αντικειμένου Daughter θα εκτελεστεί η καινούρια της έκδοση.

Ας δούμε τώρα κάτι άλλο. Η Java μας δίνει την δυνατότητα να μην επιτρέψουμε σε καμία υποκλάση να παρακάμψει κάποια μέθοδό μας. (Το γιατί να κάνουμε κάτι τέτοιο ίσως να μην είναι τόσο προφανές, αλλά μην ξεχνάτε ότι τις κλάσεις μας μπορεί οποιοσδήποτε να τις πάρει και να τις κάνει extend με όποιο τρόπο προτιμαει!)
Μπορούμε λοιπόν να προστατέψουμε τη μέθοδό μας έτσι ώστε να μην παρακαμφθεί. Αυτό γίνεται (ξανά) με τη χρήση της λέξης-κλειδί final. Δηλαδή να γράψουμε κάτι σαν:
Μορφοποιημένος Κώδικας: Επιλογή όλων
public final void someMethod(...) {


Αν μία μέθοδος είναι final δεν μπορεί να παρακαμφθεί. Αν πάτε μέσα από κάποια υποκλάση να δηλώσετε άλλη μέθοδο με τα ίδια χαρακτηριστικά, ο compiler θα σας βγάλει error.
Ας το δοκιμάσουμε: αφού είδαμε πως η method1() της κλάσης Daughter παρέκαμψε αυτήν της κλάσης Mother, ας δούμε τι θα γίνει αν δηλώσουμε τη δεύτερη ως final. Πάμε λοιπόν στην Mother και προσθέτουμε το final στη δήλωση της method1().
Μορφοποιημένος Κώδικας: Επιλογή όλων
public final void method1() {

Κάντε compile, και έπειτα προσπαθήστε να κάνετε compile και την Daughter. Ο compiler θα παραπονεθεί για την μέθοδο method1() που προσπαθεί να παρακάμψει μία final μέθοδο. Αυτό ήταν!

Κλείνοντας αυτή την ενότητα να σημειώσουμε ότι στην ουσία, με το να παρακάμπτουμε μία static μέθοδο δεν καταφέρνουμε και πολλά, μιας και μπορεί κάποιος όποια στιγμή το θελήσει να γράψει MotherClass.theStaticMethod(...) και να χρησιμοποιήσει την «παλαιά» έκδοση..



2.7 Κληρονομικότητα και Typecasting
Typecasting ονομάζουμε την ιδιότητα ενός τύπου να αναγνωρίζεται (και να αξιοποιείται) σαν να ήταν άλλος τύπος. Αυτό το κάναμε αρκετές φορές με τους βασικούς τύπους αντικειμένων όταν πχ γράφαμε (char)my_int αντί για σκέτο myint, και έτσι μετατρέπαμε έναν int σε char.
Λοιπόν το ίδιο μπορεί να γίνει με τα αντικείμενα! Μπορούμε να γράφουμε δηλαδή το είδος του αντικειμένου σε παρένθεση, και μετά το αντικείμενο που θέλουμε να μετατρέψουμε! Πχ (String)my_object. Όμως εδώ θέλει κάποια προσοχή γιατί δεν μπορούμε να μετατρέψουμε ό,τι θέλουμε σε ό,τι θέλουμε. Για παράδειγμα αν γράψουμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
Scanner sc = new Scanner(System.in);
String s = (String)sc;

Ο compiler θα σας πει ότι δεν ξέρει πως να μετατρέψει το Scanner σε String (και με το δίκιο του!)...

Αν όμως γράψουμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
Daughter d = new Daughter(5);
Mother m = (Mother)d;

Ο compiler θα κάνει μια χαρά τη μετατροπή από Daughter σε Mother. Να σημειώσουμε ωστόσο ότι δεν θα δημιουργήσει ένα Mother, απλώς θα δημιουργήσει μία μεταβλητή τύπου Mother. Αν όμως πάτε να κάνετε το αντίθετο:
Μορφοποιημένος Κώδικας: Επιλογή όλων
Mother m = new Mother(5);
Daughter d = (Daughter)m;

Ο compiler θα σας βγάλει error ότι δεν μπορεί να το κάνει (και αν όχι ο compiler, όταν πάτε να τρέξετε το πρόγραμμα θα πάρετε εκεί το error). Πώς εξηγούνται όλα αυτά; Γιατί κάποιες μετατροπές γίνονται και κάποιες όχι; Εξηγώ:

Στην πραγματικότητα, ένα αντικείμενο Daughter είναι την ίδια στιγμή και Mother! Τα αντικείμενα στη Java (όπως και στην C++ και σε άλλες γλώσσες) έχουν πολλαπλή φύση. Είναι ταυτόχρονα το είδος που τα ορίσαμε, καθώς και όλα τα είδη των προγόνων του!. Γι αυτό ο compiler ξέρει να κάνει την δεύτερη από τις μετατροπές που είδαμε(από Daughter σε Mother)! Στην πραγματικότητα και δε χρειάζεται καμία μετοτροπή, και ο όρος (Mother) μπορεί να σβηστεί! Είναι περιττός. Παρ όλα αυτά το αντίστροφο δεν ισχύει: ένα Mother δεν είναι Daughter, γιατί αυτό είναι απόγονος και όχι πρόγονός της.

Το typecasting όμως δεν χρησιμεύει κατά τη δήλωση μεταβλητών, αλλά παντού! Ας το δούμε στην πράξη: Θα φτιάξουμε μία static μέθοδο στην κλάση Mother, η οποία θα δέχεται όρισμα ένα Mother, και θα μας τυπώνει τα στοιχεία του.
Ας την φτιάξουμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//...μέσα στο Mother.java...
public static void info(Mother ob) {
System.out.print("Είμαι η μέθοδος info! Μου δώσατε ως όρισμα ένα αντικείμενο Mother με year= "+ob.year+"...\n"+
"...το αντικείμενο έχει επίσης μία μέθοδο, την method1. Θα την εκτελέσω:\n");
ob.method1();
}//info

Η μέθοδος αυτή όπως βλέπουμε μας δίνει τις πληροφορίες του αντικειμένου Mother που του «βάζουμε» ως όρισμα.
Ωραία λοιπόν, ας πάμε στο τέλος της mail στο MDTest.java για να προσθέσουμε την μέθοδο info με όρισμα το m που φτιάξαμε. Προσθέστε δηλαδή τις δύο εξής γραμμές:
Μορφοποιημένος Κώδικας: Επιλογή όλων
System.out.print("\n\n"); //για να ξεχωρίσουμε το νέο μήνυμα...
Mother.info(m);


Κάνετε compile τις Mother και MDTest και τρέξτε την MDTest. Θα λάβετε στο τέλος της το μήνυμα
Κώδικας: Επιλογή όλων
Είμαι η μέθοδος info! Μου δώσατε ως όρισμα ένα αντικείμενο Mother με year= 1956...
...το αντικείμενο έχει επίσης μία μέθοδο, την method1. Θα την εκτελέσω:
Είμαι η method1 της κλάσης Mother!
Το year μου είναι 1956


Ωραία μέχρι εδώ! Απλώς φτιάξαμε μία μέθοδο και την καλέσαμε σωστά;
Ας αλλάξουμε ένα γραμματάκι: στην main της MDTest, αντί για Mother.info(m) γράψτε Mother.info(d). Δηλαδή αντί για όρισμα το m, δώστε το d.
Μια στιγμή θα μου πείτε! Η μέθοδος info «θέλει» για όρισμα ένα αντικείμενο Mother. Πως εγώ θα της «δώσω» το d, που είναι Daughter;
Απάντηση: το d είναι ΚΑΙ Mother ΚΑΙ Daughter! Αυτό σημαίνει ότι όπου θα χρησιμοποιούσατε ένα Mother μπορείτε μια χαρά να χρησιμοποιήσετε και το d.
Κάντε compile και τρέξτε την MDTest. Ο compiler δε θα κάνει κανένα παράπονο! Αυτό που θα δείτε στο τέλος είναι το:
Κώδικας: Επιλογή όλων
Είμαι η μέθοδος info! Μου δώσατε ως όρισμα ένα αντικείμενο Mother με year= 1980...
...το αντικείμενο έχει επίσης μία μέθοδο, την method1. Θα την εκτελέσω:
Είμαι η method1 της κλάσης Daughter. Παρακάμπτω την συνονόματή μου method1 της κλάσης Mother!


Ας σχολιάσουμε για λίγο κάτι: στην δήλωση της info, καλείται η μέθοδος method1 του αντικειμένου ob, δηλαδή αυτού που δώσαμε ως όρισμα.
Τώρα που δώσαμε για όρισμα το d, ποια «έκδοση» της method1 καλέστηκε; Αυτή της κλάσης Mother, ή της κλάσης Daughter που παρακάμπτει την πρώτη; Όπως βλέπετε καλείται η νεότερη έκδοση. Εδώ φαίνεται για άλλη μία φορά πως λειτουργεί η παράκαμψη μεθόδων.



2.8 Γενεαλογικά δέντρα και ο κοινός πρόγονος
Όπως βλέπουμε κάθε κλάση κληρονομεί μία άλλη, η οποία με τη σειρά της κληρονομεί μία άλλη κ.ο.κ.
Αν θα σχεδιάζαμε αυτές τις σχέσεις ιεραρχίας, θα φτιάχναμε κάτι σαν γενεαλογικό δέντρο, αλλά το κάθε μέλος θα είχε μόνο ένα «γονιό».
Όσο για τις κλάσεις που δε φαίνεται να επεκτείνουν κάποια άλλη, θα είναι οι «γενάρχες» όλης της μετέπειτα οικογένειας.

Στην πραγματικότητα όμως υπάρχει κάτι που η Java μας κρύβει. Ακόμα και οι κλάσεις που δε φαίνεται να επεκτείνουν κάποια άλλη (δε γράφουν extends ...) κατά τη δήλωσή τους, ο compiler κρυφά και ύπουλα (!) τις κάνει να επεκτείνουν. Υπάρχει δηλαδή μία κλάση η οποία (θέλουμε-δε θέλουμε) είναι ο πρόγονος όλων των υπολοίπων, η βάση κάθε άλλης κλάσης.

Και αυτή έχει όνομα Object. Οποιδήποτε κλάση δε γράφει extends, είναι σαν να γράφει extends Object.
Η Object είναι μία κλάση του πακέτου java.lang (αυτό που γίνεται αυτόματα import) και παρέχει μερικές χρήσιμες μεθόδους, όπως η toString() η οποία δίνει ένα κείμενο που περιγράφει το αντικείμενο. Δοκιμάστε στο MDTest να γράψετε m.toString() ή d.toString() μέσα σε μία System.out.print για να δείτε τι θα σας βγάλει. Φυσικά εσείς ποτέ δε γράψατε καμία μέθοδο toString(), όμως η κλάση Object (την οποία αυτόματα επεκτείνει η Mother) περιέχει αυτή τη μέθοδο και άρα μπορείτε να τη χρησιμοποιήσετε!

Στην πραγματικότητα το jdk έρχεται με μία πληθώρα έτοιμων κλάσεων προς χρήση. Είναι καλό όσο μπορούμε να παίρνουμε τα έτοιμα αυτά εργαλεία και να μη φτιάχνουμε δικά μας αν δεν υπάρχει λόγος, γιατί πρώτον τα έχουν φτιάξει επιμελώς πολύ καλοί και έμπειροι προγραμματιστές, και δεύτερον οποιοσδήποτε πάρει να τρέξει τις κλάσεις μάς κατά πάσα πιθανότητα θα έχει ήδη τις έτοιμες αυτές κλάσεις (θα του ήρθαν μαζί με το jre...).
Η μισή ενασχόλησή σας (και ούτε!!) για να μάθετε Java θα είναι η ορθογραφία και οι κανόνες της γλώσσας, και η άλλη μισή θα είναι να μάθετε να χρησιμοποιείτε αυτά τα πολύ χρήσιμα αντικείμενα!

Η Java είναι μία από τις γλώσσες με το καλύτερο Documentation, δηλαδή πληροφορίες για το πως να χρησιμοποιείτε τα έτοιμα εργαλεία που σας δίνει. Για αυτούς που χρησιμοποιούν τα jdk και jre της Oracle (δηλαδή για τους περισσότερους!) υπάρχει αυτή η ιστοσελίδα που αναγράφει αναλυτικές πληροφορίες για όλα τα έτοιμα αντικείμενα που έρχονται με τα προϊόντα αυτά. Η αναφορά γίνεται για την μέχρι σήμερα πιο καινούρια έκδοση των jdk/gre, δηλαδή την 7. Αν χρησιμοποιείτε την έκδοση 6, αντικαταστήστε το '/7/' που εμφανίζεται στο url με '/6/'.
Για τις κλάσεις του gcj δεν ξέρω αν υπάρχει αντίστοιχη ιστοσελίδα, αλλά μπορώ να σας πω ότι οι περισσότερες κλάσεις του είναι πανομοιότυπες με αυτές των jdk και jre της Oracle.

Καθώς θα προγραμματίζετε σε Java, η παραπάνω ιστοσελίδα θα πρέπει να βρίσκεται σταθερά στους σελιδοδείκτες σας, σε συνδυασμό με την BigIndex που σας έδωσα στο πρώτο κεφάλαιο, και φυσικά με τον παρών οδηγό :-)

Προηγούμενο: Κλάσεις, Αντικείμενα και Μέθοδοι
Επόμενο: Πίνακες - Πέρασμα ορισμάτων

Creative Commons License
Η εργασία υπάγεται στην άδεια Creative Commons Αναφορά-Παρόμοια διανομή 3.0 Ελλάδα
Γνώσεις ⇛ Linux: Μέτριο┃ Προγραμματισμός: Java, Assembly, Fortran, μαθαίνω C/X11┃ Αγγλικά: Μέτρια
Λειτουργικό σε Η/Υ ϰ μοντέλο: Ubuntu 14.04 64-bit ┃ Τρόπος εγκατάστασης: Live USB
Προδιαγραφές ⇛ Desktop: Intel i5 2320 3.00GHz.┃ MotherBoard: Asus p8h61 -m pro
Προδιαγραφές ⇛ RAM: 4GB ┃ Τροφοδοτικό Corsair CX430

GPU: Intel 2nd Generation Core Processor Family Integrated Graphics Controller [8086:0102] {i915}
5 eth0: Realtek RTL8111/8168B PCI Express Gigabit Ethernet controller [10ec:8168] (rev 06) ⋮ wlan0: 0b05:1723 ASUS WL-167G v2 802.11g Adapter [Ralink RT2571W]
Οθόνη Schaub Lorenz (Tv)
alkismavridis
punkTUX
punkTUX
 
Δημοσιεύσεις: 273
Εγγραφή: 18 Μαρ 2009, 18:46
Εκτύπωση

  • ΣΧΕΤΙΚΑ ΘΕΜΑΤΑ
    ΑΠΑΝΤΗΣΕΙΣ
    ΠΡΟΒΟΛΕΣ
    ΣΥΓΓΡΑΦΕΑΣ

Επιστροφή στο Μαθήματα Java