Java programming note 8: container

Posted by jenniferG on Mon, 24 Jan 2022 21:30:57 +0100

Java programming note 8: container (I)

Source: PHP Chinese network

Container is an important part of programming language. Container and language style are closely related, such as lists, tuples, maps, etc. in Python, slicing and mapping of Go, etc.

This article will explore containers in Java.

Collection

Containers in Java can be roughly divided into two categories: collection and map. Collection includes containers that can store a series of elements, such as List, Set and Query. Map represents a mapping type that can hold key value pairs.

Collection can be translated into a Set, but Set can also be translated into a Set, but the two are essentially different. The former generally refers to a kind of container that can store elements, while the latter refers to a mathematical concept, that is, a Set that does not contain duplicate elements, which is expressed as a de duplication container in Java.

Collection is an interface that extends from the Iterable interface. All containers of collection type will implement this interface. The following is the definition of the collection interface extracted from the Java source code:

public interface Collection<E> extends Iterable<E> {
    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);

    default <T> T[] toArray(IntFunction<T[]> generator) {
        return toArray(generator.apply(0));
    }

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean removeAll(Collection<?> c);

    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

    boolean retainAll(Collection<?> c);

    void clear();

    boolean equals(Object o);

    int hashCode();

    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

The more important methods are:

  • int size();, Returns the number of elements contained in the container.
  • boolean isEmpty();, Determine whether the container is empty.
  • boolean contains(Object o);, Determine whether the container contains an element.
  • Iterator<E> iterator();, Returns the iterator of a container.
  • Object[] toArray();, Returns an array of all elements in a container. If the elements in the container are ordered, the order of the elements in the array must be consistent.
  • <T> T[] toArray(T[] a);, Returns an array of container elements (generic version).
  • boolean add(E e);, Add elements to the container.
  • boolean remove(Object o);, Removes the given element, if any, from the container.
  • boolean containsAll(Collection<?> c);, Judge whether the given collection container is contained by the current container.
  • boolean addAll(Collection<? extends E> c);, Adds elements from the given collection container to the current container.
  • boolean removeAll(Collection<?> c);, Deletes the elements in the given collection container from the current container.
  • boolean retainAll(Collection<?> c);, Keep all elements in container C in the current container, in other words, elements that are not in container C will be deleted from the current container.
  • void clear();, Empty the container.
  • boolean equals(Object o);, Determines whether the given object is equal to the container.

Here is an example of the use of Collection. In this example, ArrayList is used as the real container and the Collection handle is used for operation:

package ch8.collection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Random;

public class Main {
    private static Random random = new Random();

    private static Collection<Integer> getRandomNumbers(int times) {
        Collection<Integer> collection = new ArrayList<Integer>();
        for (int i = 0; i < times; i++) {
            collection.add(random.nextInt(100));
        }
        return collection;
    }

    public static void main(String[] args) {
        Collection<Integer> numbers = new ArrayList<Integer>();
        numbers.add(100);
        System.out.println(numbers);
        Collection<Integer> numbers2 = getRandomNumbers(3);
        System.out.println(numbers2);
        numbers.addAll(numbers2);
        System.out.println(numbers);
        System.out.println(numbers.contains(100));
        System.out.println(numbers.containsAll(numbers2));
        numbers.remove(100);
        System.out.println(numbers);
        numbers.removeAll(numbers2);
        System.out.println(numbers);
        System.out.println(numbers.add(99));
        System.out.println(numbers);
        numbers.clear();
        System.out.println(numbers);
        // [100]
        // [60, 95, 44]
        // [100, 60, 95, 44]
        // true
        // true
        // [60, 95, 44]
        // []
        // true
        // [99]
        // []
    }
}

Although in practice, Collection is rarely used as a handle to ArrayList or LinkedList objects, List is used instead. However, the above example can still illustrate the purpose of the relevant methods in the Collection interface.

generic paradigm

The type of Collection < integer > used in Collection is described above, and the part in < > is called "generic". A detailed introduction to generics will be provided in subsequent articles. This only explains why generics are used.

Before Java se 5, there were no generics. In fact, all elements in the container were saved as Object types:

package ch8.generic;

import java.util.ArrayList;
import java.util.List;

import util.Fmt;

class Apple {
    private static int counter = 0;
    private int id = ++counter;

    public void eat() {
        Fmt.printf("Apple(%d) is eated.\n", id);
    }
}

class Orange {
}

class RedApple extends Apple {
}

class GreenApple extends Apple {
}

public class Main {
    private static void printApples(List apples) {
        for (Object object : apples) {
            Apple apple = (Apple) object;
            apple.eat();
        }
    }

    public static void main(String[] args) {
        List apples = new ArrayList();
        apples.add(new Apple());
        apples.add(new RedApple());
        apples.add(new GreenApple());
        printApples(apples);
        // Apple(1) is eated.
        // Apple(2) is eated.
        // Apple(3) is eated.
        apples.add(new Orange());
        printApples(apples);
        // Apple(1) is eated.
        // Apple(2) is eated.
        // Apple(3) is eated.
        // Exception in thread "main" java.lang.ClassCastException: class
        // ch8.generic.Orange cannot be cast to class ch8.generic.Apple
        // (ch8.generic.Orange and ch8.generic.Apple are in unnamed module of loader
        // 'app')
        // at ch8.generic.Main.printApples(Main.java:29)
        // at ch8.generic.Main.main(Main.java:40)
    }
}

As shown in the above example, sometimes this will cause some trouble, such as "accidentally" adding types we don't need in the container and generating runtime ClassCastException when we try to convert them to the types we need.

If generics are used, all these problems can be checked at compile time. And the element you take out from the container is the target type directly, without manual conversion.

List

List is a more common container interface than Collection. It inherits from Collection and adds operations related to digital index on the basis of Collection interface.

The following List definition is excerpted from the Java source code, and the duplicate parts with the Collection interface are deleted:

public interface List<E> extends Collection<E> {
    boolean addAll(int index, Collection<? extends E> c);

    @SuppressWarnings({ "unchecked", "rawtypes" })
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

    E get(int index);

    E set(int index, E element);

    void add(int index, E element);

    E remove(int index);

    int indexOf(Object o);

    int lastIndexOf(Object o);

    ListIterator<E> listIterator();

    ListIterator<E> listIterator(int index);

    List<E> subList(int fromIndex, int toIndex);

    @SafeVarargs
    @SuppressWarnings("varargs")
    static <E> List<E> of(E... elements) {
        switch (elements.length) { // implicit null check of elements
            case 0:
                @SuppressWarnings("unchecked")
                var list = (List<E>) ImmutableCollections.EMPTY_LIST;
                return list;
            case 1:
                return new ImmutableCollections.List12<>(elements[0]);
            case 2:
                return new ImmutableCollections.List12<>(elements[0], elements[1]);
            default:
                return ImmutableCollections.listFromArray(elements);
        }
    }

    static <E> List<E> copyOf(Collection<? extends E> coll) {
        return ImmutableCollections.listCopy(coll);
    }
}

The more important methods are:

  • boolean addAll(int index, Collection<? extends E> c);, Adds an element from the specified container to the specified location.
  • Default void sort (comparator <? Super E > C) to sort the elements in the container (the sorting rule needs to be specified).
  • E get(int index);, Maybe an element at the specified location.
  • E set(int index, E element);, Replaces the element at the specified location.
  • void add(int index, E element);, Adds an element at the specified location.
  • E remove(int index);, Deletes an element from the specified location and returns the element.
  • int indexOf(Object o);, Returns the position where the specified element first appears in the container, - 1 means it does not exist.
  • int lastIndexOf(Object o);, Returns the location of the last occurrence of the specified element in the container, - 1 indicates that it does not exist.
  • ListIterator<E> listIterator();, Returns a list iterator.
  • ListIterator<E> listIterator(int index);, Returns a list iterator (iterating from the specified location).
  • List<E> subList(int fromIndex, int toIndex);, Returns a sublist (not including the element of toindex location), which shares the underlying storage with the original list, so it will affect each other.

Here are some simple tests of these methods:

