Home > Mobile >  Generic working : compile time vs runtime
Generic working : compile time vs runtime

Time:04-17

What actually happens in a generic class when we compile the file and what happens at runtime? how does T behave at compile time vs runtime? What was the main purpose of introducing generics? Since we can do the same thing with the Object class. I am very much confused and have spent 3 months understanding this generic topic. Kindly anyone here, explain it in every detail. Thanks

//demo class, basically what is happening here ?
class Generic<T>{
Generic(){
T[] arr = (T[]) new Object[5];
}
public static void main(String [] args) {
new Generic();
}
}  // class


// another demo class , let say i have a Student class
class AnotherGeneric<T extends Student> {
T fun(){
T data = (T)new Object();
return data;
}

public static void main(String[] args) {
Student std = new AnotherGeneric<Student>().fun();
}
}// class

CodePudding user response:

Mostly, generics just disappear entirely at runtime. Generics is, in essence, "compiler checked documentation". It's a way to get both of these things at the same time:

  • You have a method that returns, say, a List. You'd like to document that the list only contains strings.
  • You'd like for the compiler to be aware of this and tell users who treat that list as if it contains something other than strings to go: "Hey, there - hang on. I don't think you understand how this method works, given that you appear to be treating it as if it has non-strings in it, which it won't, as the documentation says that it won't". Or vice versa: "Hey there - hang on. You documented that the list you return only contain strings but you appear to be attempting to stuff a number in there. That doesn't make sense. I shall not compile this inconsistency until you fix it".

And very much in last place, generics makes your code very slightly shorter, as the compiler will inserts casts for you silently. When the method is documented to return only a list of strings, when you call that method and then call .get(0) on the result, the compiler "pre-casts" it to a String for you.

That's it. It doesn't change anything at runtime. Those casts are even generated by the compiler.

So, how does it work:

  • In signatures, generics is compiled into the class file, but the JVM treats these as effectively 'a comment' - the JVM completely ignores them. The point of this is solely for the benefit of javac, who can read these comments and act accordingly. In other words, the fact that ArrayList has generics needs to be known by javac in order to properly compile the line new ArrayList<String>() - and how does javac know? By checking the class file that contains the ArrayList code. Signatures are:

    • The name of a class.
    • The extends and implements clauses of a class.
    • The type of every field, and the name of every field.
    • The return type of every method, and the name of every method.
    • The type (not name) of every parameter of a method.
    • The throws clause of a method.

Everywhere else, generics just disappear. So, if you write inside a method: List<String> list = new ArrayList<String>();, the code you end up with is JUST new ArrayList() in the class file. That string is just gone. It also explains why given a List<?> x; there is simply no way to ask this list: What is your component type. Because it is no longer available at runtime.

  • Javac uses this information to figure out what to do.
  • For the purposes of compilation, ALL generics-typed stuff is compiled as if they are their lower bound.

What about generic casts?

The closest other java language feature to a generic cast is @SuppressWarnings. A generic cast does literally nothing. It's just you telling the compiler: Shut up, I know what I'm doing (hence, you best really know what you are doing to use them!!).

For example, given:

void foo(List<?> x) {
  List<String> y = (List<String>) x;
}

The compiler does nothing. There is no way for the compiler to generate code that actually checks if x really is a List. The above code cannot throw an exception, even if there are non-strings in that list. As I said before, generics also cause the compiler to inject casts. So, if you later write:

x.get(0).toLowerCase();

That will compile (there is no need to cast x.get(0) to String, however, it is compiled that way!) - and if you pass a list to this method that has a non-string object as first item, that line throws a ClassCastException even though you didn't write any casts on that line. That's because the compiler inserted a cast for you.

Think about it like this: Generics are for linking types in signatures.

Imagine you want to write a method that logs its argument and then just returns it. That's all it does.

You want to now 'link' the type of the argument to the return type: You want to tell the compiler and all users of this method: Whatever type you feed into this method is identical to the type that rolls of it.

In normal java you cannot do this:

Object log(Object o) {
  log.mark("Logged: {}", o);
   return o;
}

The above works fine but is annoying to use. I can't do this:

  String y = scanner.next();
  new URL(log(y)).openConnection();

The reason I can't do that, is the log(y) expression is of type Object, and the URL constructor requires a String. Us humans can clearly see that log(y) is obviously going to return a string, but the signature of the log method doesn't indicate this at all. We have to look at the implementation of log to know this, and perhaps tomorrow this implementation changes. The log method does not indicate that any future updates will continue to just 'return the parameter' like this. So javac does not let you write this code.

But now we add generics:

public <T> T log(T o) {
 log.mark("Logged: {}", o);
 return o;
}

And now it works fine. We've told the compiler that there exists a link between the 2 places we used T in this code: The caller gets to choose what T ends up being, and the compiler ensures that no matter what the caller chose, your code works.

Hence, if you define a type parameter and use it exactly 0 or 1 times, it's virtually always either a bug or a weird hack. The point is to link things and '0 or 1 times' is obviously not linking things.

Generics goes much further than this, your question's scope is far too broad. If you want to know every detail, read the Java Lang Spec, which gets into hopeless amounts of detail that will take your 6 months to even understand. There's no real point to this. You don't need to know the chemical composition of brake fluid to drive a car either.

CodePudding user response:

This is the way I was taught the importance of generics.

Imagine that you were blindfolded, then told to do some basic task, such as move boxes from one side of the room to the other. Now also imagine that the room is full of other blindfolded people doing exactly the same thing as you.

Programming without generics would be tell all of these people to do their tasks, and then run the risk of them accidentally crashing into each other and damaging the boxes.

Programming with generics would be to sit down with each blindfolded person, and give all of them a very specific plan beforehand. For example, tell one of them to go forward 10 feet, grab the box on the floor in front of them, turn 180 degress, then go 10 feet, then put the box down. Then (and this is the important part) you draw a map of all of the plans and make sure that each of the blindfolded people's paths CANNOT cross each other. That is what generics give you. If you can prove that none of paths cross each other, then it doesn't matter if they are blindfolded - they cannot bump into each other - by design!

Once you can prove that they cannot bump into each other, you can start doing something more complex, like telling one blindfolded person to hand a box to another blindfolded person. And if you get really good at it, you can have paths that actually do cross, but only one person is crossing the intersection at the time.

That is the power of generics in Java - you can perform unsafe actions safely by planning it all ahead of time - at compile time! Then, when you are at runtime, it doesn't matter that you are blind - you know exactly what to do, and you have proven that you cannot crash into anyone else. As a result, when you actually do the task, you don't slowly shuffle forwards, putting your hands in front of you, constantly checking in fear that you will bump into someone else. You sprint headfirst forwards, blindly, but confident that you cannot fail, because the entire path has been mapped out for you.

Now, I should mention, the only way Java generics work is by ensuring none of the paths cross. You do this by turning on warnings when you compile your java code. If you get warnings about unchecked or raw, then that means your code is not safe, and you need to fix your plan. Once you compile with no warnings related to generics, you can be certain that your types are safe and will not crash into each other unexpectedly.

And finally, generics are powerful, but they do not play well with nulls. If you let nulls sneak into your code, that is a blindspot which generics cannot protect you from. Be very certain to limit, if not remove, the nulls in your code, otherwise your generics may not be bulletproof. If you avoid nulls and compile without warnings, you can guarantee that your code will never run into a type error unexpectedly.

  • Related