Über Methoden von 'Object': clone(), hashCode() und equals()

EDIT: Abgetrennt von http://forum.byte-welt.net/threads/4929-Java-Quiz/page10?p=86132&viewfull=1#post86132 , zur Aussage "Nebenbei bemerkt gehören für mich Cloneable und die clone()-Methode in die Top-Ten der Java-Hässlichkeiten."

Was ist denn so schlimm daran ein objekt zu klonen?

An sich nichts. Nur die Art, wie es in Java gemacht wird/werden muss, ist … naja… es gibt ein 3-teiliges Tutorial dafür unter http://www.angelikalanger.com/Articles/EffectiveJava/05.Clone-Part1/05.Clone-Part1.html … Bisher habe ich jedenfalls um clone() auch einen großen Bogen gemacht. Copy-Constructors tun’s meistens…

Wie schon Marco13 gesagt hat: Eigentlich eine gute Idee, aber schlecht umgesetzt. Jedes Objekt hat eine clone-Methode. Nur kann man sie nicht sicher aufrufen, weil normalerweise eine CloneNotSupportedException geworfen wird. Nur wenn das Cloneable-Interface implementiert wird, funktioniert die Methode - dann kann man in der Implementierung auch super.clone() aufrufen, selbst wenn die Super-Klasse nicht Cloneable ist (also z.B. Object). Alles verwirrend und ziemlich „magisch“. Wahrscheinlich wäre es am besten, die clone-Methode in das Cloneable-Interface zu verbannen, und die super.clone()-Magie durch eine Reflection-Methode an Class zu ersetzen (die dann meinetwegen eine CloneNotSupportedException werfen kann).

Meiner Meinung nach gehört sowieso eine Menge Krempel aus Object entfernt: clone() nach Cloneable, wait(), notify() u.s.w. in ein neues Interface Lockable, equals() in ein neues Interface Equals, hashCode() in ein neues Interface Hashable extends Equals.

[QUOTE=Landei]
Meiner Meinung nach gehört sowieso eine Menge Krempel aus Object entfernt: clone() nach Cloneable, wait(), notify() u.s.w. in ein neues Interface Lockable, equals() in ein neues Interface Equals, hashCode() in ein neues Interface Hashable extends Equals.[/QUOTE]

Ein in der Praxis IMHO sehr gewichtiger Pluspunkt von Java ist die Rückwärtskompatibilität. Gib mir eine ZIP mit irgendeinem Java-Projekt das vor 10 Jahen mit Java 1.3 geschrieben wurde: Auspacken, in Eclipse laden, compliert und läuft. (Und wer nicht erkennt, was daran so erstrebenswert ist: Wir hätten hier noch ein Projekt unter Visual Studio 6 mit Qt 3.0.4 - wer Lust hat, das auf einem aktuellen System lauffähig zu machen: Einfach bescheid sagen :D).
Aus … eher „akademischer“ Sicht gibt es natürlich etliche Altlasten in Java - was ja nicht verwunderlich ist. (BTW: „Akademisch“ bedeutet nicht, dass diese Altlasten einem nicht auch in der Praxis „schönere“ Lösungen verbauen).
Aber gerade sowas wie Hashable extends Equals irrtiert mich jetzt: Ich hätte da jetzt eher einen Vorschlag wie HashFunction und EqualityTest erwartet… :confused:

Das hatte mich im ersten Moment auch stutzig gemacht, ergibt aber durchaus Sinn, wenn man sich den Contract von [japi]Object#hashCode()[/japi] ansieht:

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

Daher muss man die equas Methode überschreiben, wenn man eine hashCode Methode implementiert.

Edit: der Umkehrschluss gilt… Also müsste eigentlich Equals extends Hashable gelten…

Ja, diese Vererbung meinte ich gar nicht. Wegen “equals -> gleicherHashcode” würde das ja schon Sinn machen (wobei “gleicherHashcode -> equals” eben NICHT gilt). Es ging vielmehr darum, dass es ja dann lauten müßte
Object implements Equals, Hashable, Whatever…
womit der Vorteil IMHO weniger offensichtlich wäre, als wenn es dafür dedizierte Funktionen (d.h. Methoden mit nur einem Interface) gäbe. Also, im Moment gibt es sowas wie
personList.contains(person);
und dort wird implizit equals verwendet. Wenn man jetzt die contains-Methode verwenden will, um herauszufinden, ob es in der Liste eine Person gibt, die die gleichen Namen hat, wie die gegebene, dann geht das erstmal nicht. Mit sowas wie
personList.contains(person, PersonPredicates.namesAreEqual());
würde das gehen. Zum Glück wird mit Java 8 (Predicates, Functions…) schon ein groooßer Schritt in diese (IMHO richtige) Richtung gemacht. Aber ‘Object’ nochmal anzufassen (bzw. Methoden rauszunehmen) geht eben nicht…

