ΚΛΑΣΕΙΣ ΚΑΙ ΑΝΤΙΚΕΙΜΕΝΑ
Στην ενότητα Βασικές Έννοιες Αντικειμενοστρεφούς Προγραμματισμού είδαμε το παράδειγμα μιας κλάσης ποδηλάτου Bicycle με τις υποκλάσεις αγωνιστικού, ανωμάλου και πολυθέσιου ποδηλάτου. Εδώ βλέπουμε μια πιθανή υλοποίηση της κλάσης Bicycle:
public class Bicycle
{
// Η κλάση ποδήλατο έχει τρία πεδία
public int cadence;
public int gear;
public int speed;
// Η κλάση ποδήλατο έχει 1 constructor
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
}
// Η κλάση ποδήλατο έχει 4 μεθόδους
public void setCadence(int newValue)
{
cadence = newValue;
}
public void setGear(int newValue)
{
gear = newValue;
}
public void applyBrake(int decrement)
{
speed -= decrement;
}
public void speedUp(int increment)
{
speed += increment;
}
}
Και εδώ μία υλοποίηση του ποδηλάτου ανωμάλου δρόμου που προεκτείνει την Bicycle
public class MountainBike extends Bicycle
{
// Η υποκλάση MountainBike έχει 1 πεδίο
public int seatHeight;
// Η υποκλάση MountainBike έχει 1 constructor
public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear)
{
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
//Η υποκλάση MountainBike έχει 1 μέθοδο
public void setHeight(int newValue)
{
seatHeight = newValue;
}
}
Η υποκλάση MountainBike κληρονομεί όλα τα πεδία και τις μεθόδους της Bicycle και ορίζει επίσης το πεδίο seatHeight και την μέθοδο setHeight.
Στην ενότητα αυτή θα εξηγηθούν τα συστατικά υλοποίησης μιας κλάσης.
Δήλωση Κλάσης
Έχουμε δει ότι οι κλάσεις ορίζονται κάπως έτσι:
class MyClass
{
//ορισμός πεδίων
//ορισμός constructor
//ορισμός μεθόδων
}
Η κλάση περιέχει στο σώμα της (ενδιάμεσα στις αγκύλες) όλα τα στοιχεία κώδικα που χρειάζονται στον κύκλο της ζωής ενός αντικειμένου-στιγμιότυπου της κλάσης αυτής:
class MyClass extends MySuperClass implements YourInterface
{
//ορισμός πεδίων
//ορισμός constructor
//ορισμός μεθόδων
}
class MyClass extends MySuperClass
{
//ορισμός πεδίων
//ορισμός constructor
//ορισμός μεθόδων
}
class MyClass implements YourInterface1, YourInterface2
{
//ορισμός πεδίων
//ορισμός constructor
//ορισμός μεθόδων
}
Όπως θα δούμε αργότερα, μπορούμε να προσθέσουμε στην αρχή του ορισμού μιας κλάσης modifiers (μετατροπείς) όπως public και private, οι οποίοι καθορίζουν την προσβασιμότητα άλλων κλάσεων στην κλάση αυτή. Επίσης παρακάτω θα δούμε τη χρήση των extends και implements .
Ο ορισμός μιας κλάσης συνοπτικά περιλαμβάνει:
Δήλωση Μελών Κλάσης
Όπως έχουμε δει υπάρχουν αρκετά είδη μεταβλητών :
public int cadence;
public int gear;
public int speed;
Ο ορισμός πεδίων περιλαμβάνει τα παρακάτω μέρη:
Access Modifiers (Μετατροπείς Προσβασιμότητας)
Ο πρώτος μετατροπέας ελέγχει ποιες κλάσεις έχουν πρόσβαση στο πεδίο. Για την ώρα βλέπουμε μόνο τους public και private. Τους υπόλοιπους θα τους δούμε αργότερα. Οι μετατροπείς χρησιμοποιούνται για να ελέγξουν την προσβασιμότητα στις κλασεις, στα πεδία και στις μεθόδους τους.
public class Bicycle
{
private int cadence;
private int gear;
private int speed;
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
}
public int getCadence()
{
return cadence;
}
public void setCadence(int newValue)
{
cadence = newValue;
}
public int getGear()
{
return gear;
}
public void setGear(int newValue)
{
gear = newValue;
}
public int getSpeed()
{
return speed;
}
public void applyBrake(int decrement)
{
speed -= decrement;
}
public void speedUp(int increment)
{
speed += increment;
}
}
Τύποι μεταβλητών
Οι μεταβλητές πρέπει απαραίτητα να έχουν τύπο. Εκτός από απλές μεταβλητές ,όπως int, οι μεταβλητές μπορούν να αναφέρονται και σε String, πίνακα ή οποιοδήποτε αντικείμενο.
Ονοματολογία
Θυμηθείτε ότι οι μεταβλητές και οι κλάσεις έχουν κατά συνθήκη την ίδια ονοματολογία στην Java με διαφορά ότι
το πρώτο γράμμα του ονόματος κλάσης πρέπει να είναι κεφαλαίο. Επίσης η πρώτη λέξη του ονόματος μιας μεθόδου πρέπει να είναι ρήμα με το πρώτο γράμμα μικρό.
Ορισμός Μεθόδων - τεχνική overloading
Έχουμε ήδη αναφερθεί στις μεθόδους πολλές φορές. Για παράδειγμα:
public double calculateAnswer(double wingSpan, int numberOfEngines,
double length, double grossTons)
{
//υπολογισμοί εδώ
}
Το στοιχεία που απαιτούνται για να δηλωθεί μια μέθοδος είναι ο τύπος επιστροφής της, το όνομα, ένα ζεύγος από παρενθέσεις μέσα στο οποίο δηλώνονται οι παράμετροι εισόδου τιμών, και το σώμα της μεθόδου που περιλαμβάνει τον κώδικα.
Γενικότερα , οι μέθοδοι έχουν 6 συστατικά με σειρά σύνταξης:
Το αποτύπωμα της παραπάνω μεθόδου είναι το εξής:
calculateAnswer(double, int, double, double)
Ονοματολογία
Παρόλο που το όνομα της μεθόδου μπορεί να είναι οποιοδήποτε, υπάρχουν κάποιοι τυπικοί περιορισμοί στην ονοματολογία των μεθόδων. Όπως είπαμε πιο πριν, μια μέθοδος είθισται να ξεκινά με ρήμα ακολουθούμενο με ουσιαστικά ή επιρρήματα με το πρώτο γράμμα κεφαλαίο σε μία συνεχή λέξη. Εδώ παραθέτονται μερικά παραδείγματα:
run
runFast
getBackground
getFinalData
compareTo
setX
isEmpty
Στην Java δεν μπορούμε να έχουμε δύο μεθόδους με το ίδιο όνομα μέσα στην ίδια κλαση, εκτός κι αν χρησιμοποιούμε τη μέθοδο overloading (υπερφόρτωση).
Overloading
Η Java υποστηρίζει την τεχνική υπερφόρτωσης μίας μεθόδου. Πολλαπλές μέθοδοι μπορούν να μοιράζονται το ίδιο όνομα αλλά δεν μπορούν να υπάρχουν παραπάνω από μία με το ίδιο αποτύπωμα. Περισσότερα θα δούμε στην ενότητα Interfaces και κληρονομικότητα.
Ας πούμε ότι έχουμε μία κλάση που χρησιμοποιεί καλλιγραφία για να ζωγραφίσει δεδομένα στην οθόνη. Για λογική ευχρηστία μπορούμε να παρέχουμε μία μέθοδο draw η οποία να δέχεται διαφορετικούς τύπους δεδομένων σαν όρισμα και να τυπώνει τα δεδομένα τους στην οθόνη. Στην πραγματικότητα βέβαια δεν είναι μια η μέθοδος αλλά πολλές διαφορετικές που έχουν το ίδιο όνομα αλλά όμως διαφορετικές παραμέτρους εισόδου, δηλαδή διαφορετικό αποτύπωμα.
public class DataArtist
{
public void draw(String s)
{
}
public void draw(int i)
{
}
public void draw(double f)
{
}
public void draw(int i, double f)
{
}
}
Η τεχνική υπερφόρτωσης παρέχει ευκολία στην χρήση της μεθόδου αλλά πρέπει να χρησιμοποιείται με φειδώ.
Σημείωση: Δεν είναι δυνατό να δηλωθούν 2 μέθοδοι με το ίδιο αποτύπωμα αλλά διαφορετική επιστροφή καθώς θεωρείται ως διπλή δήλωση της ίδιας μεθόδου αφού η επιστροφή δεν λογίζεται ως μέρος του αποτυπώματος.
Constructors
Μια κλάση έχει τουλάχιστον ένα constructor (κατασκευαστή), μια ειδική μέθοδο η οποία καλείται όποτε δημιουργούμε ένα object από την κλάση αυτή. Η μέθοδος κατασκευής έχει ακριβώς το ίδιο όνομα με την κλάση και δεν ορίζουμε για αυτή τύπο επιστροφής. Για παράδειγμα, η Bicycle έχει ένα constructor:
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
}
Για να δημιουργήσουμε ένα καινούριο Bicycle αντικείμενο με το όνομα myBike, καλούμε τον constructor με τον τελεστή new:
Bicycle myBike = new Bicycle(30, 0, 8);
Η έκφραση new Bicycle(30, 0, 8) δημιουργεί-δεσμεύει χώρο στην μνήμη για ένα αντικείμενο και αρχικοποιεί τα πεδία του.
Παρόλο που το Bicycle έχει μόνο ένα constructor, θα μπορούσε να έχει και άλλους (με την χρήση της τεχνικής υπερφόρτωσης), συμπεριλαμβανομένου και του constructor χωρίς παραμέτρους:
public Bicycle()
{
gear = 1;
cadence = 10;
speed = 0;
}
Και η κλήση
Bicycle yourBike = new Bicycle();
καλεί τον κενό από παραμέτρους constructor για να δημιουργήσει ένα νέο αντικείμενο Bicycle με το όνομα yourBike.
Αν ορίσουμε έναν constructor τότε μόνο αυτός μπορεί να δημιουργήσει στιγμιότυπο της κλάσης του. Μπορούμε, επίσης, να μην ορίσουμε κανένα constructor για την κλάση μας. Σε αυτή την περίπτωση ο compiler παρέχει αυτόματα έναν constructor χωρίς παραμέτρους. Αυτός ο constructor υποθέτει ότι και η ενδεχόμενη superclass έχει έναν constructor χωρίς παραμέτρους και θα τον καλέσει αυτόματα.. Αν η superclass δεν έχει ορισμένο constructor χωρίς παραμέτρους θα πάρουμε μήνυμα λάθους.
Μπορούμε επίσης να καλέσουμε και με κώδικα τον constructor της superclass όπως θα δούμε στην ενότητα interfaces και κληρονομικότητα.
Μπορούμε να χρησιμοποιήσουμε μετατροπείς προσβασιμότητας (access modifiers) όπως στις απλές μεθόδους.
Σημείωση: Αν μια άλλη κλάση δεν έχει πρόσβαση στον constructor μιας κλάσης δεν μπορεί να δημιουργήσει στιγμιότυπο. Θυμηθείτε επίσης ότι μία κλάση με μια μέθοδο main είναι η πρώτη που καλείται και αυτή καλεί τους constructors των πρώτων κλάσεων που χρησιμοποιεί.
Δημιουργία και Χρηση Object
Ένα τυπικό πρόγραμμα στην Java δημιουργεί πολλά αντικείμενα τα οποία αλληλεπιδρούν μεταξύ τους με την κλήση μεθόδων. Με την χρήση αντικειμένων ,ένα πρόγραμμα μπορεί να κάνει διάφορα πράγματα, όπως να υλοποιεί ένα γραφικό περιβάλλον μέχρι να δημιουργεί animations και να στέλνει και να λαμβάνει πληροφορίες από ένα δίκτυο. Όταν το αντικείμενο τελειώσει την δουλεία που έχει να κάνει η μνήμη που κατέχει απελευθερώνεται.
Παρακάτω βλέπουμε ένα μικρό πρόγραμμα που θα χρησιμοποιήσουμε ως παράδειγμα για να μιλήσουμε για αντικείμενα και την χρήση τους
CreateObjectDemo, Point ,Rectangle ,
public class CreateObjectDemo {
public static void main(String[] args) {
// Declare and create a point object and two rectangle objects.
Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);
// display rectOne's width, height, and area
System.out.println("Width of rectOne: " + rectOne.width);
System.out.println("Height of rectOne: " + rectOne.height);
System.out.println("Area of rectOne: " + rectOne.getArea());
// set rectTwo's position
rectTwo.origin = originOne;
// display rectTwo's position
System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);
// move rectTwo and display its new position
rectTwo.move(40, 72);
System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);
}
}
Το πρόγραμμα αυτό δημιουργεί, επεξεργάζεται και επιστρέφει πληροφορίες από διάφορα αντικείμενα.
Εδώ έχουμε την επιστροφή του:
Width of rectOne: 100
Height of rectOne: 200
Area of rectOne: 20000
X Position of rectTwo: 23
Y Position of rectTwo: 94
X Position of rectTwo: 40
Y Position of rectTwo: 72
Όπως έχουμε δει μια κλάση είναι το πρωτότυπο ενός αντικειμένου, η «συνταγή». Κάθε μια από τις παρακάτω δηλώσεις του παραπάνω παραδείγματος δημιουργεί ένα αντικείμενο και το καταχωρεί σε μεταβλητές:
Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);
Η πρώτη δήλωση δημιουργεί ένα αντικείμενο της κλάσης Point, και η δεύτερη και Κάθε μία από αυτές τις δηλώσεις έχει 3 μέρη:
Δήλωση μεταβλητής που δείχνει σε αντικείμενο
¨Όπως έχουμε δει ως τώρα για να δηλώσουμε μια μεταβλητή χρησιμοποιούμε την εξής σύνταξη:
«τύπος» «όνομα»;
Αυτή η δήλωση ενημερώνει τον compiler ότι θα χρησιμοποιήσουμε ένα όνομα για να αναφερόμαστε σε δεδομένα του συγκεκριμένου τύπου. Με τους απλούς τύπους δεδομένων αυτή η δήλωση αναφοράς δεσμεύει επίσης ανάλογο χώρο στην μνήμη για τον τύπο μεταβλητής.
Επίσης μπορεί να γίνει μόνο δήλωση αναφοράς σε μια γραμμή. Για παράδειγμα:
Point originOne;
Αν ορίσουμε την originOne έτσι, η τιμή της θα είναι αδιευκρίνιστη έως ότου ένα αντικείμενο δημιουργηθεί και ανατεθεί σε αυτή. Με την απλή δήλωση αναφοράς δεν δημιουργείται όμως ένα αντικείμενο. Για να γίνει αυτό πρέπει να χρησιμοποιηθεί ο τελεστής new όπως περιγράφεται παρακάτω.
Μια μεταβλητή, η οποία δεν αναφέρεται ακόμα σε αντικείμενο, μπορεί να αναπαρασταθεί ως εξής(όνομα μεταβλητής, και μια αναφορά που δείχνει στο κενό):
public class Bicycle
{
// Η κλάση ποδήλατο έχει τρία πεδία
public int cadence;
public int gear;
public int speed;
// Η κλάση ποδήλατο έχει 1 constructor
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
}
// Η κλάση ποδήλατο έχει 4 μεθόδους
public void setCadence(int newValue)
{
cadence = newValue;
}
public void setGear(int newValue)
{
gear = newValue;
}
public void applyBrake(int decrement)
{
speed -= decrement;
}
public void speedUp(int increment)
{
speed += increment;
}
}
Και εδώ μία υλοποίηση του ποδηλάτου ανωμάλου δρόμου που προεκτείνει την Bicycle
public class MountainBike extends Bicycle
{
// Η υποκλάση MountainBike έχει 1 πεδίο
public int seatHeight;
// Η υποκλάση MountainBike έχει 1 constructor
public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear)
{
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
//Η υποκλάση MountainBike έχει 1 μέθοδο
public void setHeight(int newValue)
{
seatHeight = newValue;
}
}
Η υποκλάση MountainBike κληρονομεί όλα τα πεδία και τις μεθόδους της Bicycle και ορίζει επίσης το πεδίο seatHeight και την μέθοδο setHeight.
Στην ενότητα αυτή θα εξηγηθούν τα συστατικά υλοποίησης μιας κλάσης.
Δήλωση Κλάσης
Έχουμε δει ότι οι κλάσεις ορίζονται κάπως έτσι:
class MyClass
{
//ορισμός πεδίων
//ορισμός constructor
//ορισμός μεθόδων
}
Η κλάση περιέχει στο σώμα της (ενδιάμεσα στις αγκύλες) όλα τα στοιχεία κώδικα που χρειάζονται στον κύκλο της ζωής ενός αντικειμένου-στιγμιότυπου της κλάσης αυτής:
- Ο constructor, που είναι μια ειδική μέθοδος για την δημιουργία αντικειμένων της κλάσης.
- Τα πεδία, που αποθηκεύουν την κατάσταση των αντικειμένων της κλάσης.
- Οι μέθοδοι, που υλοποιούν την συμπεριφορά της κλάσης.
class MyClass extends MySuperClass implements YourInterface
{
//ορισμός πεδίων
//ορισμός constructor
//ορισμός μεθόδων
}
class MyClass extends MySuperClass
{
//ορισμός πεδίων
//ορισμός constructor
//ορισμός μεθόδων
}
class MyClass implements YourInterface1, YourInterface2
{
//ορισμός πεδίων
//ορισμός constructor
//ορισμός μεθόδων
}
Όπως θα δούμε αργότερα, μπορούμε να προσθέσουμε στην αρχή του ορισμού μιας κλάσης modifiers (μετατροπείς) όπως public και private, οι οποίοι καθορίζουν την προσβασιμότητα άλλων κλάσεων στην κλάση αυτή. Επίσης παρακάτω θα δούμε τη χρήση των extends και implements .
Ο ορισμός μιας κλάσης συνοπτικά περιλαμβάνει:
- Όρισμα προσβασιμότητας
- Όρισμα ονόματος κλάσης
- Όρισμα extends + όνομα μητρικής κλάσης
- Όρισμα implements + ονόματα ενός ή περισσοτέρων interface που υλοποιούνται.
- Το σώμα της κλάσης μέσα σε {}
Δήλωση Μελών Κλάσης
Όπως έχουμε δει υπάρχουν αρκετά είδη μεταβλητών :
- Μέλη μιας κλάσης— δηλαδή τα πεδία
- Μεταβλητές μέσα σε μεθόδους ή block κώδικα — τοπικές μεταβλητές.
- Μεταβλητές στο όρισμα μεθόδων — παράμετροι.
public int cadence;
public int gear;
public int speed;
Ο ορισμός πεδίων περιλαμβάνει τα παρακάτω μέρη:
- Μηδέν ή περισσότερους modifier, όπως public ή private.
- Τον τύπο του πεδίου.
- Το όνομα του πεδίου.
Access Modifiers (Μετατροπείς Προσβασιμότητας)
Ο πρώτος μετατροπέας ελέγχει ποιες κλάσεις έχουν πρόσβαση στο πεδίο. Για την ώρα βλέπουμε μόνο τους public και private. Τους υπόλοιπους θα τους δούμε αργότερα. Οι μετατροπείς χρησιμοποιούνται για να ελέγξουν την προσβασιμότητα στις κλασεις, στα πεδία και στις μεθόδους τους.
- public — το πεδίο είναι ορατό από όλες τις κλάσεις.
- private — το πεδίο είναι ορατό μόνο από την κλάση που το ορίζει.
public class Bicycle
{
private int cadence;
private int gear;
private int speed;
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
}
public int getCadence()
{
return cadence;
}
public void setCadence(int newValue)
{
cadence = newValue;
}
public int getGear()
{
return gear;
}
public void setGear(int newValue)
{
gear = newValue;
}
public int getSpeed()
{
return speed;
}
public void applyBrake(int decrement)
{
speed -= decrement;
}
public void speedUp(int increment)
{
speed += increment;
}
}
Τύποι μεταβλητών
Οι μεταβλητές πρέπει απαραίτητα να έχουν τύπο. Εκτός από απλές μεταβλητές ,όπως int, οι μεταβλητές μπορούν να αναφέρονται και σε String, πίνακα ή οποιοδήποτε αντικείμενο.
Ονοματολογία
Θυμηθείτε ότι οι μεταβλητές και οι κλάσεις έχουν κατά συνθήκη την ίδια ονοματολογία στην Java με διαφορά ότι
το πρώτο γράμμα του ονόματος κλάσης πρέπει να είναι κεφαλαίο. Επίσης η πρώτη λέξη του ονόματος μιας μεθόδου πρέπει να είναι ρήμα με το πρώτο γράμμα μικρό.
Ορισμός Μεθόδων - τεχνική overloading
Έχουμε ήδη αναφερθεί στις μεθόδους πολλές φορές. Για παράδειγμα:
public double calculateAnswer(double wingSpan, int numberOfEngines,
double length, double grossTons)
{
//υπολογισμοί εδώ
}
Το στοιχεία που απαιτούνται για να δηλωθεί μια μέθοδος είναι ο τύπος επιστροφής της, το όνομα, ένα ζεύγος από παρενθέσεις μέσα στο οποίο δηλώνονται οι παράμετροι εισόδου τιμών, και το σώμα της μεθόδου που περιλαμβάνει τον κώδικα.
Γενικότερα , οι μέθοδοι έχουν 6 συστατικά με σειρά σύνταξης:
- Μετατροπείς—όπως τους public, private
- Τον τύπο επιστροφής (return) — ο τύπος δεδομένων της τιμής που επιστρέφεται από την μέθοδο, ή αλλιώς void, δεν επιστρέφει τιμή.
- Το όνομα της μεθόδου—με ονοματολογία όπως αναφέρθηκε πιο πάνω.
- Η λίστα των παραμέτρων σε παρενθέσεις— ορίζονται με τον τύπο δεδομένων πρώτα και το όνομα και χωρίζονται με κόμμα. Αν δεν έχουμε παραμέτρους χρησιμοποιούμε απλά 2 παρενθέσεις.
- Τη λίστα των εξαιρέσεων—θα συζητηθεί αργότερα
- Το σώμα της μεθόδου σε αγκύλες—με τον κώδικα και τις τοπικές μεταβλητές.
Το αποτύπωμα της παραπάνω μεθόδου είναι το εξής:
calculateAnswer(double, int, double, double)
Ονοματολογία
Παρόλο που το όνομα της μεθόδου μπορεί να είναι οποιοδήποτε, υπάρχουν κάποιοι τυπικοί περιορισμοί στην ονοματολογία των μεθόδων. Όπως είπαμε πιο πριν, μια μέθοδος είθισται να ξεκινά με ρήμα ακολουθούμενο με ουσιαστικά ή επιρρήματα με το πρώτο γράμμα κεφαλαίο σε μία συνεχή λέξη. Εδώ παραθέτονται μερικά παραδείγματα:
run
runFast
getBackground
getFinalData
compareTo
setX
isEmpty
Στην Java δεν μπορούμε να έχουμε δύο μεθόδους με το ίδιο όνομα μέσα στην ίδια κλαση, εκτός κι αν χρησιμοποιούμε τη μέθοδο overloading (υπερφόρτωση).
Overloading
Η Java υποστηρίζει την τεχνική υπερφόρτωσης μίας μεθόδου. Πολλαπλές μέθοδοι μπορούν να μοιράζονται το ίδιο όνομα αλλά δεν μπορούν να υπάρχουν παραπάνω από μία με το ίδιο αποτύπωμα. Περισσότερα θα δούμε στην ενότητα Interfaces και κληρονομικότητα.
Ας πούμε ότι έχουμε μία κλάση που χρησιμοποιεί καλλιγραφία για να ζωγραφίσει δεδομένα στην οθόνη. Για λογική ευχρηστία μπορούμε να παρέχουμε μία μέθοδο draw η οποία να δέχεται διαφορετικούς τύπους δεδομένων σαν όρισμα και να τυπώνει τα δεδομένα τους στην οθόνη. Στην πραγματικότητα βέβαια δεν είναι μια η μέθοδος αλλά πολλές διαφορετικές που έχουν το ίδιο όνομα αλλά όμως διαφορετικές παραμέτρους εισόδου, δηλαδή διαφορετικό αποτύπωμα.
public class DataArtist
{
public void draw(String s)
{
}
public void draw(int i)
{
}
public void draw(double f)
{
}
public void draw(int i, double f)
{
}
}
Η τεχνική υπερφόρτωσης παρέχει ευκολία στην χρήση της μεθόδου αλλά πρέπει να χρησιμοποιείται με φειδώ.
Σημείωση: Δεν είναι δυνατό να δηλωθούν 2 μέθοδοι με το ίδιο αποτύπωμα αλλά διαφορετική επιστροφή καθώς θεωρείται ως διπλή δήλωση της ίδιας μεθόδου αφού η επιστροφή δεν λογίζεται ως μέρος του αποτυπώματος.
Constructors
Μια κλάση έχει τουλάχιστον ένα constructor (κατασκευαστή), μια ειδική μέθοδο η οποία καλείται όποτε δημιουργούμε ένα object από την κλάση αυτή. Η μέθοδος κατασκευής έχει ακριβώς το ίδιο όνομα με την κλάση και δεν ορίζουμε για αυτή τύπο επιστροφής. Για παράδειγμα, η Bicycle έχει ένα constructor:
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
}
Για να δημιουργήσουμε ένα καινούριο Bicycle αντικείμενο με το όνομα myBike, καλούμε τον constructor με τον τελεστή new:
Bicycle myBike = new Bicycle(30, 0, 8);
Η έκφραση new Bicycle(30, 0, 8) δημιουργεί-δεσμεύει χώρο στην μνήμη για ένα αντικείμενο και αρχικοποιεί τα πεδία του.
Παρόλο που το Bicycle έχει μόνο ένα constructor, θα μπορούσε να έχει και άλλους (με την χρήση της τεχνικής υπερφόρτωσης), συμπεριλαμβανομένου και του constructor χωρίς παραμέτρους:
public Bicycle()
{
gear = 1;
cadence = 10;
speed = 0;
}
Και η κλήση
Bicycle yourBike = new Bicycle();
καλεί τον κενό από παραμέτρους constructor για να δημιουργήσει ένα νέο αντικείμενο Bicycle με το όνομα yourBike.
Αν ορίσουμε έναν constructor τότε μόνο αυτός μπορεί να δημιουργήσει στιγμιότυπο της κλάσης του. Μπορούμε, επίσης, να μην ορίσουμε κανένα constructor για την κλάση μας. Σε αυτή την περίπτωση ο compiler παρέχει αυτόματα έναν constructor χωρίς παραμέτρους. Αυτός ο constructor υποθέτει ότι και η ενδεχόμενη superclass έχει έναν constructor χωρίς παραμέτρους και θα τον καλέσει αυτόματα.. Αν η superclass δεν έχει ορισμένο constructor χωρίς παραμέτρους θα πάρουμε μήνυμα λάθους.
Μπορούμε επίσης να καλέσουμε και με κώδικα τον constructor της superclass όπως θα δούμε στην ενότητα interfaces και κληρονομικότητα.
Μπορούμε να χρησιμοποιήσουμε μετατροπείς προσβασιμότητας (access modifiers) όπως στις απλές μεθόδους.
Σημείωση: Αν μια άλλη κλάση δεν έχει πρόσβαση στον constructor μιας κλάσης δεν μπορεί να δημιουργήσει στιγμιότυπο. Θυμηθείτε επίσης ότι μία κλάση με μια μέθοδο main είναι η πρώτη που καλείται και αυτή καλεί τους constructors των πρώτων κλάσεων που χρησιμοποιεί.
Δημιουργία και Χρηση Object
Ένα τυπικό πρόγραμμα στην Java δημιουργεί πολλά αντικείμενα τα οποία αλληλεπιδρούν μεταξύ τους με την κλήση μεθόδων. Με την χρήση αντικειμένων ,ένα πρόγραμμα μπορεί να κάνει διάφορα πράγματα, όπως να υλοποιεί ένα γραφικό περιβάλλον μέχρι να δημιουργεί animations και να στέλνει και να λαμβάνει πληροφορίες από ένα δίκτυο. Όταν το αντικείμενο τελειώσει την δουλεία που έχει να κάνει η μνήμη που κατέχει απελευθερώνεται.
Παρακάτω βλέπουμε ένα μικρό πρόγραμμα που θα χρησιμοποιήσουμε ως παράδειγμα για να μιλήσουμε για αντικείμενα και την χρήση τους
CreateObjectDemo, Point ,Rectangle ,
public class CreateObjectDemo {
public static void main(String[] args) {
// Declare and create a point object and two rectangle objects.
Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);
// display rectOne's width, height, and area
System.out.println("Width of rectOne: " + rectOne.width);
System.out.println("Height of rectOne: " + rectOne.height);
System.out.println("Area of rectOne: " + rectOne.getArea());
// set rectTwo's position
rectTwo.origin = originOne;
// display rectTwo's position
System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);
// move rectTwo and display its new position
rectTwo.move(40, 72);
System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);
}
}
Το πρόγραμμα αυτό δημιουργεί, επεξεργάζεται και επιστρέφει πληροφορίες από διάφορα αντικείμενα.
Εδώ έχουμε την επιστροφή του:
Width of rectOne: 100
Height of rectOne: 200
Area of rectOne: 20000
X Position of rectTwo: 23
Y Position of rectTwo: 94
X Position of rectTwo: 40
Y Position of rectTwo: 72
Όπως έχουμε δει μια κλάση είναι το πρωτότυπο ενός αντικειμένου, η «συνταγή». Κάθε μια από τις παρακάτω δηλώσεις του παραπάνω παραδείγματος δημιουργεί ένα αντικείμενο και το καταχωρεί σε μεταβλητές:
Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);
Η πρώτη δήλωση δημιουργεί ένα αντικείμενο της κλάσης Point, και η δεύτερη και Κάθε μία από αυτές τις δηλώσεις έχει 3 μέρη:
- Δήλωση Τύπου: Ο κώδικας σε έντονη γραφή είναι δηλώσεις ονομάτων μεταβλητών και η αντιστοίχιση τους με ένα τα’υπο αντικειμένου.
- Δημιουργία αντικειμένου: Η λέξη κλείδι new έιναι ένας τελεστής της Java που δημιουργεί ένα αντικείμενο.
- Αρχικοποίηση: Ο τελεστής new ακολουθείται από μια κλήση στον constructor, η οποί α αρχικοποιεί το αντικείμενο.
Δήλωση μεταβλητής που δείχνει σε αντικείμενο
¨Όπως έχουμε δει ως τώρα για να δηλώσουμε μια μεταβλητή χρησιμοποιούμε την εξής σύνταξη:
«τύπος» «όνομα»;
Αυτή η δήλωση ενημερώνει τον compiler ότι θα χρησιμοποιήσουμε ένα όνομα για να αναφερόμαστε σε δεδομένα του συγκεκριμένου τύπου. Με τους απλούς τύπους δεδομένων αυτή η δήλωση αναφοράς δεσμεύει επίσης ανάλογο χώρο στην μνήμη για τον τύπο μεταβλητής.
Επίσης μπορεί να γίνει μόνο δήλωση αναφοράς σε μια γραμμή. Για παράδειγμα:
Point originOne;
Αν ορίσουμε την originOne έτσι, η τιμή της θα είναι αδιευκρίνιστη έως ότου ένα αντικείμενο δημιουργηθεί και ανατεθεί σε αυτή. Με την απλή δήλωση αναφοράς δεν δημιουργείται όμως ένα αντικείμενο. Για να γίνει αυτό πρέπει να χρησιμοποιηθεί ο τελεστής new όπως περιγράφεται παρακάτω.
Μια μεταβλητή, η οποία δεν αναφέρεται ακόμα σε αντικείμενο, μπορεί να αναπαρασταθεί ως εξής(όνομα μεταβλητής, και μια αναφορά που δείχνει στο κενό):
Δημιουργία στιγμιότυπου κλάσης
Ο τελεστής new δημιουργεί ένα αντικείμενο μιας κλάσης, δεσμεύει χώρο για αυτό στη μνήμη και επιστρέφει μια αναφορά σε αυτό. Επίσης καλεί τον constructor της κλάσης.
Ο τελεστής new ακολουθείται πάντα από την κλήση σε ένα constructor κλάσης, του οποίου το όνομα είναι πάντα το ίδιο με την κλάσης. Επίσης επιστρέφει μια αναφορά στο αντικείμενο που τον κάλεσε. Τις περισσότερες φορές η αναφορά αντιστοιχίζεται με μια μεταβλητή κατάλληλου τύπου:
Point originOne = new Point(23, 94);
Δεν είναι πάντα υποχρεωτικό να γίνει η αντιστοίχιση αυτή. Η new μπορεί να επιστρέφει την αναφορά της και σε μία έκφραση, π.χ.
int height = new Rectangle().height;
Αυτό θα το δούμε παρακάτω.
Αρχικοποίηση αντικειμένου Εδώ έχουμε τον κώδικα για την κλάση Point:
public class Point {
public int x = 0;
public int y = 0;
//constructor
public Point(int a, int b) {
x = a;
y = b;
}
}
Αυτή η κλάση περιλαμβάνει έναν constructor. Όπως είπαμε ήδη ο constructor αναγνωρίζεται εύκολα επειδή η δήλωση του χρησιμοποιεί το ίδιο όνομα με την κλάση και δεν έχει τύπο επιστροφής. Ο constructor της κλάσης point δέχεται δύο ακέραιους ως παραμέτρους όπως φαίνεται από τον κώδικα (int a, int b). Η παρακάτω δήλωση δίνει το 23 και 94 ως τιμές αυτών των παραμέτρων:
Point originOne = new Point(23, 94);
Το αποτέλεσμα της εκτέλεσης της δήλωσης φαίνεται στην επόμενη εικόνα:
Ο τελεστής new δημιουργεί ένα αντικείμενο μιας κλάσης, δεσμεύει χώρο για αυτό στη μνήμη και επιστρέφει μια αναφορά σε αυτό. Επίσης καλεί τον constructor της κλάσης.
Ο τελεστής new ακολουθείται πάντα από την κλήση σε ένα constructor κλάσης, του οποίου το όνομα είναι πάντα το ίδιο με την κλάσης. Επίσης επιστρέφει μια αναφορά στο αντικείμενο που τον κάλεσε. Τις περισσότερες φορές η αναφορά αντιστοιχίζεται με μια μεταβλητή κατάλληλου τύπου:
Point originOne = new Point(23, 94);
Δεν είναι πάντα υποχρεωτικό να γίνει η αντιστοίχιση αυτή. Η new μπορεί να επιστρέφει την αναφορά της και σε μία έκφραση, π.χ.
int height = new Rectangle().height;
Αυτό θα το δούμε παρακάτω.
Αρχικοποίηση αντικειμένου Εδώ έχουμε τον κώδικα για την κλάση Point:
public class Point {
public int x = 0;
public int y = 0;
//constructor
public Point(int a, int b) {
x = a;
y = b;
}
}
Αυτή η κλάση περιλαμβάνει έναν constructor. Όπως είπαμε ήδη ο constructor αναγνωρίζεται εύκολα επειδή η δήλωση του χρησιμοποιεί το ίδιο όνομα με την κλάση και δεν έχει τύπο επιστροφής. Ο constructor της κλάσης point δέχεται δύο ακέραιους ως παραμέτρους όπως φαίνεται από τον κώδικα (int a, int b). Η παρακάτω δήλωση δίνει το 23 και 94 ως τιμές αυτών των παραμέτρων:
Point originOne = new Point(23, 94);
Το αποτέλεσμα της εκτέλεσης της δήλωσης φαίνεται στην επόμενη εικόνα:
Εδώ είναι ο κώδικας για την κλάση Rectangle η οποία έχει τέσσερις constructors:
public class Rectangle {
public int width = 0;
public int height = 0;
public Point origin;
// four constructors
public Rectangle() {
origin = new Point(0, 0);
}
public Rectangle(Point p) {
origin = p;
}
public Rectangle(int w, int h) {
origin = new Point(0, 0);
width = w;
height = h;
}
public Rectangle(Point p, int w, int h) {
origin = p;
width = w;
height = h;
}
// a method for moving the rectangle
public void move(int x, int y) {
origin.x = x;
origin.y = y;
}
// a method for computing the area of the rectangle
public int getArea() {
return width * height;
}
}
Κάθε constructor μας επιτρέπει να δίδουμε αρχικές τιμές για το μέγεθος και πλάτος του rectangle (παραλληλογράμμου) χρησιμοποιώντας απλούς τύπους δεδομένων ή τύπους αναφοράς. Όπως συζητήσαμε νωρίτερα στην τεχνική overloading , κάθε τέτοιος constructor έχει διαφορετικό αποτύπωμα.
Η παρακάτω δήλωση καλεί τον constructor που αρχικοποιεί το πεδίο origin με την αναφορά originOne και το width σε 100 και το height σε 200.
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Τώρα υπάρχουν δύο αναφορές στο ίδιο αντικείμενο point. Ένα αντικείμενο μπορεί να έχει πολλαπλές αναφορές σε ένα άλλο.
public class Rectangle {
public int width = 0;
public int height = 0;
public Point origin;
// four constructors
public Rectangle() {
origin = new Point(0, 0);
}
public Rectangle(Point p) {
origin = p;
}
public Rectangle(int w, int h) {
origin = new Point(0, 0);
width = w;
height = h;
}
public Rectangle(Point p, int w, int h) {
origin = p;
width = w;
height = h;
}
// a method for moving the rectangle
public void move(int x, int y) {
origin.x = x;
origin.y = y;
}
// a method for computing the area of the rectangle
public int getArea() {
return width * height;
}
}
Κάθε constructor μας επιτρέπει να δίδουμε αρχικές τιμές για το μέγεθος και πλάτος του rectangle (παραλληλογράμμου) χρησιμοποιώντας απλούς τύπους δεδομένων ή τύπους αναφοράς. Όπως συζητήσαμε νωρίτερα στην τεχνική overloading , κάθε τέτοιος constructor έχει διαφορετικό αποτύπωμα.
Η παρακάτω δήλωση καλεί τον constructor που αρχικοποιεί το πεδίο origin με την αναφορά originOne και το width σε 100 και το height σε 200.
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Τώρα υπάρχουν δύο αναφορές στο ίδιο αντικείμενο point. Ένα αντικείμενο μπορεί να έχει πολλαπλές αναφορές σε ένα άλλο.
Το επόμενο παράδειγμα δήλωσης constructor rectangle δεν χρησιμοποιεί παραμέτρους , άρα καλείται ο αντίστοιχος constructor .
Rectangle rect = new Rectangle();
Χρήση αντικειμένων
Αφού δημιουργηθεί ένα αντικείμενο μπορούμε να χρησιμοποιήσουμε την τιμή ενός απ’ τα πεδία του , να αλλάξουμε ένα από τα πεδία του, ή να καλέσουμε μία από τις μεθόδους του .
Αναφορά στα πεδία ενός αντικειμένου Τα πεδία των αντικειμένων είναι προσβάσιμα με το όνομα τους. Το όνομα αυτό πρέπει να είναι σαφές και ξεκάθαρο. Για παράδειγμα τα width και height στην κλάση Rectangle είναι προσβάσιμα με τον ακόλουθο τρόπο:
System.out.println("Width and height are: " + width + ", " + height);
Ο κώδικας που δεν βρίσκεται μέσα στην κλάση πρέπει να χρησιμοποιήσει μια αναφορά αντικειμένου ή μια έκφραση ακολουθούμενη από τον τελεστή τελεία (.) και το όνομα του πεδίου:
αναφοράAντικειμένου.όνομαΠεδίου
Ας υποθέσουμε ότι στη main δημιουργούμε ένα Rectangle αντικείμενο με όνομα rectOne. Για να καλέσουμε την προηγούμενη δήλωση εκεί η δήλωση αυτή θα είναι :
System.out.println("Width and height are: " + rectOne.width + ", " + rectOne.height);
Αν προσπαθούσαμε να καλέσουμε την προηγούμενη δήλωση στη main τα πεδία width και height δεν έχουν νόημα αφού είναι μέλη της κλάσης Rectangle.
Εκτός από αυτόν τον τρόπο χρήσης των πεδίων ενός αντικειμένου μπορούμε να χρησιμοποιήσουμε τη new όπως παρακάτω :
int height = new Rectangle().height;
Αυτή η δήλωση δημιουργεί ένα αντικείμενο Rectangle και αντί να πάρει ως επιστροφή την αναφορά στο αντικείμενο, ζητάει την τιμή του πεδίου height. Σημειώστε ότι αυτός ο τρόπος δημιουργίας αντικειμένου δεν αποθηκεύει πουθενά μια αναφορά στο αντικείμενο. Αυτό έχει ως αποτέλεσμα το αντικείμενο Rectangle να διαγραφεί από τη μνήμη αφού ολοκληρωθεί η εκτέλεση της δήλωσης.
Κλήση των μεθόδων ενός αντικειμένου Εκτός από τη χρήση της αναφοράς σε ένα αντικείμενο για την επιστροφή των τιμών των πεδίων του, μπορούμε χρησιμοποιώντας αυτή την αναφορά να καλέσουμε και τις μεθόδους του με τον ακόλουθο τρόπο: αναφοράΑντικειμένου.όνομαΜεθόδου(λίστα Παραμέτρων);
ή
αναφοράΑντικειμένου.όνομαΜεθόδου
Η κλάση Rectangle έχει δύο μεθόδους :
getArea() για να υπολογίσει την περιοχή του Rectangle και
move() για να αλλάξει τη θέση του Rectangle
Εδώ είναι ο κώδικας από το CreateObjectDemo που καλεί αυτές τις δύο μεθόδους:
System.out.println("Area of rectOne: " + rectOne.getArea());
...
rectTwo.move(40, 72);
Όπως και με τα πεδία, χρησιμοποιούμε την αναφορά rectOne ως αναφορά του αντικειμένου που ανήκει κάθε μέθοδος. Μπορούμε να χρησιμοποιήσουμε το όνομα μιας μεταβλητής αλλά και μία έκφραση η οποία γυρνά την αναφορά σε ένα αντικείμενο. Για παράδειγμα:
new Rectangle(100, 50).getArea()
Η έκφραση αυτή επιστρέφει την αναφορά σε ένα αντικείμενο τύπου Rectangle. Κάποιες μέθοδοι όπως η getArea επιστρέφουν κάποια τιμή . Αυτές τις μεθόδους μπορούμε να τις χρησιμοποιήσουμε και μέσα σε εκφράσεις και να καταχωρίσουμε την επιστρεφόμενη τιμή σε μία μεταβλητή ή να τη χρησιμοποιήσουμε μέσα σε δηλώσεις ελέγχου ροής. Για παράδειγμα
int areaOfRectangle = new Rectangle(100, 50).getArea();
Garbage Collector (σκουπιδιάρης)
Κάποιες αντικειμενοστρεφείς γλώσσες απαιτούν από τον κώδικα να δηλώνει ξεκάθαρα και τη δημιουργία αλλά και την καταστροφή αντικειμένων. Δηλαδή να διαχειριζόμαστε τη μνήμη του προγράμματος άμεσα από τον κώδικα. Η διαχείριση αυτή αποτελεί συχνά πηγή σφαλμάτων.
Η Java επιτρέπει να δημιουργήσουμε όσα αντικείμενα θέλουμε (ανάλογα με τη χωρητικότητα της μνήμης του μηχανήματος) και να μην ασχολούμαστε με τη διαχείριση της μνήμης που καταλαμβάνουν αφού πάψουν να είναι χρήσιμα. Τη δουλειά αυτή την κάνει για μας μια διεργασία που λέγεται Garbage Collector.
O Garbage Collector αναζητά τα αντικείμενα, στα οποία δεν έχουμε πλέον αναφορά μέσα στο πρόγραμμα μας, και απελευθερώνει τη μνήμη που καταλαμβάνουν καταστρέφοντας τα. Επίσης μπορούμε να προκαλέσουμε την καταστροφή ενός αντικειμένου θέτοντας την ειδική τιμή null στην αναφορά του. Η διεργασία αυτή είναι μέλος του runtime της Java και λειτουργεί αυτόματα για όσα προγράμματα Java εκτελούνται.
Επιστροφή τιμών από μέθοδο
Προηγουμένως μιλήσαμε για μεθόδους που επιστρέφουν κάποια τιμή. Μια μέθοδος επιστρέφει στον κώδικα που την κάλεσε όταν συμβεί ένα από τα παρακάτω:
· Ολοκληρωθούν όλες οι δηλώσεις της μεθόδου
· Φτάσει σε μια δήλωση return
· Δημιουργηθεί μια exception (που θα δούμε αργότερα)
Η δήλωση return μπορεί να χρησιμοποιηθεί για να επιστρέψει την τιμή που ορίζει ότι επιστρέφει μια μέθοδος. Επίσης μπορεί να χρησιμοποιηθεί και στην περίπτωση που μια μέθοδος επιστρέφει void (κενό ή τίποτα) .
return;
Σε αυτή την περίπτωση μπορούμε να χρησιμοποιήσουμε τη return προαιρετικά για τον άμεσο τερματισμό της εκτέλεσης τη μεθόδου (π.χ. μέσα σε δήλωση ελέγχου ροής) και την επιστροφή στο σημείο που αυτή κλήθηκε.
Σε περίπτωση όμως που η μέθοδος έχει δηλωμένο τύπο επιστροφής τότε πρέπει απαραίτητα να επιστρέψει τιμή ή αναφορά αυτού του τύπου.
// a method for computing the area of the rectangle
public int getArea() {
return width * height;
}
Η λέξη κλειδί this
Στον κώδικα μιας κλάσης και συγκεκριμένα εντός των constructors και των μεθόδων μπορούμε να χρησιμοποιήσουμε τη λέξη this. Η this είναι ένας τελεστής που αναφέρεται στο τρέχον αντικείμενο - στιγμιότυπο, του οποίου ο constructor ή η μέθοδος καλείται.
Χρήση της this με πεδίο
Ο πιο συχνός λόγος που χρησιμοποιούμε την this σε ένα πεδίο είναι γιατί έχουμε το ίδιο όνομα σε μια παράμετρο, π.χ. η κλάση Point μπορεί να γραφτεί έτσι:
public class Point {
public int x = 0;
public int y = 0;
//constructor
public Point(int a, int b) {
x = a;
y = b;
}
}
το οποίο μπορεί να γραφτεί και έτσι:
public class Point {
public int x = 0;
public int y = 0;
//constructor
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Για να αντιγράψουμε τις τιμές των παραμέτρων στα πεδία της κλάσης χρησιμοποιούμε την this για να διαχωρίσουμε την αναφορά μας στα πεδία και όχι στις παραμέτρους.
Χρήση της this με Constructor
Σε προηγούμενη ενότητα είδαμε την τεχνική υπερφόρτωσης μεθόδων και constructors. Μια χρήση της this είναι και η άμεση κλήση στον constructor μιας κλάσης όταν έχουμε αυτή έχει πολλαπλούς. Για παράδειγμα
public class Rectangle {
private int x, y;
private int width, height;
public Rectangle() {
this(0, 0, 0, 0);
}
public Rectangle(int width, int height) {
this(0, 0, width, height);
}
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
...
}
Η παραπάνω κλάση περιέχει πολλαπλούς ορισμούς (υπερφόρτωση) του constructor της. Μπορούμε να καλέσουμε από έναν constructor καποιον άλλο της ίδιας κλάσης.( Σε κάθε περίπτωση η κλήση στον constructor πρέπει να είναι η πρώτη δήλωση.) Η τεχνική αυτή μας επιτρέπει να παρέχουμε εναλλακτικούς τρόπους δημιουργίας του αντικειμένου ανάλογα με τις πληροφορίες που έχουμε διαθέσιμες για να αρχικοποιήσουμε στα πεδία του.
Έλεγχος πρόσβασης σε μεθόδους
Οι τελεστές πρόσβασης καθορίζουν εάν άλλες κλάσεις μπορούν να χρησιμοποιούν ένα συγκεκριμένο πεδίο ή μέθοδο:
· Σε επίπεδο κλάσης — public ή package private(χωρίς τελεστή)
· Σε επίπεδο μέλους κλάσης — public, private, protected ή (package-private χωρίς τελεστή)
Μία κλάση μπορεί να οριστεί με τον τελεστή public εφόσον θέλουμε να είναι ορατή από κλάσεις σε άλλα packages, τόσο αυτή όσο και τα ορατά μέλη της . Εάν η κλάση δεν έχει όρισμα τελεστή πρόσβασης είναι εξ’ ορισμού package private, δηλαδή είναι ορατή μόνο μέσα στο package της. (Τα packages στην Java είναι μία μέθοδος ονοματολογικής ομαδοποίησης κλάσεων που είναι σχετικές μεταξύ τους. Θα δούμε σε επόμενο κεφάλαιο περισσότερα).
Σε επίπεδο μελών της κλάσης, η χρήση της public και η μη χρήση τελεστή (package private) δίνουν στα μέλη της κλάσης (πεδία και μεθόδους) την ίδια ορατότητα όπως παραπάνω. Ο τελεστής private δηλώνει ότι ένα μέλος είναι ορατό μόνο μέσα στην κλάση του. Ο τελεστής protected δηλώνει ότι ένα μέλος είναι ορατό μόνο σε κλάσεις του ίδιου package ή και μόνο σε «παιδιά» της κλάσης που βρίσκονται σε άλλα packages.
Ο επόμενος πίνακας συνοψίζει τα παραπάνω:
Rectangle rect = new Rectangle();
Χρήση αντικειμένων
Αφού δημιουργηθεί ένα αντικείμενο μπορούμε να χρησιμοποιήσουμε την τιμή ενός απ’ τα πεδία του , να αλλάξουμε ένα από τα πεδία του, ή να καλέσουμε μία από τις μεθόδους του .
Αναφορά στα πεδία ενός αντικειμένου Τα πεδία των αντικειμένων είναι προσβάσιμα με το όνομα τους. Το όνομα αυτό πρέπει να είναι σαφές και ξεκάθαρο. Για παράδειγμα τα width και height στην κλάση Rectangle είναι προσβάσιμα με τον ακόλουθο τρόπο:
System.out.println("Width and height are: " + width + ", " + height);
Ο κώδικας που δεν βρίσκεται μέσα στην κλάση πρέπει να χρησιμοποιήσει μια αναφορά αντικειμένου ή μια έκφραση ακολουθούμενη από τον τελεστή τελεία (.) και το όνομα του πεδίου:
αναφοράAντικειμένου.όνομαΠεδίου
Ας υποθέσουμε ότι στη main δημιουργούμε ένα Rectangle αντικείμενο με όνομα rectOne. Για να καλέσουμε την προηγούμενη δήλωση εκεί η δήλωση αυτή θα είναι :
System.out.println("Width and height are: " + rectOne.width + ", " + rectOne.height);
Αν προσπαθούσαμε να καλέσουμε την προηγούμενη δήλωση στη main τα πεδία width και height δεν έχουν νόημα αφού είναι μέλη της κλάσης Rectangle.
Εκτός από αυτόν τον τρόπο χρήσης των πεδίων ενός αντικειμένου μπορούμε να χρησιμοποιήσουμε τη new όπως παρακάτω :
int height = new Rectangle().height;
Αυτή η δήλωση δημιουργεί ένα αντικείμενο Rectangle και αντί να πάρει ως επιστροφή την αναφορά στο αντικείμενο, ζητάει την τιμή του πεδίου height. Σημειώστε ότι αυτός ο τρόπος δημιουργίας αντικειμένου δεν αποθηκεύει πουθενά μια αναφορά στο αντικείμενο. Αυτό έχει ως αποτέλεσμα το αντικείμενο Rectangle να διαγραφεί από τη μνήμη αφού ολοκληρωθεί η εκτέλεση της δήλωσης.
Κλήση των μεθόδων ενός αντικειμένου Εκτός από τη χρήση της αναφοράς σε ένα αντικείμενο για την επιστροφή των τιμών των πεδίων του, μπορούμε χρησιμοποιώντας αυτή την αναφορά να καλέσουμε και τις μεθόδους του με τον ακόλουθο τρόπο: αναφοράΑντικειμένου.όνομαΜεθόδου(λίστα Παραμέτρων);
ή
αναφοράΑντικειμένου.όνομαΜεθόδου
Η κλάση Rectangle έχει δύο μεθόδους :
getArea() για να υπολογίσει την περιοχή του Rectangle και
move() για να αλλάξει τη θέση του Rectangle
Εδώ είναι ο κώδικας από το CreateObjectDemo που καλεί αυτές τις δύο μεθόδους:
System.out.println("Area of rectOne: " + rectOne.getArea());
...
rectTwo.move(40, 72);
Όπως και με τα πεδία, χρησιμοποιούμε την αναφορά rectOne ως αναφορά του αντικειμένου που ανήκει κάθε μέθοδος. Μπορούμε να χρησιμοποιήσουμε το όνομα μιας μεταβλητής αλλά και μία έκφραση η οποία γυρνά την αναφορά σε ένα αντικείμενο. Για παράδειγμα:
new Rectangle(100, 50).getArea()
Η έκφραση αυτή επιστρέφει την αναφορά σε ένα αντικείμενο τύπου Rectangle. Κάποιες μέθοδοι όπως η getArea επιστρέφουν κάποια τιμή . Αυτές τις μεθόδους μπορούμε να τις χρησιμοποιήσουμε και μέσα σε εκφράσεις και να καταχωρίσουμε την επιστρεφόμενη τιμή σε μία μεταβλητή ή να τη χρησιμοποιήσουμε μέσα σε δηλώσεις ελέγχου ροής. Για παράδειγμα
int areaOfRectangle = new Rectangle(100, 50).getArea();
Garbage Collector (σκουπιδιάρης)
Κάποιες αντικειμενοστρεφείς γλώσσες απαιτούν από τον κώδικα να δηλώνει ξεκάθαρα και τη δημιουργία αλλά και την καταστροφή αντικειμένων. Δηλαδή να διαχειριζόμαστε τη μνήμη του προγράμματος άμεσα από τον κώδικα. Η διαχείριση αυτή αποτελεί συχνά πηγή σφαλμάτων.
Η Java επιτρέπει να δημιουργήσουμε όσα αντικείμενα θέλουμε (ανάλογα με τη χωρητικότητα της μνήμης του μηχανήματος) και να μην ασχολούμαστε με τη διαχείριση της μνήμης που καταλαμβάνουν αφού πάψουν να είναι χρήσιμα. Τη δουλειά αυτή την κάνει για μας μια διεργασία που λέγεται Garbage Collector.
O Garbage Collector αναζητά τα αντικείμενα, στα οποία δεν έχουμε πλέον αναφορά μέσα στο πρόγραμμα μας, και απελευθερώνει τη μνήμη που καταλαμβάνουν καταστρέφοντας τα. Επίσης μπορούμε να προκαλέσουμε την καταστροφή ενός αντικειμένου θέτοντας την ειδική τιμή null στην αναφορά του. Η διεργασία αυτή είναι μέλος του runtime της Java και λειτουργεί αυτόματα για όσα προγράμματα Java εκτελούνται.
Επιστροφή τιμών από μέθοδο
Προηγουμένως μιλήσαμε για μεθόδους που επιστρέφουν κάποια τιμή. Μια μέθοδος επιστρέφει στον κώδικα που την κάλεσε όταν συμβεί ένα από τα παρακάτω:
· Ολοκληρωθούν όλες οι δηλώσεις της μεθόδου
· Φτάσει σε μια δήλωση return
· Δημιουργηθεί μια exception (που θα δούμε αργότερα)
Η δήλωση return μπορεί να χρησιμοποιηθεί για να επιστρέψει την τιμή που ορίζει ότι επιστρέφει μια μέθοδος. Επίσης μπορεί να χρησιμοποιηθεί και στην περίπτωση που μια μέθοδος επιστρέφει void (κενό ή τίποτα) .
return;
Σε αυτή την περίπτωση μπορούμε να χρησιμοποιήσουμε τη return προαιρετικά για τον άμεσο τερματισμό της εκτέλεσης τη μεθόδου (π.χ. μέσα σε δήλωση ελέγχου ροής) και την επιστροφή στο σημείο που αυτή κλήθηκε.
Σε περίπτωση όμως που η μέθοδος έχει δηλωμένο τύπο επιστροφής τότε πρέπει απαραίτητα να επιστρέψει τιμή ή αναφορά αυτού του τύπου.
// a method for computing the area of the rectangle
public int getArea() {
return width * height;
}
Η λέξη κλειδί this
Στον κώδικα μιας κλάσης και συγκεκριμένα εντός των constructors και των μεθόδων μπορούμε να χρησιμοποιήσουμε τη λέξη this. Η this είναι ένας τελεστής που αναφέρεται στο τρέχον αντικείμενο - στιγμιότυπο, του οποίου ο constructor ή η μέθοδος καλείται.
Χρήση της this με πεδίο
Ο πιο συχνός λόγος που χρησιμοποιούμε την this σε ένα πεδίο είναι γιατί έχουμε το ίδιο όνομα σε μια παράμετρο, π.χ. η κλάση Point μπορεί να γραφτεί έτσι:
public class Point {
public int x = 0;
public int y = 0;
//constructor
public Point(int a, int b) {
x = a;
y = b;
}
}
το οποίο μπορεί να γραφτεί και έτσι:
public class Point {
public int x = 0;
public int y = 0;
//constructor
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Για να αντιγράψουμε τις τιμές των παραμέτρων στα πεδία της κλάσης χρησιμοποιούμε την this για να διαχωρίσουμε την αναφορά μας στα πεδία και όχι στις παραμέτρους.
Χρήση της this με Constructor
Σε προηγούμενη ενότητα είδαμε την τεχνική υπερφόρτωσης μεθόδων και constructors. Μια χρήση της this είναι και η άμεση κλήση στον constructor μιας κλάσης όταν έχουμε αυτή έχει πολλαπλούς. Για παράδειγμα
public class Rectangle {
private int x, y;
private int width, height;
public Rectangle() {
this(0, 0, 0, 0);
}
public Rectangle(int width, int height) {
this(0, 0, width, height);
}
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
...
}
Η παραπάνω κλάση περιέχει πολλαπλούς ορισμούς (υπερφόρτωση) του constructor της. Μπορούμε να καλέσουμε από έναν constructor καποιον άλλο της ίδιας κλάσης.( Σε κάθε περίπτωση η κλήση στον constructor πρέπει να είναι η πρώτη δήλωση.) Η τεχνική αυτή μας επιτρέπει να παρέχουμε εναλλακτικούς τρόπους δημιουργίας του αντικειμένου ανάλογα με τις πληροφορίες που έχουμε διαθέσιμες για να αρχικοποιήσουμε στα πεδία του.
Έλεγχος πρόσβασης σε μεθόδους
Οι τελεστές πρόσβασης καθορίζουν εάν άλλες κλάσεις μπορούν να χρησιμοποιούν ένα συγκεκριμένο πεδίο ή μέθοδο:
· Σε επίπεδο κλάσης — public ή package private(χωρίς τελεστή)
· Σε επίπεδο μέλους κλάσης — public, private, protected ή (package-private χωρίς τελεστή)
Μία κλάση μπορεί να οριστεί με τον τελεστή public εφόσον θέλουμε να είναι ορατή από κλάσεις σε άλλα packages, τόσο αυτή όσο και τα ορατά μέλη της . Εάν η κλάση δεν έχει όρισμα τελεστή πρόσβασης είναι εξ’ ορισμού package private, δηλαδή είναι ορατή μόνο μέσα στο package της. (Τα packages στην Java είναι μία μέθοδος ονοματολογικής ομαδοποίησης κλάσεων που είναι σχετικές μεταξύ τους. Θα δούμε σε επόμενο κεφάλαιο περισσότερα).
Σε επίπεδο μελών της κλάσης, η χρήση της public και η μη χρήση τελεστή (package private) δίνουν στα μέλη της κλάσης (πεδία και μεθόδους) την ίδια ορατότητα όπως παραπάνω. Ο τελεστής private δηλώνει ότι ένα μέλος είναι ορατό μόνο μέσα στην κλάση του. Ο τελεστής protected δηλώνει ότι ένα μέλος είναι ορατό μόνο σε κλάσεις του ίδιου package ή και μόνο σε «παιδιά» της κλάσης που βρίσκονται σε άλλα packages.
Ο επόμενος πίνακας συνοψίζει τα παραπάνω:
Η πρώτη στήλη δείχνει το επίπεδο πρόσβασης που ορίζεται με τελεστές πάνω σε ένα πεδίο.
Η πρώτη γραμμή μας δείχνει ποιοι μπορούν να έχουν πρόσβαση στο πεδίο αυτό ανάλογα με τον τελεστή.
Οι τελεστές πρόσβασης μας επηρεάζουν με 2 τρόπους:
Η πρώτη γραμμή μας δείχνει ποιοι μπορούν να έχουν πρόσβαση στο πεδίο αυτό ανάλογα με τον τελεστή.
Οι τελεστές πρόσβασης μας επηρεάζουν με 2 τρόπους:
- Όταν χρησιμοποιούμε έτοιμες κλάσεις (π.χ. της κλάσης των πακέτων της Java για τις οποίες δεν έχουμε πρόσβαση στο πηγαίο κώδικα) οι τελεστές πρόσβασης ορίζουν σε ποια μέλη έχουμε πρόσβαση με τις κλάσεις μας.
- Όταν φτιάχνουμε τις δικές μας κλάσεις με τους τελεστές πρόσβασης ορίζουμε που μπορεί κανείς να έχει πρόσβαση στα μέλη τους.
Πώς διαλέγουμε επίπεδο προσβασιμότητας:
Για να προστατέψουμε την λογική της λειτουργίας της κλάσης μας όταν αυτή χρησιμοποιείται και από άλλους προγραμματιστές ή απλά είναι μέρος ενός μεγάλου έργου για το οποίο δεν είναι δυνατό να θυμόμαστε όλες τις λεπτομέρειες, μπορούμε να χρησιμοποιούμε τους τελεστές πρόσβασης ως εξής:
Πάντοτε να χρησιμοποιούμε τον πιο αυστηρό περιορισμό πρόσβασης για ένα μέλος αν δεν είναι απολύτως απαραίτητο αυτό να είναι ορατό εκτός της κλάσης. Δηλαδή όλα private μέχρι να χρειαστεί να είναι ορατά.
Αποφεύγουμε πάντα να χρησιμοποιούμε public πεδία εκτός αν πρόκειται για σταθερές. Αν επιτρέψουμε να επηρεάζει την τιμή ενός πεδίου άλλο αντικείμενο απευθείας τότε είναι δύσκολο να κάνουμε αλλαγές - διορθώσεις στον κώδικα μας καθώς παραβιάζεται η αρχή της ατομικότητας των αντικειμένων μας και είναι πιθανόν να καταρρεύσει η λογική του προγράμματος μας.
Κατανόηση διαφοράς μέλους στιγμιοτύπου και μέλους κλάσης
Θυμηθείτε το παράδειγμα του προγράμματος HelloWorld. Η μέθοδος που εκτελείται για να αρχίσει το πρόγραμμα μας είναι ορισμένη κάπως έτσι :
public static void main(String[] args){}
Εδώ θα εξηγήσουμε τον όρο static.
Μεταβλητές επιπέδου κλάσης
Όταν δημιουργούμε στιγμιότυπα μιας κλάσης (αντικείμενα) αυτά έχουν τα δικά τους αντίγραφα μεταβλητών (μεταβλητές στιγμιότυπου). Στην περίπτωση του ποδηλάτου η κλάση Bicycle οι μεταβλητές στιγμιότυπου είναι οι cadence, gear, and speed. Κάθε αντικείμενο Bicycle έχει τις δικές του τιμές, οι οποίες αποθηκεύονται σε ξεχωριστή θέση στη μνήμη του υπολογιστή.
Μερικές φορές όμως θέλουμε κάποιες μεταβλητές να είναι κοινές – ορατές για όλα τα αντικείμενα. Αυτό επιτυγχάνεται με τον τελεστή static. Πεδία με την λέξη static, πριν το όρισμα τους, ονομάζονται στατικά πεδία ή μεταβλητές κλάσης.
Οι μεταβλητές κλάσης:
1. είναι κοινές για όλα τα στιγμιότυπα της κλάσης
2. υπάρχουν και είναι προσβάσιμες ακόμη και χωρίς να υπάρχει κανένα στιγμιότυπο της κλάσης
3. είναι διαθέσιμες και σε άλλα αντικείμενα πέραν των στιγμιότυπων της κλάσης τους.
Για παράδειγμα, δημιουργούμε έναν αριθμό από αντικείμενα Bicycle και δίνουμε στο καθένα ένα σειριακό αριθμό id, αρχίζοντας από το 1 για το πρώτο. Επίσης, χρειαζόμαστε ένα πεδίο για να κρατάμε τον αριθμό των αντικειμένων που έχουμε δημιουργήσει ώστε να ξέρουμε πιο σειριακό αριθμό να δώσουμε στο κάθε επόμενο που δημιουργούμε. Αυτό το πεδίο δεν μπορεί να ανήκει σε κανένα στιγμιότυπο της Bicycle αλλά στην κλάση αυτή καθαυτή. Σε αυτή την περίπτωση χρειαζόμαστε μια μεταβλητή επιπέδου κλάσης , την numberOfBicycles:
public class Bicycle {
private int cadence;
private int gear;
private int speed;
// μεταβλητή στιγμιότυπου ID
private int id;
// προσθέτουμε μεταβλητή επιπέδου κλάσης
// για τον αριθμό των αντικειμένων Bicycle
private static int numberOfBicycles = 0;
...
}
Στις μεταβλητές επιπέδου κλάσης έχουμε πρόσβαση μέσω του ονόματος της κλάσης, δηλαδή:
Bicycle.numberOfBicycles
Εδώ φαίνεται ξεκάθαρα ότι είναι μέλη της κλάσης.
Προσοχη: Παρόλο που και η κλήση
myBike.numberOfBicycles
είναι σωστή, είναι λάθος πρακτική να τη χρησιμοποιούμε, καθώς δεν καθορίζει με προφανή τρόπο ότι αναφερόμαστε σε μεταβλητή κλάσης.
Χρησιμοποιώντας τον constructor του Bicycle ορίζουμε την τιμή του id για το στιγμυότυπο μας και αυξάνουμε την τιμή της μεταβλητής κλάσης numberOfBicycles:
public class Bicycle {
private int cadence;
private int gear;
private int speed;
private int id;
private static int numberOfBicycles = 0;
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
// αυξάνουμε τον αριθμό των Bicycle
// και ορίζουμε τον αριθμό ID
id = ++numberOfBicycles;
}
// καινούρια μέθοδος για να επιστρέψουμε
// το ID του στιγμιότυπου
public int getID() {
return id;
}
...
}
Μέθοδοι Κλάσης
Η Java υποστηρίζει και στατικές μεθόδους εκτός από στατικές μεταβλητές (βλέπε παράδειγμα της main). Οι στατικές μέθοδοι, που έχουν ως δεύτερο κατά σειρά τελεστή το static, μπορούν να κληθούν μέσω του ονόματος της κλάσης χωρίς την παρουσία στιγμιοτύπου:
ΟνομαΚλάσης.όνομαΜεθόδου(παράμετροι)
Προσοχη: Παρόλο που και η κλήση
όνομαΣτιγμιότυπου.όνομαΜεθόδου(παράμετροι)
είναι σωστή , είναι λάθος πρακτική να τη χρησιμοποιούμε καθώς δεν καθορίζει με προφανή τρόπο ότι αναφερόμαστε σε μέθοδο κλάσης.
Μια συνηθισμένη χρήση για τις στατικές μεθόδους είναι η παροχή πρόσβασης σε στατικά πεδία. Για παράδειγμα μπορούμε να προσθέσουμε μία στατική μέθοδο στην κλάση Bicycle για να έχουμε πρόσβαση στην μεταβλητή numberOfBicycles:
public static int getNumberOfBicycles() {
return numberOfBicycles;
}
Δεν επιτρέπονται όλοι οι συνδυασμοί μεθόδων και μεταβλητών κλάσης και στιγμιότυπου:
Σταθερές
Ο τελεστής static σε συνδυασμό με τον τελεστή final (τον είδαμε σε προηγούμενη ενότητα), στο όρισμα πεδίου χρησιμοποιείται για να ορίσει σταθερές. Ο final ορίζει ότι η τιμή του πεδίου δεν αλλάζει.
Το κλασικό παράδειγμα που μπορούμε να παραθέσουμε είναι η τιμή του π (ορίζει την σχέση του μήκους ενός κύκλου ως προς την διάμετρο του):
static final double PI = 3.141592653589793;
Η τιμή μιας σταθεράς δεν αλλάζει κατά τη διάρκεια ζωής ενός προγράμματος. Στην ονοματολογία που χρησιμοποιούμε για σταθερές βάζουμε όλα τα πρώτα γράμματα των λέξεων με κεφαλαίο και χωρίζουμε τις λέξεις με κάτω παύλα (_) My_Fantastic_Constant.
Η κλάση Bicycle Μετά από όλες τις μετατροπές σε αυτή την ενότητα η κλάση Bicycle έχει ως εξής :
public class Bicycle {
private int cadence;
private int gear;
private int speed;
private int id;
private static int numberOfBicycles = 0;
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
id = ++numberOfBicycles;
}
public int getID()
{
return id;
}
public static int getNumberOfBicycles()
{
return numberOfBicycles;
}
public int getCadence()
{
return cadence;
}
public void setCadence(int newValue)
{
cadence = newValue;
}
public int getGear()
{
return gear;
}
public void setGear(int newValue)
{
gear = newValue;
}
public int getSpeed()
{
return speed;
}
public void applyBrake(int decrement)
{
speed -= decrement;
}
public void speedUp(int increment)
{
speed += increment;
}
}
Singleton Pattern
Η δομή κώδικα Singleton αποτελεί ένα από τα πιο χρήσιμα και διαδεδομένα παραδείγματα χρήσης των στατικών μεταβλητών και πεδίων. Μας δίνει την δυνατότητα να ορίζουμε μια κλάση η οποία έχει εγγυημένα απολύτως μόνο ένα στιγμιότυπο το οποίο είναι ορατό από όλα τα αντικείμενα που έχουν πρόσβαση στο package της.
Design Patterns: Elements of Reusable Object-Oriented Software
Διάγραμμα Κλάσης Singleton
Για να προστατέψουμε την λογική της λειτουργίας της κλάσης μας όταν αυτή χρησιμοποιείται και από άλλους προγραμματιστές ή απλά είναι μέρος ενός μεγάλου έργου για το οποίο δεν είναι δυνατό να θυμόμαστε όλες τις λεπτομέρειες, μπορούμε να χρησιμοποιούμε τους τελεστές πρόσβασης ως εξής:
Πάντοτε να χρησιμοποιούμε τον πιο αυστηρό περιορισμό πρόσβασης για ένα μέλος αν δεν είναι απολύτως απαραίτητο αυτό να είναι ορατό εκτός της κλάσης. Δηλαδή όλα private μέχρι να χρειαστεί να είναι ορατά.
Αποφεύγουμε πάντα να χρησιμοποιούμε public πεδία εκτός αν πρόκειται για σταθερές. Αν επιτρέψουμε να επηρεάζει την τιμή ενός πεδίου άλλο αντικείμενο απευθείας τότε είναι δύσκολο να κάνουμε αλλαγές - διορθώσεις στον κώδικα μας καθώς παραβιάζεται η αρχή της ατομικότητας των αντικειμένων μας και είναι πιθανόν να καταρρεύσει η λογική του προγράμματος μας.
Κατανόηση διαφοράς μέλους στιγμιοτύπου και μέλους κλάσης
Θυμηθείτε το παράδειγμα του προγράμματος HelloWorld. Η μέθοδος που εκτελείται για να αρχίσει το πρόγραμμα μας είναι ορισμένη κάπως έτσι :
public static void main(String[] args){}
Εδώ θα εξηγήσουμε τον όρο static.
Μεταβλητές επιπέδου κλάσης
Όταν δημιουργούμε στιγμιότυπα μιας κλάσης (αντικείμενα) αυτά έχουν τα δικά τους αντίγραφα μεταβλητών (μεταβλητές στιγμιότυπου). Στην περίπτωση του ποδηλάτου η κλάση Bicycle οι μεταβλητές στιγμιότυπου είναι οι cadence, gear, and speed. Κάθε αντικείμενο Bicycle έχει τις δικές του τιμές, οι οποίες αποθηκεύονται σε ξεχωριστή θέση στη μνήμη του υπολογιστή.
Μερικές φορές όμως θέλουμε κάποιες μεταβλητές να είναι κοινές – ορατές για όλα τα αντικείμενα. Αυτό επιτυγχάνεται με τον τελεστή static. Πεδία με την λέξη static, πριν το όρισμα τους, ονομάζονται στατικά πεδία ή μεταβλητές κλάσης.
Οι μεταβλητές κλάσης:
1. είναι κοινές για όλα τα στιγμιότυπα της κλάσης
2. υπάρχουν και είναι προσβάσιμες ακόμη και χωρίς να υπάρχει κανένα στιγμιότυπο της κλάσης
3. είναι διαθέσιμες και σε άλλα αντικείμενα πέραν των στιγμιότυπων της κλάσης τους.
Για παράδειγμα, δημιουργούμε έναν αριθμό από αντικείμενα Bicycle και δίνουμε στο καθένα ένα σειριακό αριθμό id, αρχίζοντας από το 1 για το πρώτο. Επίσης, χρειαζόμαστε ένα πεδίο για να κρατάμε τον αριθμό των αντικειμένων που έχουμε δημιουργήσει ώστε να ξέρουμε πιο σειριακό αριθμό να δώσουμε στο κάθε επόμενο που δημιουργούμε. Αυτό το πεδίο δεν μπορεί να ανήκει σε κανένα στιγμιότυπο της Bicycle αλλά στην κλάση αυτή καθαυτή. Σε αυτή την περίπτωση χρειαζόμαστε μια μεταβλητή επιπέδου κλάσης , την numberOfBicycles:
public class Bicycle {
private int cadence;
private int gear;
private int speed;
// μεταβλητή στιγμιότυπου ID
private int id;
// προσθέτουμε μεταβλητή επιπέδου κλάσης
// για τον αριθμό των αντικειμένων Bicycle
private static int numberOfBicycles = 0;
...
}
Στις μεταβλητές επιπέδου κλάσης έχουμε πρόσβαση μέσω του ονόματος της κλάσης, δηλαδή:
Bicycle.numberOfBicycles
Εδώ φαίνεται ξεκάθαρα ότι είναι μέλη της κλάσης.
Προσοχη: Παρόλο που και η κλήση
myBike.numberOfBicycles
είναι σωστή, είναι λάθος πρακτική να τη χρησιμοποιούμε, καθώς δεν καθορίζει με προφανή τρόπο ότι αναφερόμαστε σε μεταβλητή κλάσης.
Χρησιμοποιώντας τον constructor του Bicycle ορίζουμε την τιμή του id για το στιγμυότυπο μας και αυξάνουμε την τιμή της μεταβλητής κλάσης numberOfBicycles:
public class Bicycle {
private int cadence;
private int gear;
private int speed;
private int id;
private static int numberOfBicycles = 0;
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
// αυξάνουμε τον αριθμό των Bicycle
// και ορίζουμε τον αριθμό ID
id = ++numberOfBicycles;
}
// καινούρια μέθοδος για να επιστρέψουμε
// το ID του στιγμιότυπου
public int getID() {
return id;
}
...
}
Μέθοδοι Κλάσης
Η Java υποστηρίζει και στατικές μεθόδους εκτός από στατικές μεταβλητές (βλέπε παράδειγμα της main). Οι στατικές μέθοδοι, που έχουν ως δεύτερο κατά σειρά τελεστή το static, μπορούν να κληθούν μέσω του ονόματος της κλάσης χωρίς την παρουσία στιγμιοτύπου:
ΟνομαΚλάσης.όνομαΜεθόδου(παράμετροι)
Προσοχη: Παρόλο που και η κλήση
όνομαΣτιγμιότυπου.όνομαΜεθόδου(παράμετροι)
είναι σωστή , είναι λάθος πρακτική να τη χρησιμοποιούμε καθώς δεν καθορίζει με προφανή τρόπο ότι αναφερόμαστε σε μέθοδο κλάσης.
Μια συνηθισμένη χρήση για τις στατικές μεθόδους είναι η παροχή πρόσβασης σε στατικά πεδία. Για παράδειγμα μπορούμε να προσθέσουμε μία στατική μέθοδο στην κλάση Bicycle για να έχουμε πρόσβαση στην μεταβλητή numberOfBicycles:
public static int getNumberOfBicycles() {
return numberOfBicycles;
}
Δεν επιτρέπονται όλοι οι συνδυασμοί μεθόδων και μεταβλητών κλάσης και στιγμιότυπου:
- Οι μέθοδοι στιγμιότυπου έχουν πρόσβαση σε μεταβλητές και μεθόδους στιγμιότυπων άμεσα.
- Οι μέθοδοι στιγμιότυπου έχουν πρόσβαση σε μεταβλητές και μεθόδους κλάσης άμεσα.
- Οι μέθοδοι κλάσης έχουν πρόσβαση σε μεταβλητές και μεθόδους κλάσης άμεσα.
- Οι μέθοδοι κλάσης δεν έχουν πρόσβαση σε μεταβλητές και μεθόδους στιγμιότυπων άμεσα – πρέπει να χρησιμοποιήσουν μία αναφορά σε αντικείμενο (περνάει ως παράμετρος στο όρισμα τους). Επίσης μέσα σε στατικές μεθόδους δεν μπορεί να χρησιμοποιηθεί η this καθώς δεν υπάρχει στιγμυότυπο.
Σταθερές
Ο τελεστής static σε συνδυασμό με τον τελεστή final (τον είδαμε σε προηγούμενη ενότητα), στο όρισμα πεδίου χρησιμοποιείται για να ορίσει σταθερές. Ο final ορίζει ότι η τιμή του πεδίου δεν αλλάζει.
Το κλασικό παράδειγμα που μπορούμε να παραθέσουμε είναι η τιμή του π (ορίζει την σχέση του μήκους ενός κύκλου ως προς την διάμετρο του):
static final double PI = 3.141592653589793;
Η τιμή μιας σταθεράς δεν αλλάζει κατά τη διάρκεια ζωής ενός προγράμματος. Στην ονοματολογία που χρησιμοποιούμε για σταθερές βάζουμε όλα τα πρώτα γράμματα των λέξεων με κεφαλαίο και χωρίζουμε τις λέξεις με κάτω παύλα (_) My_Fantastic_Constant.
Η κλάση Bicycle Μετά από όλες τις μετατροπές σε αυτή την ενότητα η κλάση Bicycle έχει ως εξής :
public class Bicycle {
private int cadence;
private int gear;
private int speed;
private int id;
private static int numberOfBicycles = 0;
public Bicycle(int startCadence, int startSpeed, int startGear)
{
gear = startGear;
cadence = startCadence;
speed = startSpeed;
id = ++numberOfBicycles;
}
public int getID()
{
return id;
}
public static int getNumberOfBicycles()
{
return numberOfBicycles;
}
public int getCadence()
{
return cadence;
}
public void setCadence(int newValue)
{
cadence = newValue;
}
public int getGear()
{
return gear;
}
public void setGear(int newValue)
{
gear = newValue;
}
public int getSpeed()
{
return speed;
}
public void applyBrake(int decrement)
{
speed -= decrement;
}
public void speedUp(int increment)
{
speed += increment;
}
}
Singleton Pattern
Η δομή κώδικα Singleton αποτελεί ένα από τα πιο χρήσιμα και διαδεδομένα παραδείγματα χρήσης των στατικών μεταβλητών και πεδίων. Μας δίνει την δυνατότητα να ορίζουμε μια κλάση η οποία έχει εγγυημένα απολύτως μόνο ένα στιγμιότυπο το οποίο είναι ορατό από όλα τα αντικείμενα που έχουν πρόσβαση στο package της.
Design Patterns: Elements of Reusable Object-Oriented Software
Διάγραμμα Κλάσης Singleton
Όπως μπορείτε να δείτε το η δομή του Singleton είναι απλή. Διατηρεί μία στατική αναφορά στο μοναδικό του στιγμιότυπο ενώ ο μόνος τρόπος πρόσβασης σε αυτή την αναφορά είναι μέσω μίας στατικής μεθόδου η οποία είναι υπεύθυνη και για την δημιουργία του στιγμιότυπου του.
Το κλασικό singleton
public class ClassicSingleton {
private static ClassicSingleton instance = null; //Η στατική μεταβλητή αναφοράς στην κλάση
protected ClassicSingleton()
{
//O constructor δεν είναι δημόσιος και είναι κενός
}
//επιστρέφει την αναφορά στο στιμιότυπο.
public static ClassicSingleton getInstance() {
//εάν η μοναδική αναφορά δείχνει στο κενό
//δηλαδή δεν υπάρχει το στιγμιότυπο
if(instance == null)
{
//τοτε καλούμε τον protected constructor
instance = new ClassicSingleton();
}
// και σε κάθε περίπτωση γυρνάμε την αναφορά
// του μοναδικού μας στιγμιότυπου
return instance;
}
}
Πέραν της εξασφάλισης της μοναδικότητας του στιγμιότυπου μιας Singleton κλάσης ένα άλλο πλεονέκτημα που μας δίνει είναι η εξασφάλιση ότι το στιγμιότυπο της θα δημιουργηθεί μόνο όταν και αν το χρειαστούμε, δηλαδή εξοικονομούμε system resources.
Το κλασικό singleton
public class ClassicSingleton {
private static ClassicSingleton instance = null; //Η στατική μεταβλητή αναφοράς στην κλάση
protected ClassicSingleton()
{
//O constructor δεν είναι δημόσιος και είναι κενός
}
//επιστρέφει την αναφορά στο στιμιότυπο.
public static ClassicSingleton getInstance() {
//εάν η μοναδική αναφορά δείχνει στο κενό
//δηλαδή δεν υπάρχει το στιγμιότυπο
if(instance == null)
{
//τοτε καλούμε τον protected constructor
instance = new ClassicSingleton();
}
// και σε κάθε περίπτωση γυρνάμε την αναφορά
// του μοναδικού μας στιγμιότυπου
return instance;
}
}
Πέραν της εξασφάλισης της μοναδικότητας του στιγμιότυπου μιας Singleton κλάσης ένα άλλο πλεονέκτημα που μας δίνει είναι η εξασφάλιση ότι το στιγμιότυπο της θα δημιουργηθεί μόνο όταν και αν το χρειαστούμε, δηλαδή εξοικονομούμε system resources.