Skip to the content.

2.4 Κληρονομικότητα

© Γιάννης Κωστάρας


<- Δ ->

Κληρονομικότητα

Άλλο ένα “όπλο” που έχουν οι προγραμματιστές αντικειμενοστραφών προγραμμάτων στο “οπλοστάσιό” τους για τη δημιουργία πιο κατανοητού και ευκολότερα συντηρήσιμου κώδικα είναι η κληρονομικότητα (inheritance). Επιτρέπει την επαναχρησιμοποίηση κώδικα (DRY principle - Don’t Repeat Yourself).

Τα κοινά στοιχεία δυο ή περισσοτέρων παρόμοιων κλάσεων μπορούν να οριστούν σε μια κοινή υπερκλάση (superclass). Μία υποκλάση (subclass) μπορεί να κληρονομήσει όλα τα “επιτρεπτά” γνωρίσματα και μεθόδους από τους “προγόνους” της (δηλ. όσα είναι δηλωμένα ως protected και public αλλά και package αν βρίσκεται στην ίδια βιβλιοθήκη με τους προγόνους της) εκτός από τις μεθόδους κατασκευής. Η σχέση μεταξύ μιας υπερκλάσης και μιας υποκλάσης είναι σχέση τύπου “ΕΊΝΑΙ (IS-A)”.

Μια υποκλάση μπορεί να ορίσει νέα γνωρίσματα και μεθόδους, μπορεί να υπερφορτώσει υφιστάμενες μεθόδους (overloading) ή ακόμα και να υπερσκελίσει (επανακαθορίσει, υπερκαλύψει) υφιστάμενες μεθόδους (overriding). Όλες οι κλάσεις στη Java κληρονομούν από την κλάση Object.

Σημείωση. Προσοχή! μια υποκλάση δεν μπορεί να υπερκαλύψει (override) τις μεθόδους κατασκευής και τις στατικές μεθόδους της υπερκλάσης.

Ας δούμε ένα παράδειγμα. Ο κύκλος, το ορθογώνιο παραλληλόγραμμο κλπ. είναι όλα σχήματα (κληρονομούν από την υπερκλάση Shape). Αυτό δηλώνεται με τη λέξη-κλειδί extends.

public abstract class Shape {   
  protected final Point[] points;
 
  Shape(int edges) {
     this.points = new Point[edges];
  }  

  Shape(Point[] points) {
     this.points = points;
  }

  public int getEdges() {
     return this.points.length;
  }
 
  protected abstract double area();

  protected abstract double perimeter();
}

final class Point {
  private final int x, y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public int getX() {
    return this.x;
  }

  public int getY() {
    return this.y;
  } 

}

public class Circle extends Shape {  
  private final int radius;

  Circle() {
    super(1);
	this.radius = 1;
  }  

  Circle(Point[] points, int radius) {
    super(points);
    this.radius = radius;
  }    
  
  public int getRadius() {
    return radius;
  }

  @Override
  public double area() {
      return Math.PI * (radius * radius);
  } 

  @Override
  public double perimeter() {
    return Math.PI * 2*radius;
  } 
}

public class Rectangle extends Shape {  
  private int width, height; 

  Rectangle(int width, int height) {
    super(4);
    this.width = width;
    this.height = height;
  }  

  Rectangle(Point[] points, int width, int height) {
    super(points);
    this.width = width;
    this.height = height;
  }
  
  public int getWidth() {
  	return width;
  }
  
  public int getHeight() {
  	return height;
  }
  

  @Override
  public double area() {
      return width * height;
  } 
  
  @Override
  public double perimeter() {
    return 2*width + 2*height;
  }  
}

Η λέξη-κλειδί super χρησιμοποιείται:

Η κλήση της μεθόδου κατασκευής μιας υποκλάσης καλεί τις μεθόδους κατασκευής όλων των υπερκλάσεων αυτής.

class Person {
   public Person() {
      System.out.println("(1) Person's no-arg constructor");
   }
}

class Student extends Person {
	public Student() {
		System.out.println("(2) Student's no-arg constructor");
	}
}

jshell> new Student()
(1) Person's no-arg constructor
(2) Student's no-arg constructor

Στο πιο πάνω παράδειγμα βλέπουμε ότι παρόλο που ο constructor της κλάσης Student δεν καλεί την super, η μέθοδος κατασκευής της υπερκλάσης καλείται ούτως ή άλλως.

