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

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

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

Δημοσίευσηαπό alkismavridis » 19 Μάιος 2012, 17:15

Προηγούμενο: Δομές Ελέγχου και Επανάληψης
Επόμενο: Σύνδεση προγραμμάτων - Σχέσεις μεταξύ Αντικειμένων και Κλάσεων


Κλάσεις, Αντικείμενα και Μέθοδοι


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


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

1. Κλάση
Όλα ξεκινούν από εδώ! Δεν είναι τυχαίο ότι όλα τα bytez_code έχουν κατάληξη class, ούτε ότι όλα τα προγράμματά που φτιάξαμε ξεκινούν και τελειώνουν με μία κλάση, στην ουσία είναι κλάσεις! Ο βασικός σκοπός μίας κλάσης είναι να φτιάχνει αντικείμενα. Από μία κλάση φτιάχνουμε όσα αντικείμενα θέλουμε: κανένα, ένα, δέκα ή χίλια! Γι αυτό θα πρέπει να βλέπουμε την κλάση κάτι σαν ένα καλούπι.

Η κλάση περιέχει:
i. Πεδία (δηλαδή δεδομένα: βασικούς τύπους και άλλα αντικείμενα)
ii. Μεθόδους (ορισμός σε λίγο)
iii. Constructors (μοιάζουν με μεθόδους, και σκοπός τους είναι να δημιουργούν αντικείμενα)
iv. Άλλες κλάσεις (τις λεγόμενες εσωτερικές, inner classes)

2. Αντικείμενο
Τα αντικείμενα είναι δεδομένα. Δηλώνονται, δημιουργούνται, παίρνουν τιμές (περίπου) όπως οι βασικοί τύποι. Μπορούν να «περιέχουν» βασικούς τύπους και άλλα αντικείμενα (το τι ακριβώς θα περιέχει το αντικείμενο καθορίζεται από την κλάση που το δημιουργεί). Τα αντικείμενα ωστόσο έχουν και κάτι άλλο που τα κάνει πολύ χρήσιμα: έχουν τις δικές τους μεθόδους! Αυτές οι μέθοδοι είναι που θα καθορίζουν τι θα κάνουν αυτά τα αντικείμενα!

Συνοψίζοντας τα αντικείμενα περιέχουν:
i. Δεδομένα
ii. Μεθόδους


3. Μέθοδοι
Πολλά έχουμε ακούσει ως τώρα για τις μεθόδους. Σε άλλες γλώσσες (C,C++) ονομάζονται συναρτήσεις. Τι είναι όμως;
Σε βασικές γραμμές η μέθοδος είναι ένα σύνολο από εντολές που εκτελούνται μία-μία, και βασικός σκοπός της είναι να επιστρέψει (να δώσει ως αποτέλεσμα) κάποια τιμή. Η τιμή αυτή μπορεί να είναι οποιουδήποτε τύπου (βασικός τύπος, αντικείμενο, ακόμα και τίποτα!).
Στη μέθοδο μπορούμε αν θέλουμε να περάσουμε «ορίσματα». Αυτά είναι δεδομένα (βασικοί τύποι ή αντικείμενα) που θα χρησιμοποιήσει η μέθοδος για να κάνει κάποια δουλειά.
Τέλος στη μέθοδο δίνουμε ένα όνομα. Μπορούμε να γράφουμε αυτό το όνομα μαζί με τα ορίσματα της μεθόδου κάθε φορά που θέλουμε να της πούμε να εκτελεστεί!

Συνοψίζοντας, μία μέθοδος χαρακτηρίζεται από:
i. Όνομα
ii. Ορίσματα
iii. Τύπο επιστροφής


4. Constructor
Ο Constructor μοιάζει με μέθοδο, με δύο ιδιαιτερότητες:
i. Δεν αναφέρει τύπο επιστροφής.
ii. Το όνομά του είναι ίδιο με το όνομα της κλάσης (δηλαδή δεν τον βαφτίζουμε όπως θέλουμε εμείς).

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

/εκτελεί τις πράξεις: d=120; b=true; και μετά μία-μία τις εντολές του constructor
h2 = new House(2*40, false); //ομοίως! εκτελεί τις: d=2*40; b=
Ενότητα 1: Η κλάση και τα πεδία της
1.1 Δήλωση


Το πώς δηλώνουμε κλάσεις είναι γνωστό: όλα τα προγράμματα που φτιάξαμε πριν δηλώνουν μια κλάση.

Σύμβαση: Για τις κλάσεις χρησιμοποιούμε ονόματα στα οποία κάθε λέξη αρχίζει με κεφαλαίο. Πχ: MyClass, MountainBike, NumbersGame κτλ.
Αυτή τη σύμβαση χρησιμοποιούν σχεδόν όλοι οι προγραμματιστές στην Java.


Μέσα στην κλάση μπορούμε μόνο να δηλώνουμε μεταβλητές (πεδία), μεθόδους, constructors και άλλες κλάσεις. Τίποτα άλλο! Στο σώμα της κλάσης δε μπορούμε να δώσουμε καμία εντολή!

Θα σταθούμε για λίγο στην περίπτωση των πεδίων. Μπορούμε να δώσουμε κατά τα γνωστά αρχικές τιμές την ώρα της δήλωσης.
:!:Τα πεδία που ορίζονται στο σώμα της κλάσης είναι και οι μεταβλητές που θα έχει το αντικείμενο που θα δημιουργηθεί! Στην ουσία όταν γράφετε τα πεδία σας στο σώμα της κλάσης, γράφετε τι θα περιέχει το αντικείμενό σας.
Οι μεταβλητές αυτές δημιουργούνται όταν δημιουργείται το αντικείμενο, όχι πιο πριν!
/εκτελεί τις πράξεις: d=120; b=true; και μετά μία-μία τις εντολές του constructor
h2 = new House(2*40, false); //ομοίως! εκτελεί τις: d=2*40; b=
Ας δούμε ένα παράδειγμα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
public class House {

double size;
boolean garden = true;

//...constructors...
//...methods...
}//class

Κάθε αντικείμενο της κλάσης House που θα δημιουργούμε (θα δούμε πως γίνεται αυτό), θα περιέχει δύο δεδομένα: μία boolean και μία double. Στην πραγματικότητα κάθε αντικείμενο έχει τη δική του «έκδοση» της μεταβλητής size και τη δική του έκδοση της garden. Είναι σημαντικό να το καταλάβετε αυτό: αν θα φτιάξουμε δύο αντικείμενα θα υπάρχουν δύο size και δύο garden. Αν δε φτιάξουμε κανένα αντικείμενο, δε θα υπάρχει καμία size και καμία garden!!


1.2 Χρησιμοποιώντας τις μεταβλητές
Ωραία, ας πούμε ότι φτιάξαμε δύο αντικείμενα house: το a και το b. Πώς θα χρησιμοποιήσουμε (διαβάσουμε, αλλάξουμε) τα δεδομένα που περιέχουν;

Λοιπόν κρατείστε στο νου σας ότι υπάρχουν δύο τρόποι να κάνουμε κάτι τέτοιο. Το ποιόν θα χρησιμοποιούμε κάθε φορά εξαρτάται από ένα μόνο πράγμα: αν «υπονοείται» σε ποιο αντικείμενο αναφερόμαστε ή όχι! Θα δούμε αναλυτικότερα πότε ισχύει το πρώτο και πότε το δεύτερο. Προς το παρόν κρατήστε απλώς ότι υπάρχουν αυτές οι δύο περιπτώσεις. Ας δούμε πως συμπεριφερόμαστε στην κάθε μία:

i. Δεν υπονοείται σε ποιο αντικείμενο αναφερόμαστε.
Σε αυτή την περίπτωση για να «αναφερθούμε» στο πεδίο ενός αντικειμένου πρέπει να πούμε κάτι σαν: το «τάδε» πεδίο του «τάδε» αντικειμένου. Αυτό γίνεται απλά γράφοντας πρώτα το όνομα του αντικειμένου, μία τελεία, και το όνομα του πεδίου. Κάπως έτσι δηλαδή:
Μορφοποιημένος Κώδικας: Επιλογή όλων
a.size = 30;
b.garden = true;

//ή

System.out.print(b.garden+"\n");
//και γενικά κάνοντας οτιδήποτε θα κάναμε με μία boolean και μία double:
double d = 2*a.size-1;
if (a.garden || b.garden) //...κάνε κάτι...

Ας συμφωνήσουμε τώρα ότι τέτοιες περιπτώσεις θα λέμε ότι βρίσκονται σε static πλαίσιο.

ii. Υπονοείται σε ποιο αντικείμενο αναφερόμαστε:
Εδώ τα πράγματα είναι πιο εύκολα, αφού το αντικείμενο έχει καθοριστεί, άρα το μόνο που απομένει είναι να αναφερθούμε στα πεδία του. Αυτό γίνεται κατ ευθείαν με το όνομα του πεδίου:
Μορφοποιημένος Κώδικας: Επιλογή όλων
size = 30;
garden = true;

System.out.print(garden+"\n");

double d = 2*size-1;
if (garden) //...κάνε κάτι...

Ας συμφωνήσουμε όπως και πριν, ότι τέτοιες περιπτώσεις θα λέμε ότι βρίσκονται σε non static πλαίσιο.

Το πότε βρισκόμαστε σε static και πότε σε non-static πλαίσιο σας το χρωστάω, και θα το δούμε παρακάτω. Προς το παρόν κρατήστε ότι μέσα στη main είμαστε σε static πλαίσιο.


Ενότητα 2: Δημιουργία και χρήση αντικειμένων
2.1 Ο constructor


Ωραία όλα τα παραπάνω, αλλά αν δε μάθουμε να φτιάχνουμε τα αντικείμενα δεν έχουν και πολύ νόημα.. Ας δούμε βήμα-βήμα πως γίνεται αυτό.

Αρχικά πρέπει να φτιάξουμε ένα Constructor.
Για να το κάνουμε αυτό, γράφουμε στο σώμα της κλάσης μας, το όνομα της κλάσης (= ο τίτλος του constructor) ακολουθούμενο από άδειες παρενθέσεις: ()
Ύστερα ανοίγουμε αγκύλη { στην οποία θα γράψουμε τις εντολές μας. Μόλις τις γράψουμε όλες, κλείνουμε την αγκύλη } (κάτι όπως κάναμε σε κάθε μας πρόγραμμα με τη main...)

