Java Interfaces

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

Registriere dich um viele Vorteile zu genießen! Weniger Werbung, bessere Kommunikation und vieles mehr!

  • Du programmierst Java und bist dir nicht so sicher, was die Interfaces betrifft?
    Du möchtest nachhaltigeren und besseren Code schreiben?

    Dann bist du hier richtig! In dieser Anleitung lernst du einige Grundlagen für den Umgang mit Interfaces in Java.
    Wir werden uns Polymorphie anschauen und unseren Code wiederverwendbar machen. Wir schaffen Schnittstellen für andere Entwickler.
    Interfaces in Java
    Interfaces können vielseitig eingesetzt werden und bieten einige neue Möglichkeiten.
    Es ist schwierig, das Eine ohne das Andere zu erklären, ich werde dieses Unterfangen hier dennoch versuchen.
    Diese Anleitung wird zunächst den Zweck beschreiben und dann mit konkreten Praxisbeispielen die Syntax zeigen.
    Anschließend schreiben wir noch Entities für ein Computerspiel.
    Zunächst schauen wir uns den Zweck von Interfaces an.

    Wozu Interfaces?
    Ein Interfaces wird häufig als Schnittstelle gebraucht. So kann das Entwurfsmuster "Fassade" sehr gut damit realisiert werden: Ein Interface abstrahiert den Zugriff auf ein von außen unbekanntes Subsystem. Eine weitere Einsatzmöglichkeit ist die Alternative zu abstrakten Klassen. Diese sind Interfaces sehr ähnlich und können zusätzlich noch implementierte Methoden (mit Interfaces seit Java 8 auch durch default möglich) sowie Membervariablen in allen Sichtbarkeiten enthalten.
    Ein weiterer Punkt ist der Ersatz für in Java nicht vorhandene Mehrfachvererbung: Wir erben stets von einer Klasse, entweder implizit von Object oder von einer explizit angegebenen Superclass. Darüberhinaus darf eine Klasse beliebig viele Interfaces implementieren. Der Effekt ähnelnd dem er Vererbung.
    Und zu guter Letzt wäre da noch die Polymorphie: Wir können ein Interface als Basisklasse benutzen und von dort ausgehend eine Klassenhierarchie aufbauen. Wenn wir nun eine Instanz unseres Interfaces erwarten (z.B. als Parameter bei einer Methode), können wir nur das Interface als Datentyp angeben und die konkrete Klasse theoretisch sogar erst zur Laufzeit festlegen. Das erlaubt anderen Entwickler und auch uns, mehrere (angepasste) Implementierungen zu erstellen und diese dynamisch einzubinden.


    Das erste Interface
    Es ist an der Zeit, das erste Interface anzulegen. Wir legen uns jetzt ein sog. Marker-Interface an, darunter versteht sich ein Interface ohne Inhalt.
    Wir benutzen dazu das Schlüsselwort interface.

    Java-Quellcode

    1. public interface Animal {
    2. // Hier folgt später der Inhalt
    3. }

    Was machen wir nun damit?
    Wir können jetzt eine Klasse anlegen, die das Interface implementiert. Ganz wichtig: Wir können keine Instanz von diesem Interface anlegen. Wir können darin auch keinen Konstruktor definieren. Wir müssen eine Implementierung benutzen.

    Java-Quellcode

    1. public class Dog implements Animal {
    2. // Hier müssen später Methoden festgelegt werden
    3. }
    Und hier erkennen wir auch den Sinn: Es kann keine direkte Instanz eines Tiers geben. Welches Geräusch macht ein Tier? Wie bewegt sich ein Tier? Jeder würde behaupten, das sei von Tier zu Tier unterschiedlich. Da es kein allgemeines Verhalten gibt, muss jedes Tier sein eigenes Verhalten ausführen. Das Interface wird uns gleich als Grundlage dienen.

    Aber nun zum Anlegen einer Instanz: Animal animal = new Dog(); - und schon haben wir unseren Hund, allerdings arbeiten wir danach mit dem Tier. Das ist auch nachvollziehbar: Ein Hund ist auch ein Tier.


    Methoden in Interfaces
    Um die Fähigkeiten von Interfaces noch besser ausspielen zu können, brauchen wir Methoden. Sagen wir, jedes Tier kann folgende Aktionen durchführen:
    • Einen Laut abgeben (playSound)
    • Sich bewegen (move)
    Wir werden gleich zwei Klassen schreiben: Dog (Hund) und Human (Mensch). Beide Klassen wir ein eigenes Verhalten für die beiden Methoden implementieren.
    Doch vorher müssen wir unser Interface erweitern:

    Quellcode

    1. public interface Living {
    2. void playSound();
    3. void move();
    4. }
    Ich habe das Interface jetzt umbenannt, die Bezeichnung "Living" ist für "Mensch und Hund" wohl ein wenig treffender gewählt.
    Wie sich erkennen lässt, haben wir hier Methoden ohne Method-Body angelegt.

    Abstrakte Methoden
    Diese Methoden sind nun abstrakt, d.h. sie werden durch eine Unterklasse implementiert. In C++ heißen solche Methoden pure virtual.
    Nun verhalten sich unsere Methoden wie abstrakte Methoden aus einer abstrakten Klasse - mit dem Unterschied, dass wir kein public abstract bei der Deklaration davor schreiben müssen. Das rührt daher, dass in einem Interface ausschließlich abstrakte Methoden (und default-Methoden ab Java 8 deklariert werden dürfen. Einige Entwickler bevorzugen es, das public abstract dennoch als Präfix zu setzen - Geschmackssache.

    Implementierungen
    Nun schreiben wir zwei implementierende Klassen.

    Java-Quellcode

    1. public class Dog implements Living {
    2. private int xPos;
    3. private int yPos;
    4. @Override
    5. public void playSound() {
    6. System.out.println("Wuff!");
    7. }
    8. @Override
    9. public void move() {
    10. xPos += 100;
    11. yPos += 100;
    12. }
    13. }
    14. --------------------------------------------------
    15. public class Human implements Living {
    16. private int xPos;
    17. private int yPos;
    18. @Override
    19. public void playSound() {
    20. System.out.println("Ich bin ein Mensch und kann sprechen.");
    21. }
    22. @Override
    23. public void move() {
    24. xPos += 20;
    25. yPos += 20;
    26. }
    27. }
    Alles anzeigen
    Nun haben wir ganze Arbeit geleistet und wollen eine Methode schreiben, die unsere Methoden auch aufruft. Das sieht dann wie folgt aus:

    Java-Quellcode

    1. void action(Living entity) {
    2. entity.playSound();
    3. entity.move();
    4. entity.move();
    5. entity.playSound();
    6. }
    Und jetzt darfst du mal testen: Folgende zwei Aufrufe dürften unterschiedliche Ausgaben liefern:

    • action(new Dog());
    • action(new Human());
    Fakt ist, dass beide Aufrufe möglich sind. Es wird schließlich nur ein Lebewesen erwartet, wobei es keine Rolle spielt, ob ein Mensch, ein Hund oder ein anderes Tier dahintersteckt. Ich habe dir hier übrigens schon Polymorphie untergemogelt: Polymorphismus (griech. für "Vielseitigkeit") erlaubt uns in der objektorientierten Programmierung ein Interface als Basis und beliebig viele konkrete Implementierungen.


    Vererbung mit Interfaces
    Falls du denkst: "Wow, das war schon alles?!", hast du dich geirrt. Wir werden unsere Hierarchie jetzt ausbauen und ein Entity hinzufügen, das kein Lebewesen ist. Natürlich bleibt der Zweig mit den Lebewesen erhalten und wir erben mit einem Interface von einem anderen Interface. Dazu verwenden wir das Schlüsselwort extends.
    Aus Platzgründen und als Beitrag zu einer besseren Lesbarkeit ist das folgende Codebeispiel in eine Datei geschrieben. In der Realität sind das logischerweise einzelne Dateien.

    Java-Quellcode

    1. public interface Entity {
    2. void remove();
    3. String getName();
    4. }
    5. public interface LivingEntity extends Entity {
    6. void move();
    7. void playSound();
    8. }
    9. public class Button implements Entity {
    10. @Override
    11. public void remove() {
    12. System.out.println("Ein Button wurde entfernt"");
    13. }
    14. @Override
    15. public String getName() {
    16. return "entity_button";
    17. }
    18. }
    19. public class Dog implements LivingEntity {
    20. @Override
    21. public void remove() {
    22. System.out.println("Der Hund ist gestorben!");
    23. }
    24. @Override
    25. public String getName() {
    26. return "entity_dog";
    27. }
    28. @Override
    29. public void move() {
    30. System.out.println("Der Hund hat sich bewegt!");
    31. }
    32. @Override
    33. public void playSound() {
    34. System.out.println("Wuff!");
    35. }
    36. }
    37. // Hinweis: Den Menschen spare ich mir an dieser Stelle, die Implementierung liegt auf der Hand
    Alles anzeigen
    Nun hat jedes Objekt in unserer Welt sein eigenes Verhalten. Wir könnten nun eine Methode schreiben, die eine Instanz von LivingEntity erwartet und ein Geräusch abspielt. An dieser Stelle darf der instanceof-Check nicht unerwähnt bleiben: Im folgenden Beispiel gehen wir alle Entities in unserer Welt durch und lassen einen Sound abspielen, falls es sich um ein Lebewesen handelt.

    Java-Quellcode

    1. public class World {
    2. private List<Entity> entities = new ArrayList<>();
    3. public void playSounds() {
    4. Iterator<Entity> it = this.entities.iterator();
    5. while (it.hasNext()) {
    6. Entity entity = it.next();
    7. if (entity instanceof LivingEntity) {
    8. ((LivingEntity) entity).playSound();
    9. }
    10. }
    11. }
    12. }
    Alles anzeigen
    Verbunden mit dem Check ist auch die explizite Typumwandlung.
    Hier sieht man die Polymorphie richtig in Aktion: Wir können unsere Hierarchie weiter ausbauen und haben bestimmte Methoden immer gewährleistet.


    Entwurfsmuster: Fassade

    Lass uns einen Blick auf die Fassade werfen, ein Entwurfsmuster (siehe auch GoF Seite 212). Dazu müssen wir ein kleines Beispiel annehmen: Ein Softwarehersteller liefert uns eine Bibliothek für Datenbanken. Diese ist relativ schwierig zu benutzen und erfordert z.B. drei Methodenaufrufe zum Herstellen der Verbindung. Damit unser Team es einfacher hat und sich nicht erst die langen Dokumentationen durchlesen muss, schreiben wir eine API. Diese will auch der Kunde von uns benutzen, weil er Erweiterungen programmiert.
    Im Folgenden habe ich ein komplettes Programmbeispiel in einer Datei (siehe Begründung oben) bereitgestellt und kommentiert. Dieses sollte selbsterklärend sein.
    Spoiler anzeigen


    Java-Quellcode

    1. -------------------- DatabaseLibrary --------------------
    2. public class DatabaseLibrary {
    3. /*
    4. * Database-Bibliothek von SoftwareFirma XY
    5. *
    6. * Beachte: Es muss setFileName, initialize und connect zum Herstellen der Verbindung aufgerufen werden
    7. * Schließen der Verbindung: disconnect -> disable
    8. * Absetzen einer Request: prepare -> sendRequest -> flush
    9. *
    10. * (Ja, die Methoden sind extra durcheinander)
    11. */
    12. public void setFileName(String name) {
    13. System.out.println("Datei-Name wurde gesetzt: " + name);
    14. }
    15. public void disable() {
    16. System.out.println("Ressourcen wurden wieder freigegeben!");
    17. }
    18. public void sendRequest(String command) {
    19. System.out.println("Anfrage im Buffer gelandet!");
    20. }
    21. public void connect() {
    22. System.out.println("Verbindung aufgebaut!");
    23. }
    24. public void initialize() {
    25. System.out.println("Initialisierung abgeschlossen.");
    26. }
    27. public void flush() {
    28. System.out.println("Daten abgesendet!");
    29. }
    30. public void prepare() {
    31. System.out.println("Bereite Request vor...");
    32. }
    33. public void disconnect() {
    34. System.out.println("Verbindung geschlossen!");
    35. }
    36. }
    37. -------------------- Database --------------------
    38. public interface Database {
    39. /*
    40. * Was machen wir?
    41. * Eine Schnittstelle anlegen!
    42. * Hier wird alles vereinfacht ;)
    43. */
    44. void connect();
    45. void disconnect();
    46. void query(String command);
    47. }
    48. -------------------- SimpleDatabase --------------------
    49. public class SimpleDatabase implements Database {
    50. /*
    51. * Implementierung
    52. *
    53. * Hier wird das Interface Database implementiert
    54. * Diese Klasse ist ein Wrapper für die DatabaseLibrary
    55. */
    56. private String fileName;
    57. private DatabaseLibrary handle;
    58. public SimpleDatabase(String fileName) {
    59. this.fileName = fileName;
    60. }
    61. @Override
    62. public void connect() {
    63. this.handle = new DatabaseLibrary();
    64. this.handle.setFileName(this.fileName);
    65. this.handle.initialize();
    66. this.handle.connect();
    67. }
    68. @Override
    69. public void disconnect() {
    70. this.handle.disconnect();
    71. this.handle.disable();
    72. this.handle = null;
    73. }
    74. @Override
    75. public void query(String command) {
    76. this.handle.prepare();
    77. this.handle.sendRequest(command);
    78. this.handle.flush();
    79. }
    80. }
    81. -------------------- Verwendungsbeispiel --------------------
    82. Database db = new SimpleDatabase("myDatabase.db");
    83. db.connect();
    84. db.query("INSERT INTO accounts (username, password) VALUES (kunde001, topsecret)");
    85. db.disconnect();
    Alles anzeigen







    TODO-Liste

    Hinweis: Diese Anleitung ist noch nicht vollständig fertig. Es sind noch Abschnitte zu Konstanten, Default-Methoden und statischen Methoden (die beiden letzteren nur Java 8+) geplant. Hilf bei deren Gestaltung mit, indem du Feedback zu dieser Anleitung gibst und Vorschläge für die neuen Abschnitte machst.


    Schlusswort
    Ich hoffe, dir da diese Anleitung gefallen. Vielleicht hast du sogar noch ein wenig dazugelernt.
    An dieser Stelle möchte ich noch einmal einen Dank an @ilouHD ausrichten, der mir diese Idee geliefert hat.
    Ich freue mich jederzeit über Anregungen, Verbesserungsvorschläge und Kritik.

    Janhektor

    502 mal gelesen