theme | class | headingDivider |
---|---|---|
default |
invert |
2 |
A lengthy and dry topic - but you asked for it π
Reflection ~ "inspection as declared in source"
What can be inspected?
- "raw type":
Class
- "generic type":
java.lang.reflect.Type
- names
- members
- parameters
- exceptions
- => "signatures"
- annotations
π§ Not in this talk π§
What is reflective access?
Not:
Class.getEnumConstants()
Class.getRecordComponents()
Class.getName()
& co (names, package, module)Class.isInterface()
& co (only read modifier flags)- =>
Class.getModifiers()
(also exist on methods and fields)
Pretty much everything else shown soon is considered "reflection".
OBS! This matters because with Java modules types are generally inaccessible for reflection unless they are explicitly open
for it.
Type of the element itself:
returns => | Class |
java.lang.reflect.Type |
---|---|---|
Method |
getReturnType() |
getGenericReturnType() |
Field |
getType() |
getGenericType() |
Constructor |
getDeclaringClass() |
- |
Parameter |
getType() |
getParameterizedType() |
Type of parameters:
returns => | Class[] |
java.lang.reflect.Type[] |
---|---|---|
Method |
getParameterTypes() |
getGenericParameterTypes() |
Constructor |
getParameterTypes() |
getGenericParameterTypes() |
Alternative: getParameters()
=> Parameter[]
OBS! Parameter
does not have a getIndex()
π©
Child elements of Class
es:
returns | public "children" (+inherited) | all local "children" (-inherited) |
---|---|---|
Method[] |
getMethods() |
getDeclaredMethods() |
Field[] |
getFields() |
getDeclaredFields() |
Constructor[] |
getConstructors() |
getDeclaredConstructors() |
From child back to its "parent":
"child" | "parent" element | returns |
---|---|---|
Method |
getDeclaringClass() |
Class |
Field |
getDeclaringClass() |
Class |
Constructor |
getDeclaringClass() |
Class |
Parameter |
getDeclaringExecutable() |
Executable |
Super*
Class method |
returns |
---|---|
getSuperclass() |
Class<? super T> |
getGenericSuperclass() |
Type |
getInterfaces() |
Class<?>[] |
getGenericInterfaces() |
Type[] |
Names of elements:
name | |
---|---|
Method |
getName() |
Field |
getName() |
Constructor |
getName() * |
Parameter |
getName() ** |
- *: =
getDeclaringClass().getName()
- **: OBS! only available with compiler option
-parameters
Essential:
interface AnnotatedElement {
// check
boolean isAnnotationPresent( Class<? extends Annotation> type );
// single
<A extends Annotation> A getAnnotation( Class<A> type );
// repeater
<A extends Annotation> A[] getAnnotationsByType( Class<A> type );
//...
}
@Target( ElementType.PARAMETER )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( Params.class )
@interface Param
{
String name();
}
@Target( ElementType.PARAMETER )
@Retention( RetentionPolicy.RUNTIME )
@interface Params
{
Param[] value();
}
Method | @Repeatable transparent |
---|---|
isAnnotationPresent |
no π° |
getAnnotation |
no π° |
getAnnotationsByType |
yes |
Workarounds:
- use only
getAnnotationsByType
- use
ConsistentAnnotatedElement
More surprises: @Inherited
, spring & co
@interface Name {
String value();
}
record NameValue(
String value
) implements Name {
@Override
public Class<? extends Annotation> annotationType() {
return Name.class;
}
}
This works just fine:
Name annotation = new NameValue("hello");
(also: Implemented by Proxy
)
java.lang.reflect.Type
Flavours:
Class
Types (all types before 1.5; a.k.a "raw types"):String
,Boolean
, ...ParameterizedType
:List<T>
TypeVariable
:<T>
WildcardType
:<? super Number>
,<? extends Number>
,<?>
GenericArrayType
:T[]
(What needs to be considered by code trying to handle "all types")
What is the difference?
String
is a class typeList
is a raw type of a parameterized typeList<T>
- Array types
String[]
are class types. - (confusingly
Class<?>
itself is a parameterized type)
Are types with type parameters (free type variables; here A
, B
)
interface Function<A, B> {
B apply( A a );
}
See
Class.getTypeParameters()
=>TypeVariable[]
ParameterizedType.getRawType()
=>Type
is always:Class<?>
ParameterizedType.getActualTypeArguments()
=>Type[]
Not to be confused with type arguments (here String
)
class StringList extends ArrayList<String> {}
- Are named type placeholders
- By convention use single letter upper case names
When type parameters occur directly as type (here T
) => TypeVariable
// class level type parameters
interface Predicate<T> { boolean test(T value); }
class Util {
// method level type parameters
static <T> List<T> prepend(T e, List<T> tail);
}
See
Method.getTypeParameters()
=>TypeVariable[]
Any subtype of ...
interface Adder {
int intSum( List<? extends Number> values );
}
WildcardType.getUpperBounds()
=> Type[]
Any supertype of ...
interface Calculator {
<T> void calc( T left, BinaryOperator<? super T> op, T right );
}
WildcardType.getLowerBounds()
=> Type[]
Interesting...
<?>
is just an alias for<? extends Object>
- Why are upper bounds a
Type[]
array? - Because:
<T extends Member & AnnotatedElement>
- Why are lower bounds a
Type[]
array? - Because: reasons :D ATM there can only be one lower bound
T[]
is neither...
- a class type (because its element type is not a class type)
- nor a type variable (because it is also an array type)
Tends to be forgotten
What? <? extends X>
!= <? extends X>
?
Two wildcard types are only compatible if they originate from the same capture.
Solution: Use same type variable
Why use wildcard types then?
class Adder {
static int intSum(List<? extends Number> values) {
return values.stream().mapToInt(Number::intValue).sum();
}
}
Can be called with:
Adder.intSum(List.of(1, 2, 3)); // = 6
Adder.intSum(List.of(1.4f, 2.5f, 3.6f)); // = 6
Similar...
interface Calculator {
<T> void calc( T left, BinaryOperator<?super T> op, T right );
}
Can be called with:
BinaryOperator<Number> plusInt = (left, right) -> left.intValue() + right.intValue();
Calculator c = new CalculatorImpl();
c.calc(1, plusInt, 2); // = 3
c.calc(1.4f, plusInt, 2.5f); // = 3
What does type erasure entail?
- the JVM only supports class types
- => object instances can only "know" their class type (which might be their raw type)
- => JVM call mechanics build on erased signatures
Source:
interface Consumer<T> {
void apply( T e );
}
JVM:
interface Consumer {
void apply( Object e );
}
Source:
interface Parser {
<T extends Number> T parse( String value, Function<String, T> toNumber );
}
JVM:
interface Parser {
Number parse( String value, Function toNumber );
}
Source:
interface Adder {
Number add( List<String> values, Function<String, Number> toNumber );
Integer add( List<Number> values, Function<Number, Integer> toInt );
}
JVM:
interface Adder {
Number add( List values, Function toNumber ); // π« compiler error
Integer add( List values, Function toInt ); // π« compiler error
}
Source:
interface Box<T> {
void put(T value);
}
class StringBox implements Box<String> {
public void put(String value) { }
}
JVM:
interface Box {
void put(Object value);
}
class StringBox implements Box {
public void put(String value) { }
}
Box<String> box = new StringBox();
box.put("I get bridged"); // call put(Object) from Box interface
But StringBox
defines put
as put(String)
π±
class StringBox implements Box {
public void put(String value) { }
// bridge method generated by the compiler
public void put(Object value) {
put((String) value);
}
}
Also: a "synthetic" method (modifiers)
Not a "generics" feature, but it appears as if:
interface Num {
Num addInt(Int other);
}
class Int implements Num {
Int addInt(Int other) {}
}
Return types are not part of the signature π€―
It is allowed to override them with a subtype
From this presentation...
- Reflection = "inspection as declared in source"
- Avoid raw types:
List<String>
overList<?>
overList
- Introduce type variables to capture a wildcard type as "the same=compatible type"
From experience...
- Avoid type parameters by design
- 1 is maybe unavoidable π€·
- 2 is suspicious
- >= 3 is most likely bad design :D
- => rethink the design, simplify (but don't hack around with casts or raw types)