Skip to content

Instantly share code, notes, and snippets.

@NetzwergX
Last active May 8, 2020 19:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NetzwergX/e0e09f3a10f40bdae7fac643193b8d0e to your computer and use it in GitHub Desktop.
Save NetzwergX/e0e09f3a10f40bdae7fac643193b8d0e to your computer and use it in GitHub Desktop.
Demonstration of mutability of records & defensive copying to address it
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
public class RecordMutabilityTest {
public static interface RecordWithList {
List<Date> values();
}
public static record NaiveRecord(String name, List<Date> values) implements RecordWithList {}
public static record BetterRecord(String name, List<Date> values) implements RecordWithList {
public BetterRecord {
values = Collections.unmodifiableList(values);
}
};
public static record EvenBetterRecord(String name, List<Date> values) implements RecordWithList {
public EvenBetterRecord {
values = Collections.unmodifiableList(new ArrayList<>(values));
}
};
public static record ImmutableRecord(String name, List<Date> values) implements RecordWithList {
public ImmutableRecord {
values = values.stream()
.map(Date::clone)
.map(Date.class::cast)
.collect(Collectors.toUnmodifiableList());
}
}
public static record ImmutableAlternateRecord(String name, List<Date> values) implements RecordWithList {
public ImmutableAlternateRecord {
values = values.stream()
.map(Date::clone)
.map(Date.class::cast)
.collect(Collectors.toList());
}
@Override
public List<Date> values() {
return new ArrayList<>(values);
}
}
@SuppressWarnings("deprecation")
public static void main(String[] args) {
var naive = new NaiveRecord("naive", new ArrayList<>());
System.out.println(naive);
//prints NaiveRecord[name=naive, values=[]]
naive.values().add(new Date(99, 01, 01));
System.out.println(naive);
// prints NaiveRecord[name=naive, values=[Mon Feb 01 00:00:00 CET 1999]]
var original = new ArrayList<Date>();
original.add(new Date(99, 01, 01));
// --- naive record
naive = new NaiveRecord("naive", original);
System.out.println("%s \t\t hash=%s".formatted(naive, naive.hashCode()));
naive.values().add(new Date(100, 01, 01));
System.out.println("%s \t\t hash=%s".formatted(naive, naive.hashCode()));
System.out.println("-----");
// --- better record, return unmodifiable list
original = new ArrayList<Date>();
original.add(new Date(99, 01, 01));
var better = new BetterRecord("better", original);
System.out.println("%s \t\t hash=%s".formatted(better, better.hashCode()));
// BetterRecord[name=better, values=[Mon Feb 01 00:00:00 CET 1999]] hash=-1516124796
original.add(new Date(102, 01, 01));
System.out.println("%s \t hash=%s".formatted(better, better.hashCode()));
// BetterRecord[name=better, values=[Mon Feb 01 00:00:00 CET 1999, Fri Feb 01 00:00:00 CET 2002]] hash=1357225607
System.out.println("-----");
// --- even better record, also defensively clone the list
original = new ArrayList<Date>();
var someDay = new Date(100, 01, 01);
original.add(someDay);
var evenBetter = new EvenBetterRecord("even better", original);
System.out.println("%s \t hash=%s".formatted(evenBetter, evenBetter.hashCode()));
// EvenBetterRecord[name=even better, values=[Tue Feb 01 00:00:00 CET 2000]] hash=-1168472954
someDay.setYear(99);
System.out.println("%s \t\t hash=%s".formatted(evenBetter, evenBetter.hashCode()));
// EvenBetterRecord[name=even better, values=[Mon Feb 01 00:00:00 CET 1999]] hash=1655265406
System.out.println("-----");
// -- immutable record, all tricks fail
original = new ArrayList<Date>();
someDay = new Date(100, 01, 01);
original.add(someDay);
var immutable = new ImmutableRecord("immutable", original);
System.out.println("%s \t hash=%s".formatted(immutable, immutable.hashCode()));
try {
immutable.values().add(new Date(104, 01, 01));
} catch (UnsupportedOperationException e) {
e.printStackTrace();
}
System.out.println("%s \t\t hash=%s".formatted(immutable, immutable.hashCode()));
someDay.setYear(105);
System.out.println("%s \t\t hash=%s".formatted(immutable, immutable.hashCode()));
}
@SuppressWarnings("deprecation")
@ParameterizedTest(name = "{0}")
@MethodSource("recordTestProvider")
void canNotChangeThroughGetter(RecordWithList record, List<Date> list, Date date) {
var string = record.toString();
var hash = record.hashCode();
try {
record.values().add(new Date(100, 01, 01));
} catch (UnsupportedOperationException e) {}
assertEquals(string, record.toString());
assertEquals(hash, record.hashCode());
}
@SuppressWarnings("deprecation")
@ParameterizedTest(name = "{0}")
@MethodSource("recordTestProvider")
void canNotChangeThroughOriginalList(RecordWithList record, List<Date> list, Date date) {
var string = record.toString();
var hash = record.hashCode();
list.add(new Date(101, 01, 01));
assertEquals(string, record.toString());
assertEquals(hash, record.hashCode());
}
@SuppressWarnings("deprecation")
@ParameterizedTest(name = "{0}")
@MethodSource("recordTestProvider")
void canNotChangeThroughOriginalObject(RecordWithList record, List<Date> list, Date date) throws UnsupportedOperationException {
var string = record.toString();
var hash = record.hashCode();
date.setYear(102);
assertEquals(string, record.toString());
assertEquals(hash, record.hashCode());
}
@SuppressWarnings({ "deprecation"})
private static Stream<Arguments> recordTestProvider() {
var list_naive = new ArrayList<Date>();
var date_naive = new Date(99, 01, 01);
list_naive.add(date_naive);
var record_naive = new NaiveRecord("naive", list_naive);
var list_better = new ArrayList<Date>();
var date_better = new Date(99, 01, 01);
list_better.add(date_better);
var record_better = new BetterRecord("better", list_better);
var list_even_better = new ArrayList<Date>();
var date_even_better = new Date(99, 01, 01);
list_even_better.add(date_even_better);
var record_even_better = new EvenBetterRecord("even better", list_even_better);
var list_immutable = new ArrayList<Date>();
var date_immutable = new Date(99, 01, 01);
list_immutable.add(date_immutable);
var record_immutable = new ImmutableRecord("immutable", list_immutable);
return Stream.of(
Arguments.of(record_naive, list_naive, date_naive),
Arguments.of(record_better, list_better, date_better),
Arguments.of(record_even_better, list_even_better, date_even_better),
Arguments.of(record_immutable, list_immutable, date_immutable)
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment