DEV Community

Cover image for Differences between Java and Golang in working with HashMap, map, ArrayList, and slice
Silver_dev
Silver_dev

Posted on

Differences between Java and Golang in working with HashMap, map, ArrayList, and slice

ArrayList vs slice

Data Structure

A Slice in Go is a three-component structure (24 bytes on a 64-bit system):

type slice struct {
    array unsafe.Pointer // pointer to the underlying array in the heap (8 bytes)
    len   int            // length (8 bytes)
    cap   int            // capacity (8 bytes)
}
Enter fullscreen mode Exit fullscreen mode

An ArrayList in Java is a single object containing:

class ArrayList<E> {
    private transient Object[] elementData;  // reference to an array
    private int size;                        // size
    // capacity = elementData.length
    // There is no separate cap field — it is derived from elementData.length
}
Enter fullscreen mode Exit fullscreen mode

If new ArrayList<>(5), then:

  • An internal Object[5] array is created
  • size = 0 (how many elements are actually added)
  • capacity = 5 (the maximum before the next resize)

Passing to Functions

// Java — passes an object reference (8 bytes on 64-bit)
void modify(ArrayList<Integer> list) {
    list.set(0, 100);              // modifies the original
    list.add(200);                 // modifies the original
}
Enter fullscreen mode Exit fullscreen mode
// Go — copies the slice header (fast, 24 bytes)
func modify(s []int) {
    s[0] = 100          // modifies the original (shared underlying array)
    s = append(s, 200)  // does NOT modify the original's len/cap
}
Enter fullscreen mode Exit fullscreen mode
  • In Go, passing a slice is cheap (copying 24 bytes)
  • However, append might not be reflected in the calling function
  • In Java, passing is cheap (1 reference), and you always operate on the original object

Resizing (Reallocation)

Java (ArrayList):

ArrayList<Integer> list = new ArrayList<>(5); // capacity=5
for(int i=0; i<5; i++) list.add(i);           // no reallocation
list.add(5);  // capacity = 5 + (5 >> 1) = 7 (increase 50%)
// The old array becomes garbage (eligible for GC)
Enter fullscreen mode Exit fullscreen mode

Go (slice):

s := make([]int, 0, 5)  // cap=5
s = append(s, 1,2,3,4,5) // cap=5, no reallocation
s = append(s, 6)         // cap=10 (typically *2 for <1024, then +25%)
// The old array (cap 5) becomes garbage (collected by GC)
Enter fullscreen mode Exit fullscreen mode

Growth Algorithms:

  • Go: roughly cap = cap * 2 for small sizes, cap = cap + cap/4 for large sizes
  • Java: newCapacity = oldCapacity + (oldCapacity >> 1) (1.5x)

Zeroing Out vs Retaining References

Java (on reallocation):

ArrayList<String> list = new ArrayList<>(3);
list.add("a"); list.add("b"); list.add("c");
list.add("d");  // copies references to the new array
// Old objects are NOT zeroed out, but the old array is GC'd
Enter fullscreen mode Exit fullscreen mode

Go (on reallocation):

s := make([]int, 3, 3)  // [0,0,0]
s = append(s, 1)        // new array, the old one is zeroed
Enter fullscreen mode Exit fullscreen mode

Interesting nuances:

Go:

func smallSlice() []int {
    s := make([]int, 3, 3)   // May stay on the stack if it does not escape
    return s                 // Escapes to the heap
}
Enter fullscreen mode Exit fullscreen mode

Java:

  • The ArrayList object is always on the heap
  • But the JVM can perform scalar replacement (stack allocation) for small objects

Go:

// Go: must remember to return
func addToSlice(s []int, val int) []int {
    return append(s, val)  // must return the new slice
}

// Otherwise:
func brokenAdd(s []int, val int) {
    s = append(s, val)  // BUG: won't modify the original
}
Enter fullscreen mode Exit fullscreen mode

Java:

// Java: always works correctly
void addToList(ArrayList<Integer> list, int value) {
    list.add(value);  // always modifies the original
}
Enter fullscreen mode Exit fullscreen mode

subList() — returns a view (not a copy):

ArrayList<Integer> list = new ArrayList<>(List.of(1,2,3,4,5));
List<Integer> sub = list.subList(1, 4);  // [2,3,4]
sub.set(0, 100);   // modifies the original: list = [1,100,3,4,5]
sub.add(999);      // also modifies the list

// DANGEROUS: upon structural modification of the original, the sub-list becomes invalid
list.add(6);       // sub now throws ConcurrentModificationException
Enter fullscreen mode Exit fullscreen mode