package ch8.list;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        Random random = new Random();
        list.add(random.nextInt(100));
        System.out.println(list);
        Integer[] numbers = new Integer[] { 1, 2, 3, 4, 5, 6, 7 };
        list.addAll(Arrays.asList(numbers));
        System.out.println(list);
        System.out.println(list.indexOf(3));
        list.add(3, 99);
        System.out.println(list);
        list.addAll(3, Arrays.asList(new Integer[] { 99, 1, 99 }));
        System.out.println(list);
        System.out.println(list.lastIndexOf(99));
        list.remove(new Integer(99));
        System.out.println(list);
        list.remove(4);
        System.out.println(list);
        // [91]
        // [91, 1, 2, 3, 4, 5, 6, 7]
        // 3
        // [91, 1, 2, 99, 3, 4, 5, 6, 7]
        // [91, 1, 2, 99, 1, 99, 99, 3, 4, 5, 6, 7]
        // 6
        // [91, 1, 2, 1, 99, 99, 3, 4, 5, 6, 7]
        // [91, 1, 2, 1, 99, 3, 4, 5, 6, 7]
    }
}

It should be noted that for convenience, the list composed of wrapper class Integer of base type int is used here. Because the internal comparison of elements in the list is actually through object The equals method is implemented, and the default implementation of this method is to compare the address value of the object. The equals method is rewritten by wrapper classes such as Integer and built-in types such as String, which compares the value rather than the address. Therefore, it is more convenient to use the container composed of them for example.

It should also be noted that since the element type of the container is integer, additional attention should be paid to the operations related to the numerical index of the container, such as the list remove(new Integer(99));, If it's written as a list remove(99); An "array out of bounds exception" is triggered. This is because the former calls list Remove (object o) this method is used to delete the specified element. The latter calls list Remove (int index) this method is used to delete elements from the specified location. Obviously, our container does not have as many as 100 elements. The principle here is that when the method composed of wrapper class parameters and the method composed of basic elements can meet the call, the compiler will give priority to the latter.

Arrays.asList

Another tool function that needs to be explained is arrays Aslist, this method can convert an array into a List.

Its specific definition:

    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

asList uses a variable parameter List and returns a List object implemented by ArrayList.

stay Java Programming Notes 2: initialization and cleaning - konjac black tea's blog (icexmoon.xyz) I mentioned the variable parameter list in. For the formal parameters defined in this way, we can pass multiple elements or an entire array. The asList method can also be called as follows:

package ch8.aslist;

import java.util.Arrays;
import java.util.List;

class Fruit {
}

class Apple extends Fruit {
}

class Oranger extends Fruit {
}

class RedApple extends Apple {
}

class YellowApple extends Apple {
}

public class Main {
    public static void main(String[] args) {
        Fruit[] fruits = new Fruit[] { new Apple(), new Oranger(), new RedApple(), new YellowApple() };
        List<Fruit> list = Arrays.asList(fruits);
        List<Fruit> list2 = Arrays.asList(new RedApple(), new YellowApple());
        System.out.println(list2);
    }
}

Using the asList method and colleciton The addall method makes it easy to batch add elements to containers:

...
public class Main {
    public static void main(String[] args) {
        List<Fruit> fruits = new ArrayList<Fruit>();
        fruits.addAll(Arrays.asList(new RedApple(), new YellowApple()));
        System.out.println(fruits);
        fruits.clear();
        Collections.addAll(fruits, new RedApple(), new YellowApple());
        System.out.println(fruits);
        // [ch8.aslist2.RedApple@2f92e0f4, ch8.aslist2.YellowApple@28a418fc]
        // [ch8.aslist2.RedApple@5305068a, ch8.aslist2.YellowApple@1f32e575]
    }
}

However, as in the above example, use collections The addall method can achieve similar effects and is more convenient.

Non modifiable list

In addition, List contains two static methods:

  • static <E> List<E> of(E... elements)
  • static <E> List<E> copyOf(Collection<? extends E> coll)

In fact, the of method also has several overloaded methods, which seem to be designed to optimize performance.

The purpose of these two methods is to return an unmodifiable list.

The so-called non modifiable list has the following characteristics:

  • Elements cannot be added, deleted or replaced from the list (but the element content cannot be guaranteed to be modified). Any such operation will throw an unsupported operationexception.
  • Null values are not allowed. Attempting to initialize a non modifiable list with null will throw a NullPointerException exception.
  • If all elements are serializable, the list is serializable.
  • The order of the elements in the list is consistent with the order of the given parameters or the order in the given container.

Here is a simple test:

package ch8.un_list;

import java.util.List;

class MyInteger {
    private int number;