Αν δεν ορίσουμε κάποιον constructor σε μια κλάση, τότε δημιουργείται αυτόματα ο no argument constructor, διαφορετικά αν ορίσουμε έναν constructor, δε δημιουργείται no argument constructor. Αυτό έχει ως συνέπεια κώδικας όπως ο παρακάτω να μην μεταγλωττίζεται καθώς ο εξ’ορισμού no argument constructor της Carnivor δεν βρίσκει να καλέσει no argument constructor στην υπερκλάση.

class Animal {
   public Animal(String name) {
      //...
   }
}

public class Carnivor extends Animal {}

Το @Override είναι ένα σχόλιο μεταγλώττισης (annotation) το οποίο χρησιμοποιείται από τον μεταγλωττιστή για να επιβεβαιώσει ότι υπερκαλύπτουμε (override) τη σωστή μέθοδο, διαφορετικά ο μεταγλωττιστής εμφανίζει λάθος μεταγλώττισης (method does not override or implement a method). Η γλώσσα διαθέτει κι άλλα τέτοια σχόλια μεταγλώττισης όπως:

Η Αρχή της Υποκατάστασης (Substitution Principle) μας λέει ότι όταν περιμένουμε ένα αντικείμενο μιας κλάσης, μπορούμε να παρέχουμε και μια οποιανδήποτε υπο-κλάσης αυτής της κλάσης:

Αρχή της Υποκατάστασης (Substitution Principle): σε μια μεταβλητή μιας δοθείσας κλάσης μπορεί να αποθηκευθεί μια τιμή μιας οποιασδήποτε υπο-κλάσης αυτής της κλάσης, και μια μέθοδος με μια παράμετρο μιας δοθείσας κλάσης μπορεί να κληθεί με όρισμα μια οποιαδήποτε υποκλάση αυτής της κλάσης.

Με βάση την αρχή αυτή μπορούμε να γράψουμε:

Shape shape = new Circle();

Αν δε θέλουμε να επιτρέψουμε να μπορούν να δημιουργηθούν υποκλάσεις μιας κλάσης (δηλ. δε θέλουμε να μπορεί να κληρονομηθεί) τότε τη δηλώνουμε ως final (βλ. π.χ. την κλάση Point πιο πάνω). Επίσης, αν δε θέλουμε να μπορούν να υπερκαλυφθούν (overriden) οι μέθοδοι μιας κλάσης, τότε τις δηλώνουμε final, π.χ.:

public final int getEdges() { // δεν μπορεί να υπερκαλυφθεί από τις υποκλάσεις
   return this.points.length;
}

Ένα συχνό λάθος είναι όταν καλούμε μια υπερκαλυμμένη (overridable) μέθοδο στο σώμα της μεθόδου κατασκευής (constructor). Ας δούμε ένα παράδειγμα:

class Point {
  protected final int x, y;

  public Point(int x, int y) {  // 3
	  this.x = x;  
	  this.y = y;
	  print();  // 4
  }

  public int getX() {
	  return this.x;
  }

  public int getY() {
	  return this.y;
  } 

  public void print() {
  	System.out.println("{x=" + this.x + ", y=" + this.y + "}");
  }
}

class ColouredPoint extends Point {
	private Color color = null;
	
	public ColouredPoint(int x, int y, Color color) {  // 1
		super(x, y);  // 2
		this.color = color;
	}
	
	public Color getColor() {
		return color;
	}

  	public void print() {  // 5
  		System.out.println("{x=" + this.x + ", y=" + this.y + ", color=" + this.color + "}");
  	}
}

Αν εκτελέσουμε τον παραπάνω κώδικα:

jshell> Point p = new Point(1,1)
{x=1, y=1}
p ==> Point@28c97a5

jshell> Point cp = new ColouredPoint(1,2,Color.BLACK)
{x=1, y=2, color=null}
cp ==> ColouredPoint@61e4705b

Η 2η εντολή καλεί την μέθοδο κατασκευής της ColouredPoint (1), η οποία με τη σειρά της καλεί την μέθοδο κατασκευής της υπερκλάσης (2, 3). Ο δε constructor της Point καλεί την μέθοδο print() της υποκλάσης (4) η οποία με τη σειρά της τυπώνει την τιμή της color η οποία δεν έχει προλάβει ν’ αρχικοποιηθεί ακόμα (5). Το πρόβλημα διορθώνεται είτε αν αποφύγουμε να καλέσουμε την υπερκαλυμμένη μέθοδο print() στον constructor Point είτε αν δηλώσουμε την print() ως final ώστε να μην μπορεί να υπερκαλυφθεί.

Επίσης μεγάλη προσοχή χρειάζεται όταν ορίζουμε μια στατική μέθοδο σε μια υποκλάση με την ίδια δήλωση όπως σε μια υπερκλάση. Όπως είπαμε πιο πάνω, οι στατικές μέθοδοι δεν μπορούν να υπερκαλυφθούν, με αποτέλεσμα στην ουσία να αποκρύπτουμε (hide) την μέθοδο της υπερκλάσης. Π.χ.

