What is the logic behind Arrays.copyOfRange(byte[], int, int) strange behavior?

1.8k Views Asked by At

Can anyone explain me the logic behind strange behavior of Arrays.copyOfRange(byte[], int, int))? I can illustrate what I mean with simple example:

byte[] bytes = new byte[] {1, 1, 1};
Arrays.copyOfRange(bytes, 3, 4); // Returns single element (0) array
Arrays.copyOfRange(bytes, 4, 5); // Throws ArrayIndexOutOfBoundsException

In both cases I copy ranges outside of array boundaries (i.e. start >= array.length), so the condition for error is at least strange for me (if from < 0 or from > original.length). In my opinion it should be: if from < 0 or from >= original.length. Maybe I'm missing something?

4

There are 4 best solutions below

2
On

Look, how copyOfRange works:

public static <T,U> T[] copyOfRange(U[] original, int from, int to, Class<? extends T[]> newType) {
    int newLength = to - from;
    if (newLength < 0)
        throw new IllegalArgumentException(from + " > " + to);
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, from, copy, 0,
                     Math.min(original.length - from, newLength));
    return copy;
}

The most important part is this:

System.arraycopy(original, from, copy, 0,
                     Math.min(original.length - from, newLength));

So when you're calling Arrays.copyOfRange(bytes, 3, 4); the last param of System.arraycopy "length - the number of array elements to be copied" is 0. The call of arraycopy looks like System.arraycopy(original, from, copy, 0,0);

0
On

After looking into the source of Arrays.copyOfRange(byte[] original, int from, int to)

  public static byte[] copyOfRange(byte[] original, int from, int to) {
1     int newLength = to - from;
2     if (newLength < 0)
3         throw new IllegalArgumentException(from + " > " + to);
4     byte[] copy = new byte[newLength];
5     System.arraycopy(original, from, copy, 0,
6                      Math.min(original.length - from, newLength));
7     return copy;
  }
  • at line 1 the length of the destination array is computed with 1
  • at line 4 the new array is created with size 1, all elements are 0
  • at line 6 the length of the element to be copied is computed as zero, because

    // Math.min(original.length - from, newLength));
    Math.min(3 - 3, 1)); --> returns zero
    
  • at line 5 the System.arraycopy simply does not copy anything into array copy

If your from is bytes.length + 1 then length get negative (bytes.length - from).

A small code to demonstrate

public class ArrayCopy {

    public static void main(String[] args) {
        byte[] bytes = new byte[]{11, 12, 13};
        int from = 3;
        int to = 4;
        copyOfRange(bytes, from, to);

        from = 2;
        to = 3;
        copyOfRange(bytes, from, to);

        from = 4;
        to = 5;
        copyOfRange(bytes, from, to);
    }

    static void copyOfRange(byte[] bytes, int from, int to) {
        System.out.printf("%ncopyOfRange(bytes: %s  from: %d  to: %d)%n",
                Arrays.toString(bytes),
                from,
                to
                );

        // line 1
        int newLength = to - from;
        System.out.println("int newLength = " + newLength);

        // line 2
        if (newLength < 0) {
            throw new IllegalArgumentException(from + " > " + to);
        }

        // line 4
        byte[] copy = new byte[newLength];

        // to show that in the suspicious case System.arrayCopy does nothing
        copy[0] = 42;
        System.out.println("byte[] copy   = " + Arrays.toString(copy));
        int length = bytes.length - from;
        System.out.println("int length    = " + length);
        int minLenght = Math.min(length, newLength);
        System.out.println("int minLenght = " + minLenght);

        // line 5
        System.arraycopy(bytes, from, copy, 0, minLenght);

        System.out.println("byte[] copy   = " + Arrays.toString(copy));
    }
}

output

copyOfRange(bytes: [11, 12, 13]  from: 3  to: 4)
int newLength = 1
byte[] copy   = [42]
int length    = 0
int minLenght = 0
byte[] copy   = [42]

copyOfRange(bytes: [11, 12, 13]  from: 2  to: 3)
int newLength = 1
byte[] copy   = [42]
int length    = 1
int minLenght = 1
byte[] copy   = [13]

copyOfRange(bytes: [11, 12, 13]  from: 4  to: 5)
int newLength = 1
byte[] copy   = [42]
int length    = -1
int minLenght = -1
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
0
On

The JavaDoc specifies three points regarding the arguments it expects:

One:

[from] must lie between zero and original.length, inclusive

Two:

[to] must be greater than or equal to from

Three:

[to] may be greater than original.length

For the case of Arrays.copyOfRange(bytes, 3, 4) one, two and three are true which means they are valid.

For the case of Arrays.copyOfRange(bytes, 4, 5) only two and three are true which means they are invalid.


Expected behaviour? Yes.

Unintuitive behaviour? Kinda.

If your question is secretly "why was it designed this way?" then no one can tell you the answer except the code authors.

0
On

It means you are able to create new "empty" Array of any length using this method like you described for // Returns single element (0) array situation.

The main purpose why Arrays.copyOfRange method exist, is because it create new Array "on the fly". And this new Array can be bigger than source Array, of course.

That behavior is a documented feature.

The final index of the range (to), which must be greater than or equal to from, may be greater than original.length, in which case '\u000' is placed in all elements of the copy whose index is greater than or equal to original.length - from. The length of the returned array will be to - from.

Why it's implemented like this? Assuming you are going to invoke this:

byte[] bytes = new byte[] {1, 1, 1};
byte[] newBytes = Arrays.copyOfRange(bytes, 2, 6); // Returns array length 6 - 2 = 4

It will create array [1, 0, 0, 0] and all elements which are not exist in original array will be initialized by default literal for current type. But if you want to specify from is bigger but not equals bytes.length (which is discouraged by documentation) it will cause ArrayIndexOutOfBoundsException during invoking this System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); because original.length - from will be less than 0.

So technically saying if you use such instructions:

int n = 4;
Arrays.copyOfRange(bytes, bytes.length, bytes.length + n);

assumes you just want to create new empty Array of size n. In other words, you are creating a new Array and copy nothing from source Array.