java:objects

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

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

Η δημιουργία αντικειμένων γίνεται με χρήση του τελεστή 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
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;
    }
}
Rectangle.java
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 );
  }
}
CreateObjectDemo.java
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.

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

Η παρακάτω εικόνα δείχνει τις μεταβλητές originOne, originTwo, rectOne, rectTwo μετά την αρχικοποίηση τους από τους αντίστοιχους κατασκευαστές.

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

    // 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());

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

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

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

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

(a) (b)

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

    // assign originOne value to originTwo
    originTwo = originOne;

Μετά την γραμμή αυτή η μεταβλητή originTwo δείχνει στο αντικείμενο που δείχνει και η μεταβλητή originOne. Πλέον δεν υπάρχει καμία μεταβλητή ή αναφορά που να δείχνει στο αντικείμενο τύπου 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.txt · Last modified: 2018/02/16 16:03 by gthanos