class SuperClass {
	public static void display() {
		System.out.println("I 'm the SuperClass");
	} 
}

class SubClass extends SuperClass {
	public static void display() {
		System.out.println("I 'm the SubClass");
	} 
}

SuperClass sc = new SuperClass()
SuperClass ssc = new SubClass()
jshell> sc.display()
I 'm the SuperClass

jshell> ssc.display()
I 'm the SuperClass

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

jshell> SuperClass.display()
I 'm the SuperClass

jshell> SubClass.display()
I 'm the SubClass

Αφαιρετικές/Ιδεατές Κλάσεις (Abstract class)

Ο κύριος λόγος της κληρονομικότητας είναι η εξαφάνιση διπλότυπου κώδικα (αρχή “Μην επαναλαμβάνεσαι” - “Don’t Repeat Yourself” ή DRY principle). Δεν μπορούν να δημιουργηθούν αντικείμενα από αφαιρετικές κλάσεις. Πρέπει να κληρονομηθούν και να υλοποιηθούν οι αφαιρετικές μεθόδοι τους από τις υποκλάσεις.

Μια κλάση δηλώνεται ως αφαιρετική με την λέξη-κλειδί abstract. Μια κλάση η οποία περιλαμβάνει έστω και μια «αφαιρετική» μέθοδο, καθίσταται επίσης αφαιρετική και πρέπει να δηλωθεί ως αφαιρετική (abstract). Δε μπορούμε να δημιουργήσουμε αντικείμενα μιας αφαιρετικής κλάσης εκτός κι αν παρέχουμε μια υλοποίηση όλων των αφαιρετικών μεθόδων της. Μια κλάση που κληρονομεί από μια αφαιρετική κλάση θα πρέπει να υλοποιήσει όλες τις αφαιρετικές μεθόδους της υπερκλάσης, διαφορετικά ο μεταφραστής επιβάλλει να οριστεί και η υποκλάση ως αφαιρετική.

Η κλάση Shape έχει δυο abstract μεθόδους (area() και perimeter()) και πρέπει να δηλωθεί κι αυτή ως abstract.

Το παρακάτω UML διάγραμμα αναπαριστά την ανωτέρω ιεραρχία:

Εικόνα 2.4.1 Παράδειγμα ιεραρχίας κλάσεων στη UML

Η σχέση μεταξύ των κλάσεων Shape, Circle και Rectangle ονομάζεται γενίκευση (generalization) η οποία είναι μια σχέση τύπου είναι (is a).

Abstract κλάσεις και μέθοδοι αναπαρίστανται με πλάγια γραφή. Στο παραπάνω διάγραμμα βλέπουμε μια ακόμα σχέση μεταξύ κλάσεων, την συσσωμάτωση (aggregation) η οποία είναι μια σχέση τύπου ανήκει (owns). Η σχέση αυτή δηλώνει ότι η κλάση Shape περιλαμβάνει μια συλλογή από σημεία (Points) τα οποία τα αποθηκεύει στην protected μεταβλητή points.

Όπως μπορείτε να δείτε στην Εικόνα 2.2.4 του 2ου μαθήματος αυτής τη εβδομάδας (το οποίο επαναλαμβάνεται παρακάτω)

Εικόνα 2.2.4. Διαγράμματα κλάσεων

η UML υποστηρίζει διάφορους τύπους σχέσεων μεταξύ κλάσεων. Είδαμε τη συσσωμάτωση (aggregation) αλλά υπάρχει και η σύνθετη συσσωμάτωση (composite aggregation ή composition) (σχέση τύπου περιέχει (is part of)). Η διαφορά τους είναι ότι στη δεύτερη περίπτωση, αν καταστραφεί το αντικείμενο που περιέχει τη συλλογή των συσσωματωμένων αντικειμένων, τότε καταστρέφονται και τα συσσωματωμένα αντικείμενα, ενώ στην περίπτωση της απλής συσσωμάτωσης, τα συσσωματωμένα αντικείμενα παραμένουν και μετά την καταστροφή του αντικειμένου που τα περιέχει και μπορούν να επαναχρησιμοποιηθούν από άλλα αντικείμενα. Π.χ. αν καταστραφεί ένα αντικείμενο τύπου Rectangle το οποίο περιέχει τα 4 αντικείμενα τύπου Point για τις κορυφές του, τότε ανάλογα με το αν η εφαρμογή μας υποστηρίζει το Point ως σχήμα ή όχι, η σχέση θα μπορεί να είναι aggregation (αν υποστηρίζει) ή composite aggregation (αν δεν υποστηρίζει οπότε τα σημεία θα πρέπει να καταστραφούν μαζί με το αντικείμενο). Η σύνθετη συσσωμάτωση υλοποιείται ως εμφωλιασμένη κλάση (ώστε αντικείμενά της να καταστρέφονται όταν καταστρέφονται και τα αντικείμενα που τις περιέχουν) ενώ η συσσωμάτωση ως στατική εμφωλιασμένη κλάση ώστε να μπορεί να υπάρχει και όταν αντικείμενα της κλάσης που την περιέχουν παύσουν να υπάρχουν.

