User Tools

Site Tools


java:objects

This is an old revision of the document!


Δημιουργία Αντικειμένων

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

Η δημιουργία αντικειμένων γίνεται με χρήση του τελεστή new. Για παράδειγμα, για να δημιουργήσουμε ένα αντικείμενο της κλάσης Point αρκεί να γράψουμε

  /* δημιουργεί ένα αντικείμενο τύπου Point 
   * με συντεταγμένες 3,5 και το αναθέτει στη
   * μεταβλητή p που είναι τύπου Point.
   */  
  Point p;               // (a)
  p = new Point(3,5);    // (b)

Μπορείτε να σκεφτείτε τη μεταβλητή p ως ένα δείκτη σε αντικείμενα τύπου Point. Αρχικά ο δείκτης είναι μη αρχικοποιημένος δείχνοντας στην τιμή null. Στην επόμενη γραμμή καλείται ο κατασκευαστής ο οποίος δημιουργεί ένα αντικείμενο τύπου Point με συντεταγμένες 3,5 και αναθέτει το αντικείμενο αυτό στη μεταβλητή p. Τα παρακάτω σχήματα απεικονίζουν εποπτικά την διαδικασία.

Ένα πιο εκτεταμένο παράδειγμα

Παρακάτω δίνεται η κλάση CreateObjectDemo που δημιουργεί συγκεκριμένα αντικείμενα του τύπου Point και Rectangle και εκτυπώνει τα αποτελέσματα στην κονσόλα. Οι κλάσεις Point και Rectangle δίνονται επίσης παρακάτω:

Point.java
class Point {
  int x;   // x coordinate
  int y;   // y coordinate
 
  public Point(int xPos, int yPos) {
    x = xPos;
    y = yPos;
  }
 
  int getX() {
    return x;
  }
 
  void setX(int xPos) {
    x = xPos;
  }
 
  int getY() {
    return y;
  }
 
  void setY(int yPos) {
    y = yPos;
  }
}
Rectangle.java
class Rectangle {
 
  int width, height;
  Point origin;
 
  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);
  }
 
  void setWidth(int newWidth ) {
    width = newWidth;
  }
 
  int getWidth() {
    return width;
  }
 
  void setHeight(int newHeight ) {
    height = newHeight;
  }
 
  int getHeight() {
    return height;
  }
 
  void setOrigin(Point newOrigin) {
    origin = newOrigin;
  }
 
  Point getOrigin() {
    return origin;
  }
 
  int getArea() {
       return width * height;
  }
 
  // Move rectangle origin by dx,dy
  void moveOrigin(int dx, int dy) {
    origin.setX( origin.getX() + dx );
    origin.setY( origin.getY() + dy );
  }
}
CreateObjectDemo.java
public class CreateObjectDemo {
 
  public static void main(String[] args) {
 
    // Declare variables
    Point origin1, origin2;
    Rectangle rect1, rect2;
    // Create objects
    origin1 = new Point(23, 94);
    origin2 = new Point(15, -33);
    rect1 = new Rectangle(100, 200, origin1);
    rect2 = new Rectangle(50, 100, origin2);
 
    // display rectOne's width, height, and area
    System.out.println("[rect1]  xPos: " + rect1.origin.x + ", yPos: " + rect1.origin.y);
    System.out.println("[rect2]  xPos: " + rect2.origin.x + ", yPos: " + rect2.origin.y);
 
    // set rectTwo's position
    rect2.setOrigin(origin1);
    // display rectTwo's position
    System.out.println("[rect2]  xPos: " + rect2.origin.x + ", yPos: " + rect2.origin.y);
 
    // move rectTwo and display its new position
    rect2.moveOrigin(40, -20);
    System.out.println("[rect2]  xPos: " + rect2.origin.x + ", yPos: " + rect2.origin.y);
 
    // display rectOne's position
    System.out.println("[rect1]  xPos: " + rect1.origin.x + ", yPos: " + rect1.origin.y);
 
    // assign originOne value to originTwo
    origin2 = origin1;
  }
}

Αποθηκεύστε και τα τρία αρχεία στον ίδιο κατάλογο. Εάν δεν φτιάξετε ένα project στο NetBeans, για να μεταγλωττίσετε το παραπάνω πρόγραμμα αρκεί να γράψετε:

javac Point.java              // μεταγλώττιση της κλάσης Point
javac Rectange.java           // μεταγλώττιση της κλάσης Rectangle
javac CreateObjectDemo.java   // μεταγλώττιση της κλάσης CreateObjectDemo

Για να το τρέξετε γράφετε

java CreateObjectDemo

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

[rect1]  xPos: 23, yPos: 94
[rect2]  xPos: 15, yPos: -33
[rect2]  xPos: 23, yPos: 94
[rect2]  xPos: 63, yPos: 74
[rect1]  xPos: 63, yPos: 74 

Επεξήγηση του παραπάνω κώδικα