Μέσα στον constructor συμπεριφερόμαστε σαν το αντικείμενο μόλις να δημιουργήθηκε, που σημαίνει στην περίπτωσή μας ότι μόλις δημιουργήθηκε χώρος στη μνήμη για μία boolean, τη garden και μία double, τη size. Η garden δημιουργείται αυτόματα με την τιμή true, μιας και αυτό έχουμε δηλώσει στο σώμα της κλάσης. Η size, επειδή δεν έχουμε ορίσει κάποια τιμή, θα πάρει αυτόματα την τιμή 0. πού είναι η default τιμή για τις double. Αυτό ισχύει και γενικά: όταν στο σώμα της κλάσης η μεταβλητή συνοδεύεται από μία αρχική τιμή, το φρεσκο-δημιουργημένο αντικείμενο θα έχει αυτή την τιμή στην συγκεκριμένη μεταβλητή. Αν δε συνοδεύεται, θα έχει τη default τιμή για τον συγκεκριμένο τύπο (0 για τους αριθμούς, false για τις boolean κτλ).
Μέσα στον constructor μπορούμε να δώσουμε οποιαδήποτε άλλες τιμές στις μεταβλητές, να κάνουμε πράξεις με αυτές, να τις τυπώσουμε στην οθόνη και ό,τι άλλο θέλετε!
Ο constructor είναι ένα non static πλαίσιο. Δηλαδή ισχύει η παραπάνω περίπτωση ii, στην οποία «υπονοείται» για ποιο αντικείμενο μιλάμε (αυτό που μόλις δημιουργήσαμε!). Αυτό πρακτικά σημαίνει ότι μέσα στον constructor θα αναφερόμαστε κατ' ευθείαν στα πεδία του. Ας φτιάξουμε λοιπόν τον πρώτο μας constructor:

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

double size;
boolean garden = true;

//Ακολουθεί ο constructor. Εδώ το μόνο που θα κάνει είναι να δίνει αρχικές τιμές στις μεταβλητές του αντικειμένου μας

House() {
//Όταν το πρόγραμμα φτάσει εδώ, σημαίνει ότι μόλις έχει δημιουργηθεί ένα αντικείμενο House...
size=40;
garden = false;
}//House

//...methods...
}//class


Η κλάση μας πλέον απέκτησε και constructor! Εδώ δίνει αρχικές τιμές στις size και garden, αλλά θα μπορούσε να κάνει οτιδήποτε άλλο, όπως πχ να εμφανίζει και ένα μήνυμα στην οθόνη (γιατί όχι;) κτλ κτλ...
Ίσως σας φαίνεται λίγο ανώφελο το ότι δίνουμε μία default τιμή true στην garden, και όταν εκτελείται ο constructor τη γυρίζουμε σε false. Έχετε δίκιο! Απλώς ήθελα να σας δείξω ότι μπορούμε να δίνουμε αρχικές τιμές στο σώμα της κλάσης.

2.2 Δήλωση
Ας υποθέσουμε τώρα ότι είμαστε μέσα στη main, από εκεί δηλαδή που ξεκινά η εκτέλεση του προγράμματός μας. Θέλουμε τώρα να δημιουργήσουμε ένα αντικείμενο House. Πώς το κάνουμε αυτό;
Κατ αρχήν είναι χρήσιμο, αν θέλουμε να κάνουμε κάτι με το αντικείμενό μας, να του δώσουμε ένα όνομα - να το θέσουμε με άλλα λόγια σε μία μεταβλητή, όπως κάναμε με τους βασικούς τύπους. Αυτό γίνεται ακριβώς με τον ίδιο τρόπο:/εκτελεί τις πράξεις: d=120; b=true; και μετά μία-μία τις εντολές του constructor
h2 = new House(2*40, false); //ομοίως! εκτελεί τις: d=2*40; b=
Όπως γράφαμε
Μορφοποιημένος Κώδικας: Επιλογή όλων
int g;
για να δηλώσουμε έναν int, έτσι τώρα θα γράψουμε
Μορφοποιημένος Κώδικας: Επιλογή όλων
House aHouse;
για να δηλώσουμε ένα House. Τόσο απλά! Έχουμε πεί έτσι στο πρόγραμμα ότι υπάρχει η μεταβλητή aHouse και συμβολίζει ένα αντικείμενο τύπου House.

Προσοχή: Κανένα αντικείμενο δεν έχει δημιουργηθεί ακόμα! Απλώς έχουμε δημιουργήσει μία μεταβλητή που ΘΑ αντιστοιχεί σε ένα αντικείμενο.


Για τους γνώστες C/C++ θα πώ σε αυτό το σημείο ότι το μόνο που έχουμε δημιουργήσει είναι ένας pointer. Αν δεν ξέρετε C/C++ αγνοήστε αυτό το σχόλιο.


2.3 Κλήση του constructor
Σε αυτό το σημείο θα χρησιμοποιήσουμε τον constructor μας αφού όπως είπαμε, αυτός φτιάχνει τα αντικείμενα. Να πως γίνεται:
Γράφουμε τη λέξη-κλειδί new, συνοδευόμενη από το όνομα του constructor (το όνομα συμπεριλαμβάνει και τις παρενθέσεις). Δηλαδή γράφουμε αυτό:

Μορφοποιημένος Κώδικας: Επιλογή όλων
new House();

Όταν το πρόγραμμα φτάσει σε αυτό το σημείο θα κάνει τα εξής:
1. «Παγώνει» τη ροή των εντολών που εκτελεί
2. Δημιουργεί το νέο αντικείμενο: φτιάχνει χώρο στη μνήμη για όλα τα πεδία του (αυτά που αναφέρονται δηλαδή στο σώμα της κλάσης). Αν αναφέρουν αρχική τιμή παίρνουν κατ' ευθείαν αυτή (στην περίπτωσή μας πχ. η garden γίνεται true). Αν δεν αναφέρουν τιμή (όπως πχ η size), παίρνουν την default τιμή που προβλέπεται για τον συγκεκριμένο τύπο (μηδέν για τους αριθμούς κτλ.)
3. Εκτελεί μία-μία τις εντολές του constructor μας, δηλαδή του House().
Συνήθως εδώ δίνουμε αρχικές τιμές στις μεταβλητές που δεν πήραν στο βήμα 2, αλλά μπορούμε να εκτελέσουμε οποιαδήποτε άλλη εντολή θέλουμε.
4. Συνεχίζει κανονικά τη ροή με ό,τι ακολουθεί.

Ωραία! Δημιουργήσαμε ένα αντικείμενο! Πως τώρα θα το χρησιμοποιήσουμε;
Στην πραγματικότητα η παραπάνω εντολή φτιάχνει ένα αντικείμενο, το οποίο όμως δεν «αντιπροσωπεύεται» από καμία μεταβλητή! Υπάρχει δηλαδή κάπου στη μνήμη, αλλά δεν έχουμε πρόσβαση σε αυτό. Στις 99 από τις 100 περιπτώσεις θα χρειαστεί να αναθέσουμε το αντικείμενό μας σε μία μεταβλητή για να κάνουμε κάτι με αυτό μετά τη δημιουργία του. Αυτό γίνεται εύκολα αν τη στιγμή που καλούμε τον constructor αναθέσουμε αμέσως τη μεταβλητή ως εξής:
Μορφοποιημένος Κώδικας: Επιλογή όλων
aHouse = new House();


Με αυτό τον τρόπο αναθέτουμε στη μεταβλητή aHouse (την οποία έχουμε δηλώσει νωρίτερα) να συμβολίζει το νέο μας αντικείμενο!
Τέλος να πούμε ότι πολύ συχνά τα βήματα της δήλωσης και της δημιουργίας του αντικειμένου γίνονται την ίδια στιγμή ως εξής:
Μορφοποιημένος Κώδικας: Επιλογή όλων
House aHouse = new House();

Κάτι δηλαδή σαν αυτό που κάναμε με τους βασικούς τύπους:
Μορφοποιημένος Κώδικας: Επιλογή όλων
char ch = 'g';


Ας συνεχίσουμε λοιπόν να εμπλουτίζουμε το πρόγραμμά μας, δημιουργώντας ένα αντικείμενο House με τον τρόπο που περιγράψαμε παραπάνω:

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

double size;
boolean garden = true;

House() { //constructor
size = 40;
garden = false;
}//House

public static void main(String args[]) {
House h1; //το h1 είναι προς το παρόν «κενό» αντικείμενο.

h1 = new House(); //δημιουργία του h1

System.out.print("Το σπίτι h1 έχει "+ h1.size+ " τετραγωνικά μέτρα");

if (h1.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

}//main
}//class


Παρατηρούμε ότι μετά τη δημιουργία του αντικειμένου, μπορούμε να αναφερόμαστε στα πεδία του όπως θα κάναμε με μία double ή μία boolean.
Ας περιπλέξουμε λίγο ρο προηγούμενο παράδειγμα, δημιουργώντας τρία αντικείμενα:

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

double size;
boolean garden = true;

House() { //constructor
size = 40;
garden = false;
}//House