Π.χ.

public abstract class Shape {   
  protected final Point[] points;
 
  Shape(int edges) {
     this.points = new Point[edges];
  }  

  Shape(Point[] points) {
     this.points = points;
  }

  public int getEdges() {
     return this.points.length;
  }
 
  protected abstract double area();

  protected abstract double perimeter();

  // nested class
  static final class Point {
  	private final int x, y;
  	
  	public Point(int x, int y) {
  	  this.x = x;
  	    this.y = y;
  	}
  	
  	public int getX() {
  	   return this.x;
  	}
  	
  	public int getY() {
  	   return this.y;
  	} 
  }
}

Η σχέση συσχέτιση (association) χρησιμοποιείται όταν μια κλάση σχετίζεται με μια άλλη, δηλ. την έχει ως γνώρισμά της (σχέση has a).

Π.χ.

class Fighter {
	private Weapon weapon;
}

class Weapon {
}

Εικόνα 2.4.2 Παράδειγμα association στη UML

Τέλος, αν μέσα σε μια μέθοδο μιας κλάσης χρησιμοποιούμε ένα αντικείμενο μιας άλλης κλάσης αλλά χωρίς να την ορίζουμε ως γνώρισμα της κλάσης, τότε χρησιμοποιούμε τη σχέση εξάρτηση (dependency).

Σφραγισμένες κλάσεις

Στην έκδοση 15 εμφανίστηκαν δυο νέες δεσμευμένες λέξεις, η sealed και η permits. Ας δούμε τη χρήση τους μ’ ένα παράδειγμα:

public abstract sealed class Shape
    permits Circle, Rectangle {...}

public class Circle extends Shape {...} 
public class Rectangle extends Shape {...} 
public class Triangle extends Shape {...}   // compilation error

Η sealed περιορίζει ποιες άλλες κλάσεις μπορούν να κληρονομήσουν την κλάση Shape. Βλέπουμε ότι επιτρέπει μόνο τις Circle, Rectangle με αποτέλεσμα η κλάση Triangle να εμφανίζει λάθος μεταγλώττισης. Από δω και πέρα, έχουμε διάφορες επιλογές για τις (επιτρεπόμενες) υποκλάσεις (Circle, Rectangle). Μπορούμε να τις ορίσουμε ως final (δεν επιτρέπεται να κληρονομηθούν), ως sealed (οπότε πρέπει να ορίσουμε με την permits ποιες άλλες κλάσεις επιτρέπεται να την κληρονομήσουν) ή να τις ορίσουμε όπως στο παραπάνω παράδειγμα οπότε μπορεί να τις κληρονομήσει οποιαδήποτε άλλη κλάση.

Στα μαθήματα της 1ης εβδομάδας μάθαμε για τη νέα σύνταξη της switch. Οι sealed κλάσεις επιτρέπουν την απλοποίησή τους, καθώς, όπως βλέπουμε στο παρακάτω παράδειγμα δεν απαιτείται default:

double area = switch (shape) {
  case Circle c -> Math.pow(c.radius(), 2)*Math.PI
  case Rectangle r -> r.a() * r.b()
};

Σημειώστε, ότι στην έκδοση 15 αυτό είναι ένα χαρακτηριστικό προεπισκόπισης (preview) της γλώσσας, με αποτέλεσμα για να το ενεργοποιήσετε θα πρέπει να περάσετε την παράμετρο --enable-preview κατά τη μεταγλώττιση. Θα περάσουν μια ή δυο εκδόσεις ακόμα ώστε να γίνει ένα κανονικό χαρακτηριστικό της γλώσσας.

Κλάση Object

Όλες οι κλάσεις στη Java κληρονομούν από την κλάση Object (δηλ. κάθε κλάση μπορεί να γραφτεί και ως class MyClass extends Object). Η κλάση Object δηλώνει έναν αριθμό από χρήσιμες μεθόδους:

