1

In Java and C#, all classes derive from the root class Object (java.lang.Object and System.Object respectively).

In both languages/libraries this class comes with a set of methods that are inherited by every class.

My question is: Why and when do designers add a method to the global root object class?

I'm asking because many of Object's current methods seem to me like design mistakes, primarily violations of the Separation-of-Concerns rule - but I trust the language and library designers to put a great deal of thought into their designs; do the designers now say it was a mistake to add those methods?

  • System.Object
    • Equals(Object)
      • Object reference-equality can be tested using Object.ReferenceEquals. Value-equality must be explicitly opted-into by overriding this method, but consumers won't know if value-equality is actually defined. We also have IEquatable<T> - surely it makes more sense to not have any .Equals method and instead to always test for IEquatable<T>?
        • And in most cases consumers know the types they're comparing so type-checking for IEquatable<T> is unnecessary and the compiler can make the IEquatable<T>.Equals(T) call directly (and as a bonus there's no need to do a runtime type test of the Object argument value)
        • And why define bool Equals instead of the more general int Compare? While value-comparison operations are undefined for many types of objects, the same can be said for value-equality. It seems odd to single-out this particular method.
    • GetHashCode()
      • Not every type is used as a hash-table key. This method is essentially useless unless overridden, especially if the object is mutable. As with Equals, why not define IHashable?
        • Having an explicit IHashable means that hashtable types could use it as a generic type constraint on dictionary key types to help prevent the use of the default GetHashCode() implementation where it would introduce bugs.
      • And the BCL's hash-table types allow the use of a custom hash-code generator through IEqualityComparer<T> (and all struct types can have a trivial compiler-generated method).
    • ToString()
      • While handy for debugging, we're better served with [DebuggerDisplay] instead, and most classes don't override it (the default implementation returns the type's name). Additionally we already have IConvertable.ToString() for when it it's meaningful.
    • MemberwiseClone()
      • This method strikes me as being dangerous, especially if the class' author didn't consider it being called: consider calling SqlConnection::MemberwiseClone() on an active connection. And again, the BCL has IClonable.Clone.

In Java, the situation is similar for its methods clone(), equals(Object), hashCode() and toString() - but Java also adds threading primitives directly to Object instead of in the threading library. This seems shortsighted given that threading libraries can (and do) change their designs more often than the design of the root Object should be changed.

  • See also the type theory concept of the top type – cbojar May 31 at 15:21
6

While I think this is a question for the language designers and not us (and I am personally not a language designer by any means), I can offer some perspective as someone who uses these languages.

First, having a root object makes sense. There are facilities that are common to all objects, and it makes sense to have those features all in one place.

Secondly, despite your misgivings about the default methods that are present in the object class, it really makes a lot of sense to have them there, if for no other reason than you are establishing a convention for others to follow. The notions of equality, hashability, memberwise copyability and stringification are things that are universally applicable to all objects. That you generally have to provide override implementations for these is just a testament to the flexibility and resiliency of object-orientation in general.

I'm not sure what you mean by violating Separation of Concerns. Separation of concerns applies to things like separating UI functionality from the data and business operations it represents. I don't see how that applies here.

  • I'm referring to SoC in the sense that I don't believe objects' own methods should be concerned with some of the functionality that's exposed, e.g. having thread synchronization methods on java.lang.Object. The same way that a business domain entity object shouldn't be concerned with how its serialized (which is why we prefer POCO/POJOs to subclassing some database-library-specific entity base type). – Dai May 31 at 15:15
  • Like any other software development endeavor, language design frequently involves tradeoffs. The mere existence of these tradeoffs is naturally going to raise questions about the efficacy of design decisions, since there is no "ideal" decision. – Robert Harvey May 31 at 15:19
  • @Dai I might not care how you serialize me but I may have an opinion about what data should be serialized and what data is meta data. How's a language designer going to sort that out without letting me have a say? – candied_orange May 31 at 16:25
  • @candied_orange I think the point is that the language designer doesn't have to sort this out. I tend to agree that serialization as a language/object feature is inferior to using annotations and/or introspection. Languages can have features that support serialization without being involved in the details. – JimmyJames May 31 at 17:15
2

Equals and Hashcode

These are built-in essentially to allow for any object to be added to a hashtable. It's not required for you to provide a custom equals or hashcode in order to make this work either. The default implementations work for many/most needs. I would argue that the big issue with equals is that there's a tendency to try to override this to implement algorithms. It really should only be overridden if you want to define a logical identity (like all integer objects with the value of 1 are equivalent.) And when you do that, you need to override hashcode. Otherwise leave them alone.

toString

This is a convenience for logging/debugging. In most cases, trying to override toString to implement complex logic in your application is a bad idea, IMO.

Cloning

I tend to agree that this is a mistake. In Java it's especially poorly designed because the method is defined on Object but if your class doesn't implement Clonable (which has no methods) it simply fails with an exception. They screwed up. I'm not sure there's much else to say about it. Clonable should define the clone method.

A lot of these choices have to do with trying to get developer buy-in by solving common problems out-of-the-box. It seems to me that the zeitgeist of language design has moved towards doing this with language features instead of building them into the base object type. equals and hashcode make sense but you could do this differently by adding a Hashable interface and using the default when an object doesn't implement this but I see this as a minor nit.

P.S. Java threading primitives

The wait and notify methods on Object are probably one of the most confusing things about threading and synchronization in Java. These are pretty much obsolete since the introduction of more fully-functional concurrency primitives and collections. These are all about how Java uses Objects to identify monitors in the old synchronization model. This, I think, is widely viewed as a mistake because it obfuscates the threading model and basically just confused most developers that worked with it.

1

Any of your arguments presented here are plausible in some sense.
I won't try to counter or try to explain the language designers motivation for them.

I will say that as a software engineer (for that case; for any software system), it's unreasonable to think that some language design conceived tens of years ago could answer any programmer expectations coming into the language in the future.
Decisions had to be taken and extending it without breaking backward compatibility.
Plus, it's unreasonable to think that now they can design such a perfect language (or a system) and they can just ship a new version breaking (maybe in the long run) many systems that were built using it and serve many-many customers successfully today.

There is a concept mentioned in the book "Code Complete", that a programmer should "program into the language" and not "program in the language". It's a nice concept that I think is relevant for such discussions, and you should read about it.

  • So you're basically saying "they made some bad decisions years ago and now we're stuck with them? – Robert Harvey May 31 at 14:57
  • I'm not saying that. – nadir May 31 at 15:09
  • 2
    You're saying "they made these decisions years ago and now we have to live with them in the name of backwards compatibility." If that's not what you meant to say, then perhaps reword your answer? – Robert Harvey May 31 at 15:10
  • 1
    "I'm not saying that". That's a shame. Because if you were saying that, yours would be the best answer and definitely worthy of an upvote. At least one member of the original C# design team, for example, is on record saying that C#'s equality is a total mess due to them making bad decisions years ago... – David Arno May 31 at 18:45
  • As I said, I won't try to counter or explain some of the arguments shown here. All I tried to convey in my answer (in simple words) is that I never heard of a qualified software team rejecting a project in C# because of C# equality (taken from your comment). "Design is a wicked problem" especially when your language is mature and used by countless number of people, narrowing it to "bad decision we stuck with" or sign it as a sub-standard language is just "right" in the narrow sense. Altogether a qualified engineer shouldn't feel "limited" by it. – nadir May 31 at 22:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.