public static void main(String args[]) {
House h1, h2, h3;

h1 = new House(); //δημιουργία αντικειμένων και τροποποίηση των τιμών τους
h1.size = 120; //Από 40 το πάμε στο 120
h1.garden = true; //του δίνουμε και κήπο :)

h2 = new House();
h2.size *= 2;//αυτό απλώς το διπλασιάζουμε..

h3 = new House(); //αυτό ας το αφήσουμε ως έχει..

System.out.print("Το σπίτι h1 έχει "+ h1.size+ " τετραγωνικά μέτρα");
if (h1.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

System.out.print("Το σπίτι h2 έχει "+ h2.size+ " τετραγωνικά μέτρα");
if (h2.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

System.out.print("Το σπίτι h3 έχει "+ h3.size+ " τετραγωνικά μέτρα");
if (h3.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}
}//main
}//class


Σας παρακινώ να φτιάξετε και να τρέξετε το παραπάνω πρόγραμμα! Το αποτέλεσμα που θα δείτε στην οθόνη θα σας δώσει μια και καλή να καταλάβετε ότι υπάρχουν 3 size και 3 garden. Το κάθε αντικείμενο έχει τη δική του. Αυτό είναι πολύ σημαντικό να ξεκαθαριστεί!



2.4 Πέρασμα ορισμάτων στον Constructor και Υπερφόρτωση
Για να φτιάξουμε ένα σπίτι με size = 120 και garden = true έπρεπε αρχικά να φτιάξουμε το αντικείμενο με 40, false και μετά να τα αλλάξουμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
h1 = new House();//φτιάχνει size=40, garden=false
h1.size = 120;
h1.garden = true;


Υπάρχει μήπως τρόπος να πούμε στον constructor μας κατ ευθείαν: φτιάξε ένα σπίτι με «τάδε» size και «τάδε» garden;

Η απάντηση είναι όχι απλώς ένα μεγάλο ναι, αλλά και ότι μπορούμε να «περνάμε» στον constructor μας όσα μηνύματα θέλουμε για το τι να κάνει. Έτσι γίνεται απίστευτα ευέλικτος στη χρήση του!
Τα μηνύματα αυτά του τα στέλνουμε μέσω μεταβλητών. Οι μεταβλητές αυτές ονομάζονται ορίσματα. Γράφονται μέσα στις παρενθέσεις του constructor και χωρίζονται με κόμμα. Στα πλαίσια του παραδείγματός μας οι πληροφορίες που θα χρειαστούμε είναι πόσο μεγάλο σπίτι να φτιάξει, και αν θα έχει κήπο ή όχι. Άρα τα ορίσματά μας θα είναι μία double και μία boolean. Να πως γίνεται:

i. Κατά τη δήλωση του constructor:
Μορφοποιημένος Κώδικας: Επιλογή όλων
House(double d, boolean b) {
size = d;
garden = b;
}


Μέσα στον constructor μπορούμε να κάνουμε ό,τι θέλουμε με τα d και b.
Εδώ αρκεστήκαμε στο να δώσουμε τις τιμές αυτές στο αντικείμενο, αλλά γενικά μπορούμε να κάνουμε τα πάντα.
Όπως βλέπουμε τα ορίσματα παίζουν το ρόλο δήλωσης μεταβλητής. Μέσα στον constructor μεταχειριζόμαστε τις μεταβλητές αυτές σαν... να έχουν πάρει μία αρχική τιμή αν και πουθενά αυτή δε φαίνεται (πουθενά πχ δε βλέπουμε αν η b είναι true ή false). Και εδώ ακολουθεί όλο το μυστικό...

ii. Κατά την κλήση του constructor:
Αντί να τον καλέσουμε με τις άδειες παρενθέσεις...τις γεμίζουμε! Τις γεμίζουμε με τα ορίσματα που θα περάσουμε στον constructor. Και λέγοντας ορίσματα εννοούμε εδώ συγκεκριμένες σταθερές ή μεταβλητές που έχουν πάρει αρχική τιμή. Οι τύποι των ορισμάτων πρέπει να ταιριάζουν με αυτά που γράψαμε κατά τη δήλωση του constructor.
Στην περίπτωσή μας λοιπόν θα είναι μία double και μία boolean:

Μορφοποιημένος Κώδικας: Επιλογή όλων
House h1 = new House(70, false);
ή
House h1 = new House(100, true);

ή ακόμα και με μεταβλητές όπως:
double dek = 60.8;
House h1 = new House(dek, true);

ή και με πράξεις:
double dek = 30.8;
House h1 = new House(dek*4, 40<dek);

Όλα Τα παραπάνω είναι σωστά!


Αυτό που θα κάνουν τα παραπάνω είναι να «περάσουν» τις τιμές αυτές στις μεταβλητές d και b μέσα στον constructor μας, και να τον εκτελέσουν κανονικά.
Το πρόγραμμά μας μετά την τροποποίηση αυτή θα γίνει κάπως έτσι:

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

double size;
boolean garden;

House(double d, boolean b) { //constructor
size = d;
garden = b;
}//House

public static void main(String args[]) {
House h1, h2, h3;

h1 = new House(120, true);
h2 = new House(2*40, false);
h3 = new House(40, false);

System.out.print("Το σπίτι h1 έχει "+ h1.size+ " τετραγωνικά μέτρα");
if (h1.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

System.out.print("Το σπίτι h2 έχει "+ h2.size+ " τετραγωνικά μέτρα");
if (h2.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

System.out.print("Το σπίτι h3 έχει "+ h3.size+ " τετραγωνικά μέτρα");
if (h3.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

}//main
}//class

Ας δούμε αναλυτικά τι συμβαίνει κατά την κλήση του constructor.
Ο μηχανισμός που περιγράψαμε στην υπό-ενότητα 2.3 θα γενικευτεί ως εξής:

Όταν το πρόγραμμα φτάσει στο σημείο κλήσης
1. «Παγώνει» τη ροή των εντολών που εκτελεί
2. Δημιουργεί το νέο αντικείμενο όπως περιγράψαμε
3. Να το καινούριο σημείο: Περνάει μία-μία τις τιμές των ορισμάτων εκτελώντας την ακόλουθη πράξη για κάθε όρισμα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
μεταβλητή εντός παρένθεσης στη δήλωση  =  μεταβλητή (ή σταθερά) εντός παρένθεσης στην κλήση

στην περίπτωσή μας δηλαδή, στο "h1 = new House(120, true);" θα κάνει τις πράξεις:
Μορφοποιημένος Κώδικας: Επιλογή όλων
d=120;
b=true;

Προσέξτε την αντιστοιχία 1 προς 1 των εννοιών «μεταβλητή εντός παρένθεσης στη δήλωση» και «μεταβλητή (ή σταθερά) εντός παρένθεσης στην κλήση» με τις {d, b} και {120, true}.
4. Εκτελεί μία-μία τις εντολές του constructor μας.
5. Συνεχίζει κανονικά τη ροή με ό,τι ακολουθεί.

Και σε αυτό το σημείο να τονίσουμε ξανά ότι οι μεταβλητές b και d, δηλαδή τα ορίσματα του constructor ΔΕΝ ταυτίζονται με τα πεδία του αντικειμένου μας. Είναι μηνύματα που δίνουμε στον constructor κατά την κλήση του. Από εκεί και πέρα, τη στιγμή που γράφουμε τον constructor, είμαστε ελεύθεροι αυτά τα μηνύματα να τα μεταχειριστούμε όπως θέλουμε, και όχι κατ' ανάγκη να τα δώσουμε ως πεδία στο αντικείμενο.


Ας αναλογιστούμε για λίγο τι καταφέραμε. Πριν είχαμε ένα constructor που «ήξερε ακριβώς τι να κάνει». Δε χρειαζόταν να του πούμε τίποτα (καλό συτό), αλλά και δε μπορούσαμε να του πούμε τίποτα (κακό αυτό)!
Τώρα αντίθετα έχουμε ένα που περιμένει από εμάς δύο πληροφορίες, αλλά δε δουλεύει αν δε του τις δώσουμε.

Και έρχεται η εξής απορία: το ένα αποκλείει το άλλο;
Νομίζω πως φαντάζεστε την απάντηση: όχι!
Μπορούμε να έχουμε και τα δύο. Στην πραγματικότητα μπορούμε να έχουμε όσους constructor θέλουμε, αρκεί να έχουν διαφορετική λίστα από τύπους ορισμάτων, έτσι ώστε όταν καλούμε έναν από αυτούς, το πρόγραμμα να καταλαβαίνει ποιόν εννοούμε. Εμπλουτίζουμε λοιπόν το πρόγραμμά μας ώστε να έχει και τους δύο:

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

double size;
boolean garden;

House() {
size = 40;
garden = false;
}//House

House(double d, boolean b) { //constructor
size = d;
garden = b;
}//House

public static void main(String args[]) {
House h1, h2, h3;

h1 = new House(120, true); //ορίσματα: double, boolean
h2 = new House(2*40, false);

h3 = new House(); //ορίσματα: τίποτα

System.out.print("Το σπίτι h1 έχει "+ h1.size+ " τετραγωνικά μέτρα");
if (h1.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

System.out.print("Το σπίτι h2 έχει "+ h2.size+ " τετραγωνικά μέτρα");
if (h2.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

System.out.print("Το σπίτι h3 έχει "+ h3.size+ " τετραγωνικά μέτρα");
if (h3.garden) {System.out.print(" και κήπο.\n");}
else {System.out.print(".\n");}

}//main
}//class

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


2.5 Πράξεις αντικειμένων, το κενό αντικείμενο
Οι πράξεις μεταξύ αντικειμένων που υπάρχουν στην Java είναι πολύ συγκεκριμένες. Η ιδιότητα εφαρμογής διαφόρων τελεστών σε αντικείμενα, όπως στη C++, εδώ δεν υπάρχει. Το μόνο που μπορούμε να κάνουμε είναι να αναθέτουμε αντικείμενα σε άλλα αντικείμενα με τον τελεστή = και να ελέγχουμε ισότητα ή μη, με τους τελεστές == και !=
Τα ακόλουθα λοιπόν είναι απόλυτα σωστά:
Μορφοποιημένος Κώδικας: Επιλογή όλων
House h1, h2;
h1 = new House();
h2 = new House();
//...
h2 = h1;
ή
if (h1==h2) //...
while (h1!=h2) //...

Όπως θα κάναμε δηλαδή με δύο ίδιους βασικούς τύπους!

Κάποιες φορές είναι χρήσιμο μία μεταβλητή να μην αντιπροσωπεύει κανένα αντικείμενο. Ή για να το πούμε καλύτερα, να αντιπροσωπεύει το κενό αντικείμενο.
Αυτό είναι ένα αντικείμενο που (όπως λέει και το όνομά του) δεν περιέχει κανένα πεδίο. Αναφερόμαστε σε αυτό με τη λέξη-κλειδί null. Με τον τελεστή = ή τους == και != μπορούμε να θέσουμε σε μία μεταβλητή το αντικείμενο null, ή να ελέγξουμε αν ένα αντικείμενο είναι το null. Αυτό γίνεται απλά ως εξής:

Μορφοποιημένος Κώδικας: Επιλογή όλων
House h1;
h1 = null;
ή
if (h1==null)
κτλ
Περισσότερες πληροφορίες για το πως λειτουργούν αυτοί οι τελεστές θα πούμε στο τέλος αυτού του κεφαλαίου!!


2.6 Default Constructor
Θα σημειώσουμε σε αυτό το σημείο ότι έχουμε το δικαίωμα (αν και δεν είναι καλή συνήθεια) να μην φτιάξουμε κανένα constructor, αλλά παρ' όλα αυτά να δημιουργούμε αντικείμενα. Αυτό γίνεται ως εξής:
Αν ο compiler δει ότι δεν υπάρχει κανένας constructor, αλλά εμφανίζεται ένας constructor χωρίς ορίσματα, δηλαδή έκφραση της μορφής:
Μορφοποιημένος Κώδικας: Επιλογή όλων
new House();

θα φτιάξει μόνος του ένα constructor! Το μόνο που θα κάνει ο constructor αυτός είναι να δίνει τις τιμές που έχουμε ορίσει στο σώμα της κλάσης για όλα τα πεδία του αντικειμένου. Και γι αυτά που δεν έχουμε ορίσει δίνει τις default τιμές:
0 για όλους τους αριθμούς
false για την boolean
null για αντικείμενα.


Ενότητα 3: Μέθοδοι
3.1 Δημιουργία και κλήση

Η εικόνα που έχουμε ως τώρα για τα αντικείμενα είναι κάτι σαν ένας αριθμός από δεδομένα, που ομαδοποιούμε όλα μαζί και τους δίνουμε ένα όνομα. Όπως στα παραπάνω παραδείγματα, που το αντικείμενο House δεν ήταν τίποτα άλλο από μια double και μία boolean.
Ίσως αυτή η εικόνα για τα τόσο διαφημισμένα αντικείμενα να μη σας γεμίζει το μάτι. Και έχετε δίκιο.

Ευτυχώς για εμάς τα αντικείμενα έχουν και μία άλλη δυνατότητα: να έχουν τις δικές τους μεθόδους! Αυτό τα κάνει υπερβολικά πιο χρήσιμα. Πως όμως γράφουμε και χρησιμοποιούμε μεθόδους;
Στην πραγματικότητα έχουμε δηλώσει αρκετές φορές μεθόδους. Κάθε φορά που γράφαμε την main(...) δηλώναμε μία μέθοδο. Επίσης θα δούμε πολλές ομοιότητες με τη δήλωση ενός constructor.

Θα τα δούμε βήμα-βήμα:

i. Όλα αρχίζουν με τον τύπο επιστροφής (αντίθετα με τον constructor που δεν έχει). Είπαμε ότι η μέθοδος επιστρέφει μία τιμή. Τι τιμή θέλουμε να επιστρέφει; int, double, String; Γράφουμε αυτό τον τύπο αυτό.
Υπάρχουν όμως περιπτώσεις όπου δε μας ενδιαφέρει να επιστρέψουμε καμία τιμή. Σε αυτή την περίπτωση γράφουμε τη λέξη κλειδί void (σας θυμίζει κάτι;)

ii. Βαφτίζουμε τη μέθοδό μας. Δίνουμε ένα όνομα που μας αρέσει, και κατά προτίμηση να περιγράφει το σκοπό της.

Χρησιμοποιείτε ονόματα που να αρχίζουν με μικρό γράμμα, και κάθε άλλη λέξη που περιέχει να αρχίζει με κεφαλαίο. Πχ: writeToFile(...) slowDown(...) addNumbers(...) κτλ.
Είναι μία σύμβαση που χρησιμοποιούν σχεδόν όλοι οι προγραμματιστές στην Java


iii. Γράφουμε μέσα σε παρενθέσεις τα ορίσματα της μεθόδου, χωρισμένα με κόμμα, ακριβώς όπως και στον constructor.
Αυτό πχ έχουμε κάνει επανειλημμένα με τη μέθοδο System.out.print(...). Η μέθοδος αυτή τυπώνει ένα μήνυμα στην οθόνη. Αλλά ποιο; Πρέπει να της «πούμε» ποιο να τυπώσει! Γι αυτό της περνάμε ένα όρισμα (συνήθως ένα String) που να λέει ακριβώς αυτό: τι να τυπώσει.
Υπάρχουν όμως περιπτώσεις που δεν έχουμε ανάγκη να πούμε τίποτα στη μέθοδό μας: ξέρει ακριβώς τι να κάνει. Σε αυτή την περίπτωση γράφουμε κενές παρενθέσεις! Πχ:

Μορφοποιημένος Κώδικας: Επιλογή όλων
int addNumbers(int i, int j)
void add1(int j)
double hBar()
void sayHello()


iv. Ανοίγουμε αγκύλη {, γράφουμε όλες τις εντολές που θέλουμε να κάνει η μέθοδός μας, και κλείνουμε την αγκύλη }.
Αν η μέθοδός μας δεν είναι void, πρέπει οπωσδήποτε να επιστρέψει κάποια τιμή. Αυτό το κάνουμε με την εντολή rerurn, ακολουθούμενη από την τιμή που θέλουμε να επιστρέψουμε: η τιμή αυτή φυσικά πρέπει να έχει τύπο που συμφωνεί με τον τύπο επιστροφής. Όταν μία μέθοδος επιστρέφει, στην ουσία τερματίζει. Δεν κάνει τίποτα άλλο.
Αν η μέθοδός μας είναι void, μπορούμε να χρησιμοποιήσουμε την εντολή return σκέτη, χωρίς τιμή. Αυτό απλώς θα τερματίσει τη μέθοδο, και έχει νόημα μόνο πχ μέσα σε κάποιο if-block.

Παραδείγματα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
int giveMeFive() {
System.out.print("Αυτή είναι μία ψιλό-άχρηστη μέθοδος, που απλώς επιστρέφει 5\n");
return 5;

/*με την ίδια λογική μπορούμε να γράψουμε:
int i = 5;
return i;
Μιά χαρά θα δουλέψει
Όπως και το
return 10/2; ή το
int i=3;
return i*2-1;
όλα θα κάνουν την ίδια δουλειά!*/
}//giveMeFive


void sayHelloManyTimes(int times) {

if(times<=0) {
System.out.print("Ο αριθμός που δώσατε δεν είναι θετικός, παρακαλώ δώστε θετικό αριθμό\n");
return; //μη συνεχίσεις! Τερματισμός μεθόδου
}//if

else if(times > 1000) {
System.out.print("Πολύ μεγάλος αριθμός! Δώστε αριθμό μέχρι 1000\n");
rerurn;
}//else if

for (int i=times; i>0; --i) System.out.print("Γειά σου!\n");
}//sayHelloManyTimes

boolean isGreater(int i, int j) {
return i<j;
}//isGreater


Για λόγους καθαρότητας του κειμένου σας συνιστώ, όπως έχω κάνει παραπάνω, να αφήνετε ένα tab στο σώμα της μεθόδου. Έτσι όταν έχετε ένα πρόγραμμα με πολλές ή μεγάλες μεθόδους θα βρίσκετε εύκολα την αρχή τους! Μάλιστα ο gedit έχει και μία επιλογή στις Προτιμήσεις που λέει «ενεργοποίηση αυτόματης εσοχής» η οποία θα κάνει τη ζωή σας πιο εύκολη.


Υπάρχει και η λογική του να αφήνετε ένα tab παραπάνω κάθε φορά που ανοίγετε ένα block εντολών (πχ σε κάθε if ή for κτλ). Κάτι τέτοιο προσωπικά το θεωρώ υπερβολή γιατί αν δεις ένα κείμενο με τόσα tab, πιο πολύ μπερδεύει παρά βοηθάει το μάτι. Φυσικά είστε ελεύθεροι να κάνετε ό,τι θέλετε με τα tab και τις νέες σειρές, αλλά έχετε πάντα κατά νου το καθαρό κείμενο. Δε μπορείτε να φανταστείτε πόσο βοηθάει!

v. Καλούμε τη μέθοδο που φτιάξαμε.
Η διαδικασία κλήσης της μεθόδου είναι παρόμοια με αυτή του constructor:
1. Η ροή του προγράμματος σταματά
2. Δίνονται μία-μία τιμές στα ορίσματα, όπως στον constructor
3. Εκτελούνται όλες οι εντολές της μεθόδου
4. Αν η μέθοδος επιστρέφει κάποια τιμή, η τιμή αυτή μπαίνει στη θέση που είχαμε γράψαμε το όνομα της μεθοδου
5. Συνεχίζεται η ροή του προγράμματος

Προσέξτε ένα σημαντικό σημείο: τη μέθοδο την υλοποιεί ένα αντικείμενο. Αυτό σημαίνει ότι αν δε φτιάξουμε πρώτα ένα αντικείμενο, δεν μπορούμε να καλέσουμε τη μέθοδο!
Από τη στιγμή που έχουμε το αντικείμενο, καλούμε τη μέθοδο όπως και τις μεταβλητές:
Για σε static πλαίσιο γράφουμε όνομα αντικείμένου, τελεία, όνομα μεθόδου.
Για non-static πλαίσιο γράφουμε κατ ευθείαν το όνομα της μεθόδου.

Ας πούμε ότι έχουμε μία κλάση SomeClass που έχει μία μέθοδο someMethode() που επιστρέφει ένα int. Θα εργαστούμε ως εξής:

Μορφοποιημένος Κώδικας: Επιλογή όλων
SomeClass myobj = new SomeClass();

int i = myobj.myMethode();
//επίσης σωστό είναι και το
int j = myobj.myMethode()/2 +1;
//ή ακόμα και το
System.out.print(myobj.myMethode()+"\n");

//δίδαγμα:
//χρησιμοποιούμε τη μέθοδο που επιστρέφει ένα int, ακριβώς όπως θα χρησιμοποιούσαμε ένα απλό int!!


Οι μέθοδοι αποτελούν non-static πλαίσιο. Αυτό σημαίνει ότι μπορούμε να αναφερόμαστε κατ ευθείαν στα πεδία του αντικειμένου. Αλλά... μια στιγμή! Ποιανού αντικειμένου; Ποιο είναι εδώ το αντικείμενο που «υπονοείται»;
Απάντηση: αυτό που καλεί την μέθοδο. Δηλαδή αυτό που κρύβεται πίσω από την τελεία στην κλήση της μεθόδου.

Ας δούμε το ακόλουθο παράδειγμα. Περιέχει δύο constructor, τρείς μεθόδους και τη main:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο UbuntuPC.java
public class UbuntuPC {

double version =11.10, ram;
int bits;
boolean printer;

//constructor
UbuntuPC (double vs, double r, int b, boolean pr) {
version = vs; //απλώς δίνουμε τιμές στα πεδία...
ram = r;
bits = b;
printer = pr;
}//constructor

UbuntuPC (double r, int b, boolean pr) {
ram = r; //Στη version δε δίνουμε τιμή. Άρα αυτή θα μείνει με την αρχική της: την 11.10
bits = b;
printer = pr;
}//constructor

void upgradeVersion() {
//αυτή η μέθοδος υποτίθεται ότι κάνει αναβάθμιση του ubuntu

if (version%1 <= 0.04) {version +=0.06;}
else {version +=0.94;}

System.out.print("Η εκδοσή σας αναβαθμίστηκε!\nΚαλωσήλθατε στο ubuntu "+version+"\n");
}//upgradeVersion


boolean putMoreRam(double more) {
//αυτή η μέθοδος υποτίθεται ότι προσθέτει RAM στο UbuntuPC μας. Επιστρέφει true αν αυτό γίνει με επιτυχία, false διαφορετικά

if (ram+more<=16) {
ram += more;
System.out.print("Συγχαρητήρια! η νέα σας ram είναι "+ram+" GB\n");
return true; //όλα πήγαν καλά
}//if

else {
System.out.print("Λυπάμαι, δεν μπορείτε να βάλετε περισσότερη ram... Τώρα έχετε "+ram+" GB\n");
return false;
}//else
}//putMoreRam

void whatYouAre() {
System.out.print("ubuntu version: "+version+" "+bits+" bit\nram: "+ram+" GB\n");
}//whatYouAre

public static void main(String[] args) {
UbuntuPC mypc = new UbuntuPC(10.10,8,64,false);
mypc.whatYouAre();// το αντικείμενο mypc καλεί τη μέθοδό του, την whatYouAre();

mypc.putMoreRam(2); //επιστρέφει μία boolean την οποία εδώ αγνοούμε...

mypc.whatYouAre();

}//main
}//class


Σημαντικό: Όπως είπαμε στο προηγούμενο κεφάλαιο, κάθε μεταβλητή που γεννιέται μέσα σε ένα block εντολών {}, «ζει» μέχρι το πρόγραμμα να εξέλθει από το block. Το ίδιο ισχύει και για τις μεθόδους! Άλλωστε block εντολών είναι και αυτές. Μία μεταβλητή που δηλώνεται μέσα σε μία μέθοδο, είτε στο σώμα της, είτε ως όρισμα, ζει μόνο μέσα στη μέθοδο. Μέσα από μια άλλη μέθοδο είναι σαν να μην υπάρχει. Αντίθετα, οι μεταβλητές που δηλώνονται στο σώμα της κλάσης, είναι ορατές σε όλες της μεθόδους της κλάσης.


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


3.2 Υπερφόρτωση μεθόδων
Είναι λογικό ότι κάθε συνάρτηση πρέπει να έχει ένα όνομα, το οποίο πρέπει να είναι αποκλειστικά δικό της. Αν πχ δύο συναρτήσεις είχαν το ίδιο όνομα, όταν καλούσαμε αυτό το όνομα, πως το πρόγραμμα θα ήξερε ποια να καλέσει;
Ίσως... από το επίθετο!! :-)

Και ναι είναι δυνατόν να ορίσουμε πολλές συναρτήσεις με το ίδιο όνομα. Και αυτό που θα τις ξεχωρίζει (γιατί επίθετα δεν υπάρχουν εδώ) είναι η λίστα των ορισμάτων τους, ακριβώς όπως με τους constructor.
Ο επιπλέον περιορισμός που υπάρχει εδώ είναι ότι μέθοδοι με το ίδιο όνομα πρέπει να έχουν ίδιο τύπο επιστροφής.
Ας δούμε το ακόλουθο παράδειγμα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
int myMethode() {...}

int myMethode(int i) {...}//σωστό
int myMethode(boolean b) {...}//επίσης σωστό
int myMethode(int i, double db) {...}//μιά χαρά

long myMethode(int i){...} /*ΛΑΘΟΣ
ο τύπος επιστροφής πρέπει να είναι σε όλες τις περιπτώσεις ίδιος.
Δε μπορεί ξαφνικά να γίνει long!!*/


Το φαινόμενο όπου υπάρχουν πολλές μέθοδοι με το ίδιο όνομα λέγεται υπερφόρτωση τελεστών και την έχουμε συναντήσει με τη μέθοδο System.out.print(...)
Αν το σκεφτείτε λίγο, της έχουμε βάλει από int και boolean, μέχρι String και δουλεύει με όλα!
Ας εμπλουτίσουμε λοιπόν την κλάση UbuntuPC με μία μέθοδο επιπλέον:

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

double version=11.10, ram;
int bits;
boolean printer;

UbuntuPC (double r, int b, boolean pr) {
ram = r;
bits = b;
printer = pr;
}//constructor

UbuntuPC (double vs, int r, int b, boolean pr) {
version = vs;
ram = r;
bits = b;
printer = pr;
}//constructor

void upgradeVersion() {
if (version%1 <= 0.04) {version +=0.06;}
else {version +=0.94;}

System.out.print("Η εκδοσή σας αναβαθμίστηκε!\nΚαλωσήλθατε στο ubuntu "+version+"\n");
}//upgradeVersion


boolean putMoreRam(int more) {
if (ram+more<=16) {
ram += more;
System.out.print("Συγχαρητήρια! η νέα σας ram είναι "+ram+" GB\n");
return true; //όλα πήγαν καλά
}//if

else {
System.out.print("Λυπάμαι, δεν μπορείτε να βάλετε περισσότερη ram... Τώρα έχετε "+ram+" GB\n");
return false;
}//else
}//putMoreRam

boolean putMoreRam() { //υπερφόρτωση! Δεύτερη έκδοση της putMoreRam, αυτή τη φορά χωρίς ορίσματα...
return putMoreRam(2);

/*καλεί απλώς την πιο πάνω μέθοδο με όρισμα 2. Αυτή αποτελεί απλώς μία συντομογραφία.
Κάθε φορά που θα γράφουμε putMoreRam() το πρόγραμμα θα εκτελεί τελικά την putMoreRam(2)
Θα μπορούσε βέβαια να κάνει οτιδήποτε άλλο. Είναι μία ξεχωριστή μέθοδος από την putMoreRam(double)
και θα μπορούσε να έχει τις δικές της εντολές!*/
}//putMoreRam

void whatYouAre() {
System.out.print("ubuntu version: "+version+" "+bits+" bit\nram: "+ram+" GB\n");
}//whatYouAre


public static void main(String[] args) {
UbuntuPC mypc = new UbuntuPC(8,64,false);
UbuntuPC mypc2 = new UbuntuPC(10.10,12,64,true);

mypc.whatYouAre();
mypc2.whatYouAre();

mypc.putMoreRam(4);
mypc2.putMoreRam();

mypc.whatYouAre(); //Ας δούμε τι άλλαξε!
mypc2.whatYouAre();

}//main
}//class



Ενότητα 4: Άλλες χρήσιμες έννοιες
4.1 Η λέξη-κλειδί this

Έχουμε πει ότι σε ένα non-static πλαίσιο «υπονοείται» κάποιο αντικείμενο, του οποίου τα πεδία και τις μεθόδους μπορούμε να καλούμε κατ ευθείαν. Σε μία μέθοδο, το αντικείμενο που υπονοείται είναι αυτό που κάλεσε τη μέθοδο. Σε έναν constructor είναι αυτό που μόλις δημιουργήθηκε. Εδώ τίθεται το εξής πρακτικό πρόβλημα: όταν γράφουμε τη μέθοδο ή τον constructor, δεν ξέρουμε τι όνομα έχει το αντικείμενο που δημιουργείται ή που κάλεσε τη μέθοδο. Ας μιλήσουμε πιο συγκεκριμένα:

Στη main του παραπάνω παραδείγματος γράψαμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
UbuntuPC mypc = new UbuntuPC(10.10,8,64,false);


Όταν γράφαμε όμως τον constructor ή τις μεθόδους μας, το όνομα του αντικειμένου mypc δεν υπήρχε! Μάλιστα στη main θα μπορούσαμε να δημιουργήσουμε πολλά αντικείμενα: mypc1, mypc2, Nikospc κτλ... Αν λοιπόν θέλουμε να αναφερθούμε στο αντικείμενο που καλεί τη μέθοδο, μέσα στη μέθοδο ή στο αντικείμενο που δημιουργεί ο constructor μέσα στον constructor, πώς θα το κάνουμε;

Απάντηση: με τη λέξη-κλειδί this.
Όταν βλέπουμε τη λέξη κλειδί αυτή σε ένα non-static πλαίσιο, σημαίνει: «το αντικείμενο που υπονοείται». Δηλαδή το αντικείμενο που μόλις έφτιαξε ο constructor, ή το αντικείμενο που κάλεσε τη μέθοδο.

Μπορούμε να γράφουμε και τα πεδία του αντικειμένου αυτού κανονικά με την τελεία και το όνομα του πεδίου. πχ:
Μορφοποιημένος Κώδικας: Επιλογή όλων
this.ram
είναι το ίδιο με ram
κτλ


Η χρησιμότητα της this είναι τεράστια! Μπορούμε πχ μέσα από ένα non-static πλαίσιο να τροποποιήσουμε ολόκληρο το αντικείμενο με μιάς:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//μέσα σε μία non static μέθοδο
MyObject ob = new MyObject(...);
//επεξεργαζόμαστε το ob όπως θέλουμε, και μετά...
this = ob;//θέτουμε το αντικείμενό μας ίσο με το ob!


Προφανώς χωρίς τη λέξη-κλειδί this δε θα μπορούσαμε να αναφερθούμε στο αντικείμενο σαν ένα πράγμα, αλλά μόνο ένα-ένα στα πεδία του...

Κρατήστε το συμπέρασμα:
Όταν σε μία μέθοδο βλέπουμε τον όρο this, σημαίνει: το αντικείμενο που κάλεσε τη μέθοδο.
Όταν το βλέπετε σε ένα constructor, σημαίνει: το αντικείμενο που μόλις δημιουργήσαμε.

Όμως το this έχει και μία άλλη μορφή. Μία μορφή που έχει νόημα μόνο μέσα σε ένα constructor. Και αυτή είναι να το χρησιμοποιήσουμε σαν μέθοδο, δηλαδή με παρενθέσεις και ορίσματα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
this();
ή
this(5,true);
κτλ...

Αυτό που κάνει αυτή η μορφή είναι να καλεί έναν άλλο constructor από την κλάση μας. Αυτόν που ταιριάζει στα ορίσματα που δώσαμε.
Αυτός με τη σειρά του κατασκευάζει το αντικείμενο όπως γνωρίζει. Έπειτα εκτελούνται οι υπόλοιπες εντολές που ακολουθούν την this(...).
Η δημιουργία όμως του νέου αντικειμένου είναι η πρώτη ενέργεια ενός constructor, άρα η this(...) αν υπάρχει, θα πρέπει να είναι η πρώτη εντολή μέσα στον constructor. Για παράδειγμα στην κλάση UbuntuPC που φτιάξαμε παραπάνω, θα μπορούσαμε να τροποποιήσουμε τον constructor με τα 3 ορίσματα ως εξής:
Μορφοποιημένος Κώδικας: Επιλογή όλων
UbuntuPC (double r, int b, boolean pr) {
this(11.04,r,b,pr); //καλούμε τον constructor με τα 4 ορίσματα!!

//μετά την this(...) μπορούμε αν θέλουμε, να βάλουμε άλλες εντολές...
}//constructor


Αν η εντολή this(...) εμφανίζεται μέσα σε ένα constructor, πρέπει να βρίσκεται στην αρχή του. Πρέπει δηλαδή να είναι η πρώτη του εντολή!




4.2 Static πεδία
(για τους γνώστες της C θα πρέπει να πω εδώ ότι στην Java η έννοια static είναι τελείως διαφορετική απ' ότι στη C)

Μέχρι τώρα είδαμε πεδία και μεθόδους που περιέχει το αντικείμενο για να δουλέψει. Κάθε αντικείμενο έχει τις δικές του μεταβλητές και τις δικές του μεθόδους.
Όπως και στο παραπάνω παράδειγμα της κλάσης House, δεν μπορούμε να έχουμε πρόσβαση στη μεταβλητή size αν δεν έχουμε δημιουργήσει πρώτα ένα αντικείμενο House. Στην πραγματικότητα, η μεταβλητή size δεν υπάρχει αν δεν υπάρχει (δεν έχει δημιουργηθεί) ένα House.

Παρ όλα αυτά η Java μας δίνει την δυνατότητα να ορίσουμε πεδία που ανήκουν στην ίδια την κλάση!
Τέτοια πεδία λέγονται static, και δημιουργούνται μία φορά, όταν το Virtual Machine φορτώσει την κλάση, ή όταν φορτώσουμε το εκτελέσιμό μας από τον gcj. Όσα αντικείμενα και αν κατασκευάσουμε, κανένα ή χίλια, η static μεταβλητή παραμένει μία και μοναδική. Δεν έχει «την κόπιά της» για κάθε αντικείμενο!
Άρα μία static μεταβλητή μπορούμε να την χρησιμοποιήσουμε και χωρίς να δημιουργήσουμε κανένα αντικείμενο! Σε ένα static πεδίο αναφερόμαστε κατευθείαν μα το όνομά του.

Για να δηλώσουμε μία μεταβλητή static, γράφουμε απλώς τη λέξη-κλειδί static. Πχ:

Μορφοποιημένος Κώδικας: Επιλογή όλων
static int j;
static boolean bl = true;
static final long avogardo;
κτλ


Για να αναφερθούμε σε μία static μεταβλητή, είτε γράφουμε απλώς το όνομά της, πχ:
Μορφοποιημένος Κώδικας: Επιλογή όλων
...
static int s;
...
s = 5;

είτε καλώντας την μέσα από ένα αντικείμενο:
Μορφοποιημένος Κώδικας: Επιλογή όλων
...
static int s;
...
h = new House();
h.s = 5;

Φαίνεται από τα παραπάνω ότι οποιοδήποτε αντικείμενο έχει πρόσβαση σε static μεταβλητές. Συνίσταται ωστόσο να μην αναφερόμαστε μέσω αντικειμένων, για να φαίνεται σε όσους διαβάζουν το πρόγραμμά σας ο static χαρακτήρας της μεταβλητής.

Ας δούμε τα ακόλουθα παραδείγματα για να καταλάβετε τη διαφορά μεταξύ static και non-static:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο StaticTest1.java
public class StaticTest1 {

int i=10;

StaticTest1() {} //δεν κάνει τίποτα! Δε χρειάζεται άλλωστε...

public static void main(String[] args) {
StaticTest1 t1 = new StaticTest1(),
t2 = new StaticTest1();

System.out.print(t1.i+"\n");
t2.i = 5;
System.out.print(t1.i+"\n");
}//main
}//class


Ας σκεφτούμε τι κάνει αυτό το πρόγραμμα:
Τυπώνει στην οθόνη t1.i που είναι 10, κάνει το t2.i 5, και ξανατυπώνει το t1.i, δηλαδή πάλι 10. Το αποτέλεσμα που θα πάρετε θα είναι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
10
10


Θα μου πείτε «ε δε μας είπες και καμία σοφία, δύο φορές το ίδιο νούμερο θα δούμε, αφού δεν αλλάξαμε το t1.i, το t2.i αλλάξαμε! Άλλο το ένα, άλλο το άλλο.». Και θα έχετε δίκιο!

Ας δούμε όμως τι θα γίνει στο πρόγραμμα κάνοντας τη μεταβλητή i static:

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

static int i=10;

StaticTest2() {}

public static void main(String[] args) {
StaticTest2 t1 = new StaticTest2(),
t2 = new StaticTest2();

System.out.print(t1.i+"\n");
t2.i = 5;
System.out.print(t1.i+"\n");
}//main
}//class

Τρέξτε το παραπάνω πρόγραμμα! Αυτό που θα δείτε θα είναι:

Μορφοποιημένος Κώδικας: Επιλογή όλων
10
5


Μια στιγμή... Τι έγινε; Αλλάζουμε το t1.i και αλλάζει το t2.i; Η απάντηση είναι ότι δεν υπάρχει ούτε t1.i ούτε t2.i!! Υπάρχει απλώς το i. Ακριβώς το αντίθετο δηλαδή από αυτό που σας έλεγα για τις μεταβλητές των αντικειμένων! Αυτός είναι και ο λόγος που σας πρότεινα πιο πριν να αναφερόμαστε σε static μεταβλητές με το όνομα τους μόνο, και όχι μέσω αντικειμένων. Δηλαδή να γράφουμε i και όχι t1.i γιατί αυτό μπορεί, αν και όχι λάθος, να είναι παραπλανητικό: η μεταβλητή i δεν ανήκει στο αντικείμενο t1. Δεν ανήκει σε κανένα αντικείμενο! Είναι μία και μοναδική για την κλάση μας.

Ένα σχόλιο για τον συνδυασμό "static final" για όσους ενδιαφέρονται για την αποδοτικότητα των προγραμμάτων.
Όταν εμφανίζεται ο συνδυασμός αυτός για κάποια μεταβλητή, ο compiler θα αντικαταστήσει την τιμή της παντού στο πρόγραμμα πριν το compile. Αυτό σημαίνει ότι θα είναι σαν να γράψατε παντού την τιμή κατ' ευθείαν, αντί για το όνομα της μεταβλητής. Είναι πλέον δηλαδή μία σταθερά, και όχι μία μεταβλητή! Αυτό αυξάνει την ταχύτητα πρόσβασης είναι μεγαλύτερη. Κάτι σαν το #define της C δηλαδή...



4.3 Static μέθοδοι
Με την ίδια λογική υπάρχουν και static μέθοδοι. Μία τέτοια μέθοδο μπορούμε να την καλέσουμε χωρίς να δημιουργήσουμε κάποιο αντικείμενο. Αντίθετα με τις απλές μεθόδους, μία static μέθοδος αποτελεί static πλαίσιο (σας εξέπληξα έτσι;). Αυτό σημαίνει ότι δεν «υπονοείται» κάποιο αντικείμενο, όπως συμβαίνει με τις non-static μεθόδους. Δε μπορούμε να έχουμε πρόσβαση λοιπόν σε non-static μεταβλητές κατ' ευθείαν με το όνομά τους. Γιατί τότε η ερώτηση θα ήταν: «ποιανού αντικειμένου;»
Για να δηλώσουμε μία μέθοδο static, απλώς γράφουμε τη λέξη-κλειδί static πριν από τον τύπο την επιστροφής. Όπως κάναμε δηλαδή τόσο καιρό με την main!
Και μιας και το έφερε η κουβέντα: Καταλαβαίνουμε τώρα γιατί η main πρέπει να είναι static. Δεν υπάρχει κάποιο αντικείμενο που την καλεί! Δε θα μπορούσε άλλωστε: η κλήση της main είναι η πρώτη ενέργεια που κάνει το πρόγραμμα. Άρα δεν έχει δημιουργηθεί κανένα αντικείμενο να την καλέσει!


Ας εμπλουτίσουμε τώρα την κλάση UbuntuPC, ώστε να χρησιμοποιεί static και non-static μεταβλητές και μεθόδους. Επίσης θα «εμπλουτίσουμε» την main, ώστε η χρήση του προγράμματός μας να είναι πιο... ενδιαφέρουσα! Αν δεν έχετε κουράγιο για άλλη ανάλυση κάντε διάλειμμα με ένα copy-paste το παρακάτω και παίξτε λιγάκι :) Ύστερα αναλύστε πως δουλεύει...

Μορφοποιημένος Κώδικας: Επιλογή όλων
import java.util.Scanner;
public class UbuntuPC {

static double last_version = 12.04;

double version=11.10, ram;
int bits;
boolean printer;

//------------CONSTRUCTORS------------------
UbuntuPC (double r, int b, boolean pr) {
ram = r;
bits = b;
printer = pr;
}//constructor

UbuntuPC (double vs, double r, byte b, boolean pr) {
version = vs;
ram = r;
bits = b;
printer = pr;
}//constructor

//------------METHODS------------------

static void newVersionAvailable() {
// βρισκόμαστε σε static πλαίσιο. Σε static μεταβλητές, όπως η last_version μπορούμε να αναφερόμαστε κατ' ευθείαν.
// Σε non-static όπως η ram δε μπορούμε, γιατί απλούστατα η ram δεν υπάρχει από μόνη της. Είναι μέρος κάποιου αντικειμένου,
// το οποίο η μέθοδός μας δε γνωρίζει.

if (last_version%1 <= 0.04) {last_version +=0.06;}
else {last_version +=0.94;}

System.out.print("Η έκδοση "+last_version+" του Ubuntu είναι διαθέσιμη! Δοκιμάστε την!\n");
}//newVersionAvailable

void upgradeVersion() {
if (version==last_version) { //έλεγχος αν είναι η τελευταία έκδοση
System.out.print("Η έκδοση "+version+" είναι η τελευταία έκδοση!\n");
return;
}//if

if (version%1 <= 0.04) {version +=0.06;}
else {version +=0.94;}

System.out.print("Η εκδοσή σας αναβαθμίστηκε!\nΚαλωσήλθατε στο ubuntu "+version+"\n");
}//upgradeVersion


boolean putMoreRam(double more) {
if (ram+more<=16) {
ram += more;
System.out.print("Συγχαρητήρια! η νέα σας ram είναι "+ram+" GB\n");
return true; //όλα πήγαν καλά
}//if

else {
System.out.print("Λυπάμαι, δεν μπορείτε να βάλετε περισσότερη ram... Τώρα έχετε "+ram+" GB\n");
return false;
}//else
}//putMoreRam


boolean putMoreRam() {
return putMoreRam(2);
}//putMoreRam

void whatYouAre() {
System.out.print("ubuntu version: "+version+" "+bits+" bit\nram: "+ram+" GB\n");
}//whatYouAre


public static void main(String[] args) {
UbuntuPC mypc = new UbuntuPC(8,64,false);
Scanner sc = new Scanner (System.in);

System.out.print("Καλωσήλθατε στον υπολογιστή σας!\n\nΕπιλογές:\n0: Εμφάνιση στοιχείων"+
"\n1: Αναβάθμιση Ram\n2: Αναβάθμιση έκδοσης\n3: Δημιουργία έκδοσης!!\n-1: Έξοδος\n");

loop: while (true) {

switch (sc.nextInt()) {
case 0:
mypc.whatYouAre();
break;

case 1:
System.out.print("Πόσα gb θα βάλετε;\n");
mypc.putMoreRam(sc.nextDouble());
break;

case 2:
mypc.upgradeVersion();
break;

case 3:
newVersionAvailable();
break;

case -1: break loop;
default: System.out.print("Δεν υπάρχει τέτοια επιλογή!\n");
}//switch
}//while
System.out.print("\n");
}//Main
}//class



4.4 Αντικείμενα ως ορίσματα και ως τύποι επιστροφής
Συνήθως οι αρχάριοι προγραμματιστές είναι λίγο «φοβισμένοι» με τη χρήση των αντικειμένων: δηλαδή δεν χειρίζονται με την ίδια άνεση ένα Scanner και ένα int. Πχ γράφετε με άνεση το:
Μορφοποιημένος Κώδικας: Επιλογή όλων
int myMethod() {
//...
int j = 5;
//...
//...
}

//και μετά από κάπου αλλού:
int myint = myMethod();

για να δημιουργήσετε μία μέθοδο. Ίσως όμως να μη γράφατε με την ίδια άνεση το:
Μορφοποιημένος Κώδικας: Επιλογή όλων
Scanner myMethod() {
//...
Scanner sc = new Scanner(...);
//...
//...
return sc;
}

//και μετά από κάπου αλλού:
Scanner mysc = myMethod();


Στην πραγματικότητα και τα δύο είναι απόλυτα σωστά! Νοιώστε άνετα να δημιουργείτε μεθόδους που επιστρέφουν αντικείμενα, με τον ίδιο τρόπο που δημιουργείτε μεθόδους που επιστρέφουν ένα int ή μία boolean!

Με την ίδια λογική μπορείτε να δίνετε σε μία μέθοδο ένα αντικείμενο ως όρισμα. Ας πούμε ένα String:
Μορφοποιημένος Κώδικας: Επιλογή όλων
void printMessage(String ms) {
System.out.print("Μου έδωσες ως όρισμα το "+ms+"\n");
}//printMessage

//και από κάπου αλλού:
printMessage("Γειά!!!");

Στη θέση του String μπορείτε να φανταστείτε οποιδήποτε άλλο αντικείμενο! Είτε πχ ένα Scanner, είτε ένα αντικείμενο της κλάσης που εσείς φτιάχνετε (πχ ένα UbuntuPC).

Συμπέρασμα: τα αντικείμενα μπορεί να παίζουν το ρόλο ορίσματος ή τύπου επιστροφής ακριβώς όπως οι βασικοί τύποι. Μη διαστάζετε να το δοκιμάσετε!


Ενότητα 5: Η φύση των μεταβλητών αντικειμένων - η διαφορά από τους βασικούς τύπους.
5.1 Φύση

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

Για τους τυχεράκηδες που γνωρίζουν C/C++ με τις ακόλουθες δύο σειρές θα έχω πει τα πάντα:
Όλες οι μεταβλητές αντικειμένων στη Java (εκτός των String) είναι pointer. Τέλος. Δεν υπάρχει αναφορά by refrence σε αντικείμενα!
Όλες οι μεταβλητές βασικών τύπων και String αντίθετα είναι refrences. Δεν υπάρχουν pointer σε βασικούς τύπους.

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

Βασικοί τύποι
Ας φανταστούμε τη μεταβλητή ενός βασικού τύπου σαν μία πόρτα που οδηγεί σε ένα δωμάτιο (που είναι η θέση της μνήμης όπου βρίσκεται ο βασικός τύπος μας). Να τι συμβαίνει όταν αναφερόμαστε σε τέτοιες μεταβλητές:

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

2. Κάθε φορά που γράφουμε το όνομα της μεταβλητής για να τη διαβάσουμε, το πρόγραμμα ανοίγει αυτή την πόρτα για να δει το περιεχόμενο.

3. Κάθε φορά που γράφουμε ο όνομα της μεταβλητής για να την αλλάξουμε (δηλαδή χρησιμοποιούμε τον τελεστή =) , το πρόγραμμα ανοίγει αυτή την πόρτα και κάνει τις αλλαγές στο δωμάτιο.
πχ αν έχουμε δύο int i, και j, η έκφραση "i=j;" θα κένει το εξής:
θα δει πως είναι το «δωμάτιο» της «πόρτας» j, και μετά θα πάει στο «δωμάτιο» της «πόρτας» i, και θα το κάνει ίδιο με αυτό της j. Θα έχουμε λοιπόν δύο δωμάτια που έχουν όμως το ίδιο περιεχόμενο. Είναι όμως δύο διαφορετικά δωμάτια: κάτι σαν δύο δίδυμα αδέρφια, είναι όμοια, αλλά δύο διαφορετικοί άνθρωποι.

Αντικείμενα
Θα φανταστούμε και πάλι τη μεταβλητή ενός αντικειμένου σαν μία πόρτα που οδηγεί σε ένα δωμάτιο (που είναι η θέση της μνήμης όπου βρίσκεται το αντικείμενο). Για να δούμε τι συμβαίνει:

1. Κάθε φορά που δηλώνουμε μεταβλητή αντικειμένου, το πρόγραμμα φτιάχνει μία «πόρτα». Ναι μόνο μία πόρτα! ένα κομμάτι ξύλο που δεν είναι καρφωμένο σε κανένα τοίχο!! Δεν δημιουργεί κανένα δωμάτιο!
Το δωμάτιο το δημιουργεί μόνο ο constructor. Δηλαδή η έκφραση
Μορφοποιημένος Κώδικας: Επιλογή όλων
new UbuntuPC();
δημιουργεί ένα καινούριο δωμάτιο χωρίς... καμία πόρτα! Αντίθετα η έκφραση
Μορφοποιημένος Κώδικας: Επιλογή όλων
mypc = new UbuntuPC();
δημιουργεί ένα καινούριο δωμάτιο, και του βάζει την πόρτα mypc (την οποία έχουμε δημιουργήσει πιο πριν, όταν τη δηλώνουμε). Μπορεί λοιπόν να υπάρχουν πόρτες που δεν οδηγούν σε κανένα δωμάτιο, και δωμάτια χωρίς καμία πόρτα! Δεν υπάρχει η αντιστοιχία 1 προς 1 όπως στους βασικούς τύπους!

2. Το ίδιο με πριν: Κάθε φορά που γράφουμε το όνομα της μεταβλητής για να τη διαβάσουμε, το πρόγραμμα ανοίγει αυτή την πόρτα για να δει το περιεχόμενο.

3. Εδώ κρύβεται όλη η διαφορά!
Κάθε φορά που πάμε να αλλάξουμε τη μεταβλητή ενός αντικειμένου με τον τελεστή =, αυτό που λέμε στο πρόγραμμα είναι: πάρε την πόρτα και πήγαινέ την αλλού!
Δηλαδή αν έχουμε δύο UbuntuPC, το pc1 και το pc2, η έκφραση "pc2 = pc1;" σημαίνει:

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

Και τι πρακτική σημασία θα μου πείτε έχουν όλα αυτά;
Τεράστια θα σας απαντήσω, γιατί όπως καταλαβαίνετε, αν μπούμε από μία πόρτα και κάνουμε κάποια αλλαγή, και μετά μπούμε από την άλλη πόρτα, η αλλαγή αυτή θα είναι και πάλι παρούσα.

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

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

System.out.println(i);
j=5;
System.out.println(i);
}//main
}//class

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

