Blog

Creating a Custom ArrayList in Java: A Step-by-Step Guide

When working with collections in Java, the ArrayList class is one of the most frequently used data structures due to its flexibility and powerful features. However, there may be instances where you need a customized version to suit specific requirements. This blog post will guide you through creating a simple, custom ArrayList implementation in Java, explaining each step to understand the underlying mechanics.

Introduction to ArrayList

An ArrayList in Java is a resizable array, which can grow as needed to accommodate adding items. Unlike a standard array, it provides methods to manipulate the size, and elements, and perform various operations such as adding, removing, and accessing elements.

Why Create a Custom ArrayList?

Creating a custom ArrayList allows you to:

  • Implement specific behaviors or constraints.
  • Optimize performance for particular use cases.
  • Understand the internal workings of data structures.
Step-by-Step Implementation

Our custom ArrayList, named MyArrayList, will implement basic functionalities: adding, removing, and accessing elements, along with checking the size and if it’s empty.

1. Defining the Class and Constructor

First, we define our class with a parameterized type <E> to make it generic:

package datastructure.array;

public class MyArrayList <E>{
    private Object [] elements;
    private int size;

    public MyArrayList(){
        elements = new Object[10];
    }
}

The elements array will store the list items. Initially, it has a capacity of 10 elements, but this will increase as needed. The size keeps track of the actual number of elements in the list.

2. Ensuring Capacity

Before adding new elements, we must ensure that there is enough space in the underlying array. If the array is full, we create a new array with double the capacity and copy the existing elements.

private void ensureCapacity(){
    if(size == elements.length){
        Object[] newElements = new Object[elements.length * 2];
        for(int i = 0; i < elements.length; i++){
            newElements[i] = elements[i];
        }
        elements = newElements;
    }
}

3. Adding Elements

To add an element, we first ensure capacity, then add the new element at the next available position, indicated by size, and increment size.

public void add(E e){
    ensureCapacity();
    elements[size++] = e;
}

4. Accessing Elements

To access an element, we provide its index. If the index is out of bounds, we throw an IndexOutOfBoundsException.

@SuppressWarnings("unchecked") 
public E get(int index){
    if(index >= size || index < 0){
        throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
    }
    return (E) elements[index];
}

In this code:

  • If @SuppressWarnings("unchecked") annotation is not added then getting warnings like Unchecked cast: 'java.lang.Object' to 'E'
  • The @SuppressWarnings("unchecked") annotation is used to suppress the unchecked cast warning. This tells the compiler that the programmer believes the cast to be safe and does not want the warning to be shown.

Remember, suppressing warnings should be done with caution. In this case, it’s relatively safe because the internal array (elements) is controlled and only E type elements are added to it. Therefore, the cast should be safe. However, in more complex cases or public APIs, it might be better to find a design that avoids the need for unchecked casts altogether, though that can be challenging with Java’s type system.

5. Removing Elements

Removing an element involves shifting all subsequent elements one position to the left to fill the gap. We also nullify the last element to avoid memory leaks.

public void remove(int index){
    if(index >= size || index < 0){
        throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
    }
    for(int i = index; i < size - 1; i++){
        elements[i] = elements[i + 1];
    }
    elements[--size] = null;
}

6. Size and IsEmpty

We provide two utility methods to check the number of elements and if the list is empty:

public int size(){
    return size;
}

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

7. Testing Our MyArrayList

Finally, we test our implementation with a simple main method:

public static void main(String[] args) {
    MyArrayList<Integer> myArrayList = new MyArrayList<>();
    myArrayList.add(12);
    myArrayList.add(13);
    myArrayList.add(11);

    for(int i = 0; i < myArrayList.size(); i++){
        System.out.println(myArrayList.get(i) + ", ");
    }
}

Conclusion

Through this guide, we’ve created a basic yet functional custom ArrayList in Java. While this implementation covers fundamental operations, Java’s standard ArrayList offers much more. However, understanding and building your own data structures is invaluable for grasping the concepts behind them and can be a rewarding experience.

Remember, this is a simplified version meant for educational purposes. Real-world applications would require more comprehensive methods and considerations for efficiency and safety.

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over