removeRange() — a protected method

public class MyArrayList<T> extends ArrayList<T> {
    public void removeRangePublic(int from, int to) {
        super.removeRange(from, to);  // removes without creating garbage
    }
}

// Before: size=10, capacity=20
list.removeRange(2, 5);  // removed 3 elements

// After: size=7, capacity=20 (unchanged)
Enter fullscreen mode Exit fullscreen mode
  • size decreases by the number of removed elements.
  • capacity does not change.

What happens internally:

protected void removeRange(int fromIndex, int toIndex) {
    int numMoved = size - toIndex;
    System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved);
    size = fromIndex + numMoved;  // only size decreases
    // elementData.length (capacity) remains the same
}
Enter fullscreen mode Exit fullscreen mode

trimToSize() — trims the capacity down to the current size

ArrayList<String> list = new ArrayList<>(1000);
// ... added only 10 elements
list.trimToSize();  // elementData = new Object[10], saving memory
Enter fullscreen mode Exit fullscreen mode

ArrayList.get(index) runs in O(1) time, just like a slice in Go.

// ArrayList.get() - direct access by index
public E get(int index) {
    return (E) elementData[index];  // constant time
}

// But LinkedList.get() is O(n)
LinkedList<String> linked = new LinkedList<>();
linked.get(1000);  // need to traverse 1000 nodes
Enter fullscreen mode Exit fullscreen mode

Synchronized variant
В Go:

type SafeSlice struct {
    mu sync.RWMutex
    slice []int
}

func (s *SafeSlice) Add(val int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.slice = append(s.slice, val)
}

func (s *SafeSlice) Get(i int) int {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.slice[i]
}
Enter fullscreen mode Exit fullscreen mode

Java:

// Regular ArrayList is not thread-safe
List<String> list = new ArrayList<>();
list.add("a");  // unsynchronized

// Synchronized wrapper
List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// MUST synchronize when iterating
synchronized (syncList) {
    for (String s : syncList) {  // manual sync is required
        // ...
    }
}

// ConcurrentModificationException IS POSSIBLE even in a synchronizedList
Enter fullscreen mode Exit fullscreen mode

Memory visibility (volatile vs sync/atomic)

Go:

type Data struct {
    mu sync.Mutex
    slice []int
}

func (d *Data) Add(val int) {
    d.mu.Lock()
    defer d.mu.Unlock()
    d.slice = append(d.slice, val)  // mutex guarantees visibility
}

// Without a mutex - data race:
func (d *Data) BrokenAdd(val int) {
    d.slice = append(d.slice, val)  // data race
}
Enter fullscreen mode Exit fullscreen mode

Java:

class SharedData {
    private volatile ArrayList<String> list;  // volatile for the reference

    public void update() {
        list = new ArrayList<>();  // reference change is visible to all threads
    }

    public void addElement(String s) {
        // Bad: volatile does not protect the elements
        list.add(s);  // might not be visible to other threads
    }

    // Correct approach:
    public synchronized void addSafe(String s) {
        list.add(s);  // synchronized guarantees visibility
    }
}
Enter fullscreen mode Exit fullscreen mode

HashMap vs map

Typing

// Java - type-safe, but via generics
HashMap<String, Integer> map = new HashMap<>();
map.put("key", 123);
Enter fullscreen mode Exit fullscreen mode
// Go - strict typing at the compiler level
m := make(map[string]int)
m["key"] = 123
Enter fullscreen mode Exit fullscreen mode

nil vs null

// Java
HashMap<String, String> map = null;  // allowed, but NullPointerException upon usage
map.put("key", "value");  // NullPointerException
// Cannot call methods on a null reference

map = new HashMap<>();
map.put(null, "value");  // null key is allowed
Enter fullscreen mode Exit fullscreen mode
// Go
var m map[string]string   // nil map
m["key"] = "value"        // panic: assignment to entry in nil map

m = make(map[string]string)  // must be initialized
m["key"] = "value"           // ok
// Go does not allow nil as a key
Enter fullscreen mode Exit fullscreen mode

Removing Elements

// Java
map.remove("key");      // returns the removed value (or null)
Enter fullscreen mode Exit fullscreen mode
// Go
delete(m, "key")        // does not return a value
value, ok := m["key"]   // need to check separately
Enter fullscreen mode Exit fullscreen mode

Iteration Guarantees