int num = 10;
public static void main(String[] args) {
ObjectTest2 i = new ObjectTest2(); //constructor δεν έχουμε φτιάξει, αλλά όπως είπαμε ο compiler φτιάχνει μόνος του ένα...
ObjectTest2 j = new ObjectTest2(); //μπορείτε επίσης να γράψετε απλώς ObjectTest2 j; χωρίς να το αρχικοποιήσετε

j=i;

System.out.println(i.num);
j.num=5;
System.out.println(i.num);
}//main
}//class

Τα συμπεράσματα δικά σας...

Η μοναδική εξαίρεση στους παραπάνω κανόνες: τα String. Παρ όλο που είναι αντικείμενα, ακολουθούν τους κανόνες για βασικούς τύπους.


Σχόλιο: Όταν ένα «δωμάτιο» μείνει χωρίς πόρτα, το πρόγραμμα καταλαβαίνει ότι δε το χρειαζόμαστε πλέον, και έτσι το γκρεμίζει (δηλαδή αποδεσμεύει τη μνήμη που κατείχε το αντικείμενο). Η διαδικασία αυτή ονομάζεται "garbige colection" και γίνεται αυτόματα από το πρόγραμμα ανά τακτά χρονικά διαστήματα. Δεν χρειάζεται να νοιαζόμαστε για αυτήν. Αντίθετα δηλαδή από άλλες γλώσσες (όπως η C/C++) όπου η αποδέσμευση μνήμης πρέπει να γίνεται χειροκίνητα από τον προγραμματιστή.