public class Object {
	boolean	equals(Object obj); // Indicates whether some other object is "equal to" this one.
	int	hashCode();	// Returns a hash code value for the object.
	String toString(); 	// Returns a string representation of the object.
}

Είναι πολύ σημαντικό να υπερκαλύπτουμε (override) αυτές τις μεθόδους όταν ορίζουμε τις κλάσεις μας.

Η equals() ελέγχει για ισότητα μεταξύ δυο αντικειμένων και είναι πολύ σημαντική όταν θέλουμε να συγκρίνουμε αντικείμενα. Επιστρέφει true αν ένα αντικείμενο της κλάσης είναι ίσο μ’ ένα άλλο αντικείμενο της κλάσης ή μιας υποκλάσης της κλάσης.

Η hashCode() επιστρέφει ένα μοναδικό αριθμό με βάση τις τιμές των γνωρισμάτων του αντικειμένου της κλάσης και χρησιμοποιείται όταν το αντικείμενο εισαχθεί σε μια συλλογή (για τις συλλογές θα μιλήσουμε την επόμενη εβδομάδα).

Τέλος η toString() επιστρέφει μια αναπαράσταση του αντικειμένου σε φιλική μορφή για τον προγραμματιστή. Μας βοηθάει ώστε να αναγνωρίζουμε εύκολα τι τιμές έχει το αντικείμενό μας. Η κλάση Object παρέχει μια όχι και τόσο χρήσιμη υλοποίηση της μεθόδου αυτής παρέχοντας το όνομα της κλάσης, το χαρακτήρα @ ακολουθούμενο από τον κωδικό κατακερματισμού (hash) της κλάσης.

Σε γενικές γραμμές, μια καλή υλοποίηση της μεθόδου equals() πρέπει ν’ ακολουθεί τους εξής κανόνες:

  1. Έλεγχος αν ένα αντικείμενο είναι ίδιο με τον εαυτό του χρησιμοποιώντας τον τελεστή ==
  2. Έλεγχος αν το αντικείμενο που περνάμε είναι του ίδιου τύπου, με χρήση της instanceof (η οποία καλύπτει και την περίπτωση το αντικείμενο που περνάμε να είναι null)
  3. Μετατροπή (cast) του αντικειμένου που περνάμε στον σωστό τύπο
  4. Έλεγχος για ισότητα όλων των γνωρισμάτων των δυο αντικειμένων
  5. Πάντα υπερσκελίστε την hashCode() όταν υπερσκελίζετε την equals(). Δυο ίσα αντικείμενα (με βάση την equals()) πρέπει να επιστρέφουν την ίδια τιμή hashCode()

Π.χ.

public class Car { // κλάση
  // ιδιότητες/γνωρίσματα
  private String model;
  private int maxSpeed;
  private int ccm;
  private int speed = 0;
//...
  @Override
  public boolean equals(Object o) {
	  if (o == this)
	  	return true;
	  if (!(o instanceof Car))
	  	return false;
	  Car car = (Car)o;
	  return car.model.equals(this.model) && car.maxSpeed == this.maxSpeed && car.ccm == this.ccm;
  }
  
  @Override 
  public int hashCode() {
  	int result = model.hashCode();
  	result = 31 * result + Integer.hashCode(maxSpeed);
  	result = 31 * result + Integer.hashCode(ccm);
  	return result;
	// return Objects.hash(model, maxSpeed, ccm);
  }
  
  @Override
  public String toString() {
  	return "Model: " + model + " max speed: " + maxSpeed + " ccm: " + ccm;
  }

}

ΟΠΕ όπως το NetBeans παρέχουν δυνατότητες ώστε να παράγετε εύκολα και γρήγορα αυτές τις μεθόδους. Απλά κάντε δεξί κλικ μέσα σε μια κλάση και επιλέξτε Insert code –> Generate –> equals() and hashCode() και αντίστοιχα Insert code –> Generate –> toString(). Θα εμφανιστεί ένα διαλογικό παράθυρο που θα σας ρωτήσει ποια γνωρίσματα θέλετε να χρησιμοποιήσετε. Επιλέξτε τα ίδια γνωρίσματα για τις equals() και hashCode(). Όταν πατήσετε ΟΚ το NetBeans θα δημιουργήσει τις μεθόδους αυτές για εσάς. Επίσης, το σχόλιο μεταγλώττισης @AutoValue της Google κάνει την ίδια δουλειά.

Για μια πιο λεπτομερή περιγραφή αυτών των μεθόδων και πώς θα πρέπει να τις υλοποιείτε, βλ. πηγή [2] κεφ. 3.

