Есть множество базовых методов, которые мы регулярно используем, даже не задумываясь. Ну а что, если задуматься и посмотреть, каким образом реализованы некоторые простые, на первый взгляд, методы?
Думаю, это поможет нам стать на шаг ближе к Java)
Представим ситуацию, в которой нам нужно вытащить определенный символ в какой-то строке. Как мы это можем сделать в Java? Например, с помощью вызова метода
Java String charAt
.
О методе charAt()
мы и поговорим в сегодняшней статье.Синтаксис
char charAt(int index)
возвращает значение char по указанному индексу. Индекс колеблется от 0 до length()-1
.
То есть, первое char
значение последовательности находится в index 0
, следующее — в index 1
и т.д., как и в случае с индексацией массива.Пример
public static void main(String[] args) {
System.out.print("JavaRush".charAt(0));
System.out.print("JavaRush".charAt(1));
System.out.print("JavaRush".charAt(2));
System.out.print("JavaRush".charAt(3));
}
В первой строке берется первый символ, во второй — второй, и так далее.
Так как здесь используется не println
, а print
, без перехода на новую строку, мы получим вывод в консоль:
Java
Если char
под заданным индексом представлен в виде юникода, результатом работы метода java charAt()
будет символ, который представляет данный юникод:
System.out.println("J\u0061vaRush".charAt(1));
Вывод в консоль:
a
Что “под капотом”
Как же оно работает, спросите вы?Дело в том, что в каждом объектеString
есть массив byte
с байтами элементов данной строки:
private final byte[] value;
А вот и сам метод chatAt
:
public char charAt(int index) {
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}
isLatin1
— флаг, указывающий на то, есть ли в нашей строке только латинские символы или нет. От это зависит, какой метод будет вызываться далее.isLatin1 = true
Если в строке есть только латинские символы, вызывается статический методcharAt
класса StringLatin1
:
public static char charAt(byte[] value, int index) {
if (index < 0 || index >= value.length) {
throw new StringIndexOutOfBoundsException(index);
}
return (char)(value[index] & 0xff);
}
Первым делом проверяется, что пришедший индекс больше либо равен 0, и что он не выходит за пределы внутреннего массива байт, и если это не так, то кидается исключение — new StringIndexOutOfBoundsException(index)
.
Если же проверки пройдены, далее берется нужный нам элемент.
В конце мы видим:&
расширяет для двоичной операции дляbyte
побитово0xff
ничего не делает, но&
требует аргумент(char)
приводит данные по таблице ASCII кchar
isLatin1 = false
Если же у нас присутствовали не только латинские символы, будет использоваться классStringUTF16
и вызываться его статический метод:
public static char charAt(byte[] value, int index) {
checkIndex(index, value);
return getChar(value, index);
}
Который в свою очередь вызывает:
public static void checkIndex(int off, byte[] val) {
String.checkIndex(off, length(val));
}
А он делегирует статическому методу String
:
static void checkIndex(int index, int length) {
if (index < 0 || index >= length) {
throw new StringIndexOutOfBoundsException("index " + index +
", length " + length);
}
}
Здесь, собственно, происходит проверка на допустимость индекса: опять же, положительный ли он либо ноль, и не выходил ли он за пределы массива.
Но в классе StringUTF16
в методе charAt
более интересным будет вызов второго метода:
static char getChar(byte[] val, int index) {
assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
index <<= 1;
return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
((val[index] & 0xff) << LO_BYTE_SHIFT));
}
Приступим же к разбору, что тут собственно происходит.
Первым делом в начале метода идёт ещё одна проверка допустимости индекса.
Чтобы понять происходящее далее, нужно уяснить: когда не латинский символ попадает в массив value
, его представляют два байта (две ячейки массива).
Если у нас есть строка из двух кириллических символов — “ав”, то:
- для ‘а’ это пара байтов — 48 и 4;
- для ‘в’ — 50 и 4.
value
— {48, 4, 50, 4}
Собственно, в этом методе и идёт работа с двумя ячейками массива value
.
Поэтому дальше идёт сдвиг index <<= 1;
, чтобы попасть непосредственно на индекс первого байта искомого символа в массиве value
.
А теперь допустим, что у нас есть строка "абвг"
.
Тогда массив value будет выглядет так: {48, 4, 49, 4, 50, 4, 51, 4}.
Мы запрашиваем третий элемент строки, и тогда двоичное представление — 00000000 00000011.
При сдвиге на 1, мы получим 00000000 00000110, то есть index = 6
.
Чтобы освежить знания по побитовым операциям, можешь почитать вот эту статью.Также мы видим некоторые переменные:
HI_BYTE_SHIFT
в данном случае равно 0.
LO_BYTE_SHIFT
в данном случае равно 8.
В последней строке данного метода:
- Берется элемент из массива value и побитово сдвигается на
HI_BYTE_SHIFT
, то есть 0, при этом увеличиваяindex +1
.В примере с строкой
"абвг"
, шестой байт — 51 — так бы и остался, но при этом увеличивается индекс до 7. - После этого берется следующий элемент массива и так же побитово сдвигается, но на LO_BYTE_SHIFT , то есть на 8 битов.
И если у нас это был байт 4, который имеет двоичное представление — 00000000 00000100, то после сдвига на 8 битов у нас будет 00000100 00000000. Если целым числом — 1024.
- Далее для этих двух значений следует операция
| (OR)
.И если у нас были байты 51 и 1024, которые в двоичном представлении выглядели как 00000000 00110011 и 00000100 00000000, то после операции
OR
мы получим 00000100 00110011, что значит число 1075 в десятичной системе.Ну и в конце концов, переводится число 1075 в тип char, а при переводе int -> char используется таблица ASCII, и в ней под номером 1075 стоит символ ‘г’.
charAt()
в Java-программировании.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