    public MyInteger(int number) {
        this.number = number;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (obj instanceof MyInteger) {
            MyInteger other = (MyInteger) obj;
            return other.number == this.number;
        } else {
            return super.equals(obj);
        }
    }

    @Override
    public String toString() {
        return Integer.toString(this.number);
    }

    public void setValue(int value) {
        this.number = value;
    }
}

public class Main {
    private static MyInteger[] getNumbers() {
        int length = 10;
        MyInteger[] numbers = new MyInteger[length];
        for (int i = 0; i < length; i++) {
            numbers[i] = new MyInteger(i);
        }
        return numbers;
    }

    public static void main(String[] args) {
        MyInteger[] numbers = getNumbers();
        List<MyInteger> list = List.of(numbers);
        System.out.println(list);
        // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        list.get(0).setValue(99);
        System.out.println(list);
        // [99, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        list.remove(1);
        // Exception in thread "main" java.lang.UnsupportedOperationException
    }
}

You can see that the generated "non modifiable list" cannot delete the element, but the value in the element can be modified (this is determined by the implementation of MyInteger).

iterator

Iterator is actually a design pattern, which is Design pattern with Python 9: iterator pattern - konjac black tea's blog (icexmoon.xyz) I have explained it in detail. Because iterating on containers itself is quite frequent in programming, most programming languages choose to support them through standard libraries or even built-in syntax.

Java supports this through the interfaces iteratable and Iterator.

Definition of Iterable:

public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Definition of Iterator:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

Where iteratable represents an iteratable object and Iterator represents a concrete Iterator.

This implementation is very similar to Python.

The most critical is the Iterator's hasNext and next methods, through which the Iterator can be used to traverse the elements in the container.

The advantage of this is that the responsibility of the class itself can be separated from the function of iteration, and the iterator can specifically undertake the work of iteration. A specific example is given below:

package ch8.iterator2;

import java.util.Arrays;
import java.util.Iterator;
import java.util.Random;

class RandomNumbers implements Iterable<Integer> {
    private static Random random = new Random();
    private int[] numbers;

    public RandomNumbers(int size) {
        if (size <= 0) {
            throw new Error();
        }
        numbers = new int[size];
        for (int i = 0; i < size; i++) {
            numbers[i] = random.nextInt(100);
        }
    }

    @Override
    public String toString() {
        return Arrays.toString(numbers);
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            private int cursor = -1;

            @Override
            public boolean hasNext() {
                if (cursor + 1 < numbers.length) {
                    return true;
                }
                return false;
            }

            @Override
            public Integer next() {
                cursor++;
                return numbers[cursor];
            }

        };
    }
}

public class Main {
    private static void printIterable(Iterable<Integer> iterable) {
        Iterator<Integer> iterator = iterable.iterator();
        while (iterator.hasNext()) {
            int num = iterator.next();
            System.out.print(num + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        RandomNumbers rn = new RandomNumbers(10);
        System.out.println(rn);
        printIterable(rn);
        // [68, 31, 66, 75, 47, 45, 18, 48, 37, 58]
        // 68 31 66 75 47 45 18 48 37 58 
    }
}

In this example, the RandomNumbers class represents a random integer sequence of a specified length. After it implements the iterator < integer > interface, it can be traversed and output in a manner similar to that in the printiteratable function. The best thing is that the printiteratable function itself is quite reusable. It can traverse any type that implements the iteratable < integer > interface, which is the power of the iterator pattern.

Here, the iterator method is directly implemented with anonymous classes. See for the description of anonymous classes Java Programming Notes 7: internal classes - konjac black tea's blog (icexmoon.xyz).

Even better, just like in Python, you can use for in... Syntax to traverse the types that implement the iterator protocol. In Java, you can also directly use foreach syntax to traverse the types that implement the iteratable interface:

...
public class Main {
    public static void main(String[] args) {
        RandomNumbers rn = new RandomNumbers(10);
        System.out.println(rn);
        for (int num : rn) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

This approach is undoubtedly more concise than the previous approach, so it is usually used to traverse iteratable objects.

List iterator

You may have noticed that the List interface has a listiterator method that returns an object of type listiterator. In fact, listiterator is an interface extended from Iterator:

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();
    E next();
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);
}

The ordinary iterator described earlier can only use the hasNext and next methods to iterate forward, but the ListIterator can perform "two-way iteration". This is reflected in its additional methods:

Here, "before" refers to the right side of the array and the side where the numerical index increases, and "after" refers to the left side of the array.

  • Whether hasPrevious can iterate backward.
  • previous iterates back and returns an element.
  • nextIndex, which returns the numerical index of the next element when iterating forward.
  • previousIndex, which returns the numerical index of the next element in backward iteration.

In addition to the methods required for these necessary two-way iterations, it also provides some methods to modify the elements in the container:

  • remove to delete the current element.
  • set to replace the current element.
  • Add to add an element to the current location.

Here is a simple demonstration of the use of list iterators:

package ch8.list_iterator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<Integer>();
        Collections.addAll(numbers, 1, 2, 3, 4, 5);
        System.out.println(numbers);
        ListIterator<Integer> li = numbers.listIterator();
        while (li.hasNext()) {
            if (li.nextIndex() == 2) {
                li.add(Integer.valueOf(99));
            }
            System.out.print(li.next() + " ");
        }
        System.out.println();
        while (li.hasPrevious()) {
            System.out.print(li.previous() + " ");
        }
        System.out.println();
        // [1, 2, 3, 4, 5]
        // 1 2 3 4 5 
        // 5 4 3 99 2 1
    }
}

