java:new_interfaces

Διεπαφές (Interfaces)

Εισαγωγικά

Κατά την ανάπτυξη προγραμμάτων είναι σημαντικό να είναι τυποποιημένος ο τρόπος αλληλεπίδρασης ανάμεσα σε διαφορετικά συστήματα. Η ύπαρξη ενός “συμβολαίου” το οποίο καθορίζει ακριβώς το πώς μπορεί μια εφαρμογή να αλληλεπιδράσει με μια άλλη διευκολύνει την ανεξάρτητη ανάπτυξη κώδικα από διαφορετικές ομάδες.

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

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

Για παράδειγμα, θεωρήστε πως όλοι οι κατασκευαστές οικιακών συσκευών οι οποίες χρησιμοποιούν χρονόμετρα εγγυούνται ότι θα τηρήσουν ένα συμβόλαιο αλληλεπίδρασης με το χρονόμετρο σύμφωνα με το οποίο οι συσκευές τους θα υλοποιούν τις μεθόδους χρονομέτρου setTimer, startTimer και endTimer. Ένας κατασκευαστής κλιματιστικών που θέλει να χρησιμοποιήσει χρονόμετρο για να μπορεί να σταματήσει η λειτουργία της συσκευής μετά από ένα χρονικό διάστημα υλοποιεί αυτές τις τρεις μεθόδους. Ένας κατασκευαστής κουζινών που θέλει να χρησιμοποιήσει χρονόμετρο ώστε μια κουζίνα να παράγει ένα ήχο μετά από ένα χρονικό διάστημα επίσης υλοποιεί αυτές τις μεθόδους.

Οι υλοποιήσεις για τις κουζίνες και τα κλιματιστικά είναι εντελώς διαφορετικές και γίνονται ανεξάρτητα η μία από την άλλη. Η εγγύηση είναι μόνο ότι οι μέθοδοι έχουν υλοποιηθεί και παρέχουν ασφαλή και ομοιόμορφο τρόπο αλληλεπίδρασης με τα χρονόμετρα των συσκευών.

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

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

Τα Java Interfaces

Στη Java ένα “συμβόλαιο” όπως περιγράφεται πιο πάνω λέγεται interface (διεπαφή).

Υπακούοντας στην παραπάνω αρχή, η Java εισάγει την έννοια της διεπαφής (interface). Τα interfaces στη Java λειτουργούν ως προ-συμφωνημένες διεπαφές μεταξύ προγραμματιστών. Κάθε Interface έχει κατά κανόνα δύο μέρη

  1. τους προγραμματιστές που το υλοποιούν και
  2. τους προγραμματιστές που το χρησιμοποιούν.

Οι δεύτεροι εάν θέλουν όμως να επωφεληθούν από την λειτουργικότητα που υποδεικνύει ένα interface θα πρέπει να χρησιμοποιούν τις μεθόδους του στα προγράμματά τους και να επιλέγουν προς χρήση κλάσεις που υλοποιούν το συγκεκριμένο interface. Με αυτόν τον τρόπο προσπαθούμε να γράψουμε κώδικα που είναι αρκετά γενικός, δηλαδή δεν στηρίζεται απαραίτητα σε συγκεκριμένες κλάσεις, αλλά σε ομάδες κλάσεων που έχουν ως κοινό χαρακτηριστικό την υλοποίηση του συγκεκριμένου interface.

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

Από τα παραπάνω εξάγεται ότι το interface είναι μία προσυμφωνημένη διεπαφή η οποία δεσμεύει και τις δύο πλευρές, δηλαδή οι χρήστες του δεσμεύονται να χρησιμοποιούν τις μεθόδους του interface και οι προγραμματιστές που το υλοποιούν να το υλοποιούν στις κλάσεις τους. Παρακάτω θα δούμε την υποστήριξη των interfaces από τον compiler της Java και τους κανόνες που τα διέπουν.

Ορίζοντας ένα Interface

Παρακάτω δίνεται το παράδειγμα ορισμού ενός Interface.

public interface ApplianceTimer{
 
    // constant declarations    
    int TIMEOUT = 20000;
 
    // method signatures
    public int setTimer(int t);
    public int startTimer(int t);
    public int stopTimer();
}

