====== Δημιουργία Αντικειμένων ====== Μέχρι τώρα αναφέραμε στην [[ oop:introduction | "Εισαγωγή στον Αντικειμενοστραφή Προγραμματισμό" ]] ότι η κλάση είναι το βασικό σχέδιο μέσα από το οποίο δημιουργούνται επιμέρους αντικείμενα που φέρουν τα χαρακτηριστικά της κλάσης. Επίσης, δείξαμε πως ορίζουμε μία κλάση μέσα από παραδείγματα, αλλά δεν δείξαμε πως δημιουργούμε αντικείμενα από τις κλάσεις που ορίσαμε. Η δημιουργία αντικειμένων γίνεται με χρήση του τελεστή **new**. Για παράδειγμα, για να δημιουργήσουμε ένα αντικείμενο της κλάσης **Point** αρκεί να γράψουμε /* δημιουργεί ένα αντικείμενο τύπου Point * με συντεταγμένες 3,5 και το αναθέτει στη * μεταβλητή p που είναι τύπου Point. */ Point p; // (a) p = new Point(3,5); // (b) Μπορείτε να σκεφτείτε τη μεταβλητή **p** ως ένα δείκτη σε αντικείμενα τύπου **Point**. Αρχικά ο δείκτης είναι μη αρχικοποιημένος δείχνοντας στην τιμή **null**. Στην επόμενη γραμμή καλείται ο κατασκευαστής ο οποίος δημιουργεί ένα αντικείμενο τύπου **Point** με συντεταγμένες 3,5 και αναθέτει το αντικείμενο αυτό στη μεταβλητή **p**. Τα παρακάτω σχήματα απεικονίζουν εποπτικά την διαδικασία. {{ java:createobject.png }} ===== Ένα πιο εκτεταμένο παράδειγμα ===== Παρακάτω δίνεται η κλάση **CreateObjectDemo** που δημιουργεί συγκεκριμένα αντικείμενα του τύπου **Point** και **Rectangle** και εκτυπώνει τα αποτελέσματα στην κονσόλα. Οι κλάσεις **Point** και **Rectangle** δίνονται επίσης παρακάτω: public class Point { private int x; private int y; public Point(int xPos, int yPos) { x = xPos; y = yPos; } public int getX() { return x; } public void setX(int xPos) { x = xPos; } public int getY() { return y; } public void setY(int yPos) { y = yPos; } } public class Rectangle { // fields private int width; private int height; private Point origin; // constructors public Rectangle(int initWidth, int initHeight, Point initOrigin) { width = initWidth; height = initHeight; origin = initOrigin; } public Rectangle(int initWidth, int initHeight, int originX, int originY) { width = initWidth; height = initHeight; origin = new Point(originX,originY); } // methods public void setWidth(int newWidth ) { width = newWidth; } public int getWidth() { return width; } public void setHeight(int newHeight ) { height = newHeight; } public int getHeight() { return height; } public void setOrigin(Point newOrigin) { origin = newOrigin; } public Point getOrigin() { return origin; } public int getArea() { return width * height; } // Move rectangle origin by dx,dy public void moveOrigin(int dx, int dy) { origin.setX( origin.getX() + dx ); origin.setY( origin.getY() + dy ); } } public class CreateObjectDemo { public static void main(String[] args) { // Declare variables Point originOne, originTwo; Rectangle rectOne, rectTwo; // Create objects originOne = new Point(23, 94); originTwo = new Point(15, -33); rectOne = new Rectangle(100, 200, originOne); rectTwo = new Rectangle(50, 100, originTwo); // display rectOne's width, height, and area System.out.println("[rectOne] xPos: " + rectOne.getOrigin().getX() + ", yPos: " + rectOne.getOrigin().getY()); System.out.println("[rectOne] width: " + rectOne.getWidth() + ", height: " + rectOne.getHeight()); // set rectTwo's position rectTwo.setOrigin(originOne); // display rectTwo's position System.out.println("[rectTwo] xPos: " + rectTwo.getOrigin().getX() + ", yPos: " + rectTwo.getOrigin().getY()); // move rectTwo and display its new position rectTwo.moveOrigin(40, -20); System.out.println("[rectTwo] xPos: " + rectTwo.getOrigin().getX() + ", yPos: " + rectTwo.getOrigin().getY()); // display rectOne's position System.out.println("[rectOne] xPos: " + rectOne.getOrigin().getX()+", yPos: " + rectOne.getOrigin().getY()); // assign originOne value to originTwo originTwo = originOne; } } Αποθηκεύστε και τα τρία αρχεία στον ίδιο κατάλογο. Για να μεταγλωττίσετε τα παραπάνω πρόγραμμα αρκεί να γράψετε javac Point.java // μεταγλώττιση της κλάσης Point javac Rectange.java // μεταγλώττιση της κλάσης Rectangle javac CreateObjectDemo.java // μεταγλώττιση της κλάσης CreateObjectDemo Για να το τρέξετε γράφετε java CreateObjectDemo Το παραπάνω πρόγραμμα τυπώνει τα εξής στην κονσόλα. [rectOne] xPos: 23, yPos: 94 [rectOne] width: 100, height: 200 [rectTwo] xPos: 23, yPos: 94 [rectTwo] xPos: 63, yPos: 74 [rectOne] xPos: 63, yPos: 74 ==== Επεξήγηση του παραπάνω κώδικα ===== Στο παραπάνω πρόγραμμα ορίζονται στη μέθοδο ''main'' τα εξής // Declare variables Point originOne, originTwo; Rectangle rectOne, rectTwo; // Create objects originOne = new Point(23, 94); originTwo = new Point(15, -33); rectOne = new Rectangle(100, 200, originOne); rectTwo = new Rectangle(50, 100, originTwo); Οι πρώτες δύο γραμμές ορίζουν τις μεταβλητές ''originOne'', ''originTwo'',''rectOne'' και ''rectTwo''. Οι μεταβλητές δυνητικά δείχνουν σε τύπους δεδομένων **Point** και **Rectangle**. Προς το παρόν όμως το περιεχόμενο των μεταβλητών αυτών είναι απροσδιόριστο (στην πραγματικότητα ο compiler αρχικοποιεί τις μεταβλητές αυτές στην τιμή **null**). __Σε αναλογία με την γλώσσα C__, φανταστείτε τις μεταβλητές αυτές ως pointers που δεν είναι αρχικοποιημένοι σε κάποια υφιστάμενη διεύθυνση μνήμης. Η τιμή τους είναι απροσδιόριστη και η προσπάθεια να γράψουμε στη διεύθυνση μνήμης όπου δείχνουν θα προκαλέσει τερματισμό του προγράμματος από το λειτουργικό σύστημα. Η παρακάτω εικόνα δείχνει την ύπαρξη των τεσσάρων μη αρχικοποιημένων δεικτών. Όλοι αρχικά δείχνουν στην τιμή **null**. {{:java:createobjectdemoreference1.png|}} Αμέσως μετά τις δηλώσεις των μεταβλητών ακολουθούν οι κλήσεις των κατασκευαστών των αντίστοιχων κλάσεων. Όπως προαναφέραμε, ο κατασκευαστής όταν καλείται δεσμεύει την απαραίτητη μνήμη για το αντικείμενο, αρχικοποιεί τα πεδία του αντικειμένου και επιστρέφει μία αναφορά προς το αντικείμενο που δημιούργησε. Η παρακάτω εικόνα δείχνει τις μεταβλητές ''originOne'', ''originTwo'', ''rectOne'', ''rectTwo'' μετά την αρχικοποίηση τους από τους αντίστοιχους κατασκευαστές. {{:java:createobjectdemoreference2.png|}} Στη συνέχεια ακολουθούν οι εξής γραμμές κώδικα: // set rectTwo's position rectTwo.setOrigin(originOne); // display rectTwo's position System.out.println("[rectTwo] xPos: " + rectTwo.getOrigin().getX() + ", yPos: " + rectTwo.getOrigin().getY()); // move rectTwo and display its new position rectTwo.moveOrigin(40, -20); System.out.println("[rectTwo] xPos: " + rectTwo.getOrigin().getX() + ", yPos: " + rectTwo.getOrigin().getY()); // display rectOne's position System.out.println("[rectOne] xPos: " + rectOne.getOrigin().getX()+", yPos: " + rectOne.getOrigin().getY()); Στις γραμμές αυτές συμβαίνουν τα εξής: - Το αντικείμενο ''rectTwo'' επιλέγει ως πεδίο origin to ''originOne''. Στη συνέχεια, μέσω του ''rectTwo'' μεταβάλλονται οι συντεταγμένες του αντικειμένου ''originOne''. - Εκτυπώνονται οι αλλαγές για το ''rectTwo'' στην κονσόλα. - Εκτυπώνονται οι αλλαγές για το ''rectOne'' στην κονσόλα. Παρατηρούμε ότι οι συντεταγμένες του πεδίου **origin** άλλαξαν και για το αντικείμενο ''rectOne''. Tα παρακάτω δύο σχήματα αποτυπώνουν α) την αλλαγή του πεδίου **origin** του ''rectTwo'', ώστε να δείχνει στο αντικείμενο ''rectOne'' και β) την αλλαγή των περιεχομένων του ''originOne'' μέσω του αντικειμένου ''rectTwo''. Οι αλλαγές αυτές επηρεάζουν και το πεδίο **origin** του ''rectOne'' που δείχνει στο κοινό αντικείμενο ''origineOne''. Παρατηρείστε ότι πλέον μόνο η μεταβλητή ''originTwo'' δείχνει στο αντικείμενο τύπου **Point** με συντεταγμένες 15, -33. | **(a)** | | **(b)** | | {{:java:createobjectdemoreference3.png|}} | |{{:java:createobjectdemoreference4.png|}} | Τέλος το πρόγραμμα τελειώνει με την γραμμή κώδικα: // assign originOne value to originTwo originTwo = originOne; Μετά την γραμμή αυτή η μεταβλητή ''originTwo'' δείχνει στο αντικείμενο που δείχνει και η μεταβλητή ''originOne''. Πλέον δεν υπάρχει καμία μεταβλητή ή αναφορά που να δείχνει στο αντικείμενο τύπου **Point** με συντεταγμένες 15, -33. Το αντικείμενο αυτό θα διαγραφεί αυτόματα από την λειτουργία **[[#garbage_collection|Garbage Collection]]** του JVM. {{:java:createobjectdemoreference5.png|}} ===== Επεξήγηση της χρήσης του τελεστή new ===== Προκειμένου να δημιουργηθούν νέα αντικείμενα χρησιμοποιείται ο τελεστής **new**. O τελεστής **new** χρησιμοποιείται συνήθως με τον κατασκευαστή μίας κλάσης προκειμένου να κάνει τα εξής: - Δέσμευση της απαραίτητης μνήμης και δημιουργία του αντικειμένου. Η αρχικά ορισμένη μεταβλητή δείχνει πλέον στην περιοχή μνήμης που έχει δεσμευτεί. - Αρχικοποίηση των εσωτερικών μεταβλητών (πεδίων) του αντικειμένου με κλήση του κατάλληλου κατασκευαστή της κλάσης. Εάν δεν έχει οριστεί κατασκευαστής τότε ο τελεστής **new** καλείται με χρήση του default κατασκευαστή (default constructor) που δεν έχει ορίσματα (π.χ. ''MyObject obj = new MyObject();'', όπου για την κλάση ''MyObject'' δεν έχει οριστεί κανένας κατασκευατής). Κατά την χρήση **primitive** τύπων δεδομένων σε ένα πρόγραμμα (int, float, double) δεν απαιτείται η χρήση του τελεστή **new**. Ο λόγος που συμβαίνει αυτό είναι ότι για τους συγκεκριμένους τύπους δεδομένων, η διαδικασία δέσμευσης της μνήμης είναι στατική, καθώς είναι γνωστό εκ των προτέρων το εύρος μνήμης που απαιτούν. Οι **primitive** τύποι δεδομένων αποθηκεύονται πάντοτε στο **stack** της μεθόδου μέσα στην οποία δηλώνονται. Αντίθετα, για τα αντικείμενα των κλάσεων ο compiler δεν μπορεί να γνωρίζει εκ των προτέρων το μέγεθος τους, καθώς με την σειρά τους αυτά μπορεί να περιέχουν άλλα αντικείμενα, τα οποία με την σειρά τους μπορεί να περιέχουν άλλα αντικείμενα κ.ο.κ. Για τον λόγο αυτό, για τα αντικείμενα δεσμεύεται πάντοτε η απαραίτητη μνήμη δυναμικά, στην περιοχή δυναμικής δέσμευσης μνήμης που παραδοσιακά ονομάζεται **heap**. ===== Πολλαπλοί κατασκευαστές σε μία κλάση ===== Μία κλάση μπορεί να έχει πολλούς διαφορετικούς κατασκευαστές. Κάθε κατασκευαστής ορίζει μία διαφορετική αρχικοποίηση των εσωτερικών μεταβλητών των αντικειμένων που δημιουργούνται με βάση το "σχέδιο" της κλάσης. Το ποιoς κατασκευαστής θα κληθεί εξαρτάται από τον τύπο, τη σειρά και τον αριθμό των ορισμάτων σε αναλογία με την υπερφόρτωση μεθόδων. Ας υποθέσουμε την παρακάτω μέθοδο **main**, στην οποία καλούνται δύο διαφορετικοί κατασκευαστές για δύο διαφορετικά αντικείμενα της ίδιας κλάσης **Rectangle**. Παρατηρήστε ότι η μεταβλητή **rectOne** αρχικοποιείται από την 1ο κατασκευαστή της κλάσης και η μεταβλητή **rectTwo** από τον 2ο κατασκευαστή. public class CreateRectangleObjects { public static void main(String []args) { Point originOne = new Point(10,5); rectOne = new Rectangle(100, 200, originOne); rectTwo = new Rectangle(50, 100, -40, 80); } } ===== Χρήση Αντικειμένων ===== Όταν φτιάξετε ένα αντικείμενο είναι σίγουρο ότι θα θέλετε να το χρησιμοποιήσετε προκειμένου να κάνετε μία εργασία όπως να γράψετε κάτι στα δεδομένα του, να διαβάσετε από αυτά ή να χρησιμοποιήσετε κάποια από τις μεθόδους του. ==== Χρήση των πεδίων ενός αντικειμένου ==== Τα πεδία ενός αντικειμένου είναι προσβάσιμα με χρήση του ονόματος του αντικειμένου, μία τελεία '.' και το όνομα του πεδίου. Για παράδειγμα, // fields are width, height Rectangle rect = new Rectangle(10,20, -5, 22); System.out.println("Rectangle dimensions are " + rect.width + ", " + rect.height); Απαραίτητη προϋπόθεση για να δουλέψει ο παραπάνω κώδικας είναι τα πεδία width και height να είναι προσβάσιμα, δηλαδή να μην έχουν προσδιοριστή πρόσβασης τύπου **private**. Όπως προείπαμε μία καλή προγραμματιστική πρακτική είναι η απόκρυψη των πεδίων κάθε κλάσης και η δήλωση συναρτήσεων για την πρόσβαση στα δεδομένα της. Σε αυτή την περίπτωση η απευθείας πρόσβαση στα πεδία των αντικειμένων είναι μη επιτρεπτή (ο compiler δεν μεταγλωττίζει το πρόγραμμα). Η πρόσβαση σε μεταβλητές που έχουν τον προσδιοριστή **private** μπορεί να γίνει μόνο μέσω βοηθητικών συναρτήσεων (set/get) που έχουν τον προσδιοριστή **public**. ==== Χρήση των μεθόδων ενός αντικειμένου ==== Σε αναλογία με τα πεδία οι μέθοδοι ενός αντικειμένου είναι προσβάσιμες μέσω του ονόματος του αντικειμένου, μία τελεία '.' και το όνομα της μεθόδου. Για παράδειγμα, Rectangle rect = new Rectangle(10,20, -5, 22); System.out.println("Rectangle dimensions are " + rect.getWidth() + ", " + rect.getHeight() ); Ισχύουν και για τις μεθόδους όσα αναφέρονται για τους προσδιοριστές τύπου **public**, **private** των πεδίων. ===== Garbage Collection ===== Οι γλώσσες προγραμματισμού που μέχρι τώρα έχετε γνωρίσει (βλέπε **C**) αναθέτουν την ευθύνη δέσμευσης μνήμης στον προγραμματιστή μέσω των συναρτήσεων **malloc()** και **free()**. Σε αντιδιαστολή, η **JAVA** αφήνει τον προγραμματιστεί να ορίσει όσα αντικείμενα επιθυμεί και δεσμεύει την μνήμη για αυτά μέσω του τελεστή **new**. Πώς όμως αποδεσμεύεται η μνήμη που δεσμεύτηκε προηγούμενα από το πρόγραμμα μας, αλλά δεν την χρειαζόμαστε πλέον; Περιοδικά το **JVM** κοιτάει εάν υπάρχει δεσμευμένη μνήμη για αντικείμενα στα οποία δεν υφίστανται πλέον αναφορές/references που δείχνουν σε αυτά. Σε αυτές τις περιπτώσεις, ελευθερώνεται η μνήμη που έχει δεσμευτεί για τα αντικείμενα αυτής της κατηγορίας. Παράλληλα η αποδέσμευση της μνήμης και η διαγραφή των αντικειμένων πιθανόν συνεπάγεται ότι και άλλα αντικείμενα δεν διαθέτουν πια αναφορές προς αυτά κ.ο.κ. Η διαδικασία συνεχίζεται μέχρι να αποδεσμευτεί όλη η δυναμικά δεσμευμένη μνήμη. Ο μηχανισμός **Garbage Collection** απαντάται σε αρκετές γλώσσες υψηλού επιπέδου, απελευθερώνοντας τον προγραμματιστή από την ευθύνη αποδέσμευσης της μνήμης που δεσμεύτηκε προηγούμενα. Η ευθύνη της δεύσμευσης/αποδέσμευσης μνήμης δεν επαφίεται στον προγραμματιστή, πράγμα που κάνει λιγότερο επίπονο τον προγραμματισμό. Περισσότερα για την λειτουργία του Garbage Collector και τις εναλλακτικές υλοποιήσεις μπορείτε να βρείτε [[http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html|εδώ]]. |Προηγούμενο: [[:java:class_constructors | Κατασκευαστές της κλάσης ]] | [[:toc | Περιεχόμενα ]] | Επόμενο: [[:java:arrays | Πίνακες ]]|