В англоязычном сегменте сети достаточно много статьей по этой теме. Чаще всего они несут поверхностный, ознакомительный характер и иногда довольно сложно из-за языкового барьера уловить суть изучаемой темы. Надеюсь, наше небольшое путешествие в Java Collections Framework принесет вам некоторое разъяснение. Чем больше предметной информации, тем легче собрать всю мозаику воедино, отбрасывая не понятные выкладки и описания. Для более эффективного изучения количество примеров является критичным. Так же дело обстоит и с коллекциями, обойдем стороной интерфейсы, и перейдем сразу к классам, их реализующие.
Класс ArrayList
«Не стоит недооценивать важность метафор. Метафоры имеют одно неоспоримое достоинство: описываемое ими поведение предсказуемо и понятно всем людям…”.
Фернандо Дж. Корбати.
Я полностью согласен с Фернандо Дж. Корбати о важности метафор в изучение определённой предметной области. Так как программирование является смесью математики и искусства, использование метафор имеет наивысший приоритет. Для объяснения сути назначения ArrayList мы будем использовать «Транспортную» метафору. Представьте, что это обычный массив, похожий на семейный автомобиль, купленный с расчётом на количество членов семьи.
Если у нашей семьи родиться ещё один ребёнок, то придется покупать более вместительный автомобиль. Предположим, родители заведомо не знают, сколько детей у них будет и какой ещё вместимости необходим новый автомобиль. Есть вероятность прогадать и зря потратить сбережения. Так вот, ArrayList подобна городскому транспорту. Здесь нет ограничений в вместимости. Каждый в состоянии доехать из пункта A в пункт B. Поэтому если отец семейства не уверен или не знает о количестве будущих детей, он выберет городской транспорт, или в нашем случае ArrayList. Данный класс имеет три конструктора, мы будем разбирать самый простой. Давайте напишем код.
import java.util.ArrayList;
public class Collection {
public static void main(String[] args) {
// Создаем пустую коллекцию для работы с объектами класса Integer.
ArrayList<Integer> IdCustomer = new ArrayList<Integer>();
// Добавляем объекты в коллекцию.
IdCustomer.add(1);
IdCustomer.add(2);
IdCustomer.add(3);
// Выводим на консоль все значения объектов класса Integer,
// коллекции IdCustomer
System.out.println(IdCustomer);
// Роль данного вызова метода будет определена далее в статье.
IdCustomer.remove(2);
// После удаления элемента. выводим значения объектов класса Integer,
System.out.println(IdCustomer);
}
}
Вывод программы на консоль:
[1, 2, 3]
[1, 2]
Программа очень интуитивно понятна и проста, но давайте немного копнем глубже и взглянем изнутри, как всё работает. Во-первых, класс ArrayList является обобщенным, относительно нашего объявления, мы должны работать с объектами класса Integer. Однако судя по коду программы этого не происходит. Установим breakpoint на строку:
IdCustomer.add(2)
запустим Debug и войдем в метод(Step Into). Если всё прошло успешно, то обнаружим следующий код.
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Как видите, имя метода не совпадает с тем методом, в который мы входили, плюс ко всему мы оказались в классе Integer. Обратим наше внимание на строку:
// Создание объекта класса Integer c int-значением внутри
// и передача адреса объекта вызывающему методу.
return new Integer(i);
Это и есть знаменитая „Autoboxing Java“, из простого int в объект класса Integer. Теперь всё встало на свои места и вписывается в картину объявления обобщенного класса ArrayList. Далее рассмотрим метод добавления элемента в коллекцию.
public boolean add(E e) {
// Проверка вместимости массива и его расширение, если вместимости не достаточно.
ensureCapacityInternal(size + 1);
// Запись элемента в массива. переданного в метод.
elementData[size++] = e;
// Передача флага удачного выполнения метода.
return true;
}
Данный метод не только записывает элемент в коллекцию, но и поддерживает „транспортную“ метафору путем увеличение вместимости массива при определенном условие. Жесткая связь между оператором return и возвращаемым булевым значением true, говорит нам о том, что если возникнет исключительная ситуация, выполнение прервётся. Устойчивость метода оставляет желать лучшего.
Вернемся опять к нашему исходному коду. И рассмотрим следующие строку кода:
// Роль данного вызова метода будет определена далее в статье.
IdCustomer.remove(2);
Возникает вопрос, если взять в расчет авто упаковку, какой элемент удалится? По выводу на консоль мы ясно видим, что это будет 3. Итак, что-то пошло не так. Исследуя класс ArrayList, в окне Outline обнаруживаем два одинаковых имени вызываемого метода.
Рассмотрим первый метод и определим его назначение:
public E remove(int index) {
rangeCheck(index);
modCount++;
// Подготовка возврата удаляемого элемента.
E oldValue = elementData(index);
// Вычисление размера для смещения
int numMoved = size - index - 1;
if (numMoved > 0)
// Удаление(затирание) элемента.
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;
// Возврат удалённого элемента.
return oldValue;
}
Беглым анализом можно определить, что метод копирует одну часть относительно значения переменой numMoved одного и того же массива в другую позицию(index+1, index). То есть затирание и есть удаление элемента массива по его индексу, при этом старое значение возвращается методом и метка конца (null) массива перезаписывается заново, относительно новых смещений. Определённо, в нашей программе вызывается именно данный метод и авто упаковка не происходит. В противном случае после авто упаковки удалился бы элемент 2, а не элемент из массива на позиции [2]. Для ясности проверим код другой версии метода remove().
public boolean remove(Object o) {
if (o == null) {
// Поиск null-элемента
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// Удаление найденного null-элемента
fastRemove(index);
// Возврат флага-успеха
return true;
}
} else {
// Поиск элемента
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
// Удаление найденного элемента
fastRemove(index);
// Возврат флага-успеха
return true;
}
}
// Возврат флага-неудачи
return false;
}
Метод выполняет два основных действия.
- 1. Удаление null-элемента из массива.
- 2. Удаление элемента в соответствии с передаваемым аргументом.
Используя цикл, метод последовательно от 0 до size перебирает все элементы массива. Признак соответствия элемента с передаваемым в аргументе устанавливается с помощью метода:
// Сравнение полей вызывающего объекта с вызываемым.
o.equals(elementData[index])
Если соответствующий элемент будет обнаружен, он будет незамедлительно удален из массива. Для наглядности видоизменим строку удаления элемента, передав в качестве аргумента адрес на объект класс Integer:
// IdCustomer.remove(2);
IdCustomer.remove(new Integer(2));
Вывод программы на консоль:
[1, 2, 3]
[1, 3]
Предполагаю, что авто упаковка основана на приоритетных принципах, где удаление по индексу имеет более высокий приоритет.
Java Native Interface.
Native methods-methods used by a Java program but written in a different language-have been in the JDK since the beginning. As promised, the native methods interface from 1.0 has been completely rewritten and formalized. This interface is now named the Java Native Interface, or JNI for short.
Напоследок рассмотрим вывод коллекции на консоль. Для это мы используем обычный метод вывода с переводом строки:
public void println(Object x) {
// Мост ведущий к toString()
String s = String.valueOf(x);
synchronized (this) {
// Печать строки
print(s);
newLine();
}
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
Объект IdCustomer сам предоставляет строку в виде коллекции, для этого управление передаётся его внутреннему переопределенному методу toString(). Используя Debug мы можем сразу войти в тело метода или пойти другим путем и отыскать его в иерархии классов:
public abstract class AbstractCollection<E> implements Collection<E> {
// ..........Методы............
// ..........Поля................
public String toString() {
Iterator<E> it = iterator();
// Определение наполненности коллекции.
if (! it.hasNext())
return "[]";
// Создание изменяемого массива.
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
// Получение следующего элемента коллекции.
E e = it.next();
// Добавление элемента коллекции стокового представления в массив.
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
// Возврат адреса объекта класса String, содержащий всю коллекцию элементов.
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
// ..........Методы............
// ..........Поля................
}
Проанализировав код, мы можем выделить три составные части.
- 1. Определение наполненности массива;
- 2. Создание объекта класса StringBuilder, который позволяет изменять свой внутренний массив, в отличии, от класса String;
- 3. Добавление в массив значений объектов коллекции в виде стокового представления.
После выполнения метода, сформируется объект класса String, ссылка на который будет передана через метод-мост прямиком в println(Object x). Работа с классом ArrayList не ограничивается лишь методами, описанными выше. На сайте Tutorials point вы сможете найти другую дополнительную информацию по этой теме.
Автор: рекрут