About Generics and Method Signatures

Prior to the advent of JDK 1.5 the Java Language Specification gave the following explanation about the meaning of method signature:

"A method signature consists of the name of the method, and the number and types of the formal parameters to the method. A class may not declare two methods with the same method signature, or a compile-time error occurs." [JLS 8.4.2]

Notice that the return type of a method is not part of a method signature, therefore, two methods have the same parameters, even if the return type is different, the method signature would still be the same for both methods. For instance attempting somewhat like the following code snippet was illegal and yielded a compile-time error as the JLS explained:

  1. public static int sum( ints){
  2. int sum = 0;
  3. it = ints.iterator();
  4. while(it.hasNext()){
  5. i = () it.next();
  6. sum += i.intValue();
  7. }
  8. return sum;
  9. }
  10.  
  11. public static sum( words){
  12. sb = new ();
  13. it = words.iterator();
  14. while(it.hasNext()){
  15. sb.append(it.next());
  16. }
  17. return sb.toString();
  18. }

This is correct according tot he JLS specification, since both methods are receiving a list, even when both return different types, the method signatures are exactly the same. In those days, one solution of the problem would be to use arrays instead of a collection, since arrays have reifiable types. In other words, this second code snippet was legal.

  1. public static int sum(int[] numbers){
  2. int sum = 0;
  3. for(int i=0; i < numbers.length; i++){
  4. sum += numbers[i];
  5. }
  6. return sum;
  7. }
  8.  
  9. public static sum([] words){
  10. sb = new ();
  11. for(int i=0; i < words.length; i++){
  12. sb.append(words[i]);
  13. }
  14. return sb.toString();
  15. }

With the introduction of generics in JDK 1.5, now the first example should work, basically because we are able to define the explicit type of every collection now. Therefore, today, using generics, this compiles perfectly.

  1. public static int sum(List<Integer> ints){
  2. int sum = 0;
  3. for(int i : ints){
  4. sum += i;
  5. }
  6. return sum;
  7. }
  8.  
  9. public static sum(List<String> strings){
  10. StringBuilder sum = new StringBuilder();
  11. for( string : strings){
  12. sum.append(string);
  13. }
  14. return sum.toString();
  15. }

There is a small problem, though, as you know, at runtime, Java uses a type erasure for generic types. Therefore, at runtime, Java replaces List<Integer> and List<String> with its type erasure: List.

Perhaps in other post I can explain the reason for this, but mostly it is related with compatibility with legacy code. But the thing is that, if you compile the class and then use the Java disassembler command line tool javap to review the bytecode generated, you will notice that the definition of the methods are like this:

  1. public static int sum(java.util.);
  2. public static java.lang.(java.util.);

As you can see the method signatures are exactly the same. The only difference is the return type, and that's what Java uses to determine which of the two methods is being invoked at runtime.

Interestingly, this is a major difference with the array implementation of the method. With array implementation both methods could return the same type. For instance, you could change the public static String sum(String[] words) to public static int sum(String[] words) and make it return the length of the concatenated String, and this would still compile (after all the return type is not part of method signature, right?). However, this would not compile in the generics implementation, basically because at runtime, both methods would have the same method signature and the same return type, making impossible for the Java Virtual Machine to make a difference between one and the other.

I hope you find this interesting.