Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java Generics support #55

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Conversation

ZZZank
Copy link
Contributor

@ZZZank ZZZank commented Feb 13, 2025

This PR:

  • added TypeConsolidator, providing complete support of Java Generics
  • removed "resolve type variable to its bound" feature for VariableTypeInfo, in favor of using TypeConsolidator to eliminate type variables directly
  • added VariableTypeInfo#getBounds() for ProbeJS, (using #asClass() is making ProbeJS unable to handle cases like T extends A & B & C)
  • added TypeConsolidatorTest for testing how it works on method param / method return type / field .

TypeInfo#consolidate(Map<VariableTypeInfo, TypeInfo>)

The most fundamental method in type consolidating.

Almost all TypeInfo implementation is returning itself or passing the mapping to its components. Only VariableTypeInfo will try to map itself to another, consolidated type.

Implementations are carefully tweaked to make simple types always skip type consolidating, and make complex types (T[], Class<String>, ...) skip type consolidating if it's proven to be unnecessary.

Type Consolidation Mapping

aka the Map<VariableTypeInfo, TypeInfo> in TypeInfo#consolidate(...). It can be obtained via TypeConsolidator#getMapping(Class), or construct it yourself.

TypeConsolidator#getMapping(Class)

assuming that we have these classes as a rather extreme case:

// classes are named as 'XXX': A, B, C, ...
// type variables are named as 'Tx': Ta, Tb, Tc, ...
class A<Ta> {}
interface B<Tb> {}
class C<Tc> extends A<Tc> {}
class D<Td> extends C<Td> implements B<A<Td>> {}

and we're trying to get mapping for D.class, then TypeConsolidator#getMapping(Class) will work like this

  1. create mapping from super type (C), forming Tc -> Td
  2. create mapping from interfaces (B<A>), forming Tb -> A<Td>
  3. read mapping for its super class, this will give us Ta -> Tc
  4. use known mapping to "flatten" super class mapping, this will give us Ta -> Td (because Ta -> Tc and Tc -> Td)
  5. merge two mapping together: Ta -> Td, Tb -> A<Td>, Tc -> Td

This mapping can map EVERY type variables used by D's super class/interfaces to types used by D itself, this is important for performance because we only need to apply the mapping once, and no mapping from super classes are required.

2-Stage Type Consolidation

assuming that for class D we have these methods:

class D<Td> extends C<Td> implements B<A<Td>> {
    Ta getA() { ... } // inherited from A
    Tb getB() { ... } // inherited from B
    Tc getC() { ... } // inherited from C
    Td getD() { ... }
    Td setD(Td td) { ... }
}

Stage 1

The first type consolidation will happen at type initialization, for example for field:

		if (type == null) {
-			type = TypeInfo.safeOf(field::getGenericType);
+			type = TypeInfo.safeOf(field::getGenericType)
+				.consolidate(TypeConsolidator.getMapping(this.parent.type));
		}

Type consolidation in this stage will remove all usages of type variables from super classes / interfaces.

class D after this stage:

class D<Td> extends C<Td> implements B<A<Td>> {
    Td getA() { ... } // inherited from A
    A<Td> getB() { ... } // inherited from B
    Td getC() { ... } // inherited from C
    Td getD() { ... }
    Td setD(Td td) { ... }
}

Stage 2

This type consolidation will only apply to types with generics. It will be applied to Function<String, String>, or Supplier<SomeEnum>, but not ItemStack or raw type Map because there's no generic.

Type consolidation in this stage will map type variables to actual types.

Let's say we already have an instance of D, named d, via a method D<Byte> createD();.

When trying to invoke d.setD(...), the actual type Byte will be used to create another mapping: Td -> Byte.

apply the mapping we just created, then the method setD will be:

    Byte setD(Byte td) { ... }

You can verify that it's working because other type related features like type wrappers will be functional on generics after this PR.

Notes

for Pruno:

  • using #asClass() is making ProbeJS unable to handle cases like T extends A & B & C, try VariableTypeInfo#getBounds()
  • the getGenericTypeReplacement() will be redundant if this PR is merged. Try TypeConsolidator#getMapping(Class) if you still need something similar
  • Made VariableTypeInfo more usable for type dumpers #53 is basically completely overwritten, hope you don't mind
  • hi pruno

about Generics in method/constructor, for example:

public <T> void setProp(Key<T> k, T value);

It's NOT supported yet, supporting this will require finding the common super class of all related types, which can be performance expensive

@ZZZank ZZZank changed the title Full Java Generics supports Java Generics support Feb 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant