Hallo zusammen,
mir ist des öfteren aufgefallen, dass häufig folgendes programmiert wird:
List<String> liste1 = new ArrayList<>();
Ich habe mich gefragt wo hierbei der praktische Vorteil gegenüber
ArrayList<String> liste2 = new ArrayList<>();
liegt.
Ich sehe darin sogar eher einen Nachteil, man hat mit liste1 nur die Möglichkeit auf die Methoden von List zuzugreifen, während mit der liste2 zusätzlich auf die Methoden von ArrayList zugegriffen werden kann.
Ist jemand in der Lage mir dieses Geheimnis zu entlüften?
gegen das Interface, man hat später die Möglichkeit, ArrayList gegen eine andere Implementierung auszutauschen, wobei ArrayList schon gut ist
*** Edit ***
Random r = new Random();
List<Integer> li = new ArrayList(Arrays.asList(1, 2, 3)); // Arrays.asList return val immutable
for (int i = 0; i < 100000; i++) {
li.add(li.get(r.nextInt(li.size())));
}
long t1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
li.remove(r.nextInt(li.size()));
}
long t2 = System.currentTimeMillis();
li = new LinkedList<Integer>(li);
for (int i = 0; i < 100000; i++) {
li.add(li.get(r.nextInt(li.size())));
}
long t3 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
li.remove(r.nextInt(li.size()));
}
long t4 = System.currentTimeMillis();
System.out.println("li = " + li);
System.out.println("ArrayList's remove: " + (t2 - t1));
System.out.println("LinkedList's remove: " + (t4 - t3));
}```
Um das genauer auszuführen: Angenommen, du hast folgende Klasse:
class Datensammler {
ArrayList<String> daten = new ArrayList<>();
public Datensammler() {}
public void sammleDaten(String s) { daten.add(s); }
public ArrayList<String> gibDaten() { return daten; }
}
So, nun kommt es aber zu dem Fall, dass du eventuell schon am Anfang Daten hast, du möchstest also welche mitgeben, und schreibst einen zusätzlichen Konstruktor public Datensammler(ArrayList<String> daten) { this.daten = daten; }. Was ist, wenn du andere Listentypen hast? Sicher, du kannst sie immer in ArrayList umwandeln, aber das kann bei langen Listen inperformant sein. Noch interessanter ist, dass du nirgendwo irgendwelche speziellen Eigenschaften von ArrayList benutzt. Eine LinkedList kannst du beispielsweise auch als Queue verwenden, und dabei Methoden benutzen, die nicht im List-Interface vorhanden sind (etwa addLast ). Aber das tust du hier nicht. Warum muss es also eine ArrayList sein? List würde doch ausreichen. Je nach Anwendungsfall könnte eventuell Collection oder sogar Iterable ausreichen. Aber bleiben wir bei List:
class Datensammler {
List<String> daten = new ArrayList<>();
public Datensammler() {}
public Datensammler(List<String> daten) { this.daten = daten; }
public void sammleDaten(String s) { daten.add(s); }
public List<String> gibDaten() { return daten; }
}
Wird dadurch die Funktionalität irgendwie eingeschränkt? Nein, sie wird sogar erweitert. Ein Beispiel: Angenommen, du willst diese Klasse in einer Multithreading-Umgebung einsetzen. ArrayList ist nicht threadsafe, aber CopyOnWriteArrayList ist es. Die neue Version von DatenSammler kann threadsicher arbeiten, man muss nur im Konstruktor eine CopyOnWriteArrayList mitgeben - Problem gelöst.
Das ist ein bisschen so, als wenn du beim Heimwerken sagst: “Gib mir mal den Hammer!”. Du sagst nicht: “Gib mir mal den Schlosserhammer 150 Gramm mit Stahlrohrstiel von Eisenmeier”, weil dir das in dem Moment völlig egal ist. Du willst einen “abstrakten” Hammer, bekommst am Ende zwar einen ganz bestimmten “konkreten” Hammer, aber für deine Arbeit ist das nicht wichtig, solange bestimmte Rahmenbedingungen erfüllt sind (etwa kein Gummi- oder Vorschlaghammer).
Wozu der Microbenchmark? Mehr Info als im ersten Satz steckt da auch nicht drin (abgesehen von dem Vergleich ArrayList<->LinkedList).
Das Implementieren gegen Interfaces hat mehrere Vorteile:
Man fokkusiert sich auf die Schnittstellen
Entwickelt man selber die Schnittstellen, kann man seine eigene Implementierung “verstecken” (Es kann dir z.B. völlig egal sein, was Arrays.asList für eine Klasse zurückgibt, hautpsache es ist eine List).
Weils dazu auch nicht mehr zu schreiben gibt. Vielleicht noch die von Landei erwähnte Flexibilität, innerhalb einer Klasse die Implementierung der Daten des Objekts / Attribute nachträglich oder an anderer Stelle zu verändern.
Vielleicht sei noch erwähnt, dass in einem KSKB/sscce resp. der „main“-Methode List liste1 = new ArrayList(); KEIN MUSS ist und ArrayList liste2 = new ArrayList<>(); auch geschrieben werden darf, ohne dass gleich ein Holzfäller aus dem Wald kommt und einem den Kopf abhackt!!
Hart verkettete Klassen oder Vererbung mit einem verletztem Contract (LSP-Verletzung) funktioniert ja in der Regel.
Den Ärger bekommt man erst, wenn man anfängt an der Stelle zu arbeiten (Klasse mit verletztem Contract für Interface einsetzen, Implementierung austauschen usw).
Umso wichtiger ist es, dass man sich einen bestimmten Stil angewöhnt und durchgehend einhält.
(Und umso nerviger sind Kollegen die einen Schief anschauen, wenn man bei falsch überschriebenen Methoden meckert oder Begriffe wie „Single-Responsibility“ in den Raum wirft).