2017-12-03

7: Cheating at Generics

<The previous article in this series | The table of contents of this series |

Main Body START

If We Are Being Troubled with Unhandiness of Generics, Cheating Might Be an Option

About: Java Programming Language

Well-Known Unhandiness of Generics and Understanding that Generic Types Are Fantasies That Only Source Codes See

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.

-Hypothesizer

Generics is very handy except when it's unhandy.

-Rebutter

Ah, I can guess, but I won't. You should elaborate.

-Hypothesizer

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.

@Java Source Code
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.

-Rebutter

So, also 'List <T>' without 'T' determined as a concrete class is a parameterized type, in our terminology.

-Hypothesizer

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.

-Hypothesizer

. . . 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.

@Java Source Code
  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;

-Rebutter

So, that's the reason why we call also 'T' of 'i_t' a parameterized type . . .

-Hypothesizer

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.

-Rebutter

As far as I scan the reference document, it doesn't seem to mention whether casts of '(T)' or '(List <T>)' is allowed . . .

-Hypothesizer

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"?

-Rebutter

Being asked whether I interpreted so, they are literal expressions in the reference document . . .

-Hypothesizer

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'.

-Rebutter

Well, the reference document says of the compile error of this.

@Java Source Code
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>'.

-Hypothesizer

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.

-Rebutter

So, that cast is definitely illegitimate, which is the reason why that cast isn't allowed, not that "Cannot Use Casts with Parameterized Types".

-Hypothesizer

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.

-Rebutter

So, 'type erasure' really means 'type replacement'.

-Hypothesizer

Of course. The type of a variable can't be just erased: the variable needs to be of a type.

-Rebutter

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.

-Hypothesizer

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.

-Rebutter

Anyway, we can use casts with parameterized types if we don't need any run time check.

-Hypothesizer

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.

-Rebutter

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 []' . . .

-Hypothesizer

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 . . .

-Rebutter

Well, prohibiting it is unreasonable, but actually, instantiating an array of a parameterized type isn't necessary or meaningful.

-Hypothesizer

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.

@Java Source Code
List <String> [] l_listsOfStringsArray = new List <String> [1]; // Causes a compile error.

@Java Source Code
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.

@Java Source Code
T [] l_tsArray = new T [1]; // Causes a compile error.

@Java Source Code
T [] l_tsArray = (T []) new Object [1];

-Rebutter

We should note that the latter are allowed.

-Hypothesizer

Yes. "Cannot Create Arrays of Parameterized Types" means 'cannot instantiate arrays of parameterized types', not 'cannot define variables of arrays of parameterized types'.

-Rebutter

So, what are unhandy of generics?

-Hypothesizer

To say succinctly, we can't get the class of any type parameter.

-Rebutter

Ah.

-Hypothesizer

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.

-Rebutter

The well-known end run is to pass the class explicitly.

-Hypothesizer

I know, but it isn't always possible, not to mention that it's cumbersome.

-Rebutter

You mentioned it. . . . Anyway, when is it impossible?

-Hypothesizer

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> ()'.

-Rebutter

Ah, I can guess why.

-Hypothesizer

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.

A Problem with Generics

The same with the previous scene.

-Hypothesizer

I'm trying to create a method that builds a cascade map, reading a cells block on a spread sheet.

-Rebutter

What do you mean by 'cascade map'?

-Hypothesizer

For example, Map <String, Map <String, Map <String, Map <String, List <String>>>>>.

-Rebutter

Ah, the map's value is a map whose value is a map whose value is a map . . .

-Hypothesizer

And the last value is a List, in this case.

-Rebutter

And the depth of the hierarchy is variable, I guess?

-Hypothesizer

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.

@Java Source Code
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);
 }
}

-Hypothesizer

The result is this.

@Output
{key4={key3={key2={key1=[value1, value2]}}}}

-Rebutter

Ah, a recursive method.

-Hypothesizer

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>', . . .

-Rebutter

I particularly don't . . .

-Hypothesizer

. . . 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.

@Java Source Code
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);
 }
}

-Rebutter

Well, obviously, it is unfinished, having a '???' mark.

-Hypothesizer

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?

-Rebutter

You know, you can't get such information from 'U'.

-Hypothesizer

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?

-Rebutter

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. . . .

Let's Cheat!

The same with the previous scene.

-Hypothesizer

So, what can I do?

-Rebutter

May I speak freely, sir?

-Hypothesizer

You may.

-Rebutter

You can cheat.

-Hypothesizer

. . . How?

-Rebutter

. . . You don't really understand what you, yourself, said, do you?

-Hypothesizer

Well . . .

-Rebutter

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>>>'.

-Hypothesizer

Yes, . . . so?

-Rebutter

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'?

-Hypothesizer

Ah----ha. How didn't I realize that?

-Rebutter

I don't know.

-Hypothesizer

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).

@Java Source Code
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);
 }
}

-Hypothesizer

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:

@bash or cmd Source Code
gradle run -PMAIN_CLASS_NAME="test.genericstest1.Test3Test"

For Ant:

@bash or cmd Source Code
ant run -DMAIN_CLASS_NAME="test.genericstest1.Test3Test"

@Output
{key4={key3={key2={key1=[value1, value2]}}}}

-Hypothesizer

It worked!

-Rebutter

I will tell you one thing that might be shocking to you.

-Hypothesizer

Which is?

-Rebutter

You didn't particularly have to create the new method: you could do with the old method that builds 'Map <String, Object>'.

-Hypothesizer

Huh?

-Rebutter

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)'.

-Hypothesizer

!

Main Body END

References

  • 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 |