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)
}
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
}
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
}
// 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
}
- In Go, passing a slice is cheap (copying 24 bytes)
- However,
appendmight 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)
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)
Growth Algorithms:
- Go: roughly
cap = cap * 2for small sizes,cap = cap + cap/4for 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
Go (on reallocation):
s := make([]int, 3, 3) // [0,0,0]
s = append(s, 1) // new array, the old one is zeroed
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
}
Java:
- The
ArrayListobject 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
}
Java:
// Java: always works correctly
void addToList(ArrayList<Integer> list, int value) {
list.add(value); // always modifies the original
}
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
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)
-
sizedecreases by the number of removed elements. -
capacitydoes 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
}
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
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
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]
}
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
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
}
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
}
}
HashMap vs map
Typing
// Java - type-safe, but via generics
HashMap<String, Integer> map = new HashMap<>();
map.put("key", 123);
// Go - strict typing at the compiler level
m := make(map[string]int)
m["key"] = 123
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
// 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
Removing Elements
// Java
map.remove("key"); // returns the removed value (or null)
// Go
delete(m, "key") // does not return a value
value, ok := m["key"] // need to check separately
Iteration Guarantees
// Java
// No order guarantees (may change)
for (String key : map.keySet()) {
// order is undefined
}
// Go
// Intentionally randomized (since Go 1.0)
for k, v := range m {
// order is random to prevent developers from relying on it
}
Thread-safety
// Java
HashMap<String, String> map = new HashMap<>(); // not thread-safe
ConcurrentHashMap<String, String> cmap = new ConcurrentHashMap<>(); // thread-safe
// 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()
Methods vs. Built-in Functions
// Java
map.put("key", "value");
map.get("key");
map.containsKey("key");
map.size();
// 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
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)
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
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)
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
*/
// Go
make(map[string]int) // hint = 0 (normal growth)
make(map[string]int, 100) // allocation hint (not the exact capacity)
Default Values
// Java
map.get("missing") // returns null
// Go
m["missing"] // returns zero value (0 for int, "" for string)
// to distinguish from an actual "": value, ok := m["key"]
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
// 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
Top comments (0)