// Java
// No order guarantees (may change)
for (String key : map.keySet()) {
    // order is undefined
}
Enter fullscreen mode Exit fullscreen mode
// Go
// Intentionally randomized (since Go 1.0)
for k, v := range m {
    // order is random to prevent developers from relying on it
}
Enter fullscreen mode Exit fullscreen mode

Thread-safety

// Java
HashMap<String, String> map = new HashMap<>();  // not thread-safe
ConcurrentHashMap<String, String> cmap = new ConcurrentHashMap<>();  // thread-safe
Enter fullscreen mode Exit fullscreen mode
// Go
m := make(map[string]string)  // not thread-safe (data race)
var mu sync.RWMutex           // requires a mutex
// when working with a map, you must mutex lock() and unlock()
Enter fullscreen mode Exit fullscreen mode

Methods vs. Built-in Functions

// Java
map.put("key", "value");
map.get("key");
map.containsKey("key");
map.size();
Enter fullscreen mode Exit fullscreen mode
// Go
m["key"] = "value"    // built-in syntax
value := m["key"]     // if key is missing - zero value
_, ok := m["key"]     // check for existence
len(m)                // built-in function
Enter fullscreen mode Exit fullscreen mode

Internal Implementation

Java:

  • HashMap - an array of buckets with linked lists (chaining)
  • Initial number of buckets: 16
  • Upon collision: > 8 elements in a single bucket + total size > 64 → the list is converted into a red-black tree (Treeify)
  • load factor 0.75
  • You can specify the initial capacity (nearest power of two)
new HashMap<>();        // 16 buckets
new HashMap<>(100);     // 128 buckets (nearest power of 2)
Enter fullscreen mode Exit fullscreen mode

When Rehashing Occurs in Java HashMap

HashMap<String, String> map = new HashMap<>(16); // capacity=16, threshold=12 (16*0.75)

// Adding 12 elements — OK
for(int i=0; i<12; i++) map.put("key"+i, "value");
// Size = 12, threshold = 12

map.put("key12", "value");  // 13th element
// Rehash: capacity = 32, threshold = 24 (32*0.75)
// All 13 elements are redistributed
Enter fullscreen mode Exit fullscreen mode

Rehash conditions:

  • size >= threshold (threshold = capacity * load_factor)
  • Only after put() (insertion)

Go:

  • map - a hash table with "buckets" containing 8 elements each
  • Each bucket: 8 slots (a bucket with 8 elements)
  • When a bucket is full: an overflow bucket is created (not a new bucket, but an additional linked chain)
  • When load factor > ~6.5: growing occurs (doubling the number of buckets) with incremental evacuation (incremental rehashing)
make(map[string]int)        // small initial size
make(map[string]int, 100)   // ~14-16 buckets (100/8 * ~1.2)
Enter fullscreen mode Exit fullscreen mode

There is no Java-style rehash in Go. Instead:

  • Incremental evacuation (incremental rehashing)
  • During growth, new buckets are created, but elements are migrated gradually (1-2 elements at a time on each map access)
  • There is no long blocking pause for rebuilding the entire table

Size at creation

// Java
new HashMap<>();        // default capacity 16
new HashMap<>(100);     // capacity = nearest power of two (128)

// Rule: returns the nearest power of two ≥ cap
/*
    cap = 100  → 128  (2^7)
    cap = 120  → 128  (2^7)
    cap = 128  → 128  (power of two)
    cap = 129  → 256  (2^8)
    cap = 200  → 256
*/
Enter fullscreen mode Exit fullscreen mode
// Go
make(map[string]int)         // hint = 0 (normal growth)
make(map[string]int, 100)    // allocation hint (not the exact capacity)
Enter fullscreen mode Exit fullscreen mode

Default Values

// Java
map.get("missing")  // returns null
Enter fullscreen mode Exit fullscreen mode
// Go
m["missing"]  // returns zero value (0 for int, "" for string)
             // to distinguish from an actual "": value, ok := m["key"]
Enter fullscreen mode Exit fullscreen mode

Storing Structs

// Java - can be modified via get, as references are stored
HashMap<String, User> map = new HashMap<>();
map.put("key", new User("Bob"));
map.get("key").setName("John");  // works
Enter fullscreen mode Exit fullscreen mode
// Go
type User struct { Name string }

m := make(map[string]User)

u := m["key"]
u.Name = "John"  // won't compile (cannot modify a value in a map directly)

user := m["key"]
user.Name = "John"  // modify via a temporary variable
m["key"] = user     // write it back
Enter fullscreen mode Exit fullscreen mode

Top comments (0)