Στο παραπάνω πρόγραμμα ορίζονται στη μέθοδο main τα εξής

    // Declare variables
    Point origin1, origin2;
    Rectangle rect1, rect2;
    // Create objects
    origin1 = new Point(23, 94);
    origin2 = new Point(15, -33);
    rect1 = new Rectangle(100, 200, origin1);
    rect2 = new Rectangle(50, 100, origin2);

Οι πρώτες δύο γραμμές ορίζουν τις μεταβλητές origin1, origin2,rect1 και rect2. Οι μεταβλητές δυνητικά δείχνουν σε τύπους δεδομένων Point και Rectangle. Προς το παρόν όμως το περιεχόμενο των μεταβλητών αυτών είναι απροσδιόριστο (στην πραγματικότητα ο compiler αρχικοποιεί τις μεταβλητές αυτές στην τιμή null).

Σε αναλογία με την γλώσσα C, φανταστείτε τις μεταβλητές αυτές ως pointers που δεν είναι αρχικοποιημένοι σε κάποια υφιστάμενη διεύθυνση μνήμης. Η τιμή τους είναι απροσδιόριστη και η προσπάθεια να γράψουμε στη διεύθυνση μνήμης όπου δείχνουν θα προκαλέσει τερματισμό του προγράμματος από το λειτουργικό σύστημα.

Η παρακάτω εικόνα δείχνει την ύπαρξη των τεσσάρων μη αρχικοποιημένων δεικτών. Όλοι αρχικά δείχνουν στην τιμή null.

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

Η παρακάτω εικόνα δείχνει τις μεταβλητές origin1, origin2, rect1, rect2 μετά την αρχικοποίηση τους από τους αντίστοιχους κατασκευαστές.

Στη συνέχεια ακολουθούν οι εξής γραμμές κώδικα:

    // set rect2's position
    rect2.setOrigin(origin1);    
    // display rect2's position
    System.out.println("[rect2]  xPos: " + rect2.getOrigin().getX() + ", yPos: " + rect2.getOrigin().getY());
 
    // move rect2 and display its new position
    rect2.moveOrigin(40, -20);
    System.out.println("[rect2]  xPos: " + rect2.getOrigin().getX() + ", yPos: " + rect2.getOrigin().getY());
 
    // display rect1's position
    System.out.println("[rect1]  xPos: " + rect1.getOrigin().getX()+", yPos: " + rect1.getOrigin().getY());

Στις γραμμές αυτές συμβαίνουν τα εξής:

  1. Το αντικείμενο rect2 επιλέγει ως πεδίο origin to origin1. Στη συνέχεια, μέσω του rect2 μεταβάλλονται οι συντεταγμένες του αντικειμένου origin1.
  2. Εκτυπώνονται οι αλλαγές για το rect2 στην κονσόλα.
  3. Εκτυπώνονται οι αλλαγές για το rect1 στην κονσόλα. Παρατηρούμε ότι οι συντεταγμένες του πεδίου origin άλλαξαν και για το αντικείμενο rect1.

Tα παρακάτω δύο σχήματα αποτυπώνουν α) την αλλαγή του πεδίου origin του rect2, ώστε να δείχνει στο αντικείμενο rect1 και β) την αλλαγή των περιεχομένων του origin1 μέσω του αντικειμένου rect2. Οι αλλαγές αυτές επηρεάζουν και το πεδίο origin του rect1 που δείχνει στο κοινό αντικείμενο origineOne.

Παρατηρείστε ότι πλέον μόνο η μεταβλητή origin2 δείχνει στο αντικείμενο τύπου Point με συντεταγμένες 15, -33.

(a) (b)

Τέλος το πρόγραμμα τελειώνει με την γραμμή κώδικα:

    // assign origin1 value to origin2
    origin2 = origin1;

Μετά την γραμμή αυτή η μεταβλητή origin2 δείχνει στο αντικείμενο που δείχνει και η μεταβλητή origin1. Πλέον δεν υπάρχει καμία μεταβλητή ή αναφορά που να δείχνει στο αντικείμενο τύπου Point με συντεταγμένες 15, -33. Το αντικείμενο αυτό θα διαγραφεί αυτόματα από την λειτουργία Garbage Collection του JVM.

Επεξήγηση της χρήσης του τελεστή new

Προκειμένου να δημιουργηθούν νέα αντικείμενα χρησιμοποιείται ο τελεστής new. O τελεστής new χρησιμοποιείται συνήθως με τον κατασκευαστή μίας κλάσης προκειμένου να κάνει τα εξής:

  1. Δέσμευση της απαραίτητης μνήμης και δημιουργία του αντικειμένου. Η αρχικά ορισμένη μεταβλητή δείχνει πλέον στην περιοχή μνήμης που έχει δεσμευτεί.
  2. Αρχικοποίηση των εσωτερικών μεταβλητών (πεδίων) του αντικειμένου με κλήση του κατάλληλου κατασκευαστή της κλάσης. Εάν δεν έχει οριστεί κατασκευαστής τότε ο τελεστής 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 και τις εναλλακτικές υλοποιήσεις μπορείτε να βρείτε εδώ.

java/objects.1582280012.txt.gz · Last modified: 2020/02/21 10:13 by gthanos