Ο τελεστής == συγκρίνει αν δυο αντικείμενα έχουν την ίδια ταυτότητα (object ID) κι όχι αν έχουν την ίδια τιμή. Αν θέλουμε να συγκρίνουμε αν οι τιμές δυο αντικειμένων είναι ίσες τότε θα πρέπει να χρησιμοποιήσουμε την μέθοδο equals().

Σημαντική σημείωση Ενώ για τη σύγκριση πρωτογενών τύπων (raw types) χρησιμοποιούμε τον τελεστή ==, κάτι τέτοιο δε συνιστάται για τη σύγκριση αντικειμένων. Ο τελεστής == δηλώνει σύγκριση ταυτότητας των αντικειμένων αντί για ισότητα των τιμών τους. Συγκρίνει δηλ. αν η ταυτότητα του αντικειμένου objA είναι ίση με την ταυτότητα του αντικειμένου objB. Για να συγκρίνουμε αν οι τιμές των δυο αντικειμένων είναι ίσες, θα πρέπει να χρησιμοποιήσουμε τη μέθοδο equals(), δηλ. objA.equals(objB). Έτσι, π.χ.

jshell> int num1 = 5
num1 ==> 5

jshell> int num2 = 5
num2 ==> 5

jshell> num1 == num2
$1 ==> true

jshell> Integer num1 = 5
num1 ==> 5

jshell> Integer num2 = 5
num2 ==> 5

jshell> num1 == num2
$2 ==> true

jshell> num1.equals(num2)
$3 ==> true

jshell> num1 = 150
num1 ==> 150

jshell> num2 = 150
num2 ==> 150

jshell> num1 == num2
$4 ==> false

jshell> num1.equals(num2)
$5 ==> true

Η κλάση Integer θέλει μεγάλη προσοχή καθώς χρησιμοποιεί μια λανθάνουσα μνήμη (cache) για να αποθηκεύει τις τιμές -128 έως 127 όπως βλέπουμε στο παραπάνω δείγμα κώδικα. Επίσης το παραπάνω παράδειγμα μας δείχνει γιατί πρέπει ΠΑΝΤΑ να συγκρίνουμε αντικείμενα με τη μέθοδο equals().

Η τεχνική αυτή λέγεται απομνημόνευση (memoization) και ισχύουν αυτά που φαίνονται στον ακόλουθο πίνακα:

Πίνακας 2.4.1 Τιμές απομνημόνευσης (memoization) κατά το (Un)Boxing

Αρχέγονος τύπος Συσκευασμένος (boxed) τύπος Τιμές απομνημόνευσης
boolean Boolean όλες
byte Byte όλες
char Character μόνο \u0000 - \u007f ή [0, 127]
short Short μόνο [-128, 127]
int Integer μόνο [-128, 127]
long Long μόνο [-128, 127]

Η κλάση Byte αποθηκεύει στη λανθάνουσα μνήμη όλες τις τιμές του πρωτογενούς τύπου byte (οπότε μπορούμε να συγκρίνουμε δυο τύπους Byte και με τον τελεστή ==). Οι τελεστές σύγκρισης (<, >, <=, >=) παρόλ’ αυτά, δουλεύουν σωστά και με τους συσκευασμένους (boxed) τύπους δεδομένων.

Διεπαφές (Interfaces)

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

Μία διεπαφή (interface) είναι μία δομή (παρόμοια με την κλάση) η οποία μπορεί να περιλαμβάνει:

Με άλλα λόγια, αποτελεί ένα “συμβόλαιο” που θα πρέπει να υλοποιήσει μια κλάση.

public interface IShape {
	double PI = 3.1415;
	double area();
	double perimeter();
} 

Οι μέθοδοι και οι σταθερές μιας διεπαφής είναι εξ’ ορισμού public. Από την έκδοση 9 υποστηρίζονται και private μέθοδοι.

Μια κλάση μπορεί να υλοποιεί (implements) μια ή περισσότερες διεπαφές. Επίσης, μια διεπαφή μπορεί να κληρονομήσει άλλες διεπαφές (extends) αλλά όχι άλλες κλάσεις.

Μία κλάση πρέπει να υλοποιεί όλες τις μεθόδους μίας διεπαφής εκτός και αν είναι δηλωμένη ως abstract. Π.χ.

public abstract Shape implements IShape {
    protected final Point[] points;

    Shape(int edges) {
        this.points = new Point[edges];
    }

    Shape(Point[] points) {
        this.points = points;
    }

    public int getEdges() {
        return this.points.length;
    }
}

Η παραπάνω κλάση δηλώνεται ως αφαιρετική (abstract) καθώς δεν υλοποιεί τις μεθόδους της διεπαφής IShape.