Οι τελεστές ισότητας-ανισότητας και η μέθοδος equals
Κλέινοντας αυτό το κεφάλαιο-μαμούθ (ελπίζω να με συγχωρέσετε γι αυτό!) θα δούμε τι στην ουσία κάνουν οι τελεστές == και != όταν δρούν σε αντικείμενα. Και αυτό επειδή ο τρόπος που λειτουρούν ίσως είναι αρκετά διαφορετικός από αυτό που φαντάζεστε...

Ας αρχίσουμε με το παρακάτω απλό πρόραμμα (σας παρακαλώ κάντε το compile και τρέξτε το):
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο ObjectEqual.java
public class ObjectEqual {
static String s1, s2;

public static void main(String[] args) {

s1 = "Hello";
s2 = "Hello"//Τα δύο αντικείμενα είναι πανομοιώτυπα, δέν είναι;;;

System.out.print("Είναι τα δύο αντικείμενα ίσα;\nΑπάντηση: "+(s1==s2)+"\n\n");

}//main
}//class


Με έκπληξη ίσως θα διαπιστώσετε ότι το αποτέλεσμα θα είναι false!! Γιατί όμως; Τα δύο αντικείμενα δημιουρήθηκαν ακριβώς με τον ίδιο τρόπο! Τι το διαφορετικό έχουν;
Απάντηση: είναι δύο διαφορετικά «δωμάτια» ακόμα και αν μοιάζουν εντελώς μεταξύ τους! Οι δύο «πόρτες» s1 και s2, είναι καρφωμένες σε δύο ξεχωριστά δωμάτια! Αν πάτε και αλλάξετε το s1, δε θα αλλάξει και το s2, άρα άλλο το ένα άλλο το άλλο! Ακριβώς για αυτό το λόγο ο τελεστής == επιστρέφει false.
True θα επιστρέψει μόνο όταν τα δύο αντικείμενα δίπλα του είναι ένα και το αυτό! Πχ:

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

