Search code examples
javajavacbytecodejvm-bytecodeecj

Does javac generate inaccurate line numbers compared to ecj (for this particular case)?


I use the following class which has this specificity in the equals() method that the return keyword and its expressions are split into several lines (return keyword is on its own line).

package jd.core.test;

import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;

public class TimeZoneDisplayKey {

    private final TimeZone mTimeZone;
    private final int mStyle;
    private final Locale mLocale;

    public TimeZoneDisplayKey(TimeZone mTimeZone, int mStyle, Locale mLocale) {
        this.mTimeZone = mTimeZone;
        this.mStyle = mStyle;
        this.mLocale = mLocale;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mLocale, mStyle, mTimeZone);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj;
        return
                Objects.equals(mLocale, other.mLocale) &&
                mStyle == other.mStyle &&
                Objects.equals(mTimeZone, other.mTimeZone);
    }
}

I use javac -g command (from JDK 17) and javap -l to obtain the line number table. Below a reduced extract of javap for equals() to illustrate what is puzzling:

      31: aload_0
      32: getfield      #17                 // Field mLocale:Ljava/util/Locale;
      35: aload_2
      36: getfield      #17                 // Field mLocale:Ljava/util/Locale;
      39: invokestatic  #37                 // Method java/util/Objects.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z
      42: ifeq          74
      45: aload_0
      46: getfield      #13                 // Field mStyle:I
      49: aload_2
      50: getfield      #13                 // Field mStyle:I
      53: if_icmpne     74
      56: aload_0
      57: getfield      #7                  // Field mTimeZone:Ljava/util/TimeZone;
      60: aload_2
      61: getfield      #7                  // Field mTimeZone:Ljava/util/TimeZone;
      64: invokestatic  #37                 // Method java/util/Objects.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z
      67: ifeq          74
      70: iconst_1
      71: goto          75
      74: iconst_0
      75: ireturn
    LineNumberTable:
      line 26: 0
      line 27: 5
      line 29: 7
      line 30: 11
      line 32: 13
      line 33: 24
      line 35: 26
      line 36: 31
      line 37: 39
      line 39: 64
      line 36: 75

It says there is more than just return on line 36, there's return (75: ireturn) and this (31: aload_0) whereas in the source, the return keyword is on its own line.

If I look at what produces ecj for equals(), I can't see this problem:

      31: aload_0
      32: getfield      #21                 // Field mLocale:Ljava/util/Locale;
      35: aload_2
      36: getfield      #21                 // Field mLocale:Ljava/util/Locale;
      39: invokestatic  #47                 // Method java/util/Objects.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z
      42: ifeq          72
      45: aload_0
      46: getfield      #19                 // Field mStyle:I
      49: aload_2
      50: getfield      #19                 // Field mStyle:I
      53: if_icmpne     72
      56: aload_0
      57: getfield      #17                 // Field mTimeZone:Ljava/util/TimeZone;
      60: aload_2
      61: getfield      #17                 // Field mTimeZone:Ljava/util/TimeZone;
      64: invokestatic  #47                 // Method java/util/Objects.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z
      67: ifeq          72
      70: iconst_1
      71: ireturn
      72: iconst_0
      73: ireturn
    LineNumberTable:
      line 26: 0
      line 27: 5
      line 29: 7
      line 30: 11
      line 32: 13
      line 33: 24
      line 35: 26
      line 37: 31
      line 38: 45
      line 39: 56
      line 36: 70

Solution

  • Yes, it's a javac bug, although you won't notice it in this case. The line number mappings are only used for exception stack traces, and so I don't see any issues unless there's a NPE thrown on the mStyle == other.mStyle line. If I modify the equals method to be broken:

    @Override
    public boolean equals(Object obj) {
        TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj;
        return
                Objects.equals(mLocale, mLocale) &&
                mStyle == other.mStyle &&
                Objects.equals(mTimeZone, other.mTimeZone);
    }
    

    And then I call equals with null as the second argument, a NullPointerException is thrown when trying to access other.mStyle but the line number is wrong. It reports the line above which is comparing (incorrectly) the mLocale field.