Вы уже умеете создавать массивы в Java? Тогда самое время узнать что такое Arraylist. Ну что ж, поехали!
Допустим, Вы работаете программистом на компанию, которая осуществляет доставку товаров по всему миру. И Вам необходимо сохранить список городов, в которые осуществляется доставка.
Вы это можете сделать 2 способами:
1. С помощью обычного массива
2. С помощью Arraylist
1. С ПОМОЩЬЮ ОБЫЧНОГО МАССИВА
Создаем обычный массив с именем deliveryCities. В фигурных скобках прописываем значения элементов массива, то есть названия городов. Например:
В результате, будет создан массив deliveryCities на 4 элемента. И если мы заходим получить 3-й элемент массива – мы напишем deliveryCities[2], поскольку 3-й элемент имеет индекс 2. Таким образом мы можем создать массив из элементов любого типа, после чего можем работать с этими элементами.
Но что, если мы хотим добавить еще один город для доставки, например, London?
И вот Вам мой ответ: «Так сделать не получится! Потому что в Java нет возможности изменять длину массивов. А это значит, что если Вы изначально создали массив на 4 элемента, значит уже этот массив будет всегда на 4 элемента.»
Решение будет выглядеть так:
2. С ПОМОЩЬЮ ARRAYLIST
Так что же такое Arraylist? Говоря простым и понятным языком, ArrayList – это список, который очень похож на массив за исключением того, что он имеет произвольную длину и целый ряд методов, которые позволяют добавлять, удалять, заменять элементы списка в любое время и в любом месте.
А это значит, что Вы можете, например, сначала создать Arraylist на 4 элемента, а потом взять и добавить еще 7 элементов, а потом удалить 2 элемента, а потом снова добавить 15 элементов и т.д. Вы поняли идею, Arraylist, как резиновый, его можно увеличивать или уменьшать безо всяких проблем.
Работать с ArrayList очень и очень просто
Во-первых, как и в случае с массивами, нужно создать список. Создать список можно несколькими способами. Наиболее распространенным способом является следующая структура:
Взбрело мне в голову написать несколько статей, о том как реализованы некоторые структуры данных в Java. Надеюсь, статьи будут полезны визуалам (картинки наше всё), начинающим java-визуалам а также тем кто уже умеет писать new ArrayList(), но слабо представляет что же происходит внутри.
Сегодня поговорим о ArrayList-ах
ArrayList — реализует интерфейс List. Как известно, в Java массивы имеют фиксированную длину, и после того как массив создан, он не может расти или уменьшаться. ArrayList может менять свой размер во время исполнения программы, при этом не обязательно указывать размерность при создании объекта. Элементы ArrayList могут быть абсолютно любых типов в том числе и null.
Создание объекта
Только что созданный объект list, содержит свойства elementData и size.
Хранилище значений elementData есть ни что иное как массив определенного типа (указанного в generic), в нашем случае String[]. Если вызывается конструктор без параметров, то по умолчанию будет создан массив из 10-ти элементов типа Object (с приведением к типу, разумеется).
Вы можете использовать конструктор ArrayList(capacity) и указать свою начальную емкость списка.
Добавление элементов
Внутри метода add(value) происходят следующие вещи:
1) проверяется, достаточно ли места в массиве для вставки нового элемента;
2) добавляется элемент в конец (согласно значению size) массива.
Весь метод ensureCapacity(minCapacity) рассматривать не будем, остановимся только на паре интересных мест. Если места в массиве не достаточно, новая емкость рассчитывается по формуле (oldCapacity * 3) / 2 + 1. Второй момент это копирование элементов. Оно осуществляется с помощью native метода System.arraycopy(), который написан не на Java.
Ниже продемонстрирован цикл, поочередно добавляющий 15 элементов:
При добавлении 11-го элемента, проверка показывает что места в массиве нет. Соответственно создается новый массив и вызывается System.arraycopy().
После этого добавление элементов продолжается
Добавление в «середину» списка
Добавление элемента на позицию с определенным индексом происходит в три этапа:
1) проверяется, достаточно ли места в массиве для вставки нового элемента;
2) подготавливается место для нового элемента с помощью System.arraycopy();
3) перезаписывается значение у элемента с указанным индексом.
Как можно догадаться, в случаях, когда происходит вставка элемента по индексу и при этом в вашем массиве нет свободных мест, то вызов System.arraycopy() случится дважды: первый в ensureCapacity(), второй в самом методе add(index, value), что явно скажется на скорости всей операции добавления.
В случаях, когда в исходный список необходимо добавить другую коллекцию, да еще и в «середину», стоит использовать метод addAll(index, Collection). И хотя, данный метод скорее всего вызовет System.arraycopy() три раза, в итоге это будет гораздо быстрее поэлементного добавления.
Удаление элементов
Удалять элементы можно двумя способами: — по индексу remove(index) — по значению remove(value)
С удалением элемента по индексу всё достаточно просто
Сначала определяется какое количество элементов надо скопировать
затем копируем элементы используя System.arraycopy()
уменьшаем размер массива и забываем про последний элемент
При удалении по значению, в цикле просматриваются все элементы списка, до тех пор пока не будет найдено соответствие. Удален будет лишь первый найденный элемент.
Дополнение 1: Как верно заметил MikeMirzayanov, при удалении элементов текущая величина capacity не уменьшается, что может привести к своеобразным утечкам памяти. Поэтому не стоит пренебрегать методом trimToSize().
Итоги
— Быстрый доступ к элементам по индексу за время O(1); — Доступ к элементам по значению за линейное время O(n); — Медленный, когда вставляются и удаляются элементы из «середины» списка; — Позволяет хранить любые значения в том числе и null; — Не синхронизирован.
Ссылки
Пишите в комментариях пожелания/замечания и есть ли смысл продолжать.
— Я расскажу тебе сегодня про несколько ключевых слов в Java. Но начну с самого интересного – ключевого слова final. Если перевести его с английского, то получится что-то вроде финальный или окончательный.
Ключевое слово final можно добавлять при объявлении переменной, метода и класса.
— А зачем нужен этот final?
— Все довольно просто. Если мы пометили переменную словом final, то она становится неизменной:
final int i = 5; i++; //ошибка компиляции – нельзя менять значение переменной i
— Если мы пометили метод словом final, то этот метод запрещено переопределять в классах-наследниках:
class Cat < public final String getName() < return «cat»; > >
class Tiger extends Cat < public String getName() //ошибка компиляции – нельзя переопределять метод getName() < return «tiger»; > >
— Ясно. А зачем может понадобиться запрещать переопределение метода?
— Например, программист написал в этом методе много важного кода и хочет, чтобы все наследники его класса гарантированно имели заданное поведение.
Если мы пометим словом final класс, то таким образом мы запретим наследоваться от него.
public final class Cat < public String getName() < return «cat»; > >
class Tiger extends Cat //ошибка компиляции – нельзя наследоваться от класса Cat < public String getName() < return «tiger»; > >
— А зачем запрещать наследование классов?
— Ты должен понять, что запрет наследования идет не из вредности, а ради безопасности и целостности кода. Если наследование класса не запрещено, то подразумевается, что оно разрешено. И код проектировщика класса будет нормально работать и с объектами его класса и с объектами класса-наследника.
А вот если разработчик видит, что даже при небольших изменениях в его классе, все перестанет работать, как задумано, тогда ему лучше запретить наследование.
— Класс String, например, объявлен как final, как и все примитивные типы: Integer, Boolean, Double, Character,…
— Ага, понимаю. Класс String сделан immutable и если бы вдруг появились изменяемые строки, то много чего перестало бы работать.
— Ну, почти. Скажем так, все работало бы почти по-старому, но иногда возникали бы ошибки, которые было бы очень сложно найти и понять. Поэтому, в некоторых случаях наследование классов или методов не грех и запретить – меньше потом ошибок вылавливать.
— А где еще можно писать final?
— final можно писать перед переменными-аргументами функции и перед переменными в методе. Вот пример:
public void setName(final String name) < final String displayName = «Mr.»+name; … this.name = displayName; >
— А какой в этом смысл?
— Ну, смыслов – два. Во-первых, мы объявляем переменную final – если хотим сообщить другим разработчикам, что это значение – определенная константа, а не просто переменная.
Например, мы хотим рассчитать НДС от цены:
public int getPriceNDS() < final int NDS = 20; return this.price * NDS / 200; >
И во-вторых, если мы будем писать локальные или анонимные внутренние классы, то такие переменные нам понадобятся. Я расскажу о таких классах в ближайшее время. Но не сегодня.
— Ок, пока вроде ничего сложного.
— Обрати внимание, что неизменяемой становится только переменная, а не объект, на который она ссылается. Объект можно менять еще как.
— Как раз хотел спросить этот момент. А нет способа сделать объект неизменным?
— Нет, только если ты напишешь immutable класс.
Этот код скомпилируется
Этот код не скомпилируется
class Home < private final int width = 200; private final int height = 100;
public Home() < > >
class Home < private final int width; private final int height;
Но, вместе с тем, Java разрешает перенести инициализацию final-переменных класса в конструктор.
Этот код скомпилируется
Этот код не скомпилируется
class Home < private final int width = 200; private final int height;
public Home() < height = 100; > >
class Home < private final int width; private final int height;
public Home() < height = 100; > >
Более того, в разных конструкторах final-переменные можно инициализировать разными значениями. Это очень удобно:
Этот код скомпилируется
class Home < private final int width; private final int height;
public Home(int width, int height) < this.height = height; this.width = width; > >
— Действительно интересная тема, и абсолютно все понятно, спасибо, Билаабо!
2. Задачи на final.
— Билаабо принес Амиго задачи:
Задачи
1. Запретите наследование
3. Вложенные классы
— Сегодня будет очень интересная тема. Я сегодня расскажу о внутренних классах.
Если один класс объявить внутри другого, то такой класс называется внутренним.
Объекты внутренних классов при этом вложены в объекты внешних классов и могут обращаться к переменным внешних классов.
Пример
public class Car < int height = 160; ArrayList doors = new ArrayList ();
public Car < doors.add( new Door ()); doors.add( new Door ()); doors.add( new Door ()); doors.add( new Door ()); >
class Door () < public int getDoorHeight() < return (int)( height * 0.80); > > >
Обрати внимание, у класса Door(дверь) есть метод, который возвращает высоту двери – getDoorHeight, этот метод использует переменную height объекта Car.
Объект типа Door не может существовать отдельно от объекта типа Car – ведь он использует его переменные. Компилятор незаметно добавляет в конструктор и в класс Door ссылку на объект внешнего класса Car, чтобы методы внутреннего класса Door могли обращаться к переменным и вызвать методы внешнего класса Car.
— Вложенные объекты. Мне все понятно. Судя по картинке тут все элементарно.
— Так и есть. Лишь пара нюансов.
Во внутреннем классе Door имеется ссылка на объект класса Car, поэтому:
1) Нельзя создать объект Door внутри статического метода в классе Car: негде взять ссылку на объект типа Car, который неявно передается в конструктор типа Door.
Правильно
Неправильно
public class Car < public static Door createDoor() < Car car = new Car(); return car.new Door(); >
public class Door < int width, height; > >
public class Car < public static Door createDoor() < return new Door(); >
public class Door < int width, height; > >
2) Класс Door не может содержать статические переменные и методы.
Правильно
Неправильно
public class Car < public int count ; public int getCount() < return count ; >
public class Door < int width, height; > >
public class Car <
public class Door < public static int count ; int width, height;
public static int getCount() < return count ; > > >
— А если мне нужна общая переменная для всех объектов Door?
Ты всегда можешь объявить ее просто в классе Car – это и будет общая переменная для всех объектов Door, вложенных в объект Car.
3) Обрати внимание, что если внутренний класс объявлен как public, то его объекты можно создавать вне внешнего класса, но объект внешнего класса при этом обязан присутствовать:
4) И еще одно замечание – чуть не забыла.
Т.к. у нас два вложенных объекта, то в методах внутреннего объекта доступно две ссылки this:
public class Car < int width, height;
public class Door < int width, height;
Я специально объявила в классах одинаковые переменные.
Чтобы обратиться к переменной внешнего класса, когда она скрыта, или к this внешнего класса, достаточно написать «имя класса» точка this:
Как получить доступ к this внешнего (или любого) класса
Car.this
Car.Door.this
Car.Door.InnerClass2.InnerClass3.this
— Т.е. если мы внутри метода внутреннего класса пишем this, то этот this относится к внутреннему классу?
Как тебе внутренние классы, Амиго?
— Очень интересно. Я бы не сказал, что очень сложно.
Есть много ограничений, но они вполне логичны, когда ты мне объяснила, откуда и зачем они берутся.
А я уже два месяца в практических заданиях постоянно пишу вложенные классы и только сейчас понял, что же я писал на самом деле.
Спасибо за отличный урок, Элли.
— Рада, что тебе понравилось, Амиго.
4. Задачи по внутренним классам
— Привет, Амиго! Готов заниматься?
— Да, давай уже сюда свои задачи по внутренним классам, Диего:
В классе SuperUser метод getDescription должен учитывать страну и город, т.е. возвращать результат аналогичный следующему: My name is George. I’m from the USA, Seattle. Используйте возможности иннер класса.
2. Inner 3
5. Внутренние статические классы
— Итак, тема номер два – вложенные классы.
Перед объявлением внутреннего класса мы можем поставить ключевое слово – static и тогда внутренний класс станет вложенным.
Давай разберемся, что же значит слово static рядом с объявлением вложенного класса. Как ты думаешь?
— Если переменная объявлена статической, то она существует в единственном экземпляре, а если вложенный класс – статический, то можно создать всего один объект такого класса?
— Пусть слово static тут не вводит тебя в заблуждение. Если переменная объявлена статической – то она существуют в единственно экземпляре – это верно. Но статический вложенный класс больше похож на статический метод в этом плане. Слово static перед объявлением класса указывает, что этот класс не хранит в себе ссылок на объекты внешнего класса, внутри которого объявлен.
— Ага. Обычные методы втихаря хранят ссылку на объект, а статические – нет. То же и со статическими классами, я прав, Элли?
— Абсолютно. Твоя догадливость очень похвальна. Вложенные статические классы не имеют скрытых ссылок на объекты внешнего класса, в котором они объявлены.
class Zoo < private static int count = 7; private int mouseCount = 1;
public static int getAnimalCount() < return count; >
public int getMouseCount() < return mouseCount; >
public static class Mouse < public Mouse() < > public int getTotalCount() < return count + mouseCount; //ошибка компиляции. > > >
Давай посмотрим внимательно на этот пример.
К каким переменным может обращаться статический метод getAnimalCount?
— Только к статическим. Это же статический метод.
К каким переменным может обращаться метод getMouseCount?
— И к статическим, и к нестатическим. Он скрытно хранит ссылку(this) на объект типа Zoo.
— Верно. Так вот, статический вложенный класс Mouse, как и статический метод, может обращаться к статическим переменным класса Zoo, но не может обращаться к нестатическим.
Мы можем спокойно создавать объекты класса Mouse, даже когда нет ни одного созданного объекта класса Zoo. Вот как можно это сделать:
class Home < public static void main(String[] args) < Zoo.Mouse mouse = new Zoo.Mouse(); > >
Фактически класс Mouse – это самый обычный класс. Из-за того, что он объявлен внутри класса Zoo, у него есть две особенности.
1) При создании объектов вложенного класса (как класс Mouse) вне внешнего класса-родителя, надо еще указывать через точку и имя внешнего класса.
Например так: Zoo.Mouse.
2) Класс Zoo.Mouse и его объекты имеют доступ к private static переменным и методам класса Zoo ( класс Mouse ведь тоже объявлен внутри класса Zoo).
На этом на сегодня все.
— Т.е. просто дополнительное имя и все?
— Это еще проще, чем казалось на первый взгляд.
6. Задачи по внутренним статическим классам
— Да, задачи, знаю, знаю. Кстати, Диего, у тебя получились отличные задачи.
— Спасибо, что наконец-то заметил. Держи еще:
Задачи
1. Как выбрать нужное?
В методе main присвойте объекту Object obj экземпляр класса TEST Константу TEST и класс TEST менять нельзя.
2. Рефакторинг
7. Внутренние анонимные классы, примеры
— Так здоровались уже, Элли!
— Так, не спорь с тетей. В 31 веке принято здороваться опять, если не видел человека более получаса. Так что не возникай!
Так вот, новая интересная тема – размножение роботов!
— Шучу, новая тема – анонимные вложенные классы.
Иногда в Java встречается ситуация, когда нужно унаследовать класс от нескольких классов. Т.к. множественное наследование классов в Java запрещено, эту проблему решают с помощью внутренних классов: в нужном нам классе мы объявляем внутренний класс и наследуем его от требуемого класса. Пример:
Пример внутреннего класса, унаследованного от Thread
class Tiger extends Cat <
class TigerThread extends Thread < public void run() < tigerRun (); > > >
Давай разберем этот пример:
Нам нужен класс, унаследованный от Thread, чтобы переопределить у него метод run.
Для этого внутри класса Tiger мы объявили внутренней класс TigerThread, который унаследовали от Thread и у которого переопределили метод run.
Для нашего удобства в классе Tiger мы объявили два метода (аналоги методов runи startкласса Thread) –методы tigerRun&startTiger.
В методе startTiger мы создаем объект типа TigerThread и вызываем у него метод start().
При этом Java-машина создаст новую нить, и эта нить начнет работу с вызова метода run, класса TigerThread.
А этот метод в свою очередь вызовет наш метод run – метод tigerRun.
— С нитями я уже дело имел, так что вроде не очень сложно.
А обязательно называть методы tigerRun и startTiger?
— Нет, можно было назвать run и start, но я хотела дополнительно показать, что мы не наследуемся от Thread, к тому же ты мог сильнее запутаться в моем пояснении.
— Ок. Тогда все вроде понятно. Только при вызове метода startTiger второй раз мы создадим еще один класс Thread и запустим его. Не получиться ли что у нас «один тигр будет бегать в двух различных нитях»?
— Ну ты и глазастый. Согласна, это не хорошо. Тогда давай перепишем код так:
Код
class Tiger extends Cat <
private TigerThread thread = new TigerThread ();
private class TigerThread extends Thread < public void run() < tigerRun (); > > >
— Не то, чтобы отлично. Два раза все равно вызывать такой метод нельзя. Но в этот раз мы хотя бы не будем создавать вторую нить и делать вид, что все хорошо.
— Правильно, запустил второй раз, тигра – получи Exception.
— Я уже лучше тебя вижу все ошибки, Элли!
Да, ты молодец. Тогда перейдём к анонимным внутренним классам.
Обрати внимание на несколько аспектов вышеописанного кода:
1) Мы унаследовались от класса Thread, но фактически не дописали туда никакого кода. Нам скорее пришлось унаследоваться, а не «мы унаследовались с целью расширить класс Thread»
2) Будет создан всего один объект класса TigerThread.
Т.е. с целью переопределить один метод и создать один объект, мы написали целую кучу кода.
Помнишь, как я рассказывала про появление конструкторов?
До изобретения
После изобретения
TigerThread thread = new TigerThread ();
private class TigerThread extends Thread < public void run() < tigerRun (); > >
Thread thread = new Thread() < public void run() < tigerRun (); > >;
— Вижу, что код стал компактнее, но не совсем понимаю, что произошло.
— Мы можем объединить в одном месте три вещи:
А) объявление класса-наследника
Б) переопределение метода
В) объявление переменной
Г) создание объекта класса-наследника.
Фактически мы объединяем вместе две операции – объявление класса-наследника и создание его объекта:
Без анонимного класса
С использованием анонимного класса
Cat tiger = new Tiger();
class Tiger extends Cat < >
Cat tiger = new Сat() < >;
Еще раз разбираем синтаксис:
Объявление переменной типа Thread
Thread thread = new Thread();
Объявление переменной типа «анонимный класс-наследник Thread»
Thread thread = new Thread() <
Обрати внимание – мы не просто объявляем новый класс – мы создаем переменную – в конце ставится точка с запятой!
— А если мы хотим переопределить метод run, то нужно писать так:
Объявление переменной типа Thread
Thread thread = new Thread() < public void run() < System.out.println(«new run-method»); > >;
— Быстро схватываешь – молодец!
— Спасибо. А если мне нужны еще методы, которых нет у класса Thread?
— Ты можешь их дописать.
Это же полноценный внутренний класс, хоть и анонимный:
Код
Описание
Thread thread = new Thread() < public void run() < printHi(); >
public void printHi() < System.out.println(«Hi!»); > > ;
Красным цветом отмечен код создания переменной.
Зеленым – создания объекта.
Синим – код анонимного класса-наследника.
— Полноценный внутренний класс?
Т.е. я могу и переменные внешнего класса использовать?
— А в конструктор я могу что-то передавать?
— Да, но только параметры конструктора базового класса:
Класс
Объект анонимного внутреннего класса
class Cat < int x, y; Cat(int x, int y) < this.x = x; thix.y = y; > >
Cat cat = new Cat(3,4) < public void print() < System.out.println(x+» «+y); > >;
Мы не можем добавить свои параметры в чужой конструктор. Зато можем использовать переменные внешнего класса – это достаточно сильно компенсирует этот недостаток.
— А если мне все-таки очень нужно добавить в конструктор еще параметры?
— Тогда объяви обычный не анонимный внутренний класс и используй его.
— Действительно, я чуть не забыл об этом.
— Нет. Будет именно анонимный внутренний класс. Смотри примеры:
С анонимным классом
Без анонимного класса
Thread thread = new Thread() < public void run() < tigerRun (); > >;
TigerThread thread = new TigerThread ();
private class TigerThread extends Thread < public void run() < tigerRun (); > >
static Thread thread = new Thread() < public void run() < tigerRun (); > >;
static TigerThread thread = new TigerThread ();
private class TigerThread extends Thread < public void run() < tigerRun (); > >
— Ясно. Статической становится только переменная, но не класс.
На самом деле, в процессе компиляции Компилятор создает внутренние классы для всех анонимных внутренних классов. Такие классы обычно получают имена «1», «2», «3», и т.д.
8. Задачи по анонимным классам
— Кто хочет задачи по анонимным внутренним классам?
— Кто же еще, конечно я. Давай их уже. Хочу немного попрограммировать:
Задачи
1. Напряги извилины!
Метод printName должен выводить свое собственное имя, т.е. «sout» Сделайте минимум изменений.
2. Повторяем threads
Подсказка: тело метода должно начинаться так: returnnewAbstractDbSelectExecutor
4. Пример вывода для User и Location : Id=5, name=’User-5′, description=Got by executing ‘select * from USER’ Id=1, name=’Location-1′, description=Got by executing ‘select * from LOCATION’