public static void main(String[] args) {

s1 = "Hello";
s2 = s1;//Τώρα τα δύο αντικείμενα είναι πραματικά ίσα!

System.out.print("Είναι τα δύο αντικείμενα ίσα;\nΑπάντηση: "+(s1==s2)+"\n\n");

}//main
}//class
Με απλά λόγια: ο τελεστής == κοιτάει αν δύο πόρτες δείχνουν στο ίδιο δωμάτιο, και όχι αν δύο δωμάτια είναι ίδια! (σε αντίθεση με ό,τι ίνεται για τους βασικούς τύπους).

Πως όμως θα δούμε αν «τα δωμάτια είναι ίδια»;; Ή για να το πω πρακτικά σαν ένα παράδειγμα, δίνουμε στο χρήστη μία ερώτηση, και απαντάει. Διαβάζουμε την απάντησή του σαν ένα String. Πως θα δούμε αν αυτό το String ταυτίζεται με τη «σωστή» απάντηση;
Απάντηση: με την μέθοδο equals(). Αν σας γενιέται η απορία «από που ξεφύτρωσε αυτή η μέθοδος», θα σας απαντήσω ότι η απορία θα σας λυθεί στο τέλος του επομένου κεφαλαίου, άρα υπομονή :shh:
Αρκεστείτε ότι κάθε αντικείμενο μπορεί να την καλέσει, και παίρνει ως όρισμα ένα άλλο αντικείμενο! Και κάνει ακριβώς τη δουλεία που θέλουμε: μας λέει αν «δύο δωμάτια είναι όμοια»! Να πως δουλεύει:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο ObjectEqual3.java
public class ObjectEqual3 {
static String s1, s2;

public static void main(String[] args) {

s1 = "Hello";
s2 = "Hello";

System.out.print("Είναι τα δύο αντικείμενα ίσα;\nΑπάντηση: "+ s1.equals(s2) +"\n\n");

}//main
}//class

