<The previous article in this series | The table of contents of this series | The next article in this series>
Let's Convert, Not Cast, Arrays
About: Java Programming Language
Ah, we can't cast an Object [] instance that contains only String elements to the String [] type. That is so . . .
Of course, we can't.
Of course? Is that such self evident? Well, we are talking about a program like this.
void aMethod (String [] p_stringsArray) {
for (String l_string: p_stringsArray) {
System.out.println (l_string);
}
}
void anotherMethod () {
Object [] l_objectsArray = new Object [2];
l_objectsArray [0] = "A String";
l_objectsArray [1] = "Another String";
aMethod ( (String []) l_objectsArray);
}
That program can be successfully compiled, but throws an exception, "java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String", at run time.
Can it be successfully compiled? I'm rather surprised at that, not at the run time exception.
Are you? Why?
Because that is a violation of a principle of object oriented languages, and the compiler should know that at compile time.
I rather thought, "What's the harm of the casting?"
You may think that that won't cause any harm, but it is only for your program. Generally speaking, such a violation of a principle of object oriented languages can cause some harms.
I don't understand how that casting is a violation of a principle of object oriented languages.
Well, do you understand that casting of objects is intrinsically different from casting of primitives?
I think I do.
Let's review it.
When a primitive is cast, the value itself is converted. For example, let's think of this casting.
int l_int = 1;
short l_short = (short) l_int;
The value contained in 'l_int' is a four bytes value while the value contained in 'l_short' is a two bytes value: the value is converted by the casting.
On the other hand, when an object is cast, the object itself isn't converted at all. For example, let's think of this casting.
Object l_object = "A String";
String l_string = (String) l_object;
The String object, "A String", isn't converted by the casting. Casting for an object isn't a conversion of the object, but just a reassurance by the programmer to the compiler: "Mr. compiler, you may not know that 'l_object' is a kind of String, but I know and assure you that it is a kind of String. So, please let it compiled. If something goes wrong, I will take the responsibility."
Yes, you seem to understand. The compiler suppresses the compile error if 'l_object' could be a kind of String, but if 'l_object' isn't really a kind of String at run time, the runtime will throw an exception of 'java.lang.ClassCastException'.
So what?
Well, you seem to not understand that any Objects array instance is never a kind of Strings array.
Is it?
Think of the definition of Strings array.
The definition of String [] is an array into which any String instance can be put as an element.
That isn't so. The property that only String instances can be put into it is crucial. Otherwise, we couldn't do like this.
void aMethod (String [] p_stringsArray) {
for (String l_string: p_stringsArray) {
System.out.println (l_string);
}
}
Ah, if it isn't guaranteed that all the elements are Strings, we can't do that. So, a more accurate definition of String [] is an array into which any String instance and only String instances can be put as an element.
If something is able to be said 'a kind of String []', that something must fulfill the definition. That is the meaning of definition. Does any Object [] fulfill that definition?
As non-String element can be put into any Object [] instance, certainly, any Object [] instance doesn't fulfill the definition. Even if an Object [] instance contained only String elements at a time, that doesn't mean that only String elements can be put into the Object [] instance. As for my first program above, even if the Object [] instance had only String elements at the time of casting, any non-String element can be put into it afterward, for example from another thread. As the Object [] instance isn't an immutable, we can't say it's a kind of String [] because it contained only String elements at a time.
So, any Object [] instance isn't a kind of String [], and such casting should never be allowed, for otherwise, we couldn't trust something cast to String [] to be a kind of String []. What a weird world would that be? A value of a String [] variable might not be a kind of String [] . . .. Then what is the type declaration for?
On the other hand, we can do like this.
void aMethod (Object [] p_objectsArray) {
for (Object l_object: p_objectsArray) {
System.out.println (l_object.toString ());
}
}
void anotherMethod () {
String [] l_stringsArray = new String [2];
l_stringsArray [0] = "A String";
l_stringsArray [1] = "Another String";
aMethod (l_stringsArray);
}
Huh? Can we do that?
Actually, we can.
Doesn't the String [] even have to be cast to Object []?
It seems to be automatically cast to Object [], and the program doesn't throw any run time exception.
That's a problem. You know, any String [] isn't a kind of Object [].
That's more obvious than before. The definition of Object [] is an array into which any Object instance and only Object instances can be put as an element. As any String [] doesn't fulfill the definition (into which only String instances can be put), it isn't a kind of Object [].
By analogy of any String's being a kind of Object, any String [] might intuitively feel to be a kind of Object [], but that isn't so.
Any array of type 'XXX' isn't a kind of array of type 'YYY' when 'XXX' isn't 'YYY'. Whether 'XXX' is an ascendant or a descendant of 'YYY', it doesn't matter. So, casting between array types is fundamentally illegitimate.
And the compiler should be able to detect such illegitimate casting at compile time. In fact, your first program above shouldn't be successfully compiled.
The compiler lets it compiled because we can do like this.
void anotherMethod () {
Object [] l_objectsArray = new String [2];
l_objectsArray [0] = "A String";
l_objectsArray [1] = "Another String";
aMethod ( (String []) l_objectsArray);
}
Plainly speaking, "Object [] l_objectsArray = new String [2];" shouldn't be allowed because the String [] instance isn't a kind of Object []. As the compiler allows that, "(String []) l_objectsArray" becomes possible. If the compiler did legitimate type checks from the beginning, all casting between array types could be detected as errors at compile time.
To understand harms of allowing such illegitimate casting, let's think of a program like this.
public static void aMethod (Object [] p_objectsArray) {
if (p_objectsArray != null && p_objectsArray.length > 0) {
p_objectsArray [0] = new Integer (1);
for (Object l_object: p_objectsArray) {
System.out.println (l_object.toString ());
}
}
}
That seems a quite normal and infallible program: the author of the method declared the argument as Object [], and therefore treated it as such. If the method fails, it will be absurd.
However, we can easily make it fail like this.
public static void anotherMethod () {
String [] l_stringsArray = new String [2];
l_stringsArray [0] = "A String";
l_stringsArray [1] = "Another String";
aMethod (l_stringsArray);
}
It is absurd that while the argument is declared as Object [], it can't be trusted to be a kind of Object []. . . . And we didn't even do any casting.
As we do casting on our responsibility, something might go wrong if our casting is wrong. But we didn't do any casting, and such a type exception can happen . . .
After all, when we have to put an array to a variable of another array type (including passing the array to a method argument), we should forget casting, and do converting without reserve.
Casting, including auto casting, may be allowed by the compiler, and we may even get away with it, but it is never legitimate.
We can convert arrays using 'java.util.Arrays.copyOf' like this.
import java.util.Arrays;
String [] l_stringsArray = new String [2];
l_stringsArray [0] = "A String";
l_stringsArray [1] = "Another String";
Object [] l_objectsArray = Arrays. <Object, String>copyOf (l_stringsArray, l_stringsArray.length, Object [].class);
It might look lengthy compared with casting, but it is a necessary thing to do.
If calling that function feels cumbersome, I think, a syntax sugar for copying arrays should be implemented into the program language. Definitely, allowing illegitimate, weird casting shouldn't be the solution.
And if a base class of various objects array types (array types whose elements types are not primitives) is desired, I think, there should be a base abstract class of all the objects array types.
I guess, that base class allows us to get elements in the Object type, but not put anything into it, for what can be put into it can't be judged from the type.
Yes, but we can use the base type as an argument of a method and cast an instance referred to by the argument to a concrete array type, for example String [], if the instance is really a String [] instance. Then, we can put elements into it. That casting isn't illegitimate or weird, and conforms to all the principles of object oriented languages.
<The previous article in this series | The table of contents of this series | The next article in this series>