<The previous article in this series | The table of contents of this series |
If We Are Being Troubled with Unhandiness of Generics, Cheating Might Be an Option
About: Java Programming Language
Here are -Hypothesizer and -Rebutter sitting in front of a computer screen in a room on a brook among some mountains on the Bias planet.
Generics is very handy except when it's unhandy.
Ah, I can guess, but I won't. You should elaborate.
Before I elaborate, let's clarify our terminology. As it's cumbersome to formulate accurate definitions of terms, let's think in an example.
-Hypothesizer writes this in an editor on the computer screen.
public class AGenericClass <T> {
private T i_t;
private List <T> i_listOfT;
private List <String> i_listOfStrings;
public <U> U aGenericMethod (U a_u) {
return a_u;
}
}
'AGenericClass <T>' is a generic class; '<U> aGenericMethod' is a generic method; 'T' and 'U' are type parameters; 'T' of 'i_t', 'List <T>', 'List <String>', 'U' as the return type of '<U> aGenericMethod', and 'U' of 'a_u' are parameterized types; 'AGenericClass' is a raw class.
So, also 'List <T>' without 'T' determined as a concrete class is a parameterized type, in our terminology.
As I'm reading a reference document, 'Lesson: Generics (Updated)' of 'The Java™ Tutorials', . . .
-Hypothesizer opens the document in a Web browser on the computer screen.
. . . it calls 'List <String>' a parameterized type, but it doesn't mention what should 'List <T>' be called. We are calling 'List <T>' a parameterized type because, for our descriptions here, we seem to be able to treat them the same way. That is, we are concerned with whether these casts are allowed.
-Hypothesizer writes this in the 'aGenericMethod' method above.
Object l_objectAsString = "string1";
Object l_objectAsListOfStrings = new ArrayList <String> ();
i_t = (T) l_objectAsString;
i_listOfTs = (List <T>) l_objectAsListOfStrings ;
i_listOfStrings = (List <String>) l_objectAsListOfStrings;
So, that's the reason why we call also 'T' of 'i_t' a parameterized type . . .
Most people may not call 'T' of 'i_t' a parameterized type, but certainly, it's the type of 'i_t', and it's parameterized . . .
-Rebutter scan the reference document.
As far as I scan the reference document, it doesn't seem to mention whether casts of '(T)' or '(List <T>)' is allowed . . .
Ah, I think, some descriptions in the reference document are confusing. Didn't you interpret them as though we "Cannot Use Casts with Parameterized Types" and "Typically," we "cannot cast to a parameterized type unless it is parameterized by unbounded wildcards"?
Being asked whether I interpreted so, they are literal expressions in the reference document . . .
But that's your misinterpretation: we can use casts except when they are definitely illegitimate. In fact, we can do those casts above, although, certainly, we get warnings. To say more, we can even cast 'a_u' to 'T'.
Well, the reference document says of the compile error of this.
List <Integer> li = new ArrayList <> ();
List <Number> ln = (List <Number>) li;
But that isn't because casts with parameterized types aren't allowed, but because 'List <Integer>' isn't any sub class or super class of 'List <Number>'.
That's the same argument with the one we did for arrays. Any 'List <Integer>' instance isn't a kind of 'List <Number>' because the definition of 'List <Number>' is 'a list that can accept any and only Number instances', which any 'List <Integer>' doesn't fulfill because it doesn't accept some Number instances such as Double instances.
So, that cast is definitely illegitimate, which is the reason why that cast isn't allowed, not that "Cannot Use Casts with Parameterized Types".
To make things clear, we get warnings for our casts above because of so-called 'type-erasure', which means that all the type parameters are replaced with Object (if they are unbounded) or their bound types (if not), after the compilation.
So, 'type erasure' really means 'type replacement'.
Of course. The type of a variable can't be just erased: the variable needs to be of a type.
A cast is meant to be a two-phase operation: at the compile time, the compile error is suppressed, and at the run time, the instance is checked whether it conforms to the cast type.
But for any parameterized type, the run time check can't be done properly because the parameterized type isn't preserved as we hope it to be at the run time: a cast, '(T)', has been really reduced to '(Object)', which is meaningless.
Anyway, we can use casts with parameterized types if we don't need any run time check.
In fact, there are such cases: as the compiler doesn't follow intricate logics to confirm type safety, the compiler doesn't know it's type safe, but the programmer knows it's type safe, at least practically.
By the way, the description in the reference document about "Cannot Create Arrays of Parameterized Types" is also confusing.
Ah, I say, the real culprit is allowing the cast to 'Object []': as we discussed before, that cast shouldn't be allowed because 'List <Integer> []' isn't any kind of 'Object []' . . .
Instantiating an array of a parameterized type isn't really a problem at all, but in order to gloss the misdeed of allowing such illegitimate casts, it is being prohibited . . .
Well, prohibiting it is unreasonable, but actually, instantiating an array of a parameterized type isn't necessary or meaningful.
That's true. As an instance of an array of a parameterized type is really an instance of an array of the raw type or Object because of 'type erasure', these two lines are the same in the run time world.
-Hypothesizer writes these on the editor.
List <String> [] l_listsOfStringsArray = new List <String> [1]; // Causes a compile error.
List <String> [] l_listsOfStringsArray = new List [1];
Or these two lines are the same in the run time world.
-Hypothesizer writes these on the editor.
T [] l_tsArray = new T [1]; // Causes a compile error.
T [] l_tsArray = (T []) new Object [1];
We should note that the latter are allowed.
Yes. "Cannot Create Arrays of Parameterized Types" means 'cannot instantiate arrays of parameterized types', not 'cannot define variables of arrays of parameterized types'.
So, what are unhandy of generics?
To say succinctly, we can't get the class of any type parameter.
Ah.
Not being able to create any instance of any type parameter is just a secondary matter of it: if we could get the class, we would be able to create instances from it.
The well-known end run is to pass the class explicitly.
I know, but it isn't always possible, not to mention that it's cumbersome.
You mentioned it. . . . Anyway, when is it impossible?
Well, I will show a problem I'm facing right now, which doesn't look so easy to solve.
Before that, note that we can't do 'new T', but can do 'new ArrayList <T> ()'.
Ah, I can guess why.
Considering 'type erasure', 'new ArrayList <T> ()' becomes really 'new ArrayList ()' anyway whatever 'T' is. So, 'new ArrayList <T> ()' isn't particularly meaningful, but isn't any hindrance for creating the instance.
To state boldly, if we call the world of run time the reality, generic types are fantasies that only source codes see.
The same with the previous scene.
I'm trying to create a method that builds a cascade map, reading a cells block on a spread sheet.
What do you mean by 'cascade map'?
For example, Map <String, Map <String, Map <String, Map <String, List <String>>>>>.
Ah, the map's value is a map whose value is a map whose value is a map . . .
And the last value is a List, in this case.
And the depth of the hierarchy is variable, I guess?
Yes.
So, what will be the method like? . . . Certainly, the logic to build the hierarchy isn't difficult. In fact, if we are fine with the result datum type's being 'Map <String, Object>', it's easy like this (this is just a simplified model: reading the spread sheet, etc. are omitted).
-Hypothesizer opens a source file on the computer screen.
package test.genericstest1;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Test2Test {
private Test2Test () {
}
public static void main (String [] p_arguments) throws Exception {
Test2Test.test ();
}
public static void test () {
Map <String, Object> l_map = new HashMap <String, Object> ();
Test2Test.buildMap (l_map, 5);
System.out.println (l_map);
}
public static void buildMap (Map <String, Object> a_mapToBeBuilt, int a_remainingDepth) {
String l_key = String.format ("key%d", a_remainingDepth - 1);
Object l_value = null;
if (a_remainingDepth <= 1) {
// The depth has to be at least 2 to build a map of this type
return;
}
else {
if (a_remainingDepth == 2) {
List <String> l_childMap1 = new ArrayList <String> ();
l_childMap1.add ("value1");
l_childMap1.add ("value2");
l_value = l_childMap1;
}
else {
Map <String, Object> l_childMap2 = new HashMap <String, Object> ();
Test2Test.buildMap (l_childMap2, a_remainingDepth - 1);
l_value = l_childMap2;
}
}
a_mapToBeBuilt.put (l_key, l_value);
}
}
The result is this.
{key4={key3={key2={key1=[value1, value2]}}}}
Ah, a recursive method.
But we want the datum in 'Map <String, Map <String, Map <String, Map <String, List <String>>>>>', not in 'Map <String, Object>'. . . . You might wonder why I want to get the map instance in the specific generic type, 'Map <String, Map <String, Map <String, Map <String, List <String>>>>>', not in 'Map <String, Object>', . . .
I particularly don't . . .
. . . the reason is that it's handy afterwards: otherwise, I would have to cast the Object value to Map at each level of the hierarchy.
So, my first idea was to make it like this.
-Hypothesizer opens another source file on the computer screen.
package test.genericstest1;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Test3Test {
private Test3Test () {
}
public static void main (String [] p_arguments) throws Exception {
Test3Test.test ();
}
public static void test () {
Map <String, Map <String, Map <String, Map <String, List <String>>>>> l_map = new HashMap <String, Map <String, Map <String, Map <String, List <String>>>>> ();
Test3Test. <Map <String, Map <String, List <String>>>>buildMap (l_map, 5);
System.out.println (l_map);
}
@SuppressWarnings("unchecked")
public static <U> void buildMap (Map <String, Map <String, U>> a_mapToBeBuilt, int a_remainingDepth) {
String l_key = String.format ("key%d", a_remainingDepth - 1);
Map <String, U> l_value = null;
if (a_remainingDepth <= 2) {
// The depth has to be at least 3 to build a map of this type
return;
}
else {
if (a_remainingDepth == 3) {
Map <String, U> l_childMap1 = new HashMap <String, U> ();
String l_childKey = String.format ("key%d", 1);
List <String> l_grandchildList = new ArrayList <String> ();
l_grandchildList.add ("value1");
l_grandchildList.add ("value2");
l_childMap1.put (l_childKey, (U) l_grandchildList);
l_value = l_childMap1;
}
else {
/* ??? How can I get 'U' for the next iteration?
Map <String, Map <String, ?>> l_childMap2 = new HashMap <String, Map <String, ?>> ();
Test3Test. <?>buildMap (l_childMap2, a_remainingDepth - 1);
l_value = (Map <String, U>) l_childMap2;
*/
}
}
a_mapToBeBuilt.put (l_key, l_value);
}
}
Well, obviously, it is unfinished, having a '???' mark.
Yes. I don't know how to do with them . . .. To make some explanations, 'U' is parameterized because it has to be 'Map <String, Map <String, Map <String, List <String>>>>', 'Map <String, Map <String, List <String>>>', 'Map <String, List <String>>', and 'List <String>', iteratively.
At the '???' mark, I have to extract 'U' for the next iteration from the present 'U', for example 'Map <String, Map <String, List <String>>>' from 'Map <String, Map <String, Map <String, List <String>>>>'. . . . Well, how?
You know, you can't get such information from 'U'.
I know. Such information is run time information, but at the run time, 'U' is just Object. . . . So, should I explicitly pass the class into the first iteration, and extract the next 'U' class in each iteration from the class passed into the iteration?
I think, that's meaningless, because at the run time, the class is just the raw Map because of 'type erasure': you won't see it as 'Map <T, Map <T, Map <T, List <T>>>>'. As you said, there isn't such a thing in the reality. . . .
The same with the previous scene.
So, what can I do?
May I speak freely, sir?
You may.
You can cheat.
. . . How?
. . . You don't really understand what you, yourself, said, do you?
Well . . .
You, yourself, said, "generic types are fantasies". In the reality, there isn't such thing as an instance of 'Map <String, Map <String, List <String>>>'.
Yes, . . . so?
You don't have to specify the exact type for the next 'U', which is a fantasy. Why don't you just substitute Object for 'U'?
Ah----ha. How didn't I realize that?
I don't know.
Generic types are solely for compile checks. So, all what I have to do is just to silence the compiler: the result map instance is the same, a raw Map, whether I legitimately (in the world of fantasies) build the map instance or not. . . . So, I can just create an instance of Map <String, Object>, and cast it to 'U'.
-Hypothesizer edits the source file (included in a zip file; the explanation of know to use the zip file is here).
package test.genericstest1;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Test3Test {
private Test3Test () {
}
public static void main (String [] p_arguments) throws Exception {
Test3Test.test ();
}
public static void test () {
Map <String, Map <String, Map <String, Map <String, List <String>>>>> l_map = new HashMap <String, Map <String, Map <String, Map <String, List <String>>>>> ();
Test3Test. <Map <String, Map <String, List <String>>>>buildMap (l_map, 5);
System.out.println (l_map);
}
@SuppressWarnings("unchecked")
public static <U> void buildMap (Map <String, Map <String, U>> a_mapToBeBuilt, int a_remainingDepth) {
String l_key = String.format ("key%d", a_remainingDepth - 1);
Map <String, U> l_value = null;
if (a_remainingDepth <= 2) {
// The depth has to be at least 3 to build a map of this type
return;
}
else {
if (a_remainingDepth == 3) {
Map <String, U> l_childMap1 = new HashMap <String, U> ();
String l_childKey = String.format ("key%d", 1);
List <String> l_grandchildList = new ArrayList <String> ();
l_grandchildList.add ("value1");
l_grandchildList.add ("value2");
l_childMap1.put (l_childKey, (U) l_grandchildList);
l_value = l_childMap1;
}
else {
Map <String, Map <String, Object>> l_childMap2 = new HashMap <String, Map <String, Object>> ();
Test3Test. <Object>buildMap (l_childMap2, a_remainingDepth - 1);
l_value = (Map <String, U>) l_childMap2;
}
}
a_mapToBeBuilt.put (l_key, l_value);
}
}
Let's do the test.
-Hypothesizer opens a terminal on the computer screen, changes the current directory to 'coreUtilitiesTestToDisclose', and executes these commands.
For Gradle:
gradle run -PMAIN_CLASS_NAME="test.genericstest1.Test3Test"
For Ant:
ant run -DMAIN_CLASS_NAME="test.genericstest1.Test3Test"
{key4={key3={key2={key1=[value1, value2]}}}}
It worked!
I will tell you one thing that might be shocking to you.
Which is?
You didn't particularly have to create the new method: you could do with the old method that builds 'Map <String, Object>'.
Huh?
You can just cast the 'Map <String, Object>' instance to 'Map <String, Map <String, Map <String, Map <String, List <String>>>>>' by '(Map <String, Map <String, Map <String, Map <String, List <String>>>>>) (Map)'.
!
- Oracle and/or its affiliates. (2017). The Java™ Tutorials. Retrieved from https://docs.oracle.com/javase/tutorial/index.html
<The previous article in this series | The table of contents of this series |