Το αποτέλεσμα είναι το επιθυμιτό true!

Προηγούμενο: Δομές Ελέγχου και Επανάληψης
Επόμενο: Σύνδεση προγραμμάτων - Σχέσεις μεταξύ Αντικειμένων και Κλάσεων

Creative Commons License
Η εργασία υπάγεται στην άδεια Creative Commons Αναφορά-Παρόμοια διανομή 3.0 Ελλάδα
Τελευταία επεξεργασία από alkismavridis και 24 Ιαν 2013, 01:14, έχει επεξεργασθεί 14 φορά/ες συνολικά
Γνώσεις ⇛ 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
Εκτύπωση

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

Δημοσίευσηαπό clepto » 19 Μάιος 2012, 17:38

μπράβο!!
πολύ καλή η δουλειά σου!! το καλοκαίρι θα τα διαβάσω όλα :)
1 Γνώσεις Linux: Ικανοποιητικό ┃ Προγραμματισμού: Ικανοποιητικό ┃ Αγγλικών: Ικανοποιητικό
2 Ubuntu 13.04 raring 3.8.0-30-generic 64bit (en_US.UTF-8, Unity ubuntu), Ubuntu 3.8.0-19-generic, Windows 7
3 Intel Core i7-3537U CPU @ 2.00GHz ‖ RAM 3840 MiB ‖ ASUS K56CB
4 Intel 3rd Gen Core processor Graphics Controller [8086:0166] {i915}
5 wlan0: Atheros Inc. AR9485 Wireless Network Adapter [168c:0032] (rev 01) ⋮ eth0: Realtek RTL8111/8168 PCI Express Gigabit Ethernet controller [10ec:8168] (rev 0a)
clepto
antiwinTUX
antiwinTUX
 