Etwas verallgemeinert: Ich hatte mir schon öfter die Frage gestellt, nach welchen Kriterien man entscheidet, ob es eine Methode in einer Klasse einfach so gibt, oder ob sie durch ein Interface vorgegeben werden soll. Also, eine “algebraisch saubere” Lösung wäre ja, wenn man JEDE Methode NUR über ein Interface vorgeben würde. Also nicht

class Person {
    String getName() { ... }
    int getAge() { ... }
    float getHeight() { ... }
}

sondern

interface NameOwner { String getName() }
interface AgeOwner { int getAge() }
interface HeightOwner { float getHeight() }

class Person implements NameOwner, AgeOwner, HeightOwner { ... }

Aber das ist eine so allgemeine Frage, dass sie wohl eher in einen eigenen Thread gehört…

EDIT: Wenn keine Einwände kommen, würde ich den Teil ab #188 nachher mal in einen eigenen Thread abzwacken…

An irgend einer Stelle wird man sich mal von den Altlasten trennen müssen - geht doch bei anderen Sprachen auch (mit entsprechender Unterstützung durch Kompatibilitätsmodi u.s.w.). An einigen Stellen (allerdings nicht in meinem Vorschlag) kann das mit den neuen Default-Methoden geschehen. Ein separates Equalitity-Interface wäre auch denkbar, mit Fortsetzung zu Comparator:

public interface Equality<T> {
  boolean equals(T t1, T t2);
}

public interface HashFunction<T> extends Equality<T> {
  int hashCode(T t);
}

public enum Order { LT, EQ, GT }

public interface Comparator<T> extends Equality<T> {
   Order compare(T t1, T t2);
   default boolean equals(T t1, T t2) { return compare(t1,t2) == EQ; }
   default boolean less(T t1, T t2) { return compare(t1,t2) == LT; }
   default boolean greater(T t1, T t2) { return compare(t1,t2) == GT; }
   default boolean lessOrEq(T t1, T t2) { return compare(t1,t2) != GT; }
   default boolean greaterOrEq(T t1, T t2) { return compare(t1,t2) != LT; }
}

Ja, das wird auf absehbare Zeit nicht passieren, aber man wird ja noch träumen dürfen…

@cmrudolf: hashCode() ohne equals() ist aus Implementierungssicht völlig wertlos: Wenn du mit dem Hashcode in einer Hashtable suchst und in einem Bucket gelandet bist, musst du dort die einzelnen Elemente voneinander unterscheiden können. Deshalb müsste ein Interface mit hashCode() oder so immer auch ein equals() besitzen, oder eben ein Super-Interface erweitern (was besser wäre, da equals() allein ja auch schon ziemlich nützlich ist).

Richtig. Und aus Vertragssicht geht equals() nicht ohne hashCode(). Daher sind die beiden Methoden untrennbar miteinander vereint.

*** Edit ***

Ja, mach das.

*** Edit ***

Ok, wenn man natürlich sagt, man verzichtet auf die hashCode() Methode, dann wird der Vertrag ja im eigentlichen Sinne gar nicht verletzt. Nur, wenn beide Methoden existieren, muss man sich darauf verlassen können - daher ist der ursprüngliche Vorschlag, dass ein Hashable ein Equals interface erweitern könnte, gar nicht mal so verkehrt.

Genau. Der Unterschied zwischen Comparable und Comparator ist IMHO ein Paradebeispiel dafür, welchen Vorteil eine solche „externe“ Definition hätte. Wenn man eine Klasse hat wie

class Person {
    String getName() { ... }
    int getAge() { ... }
    float getHeight() { ... }
}

und die nun einmal nach Name und einmal nach Alter sortieren will, nützt einem das Interface Comparable gerade mal gar nichts. (Selbst bei Klassen, bei denen eine natürliche Ordnung existiert, stolpert man über dieses starre Comparable wenn man z.B. mal die Sortierreihenfolge umdrehen will). Wenn man sich vorstellt, wie die Flexibilität bestimmter Klassen geradezu explodieren würde, wenn es mehr solche „externe Interfaces“ gäbe, könnte man schon in Weltschmerz versinken.

Aber nochmal: Wäre die „beste“ Lösung (gaaanz theoretisch gesprochen, natürlich) dann konsequenterweise nicht, wenn man für jede einzelne Methode ein Interface definieren würde? (Bzw. für jede Kombination von Methoden? (Ja, es läuft mal wieder auf Duck-Typing raus)).

