Я трохи запізнився з грою, але я помітив деякі ключові моменти, які залишились поза увагою, особливо щодо Java 8 та ефективності Arrays.asList
.
1. Як працює цикл для кожного?
Як Ciro Сантіллі六四事件法轮功包卓轩вказав, що є зручна утиліта для вивчення байткод , який поставляється з JDK: javap
. Використовуючи це, ми можемо визначити, що наступні два фрагменти коду створюють однаковий байт-код, як у Java 8u74:
Для кожного циклу:
int[] arr = {1, 2, 3};
for (int n : arr) {
System.out.println(n);
}
Для циклу:
int[] arr = {1, 2, 3};
{
int[] iter = arr;
int length = iter.length;
for (int i = 0; i < length; i++) {
int n = iter[i];
System.out.println(n);
}
}
2. Як отримати ітератор для масиву на Java?
Хоча це не працює для примітивів, слід зазначити, що перетворення масиву в Список із Arrays.asList
не впливає на продуктивність суттєво. Вплив як на пам’ять, так і на продуктивність майже неосяжний.
Arrays.asList
не використовує звичайну реалізацію List, яка легко доступна як клас. Він використовує java.util.Arrays.ArrayList
, що не те саме, що java.util.ArrayList
. Це дуже тонка обгортка навколо масиву, розмір якої неможливо змінити. Переглядаючи вихідний код для java.util.Arrays.ArrayList
, ми можемо побачити, що він розроблений таким чином, щоб функціонально еквівалентно масиву. Накладних витрат майже немає. Зауважте, що я опустив усі, крім найрелевантнішого коду, та додав власні коментарі.
public class Arrays {
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable {
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public E get(int index) {
return a[index];
}
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
}
}
Ітератор знаходиться на java.util.AbstractList.Itr
. Що стосується ітераторів, це дуже просто; він просто дзвонить, get()
поки не size()
буде досягнутий, подібно до того, як це зробить інструкція для циклу. Це найпростіша і зазвичай найбільш ефективна реалізація Iterator
масиву.
Знову ж таки, Arrays.asList
не створює java.util.ArrayList
. Він набагато легший і придатний для отримання ітератора з незначними накладними витратами.
Примітивні масиви
Як зазначали інші, Arrays.asList
не можна використовувати на примітивних масивах. Java 8 представляє кілька нових технологій роботи з колекціями даних, кілька з яких можна використовувати для вилучення простих і відносно ефективних ітераторів з масивів. Зверніть увагу, що якщо ви використовуєте дженерики, у вас завжди буде проблема боксу-розпакування: вам потрібно буде перетворити з int на Integer, а потім повернутися на int. Хоча бокс / розпакування зазвичай незначний, він у цьому випадку впливає на продуктивність O (1) і може призвести до проблем з дуже великими масивами або на комп'ютерах з дуже обмеженими ресурсами (тобто SoC ).
Мій особистий фаворит для будь-якої операції кастингу / боксу в Java 8 - це новий API потоку. Наприклад:
int[] arr = {1, 2, 3};
Iterator<Integer> iterator = Arrays.stream(arr).mapToObj(Integer::valueOf).iterator();
API потоків також пропонує конструкції для уникнення проблем боксу, але це вимагає відмови від ітераторів на користь потоків. Існують виділені типи потоків для int, long та double (IntStream, LongStream та DoubleStream, відповідно).
int[] arr = {1, 2, 3};
IntStream stream = Arrays.stream(arr);
stream.forEach(System.out::println);
Цікаво, що Java 8 також додає java.util.PrimitiveIterator
. Це забезпечує найкраще з обох світів: сумісність з Iterator<T>
вікс-боксом, а також методи уникнення боксу. PrimitiveIterator має три вбудовані інтерфейси, які його розширюють: OfInt, OfLong і OfDouble. Усі три будуть поля, якщо next()
вони викликані, але також можуть повертати примітиви за допомогою таких методів, як nextInt()
. Новіший код, розроблений для Java 8, слід уникати використання, next()
якщо бокс не є абсолютно необхідним.
int[] arr = {1, 2, 3};
PrimitiveIterator.OfInt iterator = Arrays.stream(arr);
Iterator<Integer> example = iterator;
while (iterator.hasNext()) {
int n = iterator.nextInt();
System.out.println(n);
}
Якщо ви ще не користуєтеся Java 8, на жаль, ваш найпростіший варіант набагато менш лаконічний і майже напевно передбачає бокс:
final int[] arr = {1, 2, 3};
Iterator<Integer> iterator = new Iterator<Integer>() {
int i = 0;
@Override
public boolean hasNext() {
return i < arr.length;
}
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return arr[i++];
}
};
Або якщо ви хочете створити щось більш багаторазове:
public final class IntIterator implements Iterator<Integer> {
private final int[] arr;
private int i = 0;
public IntIterator(int[] arr) {
this.arr = arr;
}
@Override
public boolean hasNext() {
return i < arr.length;
}
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return arr[i++];
}
}
Тут ви можете обійти проблему боксу, додавши власні методи отримання примітивів, але це спрацьовує лише з вашим власним внутрішнім кодом.
3. Чи перетворюється масив у список, щоб отримати ітератор?
Ні, це не так. Однак це не означає, що внесення його до списку призведе до гіршої продуктивності за умови використання чогось легкого, наприклад Arrays.asList
.