The second parameter to substring is an exclusive upper bound - so it's allowed to be equal to the length of the string, in order to include the last character. Likewise it makes sense to allow the starting point to be "at" the end of the string, so long as the end is equal to the start, yielding an empty string.
Basically, for APIs which deal with ranges, it often makes sense to think of the indexes as being "between" characters rather than "on" them. For example:
J O H N
^ ^ ^ ^ ^
0 1 2 3 4
Both indexes have to be within the range shown, and the endIndex index must be either the same as beginIndex or to the right - then the substring is the characters between the two corresponding boundaries:
"JOHN".substring(1, 3) is "OH"
J O H N
^ ^ ^
1 3
This is exactly as documented, of course
IndexOutOfBoundsException - if the beginIndex is negative, or endIndex is larger than the length of this String object, or beginIndex is larger than endIndex.