It should be noted that not all objects that implement listiterator must implement methods such as add and remove. For example, the "immutable list" mentioned earlier. Obviously, the list iterator returned by the listiterator method of its example will not implement relevant methods. Trying to call these methods will only produce an exception.

ArrayList

There are two most common containers in the standard library that implement the List interface:

  • ArrayList
  • LinkedList

As can be seen from the name, the former is based on array and the latter is based on linked list. And their advantages and disadvantages are indeed consistent with arrays and linked lists:

  • ArrayList is better than random access. The disadvantage is that the performance of adding and deleting elements anywhere is poor.
  • LinkedList is better than adding and deleting elements anywhere, but its disadvantage is poor random access performance.

The reason why ArrayList performs poorly when adding or deleting elements is that its underlying implementation is array, so it is necessary to recreate the array when adding or deleting elements. Especially, when adding elements, consider the simplest implementation. If you need to re apply for a new array with length + 1 every time you add a new element, and copy the data, Then the efficiency is undoubtedly very poor, but this can be solved by optimizing the underlying implementation.

The implementation of ArrayList is actually very similar to Go's built-in type slicing. Therefore, although the underlying array of ArrayList will actually be expanded in a complex way, we can still implement a simple ArrayList with the idea of slicing:

package ch8.arraylist3;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.ListIterator;

import ch8.list.List;

public class SimpleArrayList<E> implements List<E> {
    private static Object[] EMPTY_ARRAY = new Object[0];
    private Object[] array;
    private int size;
    private int cap;

    public SimpleArrayList() {
        array = EMPTY_ARRAY;
    }

    public SimpleArrayList(int cap) {
        array = new Object[cap];
        this.cap = cap;
    }

