Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![License](http://img.shields.io/:license-apache-brightgreen.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
[![JavaDoc](https://img.shields.io/maven-central/v/net.jodah/typetools.svg?maxAge=60&label=javadoc&color=blue)](https://jodah.net/typetools/javadoc/)

A simple, zero-dependency library for working with types. Supports Java 1.6+ and Android.
A simple, zero-dependency library for working with types. Supports Java 8+ and Android.

## Introduction

Expand Down Expand Up @@ -144,8 +144,8 @@ TypeResolver.disableCache();

Lambda type argument resolution is currently supported for:

* Oracle JDK 8, 9
* Open JDK 8, 9
* Oracle JDK 8 up to 17
* Open JDK 8 up to 17

#### On Unresolvable Lambda Type Arguments

Expand All @@ -154,7 +154,7 @@ When resolving type arguments with lambda expressions, only type parameters used
```java
interface ExtraFunction<T, R, Z> extends Function<T, R>{}
ExtraFunction<String, Integer, Long> strToInt = s -> Integer.valueOf(s);
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(Function.class, strToInt.getClass());
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(ExtraFunction.class, strToInt.getClass());

assert typeArgs[0] == String.class;
assert typeArgs[1] == Integer.class;
Expand Down
22 changes: 19 additions & 3 deletions src/main/java/net/jodah/typetools/ReifiedParameterizedType.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.jodah.typetools;

import java.lang.reflect.ParameterizedType;
Expand Down Expand Up @@ -61,9 +77,9 @@ public String toString() {

if (ownerType != null) {
if (ownerType instanceof Class) {
sb.append(((Class) ownerType).getName());
sb.append(((Class<?>) ownerType).getName());
} else {
sb.append(ownerType.toString());
sb.append(ownerType);
}

sb.append("$");
Expand All @@ -74,7 +90,7 @@ public String toString() {
sb.append(rawType.getTypeName()
.replace(((ParameterizedType) ownerType).getRawType().getTypeName() + "$", ""));
} else if (rawType instanceof Class){
sb.append(((Class) rawType).getSimpleName());
sb.append(((Class<?>) rawType).getSimpleName());
} else {
sb.append(rawType.getTypeName());
}
Expand Down
120 changes: 100 additions & 20 deletions src/main/java/net/jodah/typetools/TypeResolver.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -33,10 +33,13 @@
import java.lang.reflect.WildcardType;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import sun.misc.Unsafe;
Expand All @@ -58,7 +61,8 @@ public final class TypeResolver {
private static Method GET_CONSTANT_POOL_SIZE;
private static Method GET_CONSTANT_POOL_METHOD_AT;
private static final Map<String, Method> OBJECT_METHODS = new HashMap<String, Method>();
private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPERS;
private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPERS_MAP;
private static final Set<Class<?>> PRIMITIVE_WRAPPERS;
private static final Double JAVA_VERSION;

static {
Expand Down Expand Up @@ -155,13 +159,14 @@ public void makeAccessible(AccessibleObject object) throws Throwable {
types.put(long.class, Long.class);
types.put(short.class, Short.class);
types.put(void.class, Void.class);
PRIMITIVE_WRAPPERS = Collections.unmodifiableMap(types);
PRIMITIVE_WRAPPERS_MAP = Collections.unmodifiableMap(types);
PRIMITIVE_WRAPPERS = Collections.unmodifiableSet(new HashSet<>(types.values()));
}

private interface AccessMaker {
void makeAccessible(AccessibleObject object) throws Throwable;
}

/** An unknown type. */
public static final class Unknown {
private Unknown() {
Expand Down Expand Up @@ -719,41 +724,116 @@ private static boolean isDefaultMethod(Method m) {
}

private static Member getMemberRef(Class<?> type) {
Object constantPool;
try {
constantPool = GET_CONSTANT_POOL.invoke(JAVA_LANG_ACCESS, type);
} catch (Exception ignore) {
return null;
Member[] constantPoolMethods = extractConstantPoolMethods(type);
//skip jacoco synthetic methods
int start = constantPoolMethods.length;
if (isInstrumentedByJacoco(type)) {
for (int i = constantPoolMethods.length - 1; i >= 0; i--) {
if (constantPoolMethods[i].getName().startsWith("$jacoco")) {
start = i;
}
}
}

Member result = null;
for (int i = getConstantPoolSize(constantPool) - 1; i >= 0; i--) {
Member member = getConstantPoolMethodAt(constantPool, i);
Method prev = null;
for (int i = start - 1; i >= 0; i--) {
Member member = constantPoolMethods[i];
// Skip SerializedLambda constructors and members of the "type" class
if (member == null
|| (member instanceof Constructor
&& member.getDeclaringClass().getName().equals("java.lang.invoke.SerializedLambda"))
|| member.getDeclaringClass().isAssignableFrom(type))
if ((member instanceof Constructor
&& member.getDeclaringClass().getName().equals("java.lang.invoke.SerializedLambda"))
|| member.getDeclaringClass().equals(type))
continue;

result = member;

// Return if not valueOf method
if (!(member instanceof Method) || !isAutoBoxingMethod((Method) member))
break;
if (isBoxingOrUnboxingMethod(member) && prev == null) {
prev = (Method) member; //retain the last member if it is boxing or unboxing
continue;
}

break;
}

if (prev != null) {
// depending on the result, if the constantpool ends with a boxing method,
// if result is a constructor, then special care is needed
// | constructor type | prev type | end result |
// | any | boxing method | boxing method |
// | primitive wrapper | unboxing method | constructor |
// | ! primitive wrapper | unboxing method | unboxing method |
if (result instanceof Constructor) {
if (isBoxingMethod(prev)) {
return prev;
} else if (isUnboxingMethod(prev) && !PRIMITIVE_WRAPPERS.contains(((Constructor<?>) result).getDeclaringClass())) {
return prev;
}
}
}

return result;
}

private static boolean isAutoBoxingMethod(Method method) {
private static Member[] extractConstantPoolMethods(Class<?> type) {
Object constantPool;
try {
constantPool = GET_CONSTANT_POOL.invoke(JAVA_LANG_ACCESS, type);
} catch (Exception ignore) {
return new Member[0];
}
ArrayList<Member> methods = new ArrayList<>();
int constantPoolSize = getConstantPoolSize(constantPool);
for (int i = 0; i < constantPoolSize; i++) {
Member method = getConstantPoolMethodAt(constantPool, i);
if (method != null)
methods.add(method);
}
return methods.toArray(new Member[methods.size()]);
}

static boolean isInstrumentedByJacoco(Class<?> type) {
// http://www.eclemma.org/jacoco/trunk/doc/faq.html
// JaCoCo [...] adds two members to the classes: A private static field $jacocoData and
// a private static method $jacocoInit(). Both members are marked as synthetic.
boolean result = false;
try {
type.getDeclaredMethod("$jacocoInit");
result = true;
} catch (NoSuchMethodException ignore) {
}
try {
type.getDeclaredField("$jacocoData");
result = true;
} catch (NoSuchFieldException ignore) {
}
return result;
}

private static boolean isBoxingOrUnboxingMethod(Member member) {
return member instanceof Method && (isBoxingMethod((Method) member) || isUnboxingMethod((Method) member));
}

private static boolean isBoxingMethod(Method method) {
Class<?>[] parameters = method.getParameterTypes();
return method.getName().equals("valueOf") && parameters.length == 1 && parameters[0].isPrimitive()
&& wrapPrimitives(parameters[0]).equals(method.getDeclaringClass());
}

private static boolean isUnboxingMethod(Method method) {
String methodName = method.getName();
String returnType = method.getReturnType().getSimpleName();
Class<?>[] parameters = method.getParameterTypes();

return method.getReturnType().isPrimitive() && parameters.length == 0
//booleanValue, byteValue, charValue, doubleValue, floatValue, intValue, longValue, shortValue
&& methodName.startsWith(returnType) && methodName.endsWith("Value")
&& (wrapPrimitives(method.getReturnType()).equals(method.getDeclaringClass())
|| method.getDeclaringClass().equals(Number.class));
}

private static Class<?> wrapPrimitives(Class<?> clazz) {
return clazz.isPrimitive() ? PRIMITIVE_WRAPPERS.get(clazz) : clazz;
return clazz.isPrimitive() ? PRIMITIVE_WRAPPERS_MAP.get(clazz) : clazz;
}

private static int getConstantPoolSize(Object constantPool) {
Expand Down
31 changes: 31 additions & 0 deletions src/test/java/net/jodah/typetools/functional/LambdaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;

import org.testng.annotations.Factory;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -263,6 +264,36 @@ public void shouldResolveSubclassArgumentsForConstructorRef() {
new Class<?>[] { String.class, Integer.class });
}

public void shouldResolveObjectClassMethodRef() {
Function<?, String> fn = Object::toString;
assertEquals(TypeResolver.resolveRawArguments(Function.class, fn.getClass()),
new Class<?>[] { Object.class, String.class });
}

public void shouldResolveObjectClassConstructorRef() {
Supplier<?> fn = Object::new;
assertEquals(TypeResolver.resolveRawArguments(Supplier.class, fn.getClass()),
new Class<?>[] { Object.class });
}

public void shouldResolvePrimitiveReturnValue() {
ToDoubleFunction<String> fn = Double::new; //this method returns Double, thus unboxing takes place
assertEquals(TypeResolver.resolveRawArguments(ToDoubleFunction.class, fn.getClass()),
new Class<?>[] { String.class });
}

public void shouldResolvePrimitiveReturnValue2() {
ToDoubleFunction<String> fn = Double::valueOf; //this method returns Double, thus unboxing takes place
assertEquals(TypeResolver.resolveRawArguments(ToDoubleFunction.class, fn.getClass()),
new Class<?>[] { String.class });
}

public void shouldResolvePrimitiveReturnValue3() {
ToDoubleFunction<Double> fn = Double::doubleValue;
assertEquals(TypeResolver.resolveRawArguments(ToDoubleFunction.class, fn.getClass()),
new Class<?>[] { Double.class });
}

public void shouldResolveTransposedSubclassArguments() {
SelectingFn<Integer, Long, String> fn = (String str) -> Integer.valueOf(str);
assertEquals(TypeResolver.resolveRawArguments(SelectingFn.class, fn.getClass()),
Expand Down