Τόσο στις κλάσεις όσο και στα interfaces ορίζεται η ιδιότητα της κληρονομικότητας. Η διαφορά είναι ότι ενώ στην κλάση μπορούμε να έχουμε μόνο μία γονική κλάση, στα interfaces μπορούμε να έχουμε περισσότερα του ενός γονικά interfaces.

Το σώμα του interface

Ένα interface μπορεί να περιέχει πεδία όπως στο παραπάνω παράδειγμα.

    int TIMEOUT = 20000;

Τα πεδία αυτά εξ ορισμού (by default) public, static, final, δηλ είναι σταθερές που ανήκουν στις κλάσεις που θα υλοποιήσουν το interface στο οποίο δηλώνονται.

Οι μέθοδοι σε ένα interface μπορούν να ανήκουν στις παρακάτω 3 κατηγορίες a) abstract methods, b) default methods, static methods. Θα εξηγήσουμε κάθε μία από τις 3 κατηγορίες παρακάτω. Όλες οι μέθοδοι σε ένα interface είναι εξ ορισμού public, κατά συνέπεια ο προσδιοριστής public μπορεί να παραληφθεί.

Υλοποιώντας ένα Interface

Μια κλασική χρήση interfaces είναι για το χαρακτηρισμό αντικειμένων τα οποία μπορούν να συγκριθούν μεταξύ τους:

Η μέθοδος Arrays.Sort μπορεί να ταξινομήσει τα περιεχόμενα ενός πίνακα που αποτελείται από οποιοδήποτε είδος αντικειμένων αλλά μόνο υπό την προϋπόθεση ότι η κλάση που αναπαριστά αυτά τα αντικείμενα υλοποιεί το interface Comparable:

public interface Comparable
{
     int compareTo(Object other);
}

Μια κλάση που υλοποιεί το Comparable πρέπει να υλοποιήσει τη μέθοδο compareTo κι εφόσον κάνει αυτό (τηρήσει το “συμβόλαιο”) η μέθοδος Arrays.Sort μπορεί να αλληλεπιδράσει με τα αντικείμενα που θέλουμε να ταξινομήσουμε ώστε να μας παρέχει την υπηρεσία ταξινόμησης. Ας δούμε ένα παράδειγμα πάνω στην κλάση Rectangle που έχουμε ορίσει σε προηγούμενες ενότητες. Σύμφωνα με το documentation του interface Comparable η compareTo πρέπει να επιστρέφει αριθμό αρνητικό, μηδέν ή θετικό αν το τρέχον αντικείμενο (this) θεωρείται “μικρότερο”, “ίσο” ή “μεγαλύτερο” από αυτό με το οποίο το συγκρίνουμε. Το πώς ορίζεται το “μικρότερο”, κτλ. για αντικείμενα Rectangle είναι απόφαση του προγραμματιστή που υλοποιεί την κλάση Rectangle. Αποφασίζουμε να συγκρίνουμε τα αντικείμενα με βάση το εμβαδό τους.

Rectangle.java
import java.util.Arrays;
 
public class Rectangle implements Comparable {
    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.setX(x);
        origin.setY(y);
    }
 
    // a method for computing
    // the area of the rectangle
    public int getArea() {
        return width * height;
    }
 
    // a method required to implement
    // the Comparable interface
    @Override
    public int compareTo(Comparable other) {
        Rectangle otherRect = (Rectangle)other;
        return this.getArea() - otherRect.getArea();             
    }
 
    public static void main(String args[]) {
      Rectangle [] shapes = new Rectangle[4];                                     
      shapes[0] = new Rectangle(4, 5);                                            
      shapes[1] = new Rectangle(1, 8);                                            
      shapes[2] = new Rectangle(2, 15);                                           
      shapes[3] = new Rectangle(9, 2);                                            
 
      System.out.println("Initial array:");                                       
      for (Rectangle rectangle : shapes) {                                        
          System.out.println(rectangle + ", area: " + rectangle.getArea());       
      }                                                                           
 
      Arrays.sort(shapes);                                                        
 
      System.out.println("Sorted array:");                                        
      for (Rectangle rectangle : shapes) {                                        
        System.out.println(rectangle + ", area: " + rectangle.getArea());       
      } 	
   }
}

Σημείωση: Αν παραλείψετε την γραμμή

        Rectangle otherRect = (Rectangle)other;

ο compiler δεν γνωρίζει ότι η μεταβλητή other ανήκει στην κλάση Rectangle και η προσπάθεια μεταγλώτισσης θα εμφανίσει λάθος (δοκιμάστε το!). Όταν μιλήσουμε για generics θα μεταβάλουμε ελαφρά τη χρήση του Comparable.

Για να εκτελέσετε το παράδειγμα κατεβάστε την κλάση Point, από εδώ.

Χρησιμοποιώντας ένα interface ως τύπο δεδομένων

Μπορείτε να χρησιμοποιήσετε ένα Java Interface ως ένα reference τύπο δεδομένων. Μπορείτε να χρησιμοποιήσετε το όνομα ενός interface ως τον τύπο μιας παραμέτρου σε μία Java μέθοδο ή τύπο μιας τοπικής μεταβλητής στο σώμα μίας μεθόδου. Προϋπόθεση είναι οι τιμές των μεταβλητών να δείχνουν σε αντικείμενα των οποίων οι κλάσεις υλοποιούν το συγκεκριμένο interface. Δείτε το παρακάτω παράδειγμα.

FinestComparator.java
public class FinestComparator {
 
    /* Παράδειγμα χρήσης interface ως τύπο τοπικής μεταβλητής στο σώμα μιας μεθόδου.
     */
    public Object findLargest(Object object1, Object object2) {
        StrictlyComparable obj1 = (StrictlyComparable)object1;
        StrictlyComparable obj2 = (StrictlyComparable)object2;
        if ((obj1).isLarger(obj2) > 0)
            return object1;
        else 
            return object2;
    }
 
    /* Παράδειγμα χρήσης interface ως τύπο τοπικής μεταβλητής στο σώμα μιας μεθόδου.
     */
    public Object findSmallest(Object object1, Object object2) {
        StrictlyComparable obj1 = (StrictlyComparable)object1;
        StrictlyComparable obj2 = (StrictlyComparable)object2;
        if ((obj1).isLarger(obj2) < 0)
            return object1;
        else 
            return object2;
    }
 
    /* η μέθοδος isEqual δίνεται εδώ με δύο διαφορετικές εκδοχές
     * οι οποίες είναι ισοδύναμες.
     */
    public boolean isEqual(Object object1, Object object2) {
        StrictlyComparable obj1 = (StrictlyComparable)object1;
        StrictlyComparable obj2 = (StrictlyComparable)object2;
        if ( (obj1).isLarger(obj2) == 0)
            return true;   
        else
            return false;
    }
 
    /* Παράδειγμα χρήσης interface ως τύπο τυπικής παραμέτρου σε μία μέθοδο.
     */
    public boolean isEqual(StrictlyComparable object1, StrictlyComparable object2) {
        if ( (object1).isLarger(object2) == 0)
            return true;   
        else
            return false;
    }
 
    public static void main(String args[]) {
        Point p = new Point(10,20);
        Rectangle rec1 = new Rectangle(p, 30, 40);
        Rectangle rec2 = new Rectangle(p, 30, 40);
        FinestComparator comp = new FinestComparator();
 
        System.out.println("rec1 is "+rec1.toString());
        System.out.println("rec2 is "+rec2.toString());
 
        if( !comp.isEqual(rec1,rec2) ) {
            System.out.println( comp.findLargest(rec1, rec2).toString()+"  is larger!");
            System.out.println( comp.findSmallest(rec1, rec2).toString()+"  is smaller!");
        } else {
            System.out.println("Objects are equal!");
        }
    }
}

Οι παραπάνω μέθοδοι δουλεύουν για οποιοδήποτε αντικείμενο η κλάση του υλοποιεί το παραπάνω interface, ώστε να μπορεί να κληθεί εσωτερικά η μέθοδος isLarger(). Παρατηρήστε τις δύο διαφορετικές αλλά ισοδύναμες εκδοχές της μεθόδου isEqual.

Μεταβάλλοντας ένα υφιστάμενο Interface

Κάποιες φορές στον προγραμματισμό εμφανίζεται η ανάγκη να μεταβάλλουμε ένα υφιστάμενο interface ή να το επεκτείνουμε. Το πρόβλημα σε αυτές τις περιπτώσεις είναι ότι οι κλάσεις που υλοποιούν το συγκεκριμένο interface με την προσθήκη ή αλλαγή των υφιστάμενων μεθόδων θα πάψουν να το υλοποιούν. Επομένως μία μονομερής αλλαγή του interface δεν είναι εφικτή. Παρόλα αυτά υπάρχουν δύο εναλλακτικές που μπορούν να μας καλύψουν.