    public SimpleArrayList(Collection<? extends E> c) {
        Object[] newArray = c.toArray();
        if (newArray.length > 0) {
            size = newArray.length;
            cap = newArray.length;
            if (c.getClass() == ArrayList.class) {
                array = newArray;
            } else {
                array = Arrays.copyOf(newArray, newArray.length, Object[].class);
            }
        } else {
            array = EMPTY_ARRAY;
        }
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public boolean contains(Object o) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private int cursor = -1;

            @Override
            public boolean hasNext() {
                if (cursor + 1 < size) {
                    return true;
                }
                return false;
            }

            @Override
            public E next() {
                cursor++;
                return (E) array[cursor];
            }

        };
    }

    ...

    @Override
    public boolean add(Object e) {
        if (cap > size) {
            array[size] = e;
            size++;
        } else if (size == 0) {
            // Initially add 1 capacity at a time
            int initCap = 1;
            array = new Object[initCap];
            array[0] = e;
            cap = initCap;
            size = 1;
        } else {
            // One time capacity expansion
            int newSize = size + 1;
            int newCap = cap * 2;
            System.out.println("do array extends,new cap is " + newCap);
            Object[] newArray = new Object[newCap];
            for (int i = 0; i < array.length; i++) {
                newArray[i] = array[i];
            }
            newArray[array.length] = e;
            array = newArray;
            cap = newCap;
            size = newSize;
        }
        return true;
    }

    ...

    public static void main(String[] args) {
        SimpleArrayList<Integer> sal = new SimpleArrayList<>();
        fillSimpleArrayList(sal);
        printSimpleArrayList(sal);
        sal = new SimpleArrayList<>(10);
        fillSimpleArrayList(sal);
        printSimpleArrayList(sal);
        sal = new SimpleArrayList<>(Arrays.asList(new Integer[]{1,2,3,4,5}));
        fillSimpleArrayList(sal);
        printSimpleArrayList(sal);
        // do array extends,new cap is 2
        // do array extends,new cap is 4
        // do array extends,new cap is 8
        // do array extends,new cap is 16
        // 0 1 2 3 4 5 6 7 8 9
        // 0 1 2 3 4 5 6 7 8 9
        // do array extends,new cap is 10
        // do array extends,new cap is 20
        // 1 2 3 4 5 0 1 2 3 4 5 6 7 8 9
    }

    private static void fillSimpleArrayList(SimpleArrayList<Integer> sal) {
        for (int i = 0; i < 10; i++) {
            sal.add(i);
        }
    }

    private static void printSimpleArrayList(SimpleArrayList sal) {
        for (Object object : sal) {
            System.out.print(object + " ");
        }
        System.out.println();
    }

}

Similar to the slice of Go, in the SimpleArrayList, the cap attribute is used to save the capacity of the current underlying array, and the size is used to save the actual number of elements in the current list. When the capacity of the underlying array needs to be expanded, the actual length of the underlying array is expanded by doubling the current capacity each time.

The test also shows this. The actual capacity of the underlying array is expanded by 2, 4, 8 and 16, which is undoubtedly much better than the way of expanding the underlying array every time an element is added.

At the same time, SimpleArrayList also provides a constructor that receives an int parameter. The constructor can directly create an underlying array with a specified capacity during initialization. If you already know the number of elements to add when using SimpleArrayList, this constructor is very useful to avoid the additional overhead caused by unnecessary underlying array expansion.

In addition, SimpleArrayList can be initialized by giving a Collection.

In fact, although the real ArrayList does not use this simple strategy to expand the underlying array, its principle and behavior are similar, and three constructors are also provided to initialize ArrayList, so the SimpleArrayList here is quite meaningful for reference.

For the complete definition of SimpleArrayList, see java-notebook/SimpleArrayList.java at main · icexmoon/java-notebook (github.com) Of course, I only implemented the key methods of the List interface, and the other methods automatically generate code. Interested children's shoes can continue to improve this code.

LinkedList

As mentioned earlier, the reason why LinkedList has poor random read and write performance is that its underlying implementation is linked list, which is the characteristic of the data structure of linked list. In fact, it can also be optimized by improving the implementation method, such as using an additional array to maintain the digital index of the linked list. Of course, this will also bring some other troubles. In fact, each type of data structure has its advantages and disadvantages, and there is no perfect data structure.

The following shows a simple LinkedList I implemented as an example of the implementation mechanism:

package ch8.linkedlist;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.ListIterator;

import ch8.list.List;

public class SimpleLinkedList<T> implements List<T> {
    private Node<T> first = null;

    public SimpleLinkedList() {
    }

    public SimpleLinkedList(Collection<? extends T> c) {
        if (c.size() > 0) {
            boolean isInited = false;
            Node<T> currentNode = null;
            for (T t : c) {
                if (!isInited) {
                    first = new Node<T>(t);
                    currentNode = first;
                    isInited = true;
                    continue;
                }
                Node<T> newNode = new Node<T>(t);
                currentNode.link(newNode);
                currentNode = newNode;
            }
        }
    }

	...
        
    @Override
    public Iterator<T> iterator() {
        return new Iterator<T>() {
            Node<T> current;

            @Override
            public boolean hasNext() {
                if (first == null) {
                    return false;
                } else if (current == null) {
                    return true;
                } else {
                    return current.hasNext();
                }
            }

            @Override
            public T next() {
                if (first == null) {
                    return null;
                } else if (current == null) {
                    current = first;
                    return current.getData();
                } else {
                    current = current.getNext();
                    return current.getData();
                }
            }

        };
    }

