“What happens if your reference refers to a different type of object than you expect?”
Its probably a programming error.
Or not.
In C++, because of the way vtable dispatching works, the program will either crash (hopefully) or the wrong member function will be called. Either way – the end result is probably fatal.
Java is slightly better. In order to call a member function, the java compiler checks to make sure that the function is defined in one of the interfaces implemented by the type of the object reference. The problem arises when the object reference has been widened to a more general type that does not define the desired member function.
How can this happen?
public class A extends Object { public void anAThing() {} … }
A myA = new A(); // make an A
someList.add(myA); // put it in a list … someList.get(0).anAThing; // error – List.get(int) returns Object.
so we have to downcast the Object reference returned by someList.get() to an A reference
((A)someList.get(0)).anAThing(); // Might be OK if someList has an A
Casting is completely type unsafe. Although in Java the result of incorrect casting is an exception. So we could write:
try { ((A)someList.get(0)).anAThing(); }
catch(ClassCastException ex) true
which allows us to handle the casting error and continue. This is a huge step up from the C++ behavior of crashing in that it allows the programmer some control over what to do if the cast is wrong. If we desire a more conventional means of writing this, it is possible to use the instanceof operator and do the check before the cast.
if(someList.get(0) instanceof A) ((A)someList.get(0)).anAThing();
which is pretty much the same thing. Of course, this forces programmers to clutter their code with all sorts of tests or try/catch blocks. It seems to me that getting away from that sort of thing was precisely the reason everybody wanted to switch to object oriented programming in the first place. Polymorphism replaced an awful lot of if ladders and switch statements and here we are putting them back in to work around a runtime system that throws an exception if we guess incorrectly about the type of an object.
Not to mention the idea that its quite possible to have this situation:
public class A extends Object { public void doAThing(); …}
public class B extends Object { public void doAThing(); …}
Object myB = new B();
((B)myB).doAThing(); // fine
((A)myB).doAThing(); // error – object is not an A!
which seems just silly. We have an object that we are pretty sure implements the operation doAThing – but that’s not good enough. We have to know exactly which interface this particular object implements in order to call that method. Thus, type defines protocol in the statically typed world.
The problem is that such an arrangement assumes the existence of what Bart Kosko calls crisp sets and hierarchies. The world isn’t nearly so neat. Its fuzzy. B might well be capable of performing some of A’s operations and in some (but perhaps not all) circumstances, B might be an excellent stand-in for an A. Its a more accurate model. To quote Kosko again. “fuzz up – precision up”.
OK, so what about the dynamically typed languages? How are they better? Assume we have the same classes A and B derived from Object and each implements doAThing.
| anA |
anA := A new.
anA doAThing.
anA := B new.
anA doAThing.
anA := ‘this is a string’.
anA doAThing.
This all works except for the last line when anA refers to a String rather than an instance of A or B. String definitely doesn’t implement doAThing. So what happens?
First, it helps to know that the runtime systems for Smalltalk and Objective C are “message sending” rather than “function calling”. When the programmer tries to send a message to an object that doesn’t respond to that message, the runtime packages the message up as an object and calls a catch-all message instead. In Smalltalk this is usually called doesNotUnderstand: message.
In Objective C, a special message called forwardInvocation: is called to give the programmer a chance to send the message to some other object such as a delegate. The default implementation of forwardInvocation: doesn’t do any forwarding. Instead it just calls another method doesNotRespondToSelector: which raises an exception.
The programmer may choose to respond to these messages in a class specific way – polymorphically, by overriding forwardInvocation: or doesNotUnderstand:
Doing this moves the error handling to a central location rather than forcing the programmer to scatter it throughout the code at the call locations. The end result is cleaner, smaller code.
Plus there’s a bonus. Being able to forward messages provides a clean mechanism for building chain of command patterns and allows an object to be “decorated” with new behaviors dynamically.
Its also easy to do distributed computing by having the forwardInvocation method perform remote procedure calls over the network without the need to do clumsy code generation of proxies and stubs common in C, CORBA, and Java RMI programs. A single proxy class can stand in for any kind of object.
The doesNotUnderstand: message can also provide a trigger for database fetching and object faulting. When a message is sent to a simple database query object that implements almost no messages, doesNotUnderstand: is invoked, the database query is executed, and the object replaced with the results of the fetch. The message is then delivered to the newly fetched object. Such faulting mechanisms can simplify programming and virtually eliminate the need for application programmers to directly interact with a database API.
These extra capabilities are nearly impossible to implement in the statically typed environments and this is clearly a case when the dynamically typed environment yields simpler application code (we don’t have all those try/catch blocks or instanceof tests). Simpler application code means greater reliability with reduced programmer effort. This all translates to faster development times and lower costs.
“This is a huge step up from the C++ behavior of crashing”
This is wrong. C++ has dynamic_cast, which will throw an exception (for references) or return NULL (for pointers) if the object can’t be cast to the provided type. Java’s “huge step” isn’t that huge afterall.
> ((A)myB).doAThing(); // error – object is not an A!
Actually, it’s not even compileable, because the compiler can prove that a B cannot be an A.
>Actually, it’s not even compileable, because the compiler can prove that a B cannot be an A.
You’ve missed the point of the example I think.
In this case, yes the compiler can tell – but separate the initialization (in a function returning Object for instance) and the point is, the compiler will happily build the program because myB *could* be an A.
[...] Black Bag Operations Network Weapons and Intelligence in the War Against “Them” « Function Calling vs Message Sending BadPage.info moved to BadPage.net » [...]