1η εναλλακτική: Να υλοποιήσουμε ένα νέο interface με παραπλήσιο όνομα, το οποίο να αποτελέσει τη νέα έκδοση του υφιστάμενου interface.

public interface DoItPlus extends DoIt {
 
   boolean didItWork(int i, double x, String s);
 
}

2η εναλλακτική: Να υλοποιήσουμε μία ή περισσότερες default μέθοδους (όσες είναι και οι αλλαγές ή οι προσθήκες) μέσα στο υφιστάμενο interface, ώστε ακόμη και εάν οι κλάσεις που υλοποιούν το συγκεκριμένο interface δεν υλοποιούν τις νέες μεθόδους, οι υλοποιήσεις στις κλάσεις αυτές να γίνονται από το ίδιο το interface.

public interface DoIt {
 
   void doSomething(int i, double x);
   int doSomethingElse(String s);
   default boolean didItWork(int i, double x, String s) {
       // Method body 
   }  
}

Default μέθοδοι

Παρατήρηση: Οι default μέθοδοι στην Java υλοποιούνται στην έκδοση 8 της γλώσσας. Μεταγλωττιστές που ανήκουν σε προηγούμενες εκδόσεις αδυνατούν να μεταγλωττίσουν τον παρακάτω κώδικά.

Ας θεωρήσουμε το παρακάτω υποθετικό interface

StringInverter.java
    public interface StringInverter {
        /** inverts a string
         */
        String invert(String str);
    }

Ας υποθέσουμε τώρα ότι η κλάση MyString υλοποιεί το παραπάνω interface.

MyString.java
import java.lang.*;
public class MyString implements StringInverter {
    String invert(String str) {
        StringBuffer strb = new StringBuffer(str);
        return strb.reverse().toString();
    }
}

Αν υποθέσουμε ότι το αρχικό interface αλλάζει στο παρακάτω

StringInverter.java
    public interface StringInverter {
        /** inverts a string
         */
        String invert(String str);
        /** splits a string in half and inverts
         * the two parts.
         */
        String invertHalf(String str);
    }

Τώρα η κλάση MyString δεν υλοποιεί πλέον το αρχικό interface. Αυτό μπορεί να συνεχίσει να υλοποιείται εάν η νέα μέθοδος έχει μία default υλοποίηση μέσα στο interface όπως παρακάτω.

StringInverter.java
    public interface StringInverter {
        /* inverts a string
         */
        String invert(String str);
        /* splits a string in half and inverts
         * the two parts
         */
        default String invertHalf(String str) {
           int len = str.length();
           String str1 = str.substring(0, len/2);
           String str2 = str.substring(len/2, len);
           return str2+str1;
        }
    }

Στατικές μέθοδοι

Εκτός από default μεθόδους ένα interface μπορεί να περιέχει και στατικές (static) μεθόδους. Οι στατικές μέθοδοι ορίζονται σε αναλογία με τις στατικές μεθόδους των κλάσεων και ουσιαστικά ανήκουν στις κλάσεις που θα υλοποιήσουν το interface και όχι στα αντικείμενα. Ένα παράδειγμα μπορείτε να δείτε παρακάτω.

StringInverter.java
    public interface StringInverter {
        /** inverts a string
         */
        String invert(String str);
        /** splits a string in half and inverts
         * the two parts
         */
        default String invertHalf(String str) {
           int len = str.length();
           String str1 = str.substring(0, len/2);
           String str2 = str.substring(len/2, len);
           return str2+str1;
        }
 
        /** removes character at index from String str
         * and returns the new String.
         */
        static String removeChar(String str, int index) {
            if( str.length()-1 < index ) {
                return str;
            }
            String str1 = str.substring(0,index);
            String str2;
            if( str.length() >= index-1 ) {
                str2 = new String("");
            }
            else {
                str2 = str.substring(index+1);
            }
            return str1+str2;
 
        }
    }
java/new_interfaces.txt · Last modified: 2016/02/26 11:15 (external edit)