Στη UML ένα interface αναπαρίσταται με δυο τρόπους όπως φαίνεται στην ακόλουθη Εικόνα 2.4.3 (βλ. και Εικόνα 2.2.4). Λέμε ότι η κλάση Shape πραγματοποιεί (realizes) τη διεπαφή IShape.

Εικόνα 2.4.3 Αναπαράσταση διεπαφής (interface) στη UML

Χαρακτηριστικές διεπαφές της γλώσσας Java:

Προσθήκες στις διεπαφές

Η έκδοση 8 πρόσθεσε τις έννοιες των στατικών (static) και εξ’ ορισμού (default) μεθόδων στις διεπαφές.

Με την εισαγωγή των ροών (streams) στην έκδοση 8, υπήρξε το πρόβλημα επεκτασιμότητας των διεπαφών. Όπως μάθαμε, μια κλάση που υλοποιεί μια διεπαφή θα πρέπει να υλοποιήσει όλες τις μεθόδους της διεπαφής. Αυτό δημιουργεί ζητήματα με κώδικα που χρησιμοποιεί παλαιότερες εκδόσεις της γλώσσας. Οι εξ’ ορισμού (default) μέθοδοι επιτρέπουν τη μελλοντική επέκταση της γλώσσας.

Ας δούμε τι εννοούμε με ένα παράδειγμα. Η διεπαφή Collection που θα δούμε στα μαθήματα της επόμενης εβδομάδας, υπάρχει ήδη από την έκδοση 1.2 της γλώσσας. Στην έκδοση 8 έπρεπε να την αλλάξουν προσθέτοντας νέες μεθόδους (π.χ. stream()). Κάτι τέτοιο όμως θα “έσπαγε” κώδικα που είχε γραφτεί πριν την έκδοση 8, καθώς ο παλιός κώδικας θα έπρεπε, όταν η εφαρμογή αναβαθμιζόταν ώστε να τρέχει με την έκδοση 8 της Java, να υλοποιήσει τις νέες μεθόδους, αλλοιώς δε θα μπορούσε να μεταγλωττιστεί. Με τη χρήση των εξ’ ορισμού μεθόδων κάτι τέτοιο δεν είναι πλέον απαραίτητο. Οι εξ’ ορισμού μέθοδοι παρέχουν μια εξ’ορισμού υλοποίηση κι έτσι δεν είναι απαραίτητο για τις κλάσεις που υλοποιούν (implements) αυτές τις διεπαφές να υλοποιήσουν τις εξ’ ορισμού μεθόδους καθώς υπάρχει ήδη μια εξ’ ορισμού υλοποίηση που μπορούν να χρησιμοποιήσουν. Με αυτόν τον τρόπο μπορούμε πλέον να επεκτείνουμε τις διεπαφές χωρίς να “χαλάμε” την προς τα πίσω συμβατότητα.

Οι εξ’ ορισμού μέθοδοι είναι μέθοδοι με μια εξ’ορισμού υλοποίηση ώστε να λύνουν το πρόβλημα της προς τα πίσω συμβατότητας (backwards compatibility) αλλά που μπορούν να επεκταθούν από τις κλάσεις που τις υλοποιούν ώστε να αλλάξουν την εξ’ορισμού υλοποίηση αν το επιθυμούν.

Μια εξ’ ορισμού (default) μέθοδος σε μια διεπαφή ορίζεται με τη λέξη-κλειδί default:

public interface AInterface {
    default String message() {
        return "Hallo A";
    }
}

public class A implements AInterface {
}

jshell> AInterface a = new A();
a ==> A@3c0ecd4b

jshell> a.message()
$1 ==> "Hallo A"		

Μια υπο-διεπαφή μπορεί να υπερκαλύψει (override) αυτή τη μέθοδο.

public interface BInterface extends AInterface {
    default String message() {
        return "Hallo B";
    }
}

public class B implements BInterface {
}

jshell> AInterface b = new B();
b ==> B@64bf3bbf

jshell> b.message()
$2 ==> "Hallo B"			

Στο ακόλουθο παράδειγμα βλέπουμε ότι η εξ’ορισμού μέθοδος μπορεί να υπερκαλυφθεί κι από μια κλάση (όχι μόνο από διεπαφή):

public class C extends B {
    public String message() {
        return "Hallo C";
    }
}

jshell> AInterface c = new C();
c ==> C@6e1ec318

jshell> c.message()
$3 ==> "Hallo C"

Έστω η ακόλουθη δήλωση:

public class D extends C implements AInterface {
}

jshell> AInterface d = new D();
d ==> D@31dc339b

