Last active
May 8, 2020 19:58
-
-
Save NetzwergX/e0e09f3a10f40bdae7fac643193b8d0e to your computer and use it in GitHub Desktop.
Demonstration of mutability of records & defensive copying to address it
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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