User Tools

Site Tools


java:interfaces

This is an old revision of the document!


Διεπαφές (Interfaces)

Εισαγωγικά

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

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

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

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

Ως παράδειγμα, θεωρήστε ένα interface το οποίο ορίζει τη χρήση ενός χρονόμετρου (Timer). Όσοι επιθυμούν να υλοποιήσουν το χρονόμετρο θα πρέπει κατ' ελάχιστο να υλοποιήσουν τις παρακάτω μεθόδους:

  • setTimer: αρχικοποίηση του timer
  • startTimer: έναρξη του timer και
  • endTimer: τερματισμός του timer

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

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

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

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

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

Η προτυποποίηση που παρέχει ο μηχανισμός του interface είναι αυτή που επιτρέπει:

  1. την εύκολη και χωρίς σφάλματα επαναχρησιμοποίηση κώδικα σε διαφορετικά προγράμματα και
  2. την αλλαγή της υφιστάμενης υλοποίησης μιας κλάσης που υλοποιεί ένα interface, δίχως οι χρήστες του interface να έχουν γνώση για τις αλλαγές αυτές.

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

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

public interface MyInterface extends Interface1, Interface2, Interface3 {
 
    // constant declarations    
    // base of natural logarithms
    double E = 2.718282;
    double PI = 3.14159;
 
    // method signatures
    public void interfaceMethod(int i, double x);
    public int interfaceMethod2(String s);
}
  • public ή κανένας προσδιοριστής πρόσβασης: Αν οριστεί public τότε το interface είναι ορατό από όλες τις κλάσεις και όλα τα πακέτα στην Java. Αν δεν οριστεί κάτι (package private) τότε το interface είναι ορατό μόνο μέσα στο πακέτο στο οποίο δηλώνεται.
  • interface: δεσμευμένη λέξη
  • το όνομα του interface: στο παράδειγμα μας MyInterface.
  • extends: ένα ή περισσότερα interfaces τα οποία επεκτείνει το συγκεκριμένο interface (comma seperated).

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

Ακριβώς όπως και οι κλάσεις κάθε interface πρέπει να βρίσκεται σε ξεχωριστό αρχείο, όπου το όνομα του αρχείου ταυτίζεται με το όνομα του interface και έχει κατάληξη .java. Για παράδειγμα το interface με όνομα MyInterface θα βρίσκεται σε ένα αρχείο με όνομα MyInterface.java.

Το σώμα του Interface

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

double E = 2.718282;
double PI = 3.14159;

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

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

public interface MyInterface extends Interface1, Interface2, Interface3 {
 
    // constant declarations    
    // base of natural logarithms
    double E = 2.718282;
    double PI = 3.14159;
 
    // method signatures
    public void interfaceMethod1(int i, double x);
    public int interfaceMethod2(String s);
}

Παράδειγμα interface - H Στοίβα (Stack)

Ας υποθέσουμε ότι θέλουμε να δηλώσουμε μέσω ενός interface της μεθόδους που πρέπει να υποστηρίζει μία στοίβα. Η στοίβα είναι μία δομή αποθήκευσης δεδομένων τύπου Last In First Out (LIFO), όπου το αντικείμενο αποθηκεύεται στη στοίβα τελευταίο εξάγεται από αυτή πρώτο. Κάθε στοίβα θα πρέπει να υποστηρίζει κατ' ελάχιστο τα εξής:

  • μέθοδο επιστροφής του μεγέθους της στοίβας.
  • μέθοδο ένθεσης στην κορυφή της στοίβας.
  • μέθοδο απόσβεσης του κορυφαίου στοιχείου.
  • μέθοδο επιστροφής του κορυφαίου στοιχείου της στοίβας χωρίς διαγραφή από τη στοίβα.

Παρακάτω δηλώνεται το interface Stack που υποστηρίζει αυτές τις λειτουργίες

Stack.java
public interface Stack {
  public int size();           // επιστροφή του μεγέθους της στοίβας.
  public void push(Object o);  // ένθεση στην κορυφή της στοίβας.
  public Object pop();         // απόσβεση από την κορυφή της στοίβας.
  public Object top();         // επιστροφή του κορυφαίου στοιχείου της στοίβας χωρίς διαγραφή του.
}

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

Μία κλάση υλοποιεί ένα interface μόνο εάν υλοποιεί ΟΛΕΣ τις μεθόδους του interface. Έάν έστω και μία μέθοδος δεν υλοποιείται τότε η κλάση δεν υλοποιεί το interface.

Σε συνέχεια του προηγούμενου παραδείγματος θα επιχειρήσουμε να δημιουργήσουμε δύο διαφορετικές κλάσεις που υλοποιούν το συγκεκριμένο interface. Οι κλάσεις αυτές είναι η ArrayStack που υλοποιεί τη στοίβα μέσω ενός πίνακα και η LinkedStack που υλοποιεί τη στοίβα διασυνδέοντας επιμέρους αντικείμενα μεταξύ τους σε αναλογία με μία διασυνδεδεμένη λίστα.

ArrayStack

ArrayStack.java
public class ArrayStack implements Stack {
  public int capacity;
  public Object []array;
  public int size;
 
  public ArrayStack() {
    this(256);
  }
 
  public ArrayStack(int capacity) {
    array = new Object[capacity];
    this.capacity = capacity;
    size = 0;
  }
 
  public int size() {
    return size;
  }
 
  public void push(Object o) {
 
    if( size >= capacity-1 ) {
      capacity *= 2;
      Object []newArray = new Object[capacity];
      for(int i=0; i<array.length; i++)
        newArray[i] = array[i];
      array = newArray;
      newArray = null;
    }
    array[size++] = o;
  }
 
  public Object pop() {
    return array[--size];
  }
 
  public Object top() {
    return array[size-1];
  }
 
  public String toString() {
    String str = "@@@@@@@@ - Stack - @@@@@@@@\n";
    for(int i=size-1; i>=0; i--)
      str += array[i].toString()+"\n";
    return str + "@@@@@@@@@@@@@@@@@@@@@@@@@@@\n";
  }  
}

LinkedStack

LinkedStack.java
class LinkedNode {
  private Object o;
  private LinkedNode next;
 
  public LinkedNode(LinkedNode nxt, Object e) {
    next = nxt;
    o = e;
  }
 
  public LinkedNode(Object e) {
    this(null, e);
  }
 
  public Object getElement() { return o; }
  public LinkedNode getNext() { return next; }
 
  public void setElement(Object e) { o = e; }
  public void setNext(LinkedNode node) { next = node; }
}
 
public class LinkedStack implements Stack {
  int size;
  LinkedNode head;
 
  public LinkedStack() {
    size=0;
    head = null;
  }
 
  public int size() {
    return size;
  }
 
  public void push(Object o) {
    // the following is OK even if head == null.
    head = new LinkedNode(head, o);
    size++;
  }
 
  public Object pop() {
    LinkedNode pN = head;
    head = head.getNext();
    size--;
    return pN.getElement();
  }
 
  public Object top() {
    return head;
  }
 
  public String toString() {
    String str = "@@@@@@@@ - Stack - @@@@@@@@\n";
    LinkedNode curr = head;
    while(curr != null)
      str += curr.getElement().toString();
    return str + "@@@@@@@@@@@@@@@@@@@@@@@@@@@\n";
  }
}

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

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

Ας υποθέσουμε τώρα ότι θέλουμε να χρησιμοποιήσουμε μία στοίβα για να αντιμεταθέσουμε τα στοιχεία ενός πίνακα χαρακτήρων. Ο πίνακας αρχικά περιέχει την αγγλική αλφαβήτα (26 χαρακτήρες) και θέλουμε να αντιστρέψουμε την σειρά με την οποία αποθηκεύονται οι χαρακτήρες στον πίνακα. Η μέθοδος invertArray παρακάτω αντιμεταθέτει τα στοιχεία του πίνακα array με χρήση της στοίβας stk.

ArrayManipulator.java
public class ArrayManipulator {
  public static void main(String []args) {
    Character alphabet[] = {'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};
    printCharacterArray(alphabet);
    System.out.println("-----------");
 
    Stack stack = new ArrayStack();
    invertArray(alphabet, stack);
 
    printCharacterArray(alphabet);
  }
 
  public static void invertArray(Object []array, Stack stk) {
    for(int i=0; i<array.length; i++)
      stk.push(array[i]);
    for(int i=0; i<array.length; i++)
      array[i] = stk.pop();
  }
 
  public static void printCharacterArray(Character []array) {
    for(Character c : array)
      System.out.print(c+"   ");
    System.out.println("");
  }
}

Παρατηρήστε ότι η μέθοδος invertArray παίρνει ως 2ο όρισμα όχι το αντικείμενο μίας κλάσης αλλά ένα αντικείμενο τύπου interface Stack! Στην πραγματικότητα αυτό που δηλώνει η συγκεκριμένη μέθοδος είναι ότι ως 2ο όρισμα παίρνει ένα αντικείμενο του οποίου η κλάση υλοποιεί το interface Stack. Επίσης, δηλώνει ότι από όλες τις διαθέσιμες μεθόδους της κλάσης μπορούν να χρησιμοποιηθούν μόνο οι μέθοδοι που δηλώνονται στο interface.

Στο παραπάνω πρόγραμμα, αλλάξτε την γραμμή

Stack stack = new ArrayStack();

σε

Stack stack = new LinkedStack();

Παρατηρήστε αν σας βγάζει κάποιο μήνυμα ο compiler και εάν εκτελώντας το πρόγραμμα παράγεται το ίδιο τελικό αποτέλεσμα. Τι συμπέρασμα βγάζετε για τη χρήση της στοίβας από την μέθοδο invertArray();

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

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

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

public interface DoIt {
 
   void doSomething(int i, double x);
   int doSomethingElse(String s);
}
 
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;
 
        }
    }

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

java/interfaces.1486113607.txt.gz · Last modified: 2017/02/03 09:20 (external edit)