Skip to content

Instantly share code, notes, and snippets.

@baharclerode
Last active August 19, 2018 18:46
Show Gist options
  • Save baharclerode/68de57291d9719e8c2b48d649a69ba98 to your computer and use it in GitHub Desktop.
Save baharclerode/68de57291d9719e8c2b48d649a69ba98 to your computer and use it in GitHub Desktop.
Memoizer Helper Utility
/*
* Copyright 2018 Bryan Harclerode
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
* to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package zone.dragon.reflection;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* Helper class that can transparently memoize SAM types
*
* @author Bryan Harclerode
* @date 8/19/2018
*/
public class Memoizer {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
/**
* Identifies the SAM on an interface, or throws an exception if {@code samType} is not a SAM interface
*
* @param samType
* The SAM type to examine
*
* @return The {@link Method} corresponding to the SAM in a SAM type
*/
private static Method getSamMethod(Class<?> samType) {
if (!samType.isInterface()) {
throw new IllegalArgumentException(samType.getName() + " is not an interface");
}
Method[] possibleMethods = samType.getMethods();
possibleMethods = Arrays
.stream(possibleMethods)
.filter(method -> !method.isDefault() && (method.getModifiers() & Modifier.STATIC) == 0)
.toArray(Method[]::new);
if (possibleMethods.length != 1) {
throw new IllegalArgumentException(samType.getName() + " is not a SAM interface");
}
return possibleMethods[0];
}
/**
* Wraps an object implementing a SAM type inside a memoizing version of that SAM, which caches the result of each invocation and
* returns cached data on subsequent invocations with the same arguments
*
* @param samType
* SAM type of the object being wrapped
* @param sam
* SAM object itself
* @param <T>
*
* @return A {@code samType} instance that wraps {@code sam} and memoizes its results.
*
* @throws Throwable
* If there was an issue building the memoizer
*/
public static <T> T memoize(Class<? super T> samType, T sam) throws Throwable {
Method samMethod = getSamMethod(samType);
samMethod.setAccessible(true);
MethodHandle samCallee = LOOKUP
.unreflect(samMethod)
.asType(MethodType.methodType(Object.class, Object.class, samMethod.getParameterTypes()))
.asSpreader(Object[].class, samMethod.getParameterCount());
ConcurrentHashMap<ArgList, Object> cachedResults = new ConcurrentHashMap<>();
String description = String.format("Memoized %s", sam);
InvocationHandler memoizer = (proxy, method, args) -> {
switch (method.getName()) {
case "equals":
return proxy == args[0];
case "hashCode":
return Objects.hashCode(proxy);
case "toString":
return description;
default:
ArgList argList = new ArgList();
argList.args = args;
return cachedResults.computeIfAbsent(argList, argList1 -> {
try {
return samCallee.invokeExact(sam, argList1.args);
} catch (Throwable throwable) {
// SneakyThrow since computeIfAbsent doesn't allow checked exceptions, but any checked exceptions thrown by the
// memoized SAM can also be thrown by the wrapping SAM
throw sneakyThrow(throwable);
}
});
}
};
//noinspection unchecked
return (T) Proxy.newProxyInstance(samType.getClassLoader(), new Class[]{samType}, memoizer);
}
/**
* Breaks compiler type checking on throw exceptions; Used to throw checked exceptions anywhere.
*
* @param t
* Checked exception that needs to be thrown
* @param <T>
* Generic erasure that tricks the compiler
*
* @return Nothing; This method always throws an exception, and thus never returns.
*
* @throws T
* The thrown exception itself
*/
private static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
//noinspection unchecked
throw (T) t;
}
/**
* Class representing a captured set of arguments, which is used to key the result cache from each memoized function
*/
private static class ArgList {
private Object[] args;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ArgList)) {
return false;
}
ArgList argList = (ArgList) o;
// Probably incorrect - comparing Object[] arrays with Arrays.equals
return Arrays.equals(args, argList.args);
}
@Override
public int hashCode() {
return args != null ? Arrays.hashCode(args) : 0;
}
}
// No instances
private Memoizer() {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment