diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main/java/de/hysky/skyblocker/utils/InstancedUtils.java | 104 | ||||
-rw-r--r-- | src/test/java/de/hysky/skyblocker/utils/InstancedUtilsTest.java | 93 |
2 files changed, 197 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/utils/InstancedUtils.java b/src/main/java/de/hysky/skyblocker/utils/InstancedUtils.java new file mode 100644 index 00000000..1da4fa11 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/InstancedUtils.java @@ -0,0 +1,104 @@ +package de.hysky.skyblocker.utils; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.runtime.ObjectMethods; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +/** + * @implNote If implementing any of these onto a class, ensure that all subclasses have an implementation of the methods too. + */ +public class InstancedUtils { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Map<Class<?>, MethodHandle> EQUALS_CACHE = new ConcurrentHashMap<>(); + private static final Map<Class<?>, MethodHandle> HASH_CODE_CACHE = new ConcurrentHashMap<>(); + private static final Map<Class<?>, MethodHandle> TO_STRING_CACHE = new ConcurrentHashMap<>(); + + public static MethodHandle equals(Class<?> type) { + if (EQUALS_CACHE.containsKey(type)) return EQUALS_CACHE.get(type); + + try { + Field[] fields = getClassFields(type); + MethodHandle[] getters = getFieldGetters(fields); + + //The field names param can be anything as equals and hashCode don't care about it. + MethodHandle equalsHandle = (MethodHandle) ObjectMethods.bootstrap(MethodHandles.lookup(), "equals", MethodHandle.class, type, "", getters); + + EQUALS_CACHE.put(type, equalsHandle); + + return equalsHandle; + } catch (Throwable t) { + LOGGER.error("[Skyblocked Instanced Utils] Failed to create an equals method handle.", t); + + throw new RuntimeException(); + } + } + + public static MethodHandle hashCode(Class<?> type) { + if (HASH_CODE_CACHE.containsKey(type)) return HASH_CODE_CACHE.get(type); + + try { + Field[] fields = getClassFields(type); + MethodHandle[] getters = getFieldGetters(fields); + + //The field names param can be anything as equals and hashCode don't care about it. + MethodHandle hashCodeHandle = (MethodHandle) ObjectMethods.bootstrap(MethodHandles.lookup(), "hashCode", MethodHandle.class, type, "", getters); + + HASH_CODE_CACHE.put(type, hashCodeHandle); + + return hashCodeHandle; + } catch (Throwable t) { + LOGGER.error("[Skyblocked Instanced Utils] Failed to create a hashCode method handle.", t); + + throw new RuntimeException(); + } + } + + public static MethodHandle toString(Class<?> type) { + if (TO_STRING_CACHE.containsKey(type)) return TO_STRING_CACHE.get(type); + + try { + Field[] fields = getClassFields(type); + MethodHandle[] getters = getFieldGetters(fields); + String fieldNames = String.join(";", Arrays.stream(fields).map(Field::getName).toArray(String[]::new)); + + MethodHandle toStringHandle = (MethodHandle) ObjectMethods.bootstrap(MethodHandles.lookup(), "toString", MethodHandle.class, type, fieldNames, getters); + + TO_STRING_CACHE.put(type, toStringHandle); + + return toStringHandle; + } catch (Throwable t) { + LOGGER.error("[Skyblocked Instanced Utils] Failed to create a toString method handle.", t); + + throw new RuntimeException(); + } + } + + private static Field[] getClassFields(Class<?> type) { + return Stream.concat(Arrays.stream(type.getDeclaredFields()), Arrays.stream(type.getFields())).distinct().toArray(Field[]::new); + } + + private static MethodHandle[] getFieldGetters(Field[] fields) throws Throwable { + ObjectOpenHashSet<MethodHandle> handles = new ObjectOpenHashSet<>(); + + for (Field field : fields) { + field.setAccessible(true); + + MethodHandle getter = MethodHandles.lookup().unreflectGetter(field); + + handles.add(getter); + } + + return handles.toArray(MethodHandle[]::new); + } +} diff --git a/src/test/java/de/hysky/skyblocker/utils/InstancedUtilsTest.java b/src/test/java/de/hysky/skyblocker/utils/InstancedUtilsTest.java new file mode 100644 index 00000000..e5dbbff6 --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/utils/InstancedUtilsTest.java @@ -0,0 +1,93 @@ +package de.hysky.skyblocker.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class InstancedUtilsTest { + + @Test + void testSameInstanceEqual() { + Vector3i vec1 = new Vector3i(8, 8, 8); + + Assertions.assertEquals(vec1, vec1); + } + + @Test + void testSameFieldValuesEqual() { + Vector3i vec1 = new Vector3i(8, 8, 8); + Vector3i vec2 = new Vector3i(8, 8, 8); + + Assertions.assertEquals(vec1, vec2); + } + + @Test + void testDifferentFieldValuesEqual() { + Vector3i vec1 = new Vector3i(8, 8, 8); + Vector3i vec2 = new Vector3i(-8, -8, -8); + + Assertions.assertNotEquals(vec1, vec2); + } + + @Test + void testHashCodeOfEqualFieldValues() { + Vector3i vec1 = new Vector3i(8, 8, 8); + Vector3i vec2 = new Vector3i(8, 8, 8); + + Assertions.assertEquals(vec1.hashCode(), vec2.hashCode()); + } + + @Test + void testHashCodeOfDifferentFieldValues() { + Vector3i vec1 = new Vector3i(8, 8, 8); + Vector3i vec2 = new Vector3i(-8, -8, -8); + + Assertions.assertNotEquals(vec1.hashCode(), vec2.hashCode()); + } + + @Test + void testToString() { + Vector3i vec1 = new Vector3i(8, 8, 8); + + Assertions.assertEquals(vec1.toString(), "Vector3i[x=8, y=8, z=8]"); + } + + @SuppressWarnings("unused") + private static class Vector3i { + final int x; + final int y; + final int z; + + Vector3i(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + @Override + public boolean equals(Object o) { + try { + return (boolean) InstancedUtils.equals(getClass()).invokeExact(this, o); + } catch (Throwable ignored) { + return super.equals(o); + } + } + + @Override + public int hashCode() { + try { + return (int) InstancedUtils.hashCode(getClass()).invokeExact(this); + } catch (Throwable ignored) { + return System.identityHashCode(this); + } + } + + @Override + public String toString() { + try { + return (String) InstancedUtils.toString(getClass()).invokeExact(this); + } catch (Throwable ignored) { + return super.toString(); + } + } + } +} |