Oder etwas praxisnäher: Nach welchen Kriterien entscheidet man, ob man eine bestimmte Methode in ein Interface auslagert? Um bei dem Suggestivbesipiel zu bleiben: Angenommen es gäbe neben der oben angedeuteten Klasse „Person“ auch noch

class Animal {  String getName() { ... } }
class Car {  String getName() { ... } }
class Food {  String getName() { ... } }

und man müßte ständig Objekte dieser Klassen nach ihrem Namen sortieren. Dann würde ein Interface NameOwner natürlich Sinn machen: Man müßte dann nur einen NameOwnerComparator erstellen, und nicht PersonByNameComparator, CarByNameComparator usw…

(Das sind die Dinge, wegen denen ich Nachts nicht schlafen kann :stumm: )

Wärs da nicht sinnvoller gleich eine Oberklasse NamedObject zu erstellen wenn der Name so ein ausschlaggebendes Kriterium ist?

Nein, weil man nur eine Klasse erweitern kann. Wenn neben dem Namen auch noch das Alter wichtig wäre, dann müsste man sich entscheiden: erweitere ich nun NamedObject oder AgedObject? Mit zwei Interfaces entsteht das Problem nicht. Und die Elternklasse kann man für wirklich sinnvolles gemeinsames Verhalten nutzen.

[QUOTE=Landei]Ein separates Equalitity-Interface wäre auch denkbar, mit Fortsetzung zu Comparator:[/QUOTE]Die Idee und Dein Vorschlag gefallen mir gut.
Ich würde nur das Order ändern:public interface Order<T> { T getLesser(); boolean isEqual(); T getGreater(); } Oder diese beiden getter in den Comparator integrieren.

bye
TT

Das Problem bei so einer Ordnung ist, dass man oft sagen kann, OB ein Element kleiner ist als ein anderes, aber dass es schwer bis unmöglich ist, für ein gegebenes Element das „nächstgößere“ zu finden. Denke an Strings mit alphabetischer Sortierung: Was sollte „Marco13“.getGreater() zurückgeben? (‚null‘ natürlich! :smiley: ) „Marco14“? Macht ja nicht viel Sinn. Ich finde, dass viele Begrifflichkeiten, die man sich in einer Programmiersprache wünschen würde, um schöne und klare Programme zu schreiben, schon lange existieren: In der Mathematik. Der Zusammenhang zwischen Algebraischen Strukturen und Klassen ist teilweise nicht so offensichtlich, wie er sein könnte oder sollte. Um solche Dinge wie http://de.wikipedia.org/wiki/Ordnungsrelation existiert ein so ausgeklügeltes und geprüftes Gerüst, dass man sich an einigen Stellen vielleicht daran anlehnen sollte. Es gibt zwar sowas wie org.jscience.mathematics.structure (JScience v4.3 API) , aber das ist etwas zu „klein“, „speziell“ und „zu wenig in die Sprache eingebettet“.

[quote=Marco13]“Marco13”.getGreater()[/quote]String soll ja das Order-Interface nicht implementieren, Aber eine Anwendung könne so aussehenComparator<String> myComparator = new Comparator<>(){/* .. */}; String derGroesste=myComparator.compare("Ich","du").getGreater();Das folgt stärker dem OO-Pinzip “tell, don’t ask!”.

bye
TT

Ach so, das kam gerade anders an. Hmja… sowas müßte man natürlich noch genauer durchdenken, … spontan sowas wie ob man das nicht verallgemeinern könnte zu comparator.max(T...t) oder gleich fold(neutralElement, binaryAssociativeOperator) :smiley: aber… naja

Nein, weil man nur eine Klasse erweitern kann. Wenn neben dem Namen auch noch das Alter wichtig wäre, dann müsste man sich entscheiden: erweitere ich nun NamedObject oder AgedObject? Mit zwei Interfaces entsteht das Problem nicht. Und die Elternklasse kann man für wirklich sinnvolles gemeinsames Verhalten nutzen.

Aber genau da kommt man in die Bredoullie, dass man zu viele kleine Interfaces für hat, was die Komplexität eines Projekts durch den Schornstein jagt. Besonders wenn wir über sowas banales wie einen Namen reden ist doch ein Interface komplett überzogen. Viel wichtiger ist da, seine Architektur auf die tatsächlichen Anforderungen auszulegen. Zum Zeitpunkt als Sun die equals() et al. Methoden in Object gepackt hat, hat man sich um sowas nicht im Traum gedanken gemacht. Aber hätte man damals die API mit lauter Interfaces für jeden getter vollgepflastert wäre Java jetzt nicht was es heute ist.

Es ist schwer, da so was allgemeines dazu zu sagen. Es hängt natürlich vom Anwendungsfall und der „Zielgruppe des entwickelten Codes“ ab. Für manche Sachen kann man ein „relativ mächtiges“ Interface definieren und das erfüllt dann seinen Zweck. Für andere Bereiche ist es sinnvoller, das ganze aufzudröseln.