jshell> d.message()

Τι πιστεύετε ότι θα τυπώσει η πιο πάνω εντολή;

"Hallo C". Με άλλα λόγια, η κλάση νικά.

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

public interface X {
    default String message() {
        return "Hallo X";
    }
}

public interface Y {
    default String message() {
        return "Hallo Y";
    }
}

public class XY implements X, Y {
}

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

public class XY implements X, Y {
    default String message() {
        return X.super.message();
    }
}

Ισχύουν οι ακόλουθοι 2 κανόνες (σε περίπτωση που έχετε αμφιβολίες):

Όσων αφορά τις στατικές μεθόδους στις διεπαφές:

jshell> interface Z { static void print() { System.out.println("Hello Z"); }}
|  created interface Z

jshell> class ZZ implements Z {}
|  created class ZZ

jshell> Z z = new ZZ()
z ==> ZZ@3327bd23

jshell> Z.print()
Hello Z

Παραδείγματα στατικών μεθόδων σε διεπαφές του Java API: Comparator.reverseOrder, Collections.reverseOrder, Collections, sort κ.ά.

Η Java 9 πρόσθεσε τη δυνατότητα ορισμού ιδιωτικών μεθόδων (private methods) στις διεπαφές. Πριν από την έκδοση 9, όλες οι μέθοδοι στις διεπαφές έπρεπε να είναι public. Με την εισαγωγή ιδιωτικών μεθόδων στις διεπαφές, μπορείτε να αναδιοργανώσετε (refactor) τον κώδικα default μεθόδων ώστε να εισάγετε private utility methods. Οι διεπαφές πλέον γίνονται όλο και περισσότερο όμοιες με τις abstract κλάσεις.

Τα δικαιώματα πρόσβασης στις μεθόδους των διεπαφών είναι πλέον όπως περιγράφονται στον ακόλουθο πίνακα:

Πίνακας 2.4.2 Τροποποιητές πρόσβασης στις δηλώσεις μεθόδων στις διεπαφές

Τροποποιητής Πρόσβασης Υποστηρίζεται Σχόλια
public abstract Ναι ≥ JDK 1
public static Ναι ≥ JDK 8
public default Ναι ≥ JDK 8
private static Ναι ≥ JDK 9, δεν μπορεί να επεκταθεί (override)
private Ναι ≥ JDK 9, δεν μπορεί να επεκταθεί (override)
private abstract Όχι από τη μία πρέπει να επεκταθεί (abstract) από την άλλη δεν μπορεί (private)
private default Όχι από τη μία θα πρέπει να μπορεί να επεκταθεί (abstract) από την άλλη δεν μπορεί (private)

Ανώνυμες Εσωτερικές (inner) κλάσεις

Στο προηγούμενο μάθημα μιλήσαμε για τις εσωτερικές (inner) και τις εμφωλιασμένες (nested) κλάσεις. Αφήσαμε τις ανώνυμες εσωτερικές κλάσεις (anonymous inner classes) γι’ αυτό το μάθημα. Οι ανώνυμες εσωτερικές κλάσεις είναι μη στατικές εσωτερικές κλάσεις χωρίς όνομα. Υλοποιούν κάποια διεπαφή (interface) ή επεκτείνουν μια κλάση με προκαθορισμένη μέθοδο κατασκευής. Συνδυάζουν τον ορισμό και δημιουργία ενός αντικειμένου μίας εσωτερικής κλάσης σε ένα βήμα:

jshell> public interface MyInterface {
  public void aMethod();
}
|  created interface MyInterface

jshell> public class Test {
  public Test() {	
    new MyInterface() {  // ανώνυμη εσωτερική κλάση
      @Override
      public void aMethod() {
        System.out.println("Anonymous class aMethod()");
      }
    }.aMethod();  // Anonymous class' aMethod()
  }
}
|  created class Test

jshell> new Test()
Anonymous class aMethod()
$75 ==> Test@679b62af

Ασκήσεις

1) Μετατρέψτε το ακόλουθο UML διάγραμμα σε κώδικα Java

2) Σε συνέχεια της άσκησης 1. του 1ου μαθήματος αυτής της εβδομάδας, δημιουργήστε μια κλάση Person με τα εξής γνωρίσματα:

και μετατρέψτε την κλάση Student ώστε να κληρονομεί από αυτήν. Δημιουργήστε μια κλάση Teacher που να κληρονομεί από την Person και επιπλέον τα γνωρίσματα:

Πηγές

  1. Evans B. (2019), Inside the Language: Sealed Types, Java Magazine.
  2. Jenkov Nested Classes

<- Δ ->