	...
    @Override
    public boolean add(T e) {
        if (first == null) {
            first = new Node<T>(e);
        } else {
            Node<T> newNode = new Node<T>(e);
            Node<T> lastNode = getLastNode();
            lastNode.link(newNode);
        }
        return true;
    }
	
    ...

    private Node<T> getLastNode() {
        if (first == null) {
            return first;
        }
        Node<T> current = first;
        while (true) {
            if (current.hasNext()) {
                current = current.getNext();
            } else {
                break;
            }
        }
        return current;
    }

    private class Node<T> {
        private Object data;
        private Node<T> next;

        public Node(T data) {
            this.data = data;
        }

        public T getData() {
            return (T) data;
        }

        public void link(Node<T> next) {
            this.next = next;
        }

        public boolean hasNext() {
            return next != null;
        }

        public Node<T> getNext() {
            return next;
        }
    }

    public static void main(String[] args) {
        SimpleLinkedList<Integer> numbers = new SimpleLinkedList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        for (Integer integer : numbers) {
            System.out.print(integer + " ");
        }
        System.out.println();
        numbers = new SimpleLinkedList<>(Arrays.asList(new Integer[] { 1, 2, 3, 4, 5 }));
        numbers.add(99);
        for (Integer integer : numbers) {
            System.out.print(integer + " ");
        }
    }
}

Similarly, this class only implements the methods required for demonstration, and most of the methods of List are not implemented. But this class can still explain the underlying linked List mechanism.

  • Here, the private internal class Node is used as the implementation class of the linked list, considering that the linked list Node should only be used by SimpleLinkedList.
  • The implementation here is quite "rough" and can be further optimized. For example, an additional node < T > reference is used to save the last linked list node. In this way, when calling the add method, there is no need to traverse the whole linked list, but only need to add a new node to the last node.

The real LinkedList class adds a large number of additional methods based on the List interface, some of which have similar functions, such as:

package ch8.linkedlist;

import java.util.Collections;
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        LinkedList<Integer> ll = new LinkedList<>();
        Collections.addAll(ll, 1, 2, 3, 4, 5);
        System.out.println(ll.getFirst());
        System.out.println(ll.element());
        System.out.println(ll.peek());
        // 1
        // 1
        // 1
    }
}

getFirst, element and peek can all return the first element, which may confuse some developers.

In fact, these methods are designed so that LinkedList can more "simulate" some other data structures, such as stacks or queues.

Stack

Using LinkedList, we can easily implement a stack:

package ch8.stack;

import java.util.LinkedList;

public class Stack<T> {
    private LinkedList<T> datas = new LinkedList<>();

    public T pop() {
        return datas.removeFirst();
    }

    public void push(T data) {
        datas.addFirst(data);
    }

    public T peek() {
        return datas.getFirst();
    }

    public boolean empty() {
        return datas.isEmpty();
    }

    @Override
    public String toString() {
        return datas.toString();
    }
}

Perform a simple test:

package ch8.stack;

public class Main {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < 5; i++) {
            System.out.println("push " + i);
            stack.push(i);
            System.out.println(stack);
        }
        System.out.println("starting pop stack.");
        while (!stack.empty()) {
            Integer num = stack.pop();
            System.out.println(num + " is poped.");
            System.out.println(stack);
        }
        // push 0
        // [0]
        // push 1
        // [1, 0]
        // push 2
        // [2, 1, 0]
        // push 3
        // [3, 2, 1, 0]
        // push 4
        // [4, 3, 2, 1, 0]
        // starting pop stack.
        // 4 is poped.
        // [3, 2, 1, 0]
        // 3 is poped.
        // [2, 1, 0]
        // 2 is poped.
        // [1, 0]
        // 1 is poped.
        // [0]
        // 0 is poped.
        // []
    }
}

In fact, the standard library also implements a stack: Java util. Stack. However, this class is actually implemented by inheriting the Vector class. In addition to providing pop, push and other methods necessary for the stack, it inherits a large number of Vector methods that are not needed. From the standard of design pattern, this is not very appropriate. Therefore, the stack class implemented in the above way is more in line with the single behavior principle.

I was going to use an article to summarize the container, but I found it unrealistic and too long. The rest will be put in the next article.

Thank you for reading.

reference material:

Topics: Java Container list iterator