Δημοσιεύσεις: 4102
Εγγραφή: 07 Ιαν 2010, 16:27
Τοποθεσία: Πάτρα
Launchpad: christriant
IRC: Clepto
Εκτύπωση

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

Δημοσίευσηαπό alkismavridis » 19 Μάιος 2012, 17:53

Σε ευχαριστώ πάρα πολύ :)
Ελπίζω να σε βοηθήσει και εσένα και πολλούς άλλους!
Γνώσεις ⇛ 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
Εκτύπωση

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

Δημοσίευσηαπό the_eye » 19 Μάιος 2012, 17:53

Πάρα πολύ καλή η ανάλυσή σου :clap:
Όσο λιγότερο κλειστό λογισμικό έχεις, τόσα λιγότερα προβλήματα.
1 Γνώσεις ⇛ Linux: Καλό ┃ Προγραμματισμός: Ναι PHP, MySQL ┃ Αγγλικά: Καλά
2 Ubuntu 22.04 Jammy Jellyfish 5.15.0-58-generic 64bit (el_GR.UTF-8, ubuntu:GNOME ubuntu)
3 Intel Core i3-6100 CPU @ 3.70GHz ‖ RAM 7836 MiB ‖ Gigabyte B150M-HD3 DDR3-CF - Gigabyte B150M-HD3 DDR3
4 Intel HD Graphics 530 [8086:1912] {i915}
5 enp1s0: Realtek RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller [10ec:8168] (rev 15)
Οδηγοί Ubuntu Βίντεο Οδηγοί
Άβαταρ μέλους
the_eye
Διαχειριστής
Διαχειριστής
 
Δημοσιεύσεις: 11672
Εγγραφή: 16 Μαρ 2010, 17:19
Launchpad: ntoulasd
IRC: the_eye_
Εκτύπωση

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

Δημοσίευσηαπό simosx » 19 Μάιος 2012, 23:46

Μέχρι στιγμής πολύ όμορφη δουλειά.
Έχουμε επιτέλους μια αντάξια ενότητα για προγραμματισμό σε Java, viewforum.php?f=68
και βοηθάει και για προγραμματισμό σε Android.
προσωπικό ιστολόγιο ϗ πλανήτης Ubuntu-gr
Συμβάλετε και εσείς στο ελληνικό βιβλίο Ubuntu!
1 Γνώσεις Linux: Πολύ καλό ┃ Προγραμματισμού: Πολύ καλό ┃ Αγγλικών: Πολύ καλό
2 Ubuntu 13.10 saucy 3.11.0-031100rc1-generic 64bit (el_GR.UTF-8, Unity ubuntu)
3 AMD E-450 APU with Radeon HD Graphics ‖ RAM 3555 MiB ‖ Sony Corporation VAIO
4 AMD nee ATI Wrestler [Radeon HD 6320] [1002:9806] {fglrx_pci}
5 eth0: Atheros Inc. AR8151 v2.0 Gigabit Ethernet [1969:1083] (rev c0) ⋮ wlan0: Atheros Inc. AR9285 [168c:002b] (rev 01)
Φτιάξτε και εσείς τη δική σας υπογραφή (παραπάνω κείμενο) αυτόματα με κλικ εδώ!
simosx
Επίτιμο μέλος
Επίτιμο μέλος
 
Δημοσιεύσεις: 10334
Εγγραφή: 11 Μάιος 2008, 18:52
Launchpad: simosx
IRC: simosx
Εκτύπωση

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

Δημοσίευσηαπό alkismavridis » 20 Μάιος 2012, 00:05

Σας ευχαριστώ πολύ παιδιά! Χαίρομαι πολύ που σας αρέσει! :D

Τι εννοείς για το android, γιατί δεν έχω χρησιμοποιήσει ποτέ..
Γνώσεις ⇛ 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
Εκτύπωση

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

Δημοσίευσηαπό clepto » 20 Μάιος 2012, 00:21

στο android μπορείς να προγραμματίσεις με java :)
1 Γνώσεις Linux: Ικανοποιητικό ┃ Προγραμματισμού: Ικανοποιητικό ┃ Αγγλικών: Ικανοποιητικό
2 Ubuntu 13.04 raring 3.8.0-30-generic 64bit (en_US.UTF-8, Unity ubuntu), Ubuntu 3.8.0-19-generic, Windows 7
3 Intel Core i7-3537U CPU @ 2.00GHz ‖ RAM 3840 MiB ‖ ASUS K56CB
4 Intel 3rd Gen Core processor Graphics Controller [8086:0166] {i915}
5 wlan0: Atheros Inc. AR9485 Wireless Network Adapter [168c:0032] (rev 01) ⋮ eth0: Realtek RTL8111/8168 PCI Express Gigabit Ethernet controller [10ec:8168] (rev 0a)
clepto
antiwinTUX
antiwinTUX
 
Δημοσιεύσεις: 4102
Εγγραφή: 07 Ιαν 2010, 16:27
Τοποθεσία: Πάτρα
Launchpad: christriant
IRC: Clepto
Εκτύπωση

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

Δημοσίευσηαπό alkismavridis » 20 Μάιος 2012, 14:46

Off topic:
Ξέρεις κάτι φίλε clepto..
Εγώ έχω iPhone και τελικά... Το έχω μετανοιώσει πολύ. Για πολλούς και διάφορους λόγους...
Μόλις μου έδωσες άλλον ένα! :(


Πέρα από την πλάκα, ειλικρινά χαίρομαι πάντος που ένα open-source λογισμικό για κινητά είναι τόσο δυνατό, και θέλω πραγματικά να εγκαταλείψω το μήλο! :-)
Γνώσεις ⇛ 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
Εκτύπωση

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

Δημοσίευσηαπό g1wrg0s » 23 Νοέμ 2012, 20:51

Καλησπερα.

Εστω οτι B extends Α.
Α aObject=new B();
Ο τυπος της μεταβλητης καθοριζει ποιες μεταβλητες στιγμιοτυπων μπορω να εχω access;

Ευχαριστω.
Spoiler: show
1 Γνώσεις Linux: Πρώτα βήματα ┃ Προγραμματισμού: Πρώτα βήματα ┃ Αγγλικών: Πρώτα βήματα
2 Ubuntu 12.10 quantal 3.10.20-031020-generic 32bit (el_GR.UTF-8, Unity ubuntu), Windows 8
3 Intel Core i5-3230M CPU @ 2.60GHz ‖ RAM 7923 MiB ‖ Acer VA50_HC_CR - Acer Aspire V3-571G
4 Intel 3rd Gen Core processor Graphics Controller [8086:0166] {i915} ⋮ nVidia Device [10de:0fe1] {}
5 eth0: Broadcom NetLink BCM57785 Gigabit Ethernet PCIe [14e4:16b5] (rev 10) ⋮ wlan0: Atheros Inc. AR9462 Wireless Network Adapter [168c:0034] (rev 01)
g1wrg0s
punkTUX
punkTUX
 
Δημοσιεύσεις: 196
Εγγραφή: 26 Μάιος 2012, 10:29
Εκτύπωση

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

Δημοσίευσηαπό alkismavridis » 13 Ιαν 2013, 15:33

Ναι, αλλά πάντα μπορείς να κάνεις μετατροπή
(B (anObject) ).a_field_wich_class_A_doesnt_have
και έτσι να το προσπελάσεις!
Γνώσεις ⇛ 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