Um zwei Extrembeispiele zu nennen, die mir gerade einfallen: Bei der Wahl einer Matrix-Library für Java reicht die Spannbreite da von JAMA, wo die Matrix-Klasse so aussieht: http://math.nist.gov/javanumerics/jama/doc/Jama/Matrix.html , bis zum „Universal Java Matrix Package“ ( http://sourceforge.net/projects/ujmp/files/ujmp-complete/0.2.5/ ), das komplett engineered ist - wie man an einem kurzen Codeauszug schon erkennt:

public class DefaultDenseDoubleMatrix2D extends AbstractDenseDoubleMatrix2D implements
		HasColumnMajorDoubleArray1D { ... }

public abstract class AbstractDenseDoubleMatrix2D extends AbstractDenseDoubleMatrix implements
		DenseDoubleMatrix2D { ... }

public interface DenseDoubleMatrix2D extends DoubleMatrix2D, DenseDoubleMatrix, DenseMatrix2D { ... }

public interface DoubleMatrix2D extends DoubleMatrix, GenericMatrix2D<Double> { ... }

public interface GenericMatrix2D<A> extends GenericMatrix<A>, Matrix2D { ... }

public interface Matrix2D extends Matrix { ... }

public interface Matrix extends CoreObject, ExportMatrixInterface, CoordinateFunctions,
		GettersAndSetters, BasicMatrixProperties, CanPerformCalculations, DistanceMeasures,
		Comparable<Matrix>, HasAnnotation, Conversions { ... }

Natürlich wird der Bauingenieur, der mal kurz eine LU-Zerlegung braucht, zu ersterem greifen, und jemand der irgendein universelles Framework entwickeln will, eher zu letzterem.

Aber es ist eben so, dass die „untere Grenze“, dessen was überhaupt möglich ist, durch die Sprache selbst festgelegt ist. Object hat ‚equals‘, und wer irgendwas abstraktes machen will, wo er ein ‚Equality‘-Interface brauchen könnte, hat einfach Pech gehabt.

Das ganze ist aber erstmal nicht pauschal wertend gemeint. Es gibt Sprachen, die solche Abstraktionen unterstützen, die aber dafür vielleicht weniger zugänglich (oder, wenn man es so nennen will, „praxistauglich“) sind als Java. Trotzdem kann man ja mal drüber reden, welche Möglichkeiten die eine oder andere Abstraktion gebracht hätte :slight_smile:

Da sind wir ja auf einem Nenner. MMn hat sich die OOP seit es Java gibt beträchtlich weiterentwickelt, wodurch sich eben auch diese untere Grenze verschoben hat. Es ist heute einfach mehr möglich als 1995.

[QUOTE=Marco13]Es ist schwer, da so was allgemeines dazu zu sagen. Es hängt natürlich vom Anwendungsfall und der “Zielgruppe des entwickelten Codes” ab. Für manche Sachen kann man ein “relativ mächtiges” Interface definieren und das erfüllt dann seinen Zweck. Für andere Bereiche ist es sinnvoller, das ganze aufzudröseln.

Um zwei Extrembeispiele zu nennen, die mir gerade einfallen: Bei der Wahl einer Matrix-Library für Java reicht die Spannbreite da von JAMA, wo die Matrix-Klasse so aussieht: http://math.nist.gov/javanumerics/jama/doc/Jama/Matrix.html , bis zum “Universal Java Matrix Package” ( http://sourceforge.net/projects/ujmp/files/ujmp-complete/0.2.5/ ), das komplett engineered ist - wie man an einem kurzen Codeauszug schon erkennt:[/QUOTE]

Ich hoffe sehr, dass das mit Java 8 noch weiter “aufgedröselt” wird, in dem die Vererbung bis zur Laufzeit aufgeschoben wird, in dem man erst dann die nötigen Interfaces hinzufügt, wenn man sie wirklich benötigt, also etwas “Decorator”-artiges.

Solche Dinge zur Laufzeit zu machen, stelle ich mir schwierig vor. Würde das nicht schon eine SEHR starke Abkehr von den Compilezeit-Typchecks bedeuten? Also, ich denke, die sollten/müßten schon noch zur Compilezeit testbar sein. Vor (inzwischen kann man schon sagen: etlichen) Jahren bin ich mal über “cJ: Enhancing Java with Safe Type Conditions” ( http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.96.9590&rep=rep1&type=pdf ) gestolpert, und fand die Idee sowohl cool (weil mächtig), aber gleichzeitig so wenig “invasiv” in bezug auf das “Vanilla Java”, dass es (für mich, als Laien) zumindest sehr überzeugend aussah.