7

This is kind of strange, but code speaks more then words, so look at the test to see what I'm doing. In my current setup (Java 7 update 21 on Windows 64 bit) this test fails with ArrayIndexOutOfBoundsException, but replacing the test method code with the commented code, it the works. And I wonder if there is any part of the Java specification that would explain why.

It seems to me, as "michael nesterenko" suggested, that the value of the array field is cached in the stack, before calling the method, and not updated on return from the call. I can't tell if it's a JVM bug or a documented "optimisation". No multi-threading or "magic" involved.

public class TestAIOOB {
    private String[] array = new String[0];
    private int grow(final String txt) {
        final int index = array.length;
        array = Arrays.copyOf(array, index + 1);
        array[index] = txt;
        return index;
    }
    @Test
    public void testGrow() {
        //final int index = grow("test");
        //System.out.println(array[index]);
        System.out.println(array[grow("test")]);
    }
}
ngrashia
  • 9,869
  • 5
  • 43
  • 58
Sebastien Diot
  • 7,183
  • 6
  • 43
  • 85
  • 2
    just a guess, when you call `grow` from array it is already in stack and thus link is not updated, but if you call `grow` before and then use index, link to array is loaded after it is updated and thus it works. just a guess. perhaps looking at byte code might help – michael nesterenko Jun 22 '13 at 21:55
  • 1
    I wouldn't call it "cached in the stack". I think this is a Java (the langauge) question. You are referencing a name, which is resolved *before* using/applying the resolved value associated with it. So if the name is reassigned in between, you've got the wrong value. – Janus Troelsen Jun 22 '13 at 22:12
  • 1
    Here's a simpler test case: `public class TestAIOOB { static Object[] array; static int reassign() { array = new Object[] { new Object() }; return 0; } public static void main(String[] args) { System.out.println(array[reassign()]); } }`. This throws (like it should), and you're asking why. – Janus Troelsen Jun 22 '13 at 22:25
  • It's worth mentioning that it's not just Java. The array reference is evaluated before the `grow` call in JavaScript ([example](http://jsbin.com/aregux/1)), C# ([example](http://pastie.org/8071459)), and C ([example](http://pastie.org/8071498)) as well, and presumably others but I didn't try them. (You can run either of those last two online here: http://www.compileonline.com/) – T.J. Crowder Jun 23 '13 at 08:17

3 Answers3

6

This is well defined by the Java Language Specification: to evaluate x[y], first x is evaluated, and then y is evaluated. In your case, x evaluates to a String[] with zero elements. Then, y modifies a member variable, and evaluates to 0. Trying to access the 0th element of the already-returned array fails. The fact that the member array changes has no bearing on the array lookup, because we're looking at the String[] that array referenced at the time we evaluated it.

amalloy
  • 89,153
  • 8
  • 140
  • 205
2

This behavior is mandated by the JLS. Per 15.13.1, "An array access expression is evaluated using the following procedure: First, the array reference expression is evaluated. If this evaluation completes abruptly, then the array access completes abruptly for the same reason and the index expression is not evaluated. Otherwise, the index expression is evaluated. [...]".

Michał Kosmulski
  • 9,855
  • 1
  • 32
  • 51
0

Compare the compiled Java code by using javap -c TestAIOOB

Uncommented code:

public void testGrow();
  Code:
   0:   getstatic       #6; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   aload_0
   4:   getfield        #3; //Field array:[Ljava/lang/String;
   7:   aload_0
   8:   ldc     #7; //String test
   10:  invokespecial   #8; //Method grow:(Ljava/lang/String;)I
   13:  aaload
   14:  invokevirtual   #9; //Method java/io/PrintStream.println:(Ljava/lang/St
ing;)V
   17:  return

Commented code:

public void testGrow();
  Code:
   0:   aload_0
   1:   ldc     #6; //String test
   3:   invokespecial   #7; //Method grow:(Ljava/lang/String;)I
   6:   istore_1
   7:   getstatic       #8; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_0
   11:  getfield        #3; //Field array:[Ljava/lang/String;
   14:  iload_1
   15:  aaload
   16:  invokevirtual   #9; //Method java/io/PrintStream.println:(Ljava/lang/Str
ing;)V
   19:  return

In the first the getfield happens before the call to grow and in the second it happens after.

selig
  • 4,834
  • 1
  • 20
  • 37