⚠️ Lisez cette lettre dans votre navigateur pour profiter des corrections de dernière minute
Les liens marqués avec 🔒 sont accessibles seulement aux membres du groupe Theodo
Pôle de formation Craft
Je parle ici de mes activités transverses au sein de Sipios
Pair Programming
Rapidement après mon arrivée à Sipios j’ai mis en place dans mon agenda un créneau réservable de pair programming, avec un robot qui relance régulièrement sur notre outil de messagerie interne.
Ce système a très bien fonctionné jusqu’à présent mais je sens qu’il s’essouffle. Ce mois ci pour la première fois mes créneaux tardent à être tous réservés. De plus le gain pour Sipios est limité car les devs qui réservent des créneaux sont souvent les mêmes. Ceux là ont bien progressé en particulier en terme de pratique de développement et de maîtrise de leur IDE mais d’autres auraient besoin d’accompagnement.
Ma résolution pour 2024 : donner un nouveau souffle à la pratique du PP en étant plus proactif et en allant chercher les équipes en difficulté plutôt que d’attendre les volontaires.
Pour rephraser la première valeur du manifeste agile :
Ritualisation > Automatisation
Indicateur DDD (le retour)
Après 1 an d’expérimentation je dois me rendre à l’évidence : c’est encore trop painful pour les tech-leads de maintenir l’indicateur à jour sur leur projet, surtout qu’il n’a aucun impact sur leur quotidien.
Ma résolution pour 2024 : réfléchir plus sérieusement à la différence entre indicateur et objectif. Donner des objectifs clairs aux équipes et collecter automatiquement les indicateurs et les coupler à un dashboard Notion.
Meetup Spring Paris
Kudos à thibaultM pour avoir repris en main l’organisation des Meetups Spring Paris et avoir lancé la chaîne Youtube !
Veille
Quelques pépites que j’ai envie de partager avec vous
Vous le savez sans doute, les compilateurs optimisent le code, par exemple pour éliminer le code mort ou simplifier les expressions constantes (constant folding).
Par exemple cette expression “int x = 2 * 3
” ne générera pas l’opération de multiplication à l’exécution. Le compilateur génère un code qui affecte directement 6 à la variable x
.
De même quand le compilateur voit cette expression : “int z = 0 * x
” le code généré affectera directement zéro à z
sans même chercher à récupérer la valeur de x
.
Ceci fonctionne très bien pour les constantes littérales, mais en Java qu’en est-il des champs final
?
Malheureusement il n’est généralement pas possible d’optimiser les champs final car on peut quand même les modifier. Si si ! Regardez :
import java.lang.reflect.Field;
public class ModifyFinalField {
private final int modifyMeIfYouCan;
private ModifyFinalField(int value) {
modifyMeIfYouCan = value;
}
public static void main(String[] args) throws Exception {
ModifyFinalField victim = new ModifyFinalField(1);
System.out.println(victim.modifyMeIfYouCan);
Field field = ModifyFinalField.class.getDeclaredField("modifyMeIfYouCan");
field.setAccessible(true);
field.setInt(victim, 2);
System.out.println(victim.modifyMeIfYouCan);
}
}
Pour le compilateur, on ne peut pas faire confiance (trust) à un champ d’instance final pour être vraiment constant.
Heureusement tout n’est pas perdu ! Dans cet article, Per-Åke Minborg nous montre comment les records peuvent sortir le compilo de l’ornière et lui permettre d’optimiser joyeusement notre code Java !
Puzzler Investigation
Ici un petit challenge pour apprendre en s’amusant !
La dernière fois nous avons vu que lorsque la JVM tombe sur une instruction invokedynamic
elle demande à une BootstrapMethod
de lui fournir un CallSite
qui représente le code à exécuter.
Nous avons vu que le compilateur Java délègue à la méthode LambdaMetafactory.metafactory
du JDK la création du CallSite
:
L’appel à validateMetafactoryArgs
va vérifier que l’arité de la méthode appelée est compatible avec l’arité de l’interface fonctionnelle, que les types des arguments sont compatibles et que le type de retour est compatible.
Une fois ces vérifications effectuées, la méthode construit le CallSite
qui sera ensuite appelée directement sans repasser par la metafactory.
CallSite
Qu’est-ce qu’un CallSite
? La javadoc nous dit :
A
CallSite
is a holder for a variableMethodHandle
, which is called its target. […]
et
A method handle is a typed, directly executable reference to an underlying method, constructor, field, or similar low-level operation […]
On a donc on objet qui encapsule une référence vers une méthode ou un champ (la target
).
NB : le contrat nous dit que la target
peut varier. Cependant la metafactory retourne toujours une instance de ConstantCallSite
dont la target
est immutable.
MethodHandle
Penchons nous maintenant sur le method handle encapsulé dans le CallSite
.
Pour cela lisons la javadoc de la méthode buildCallSite
:
[…] Generate a class file which implements the functional interface, define the class, if there are no parameters create an instance of the class which the CallSite will return, otherwise, generate handles which will call the class' constructor.
Cette méthode génère une classe dans la mémoire de la JVM. La classe étend directement java.lang.Object
et implémente l’interface fonctionnelle.
De plus elle est hidden et interne à la classe contenant les invocations dynamiques.1
Là où la javadoc n’est pas très claire c’est quand elle parle du nombre de paramètres. En fait il s’agit des paramètres de la factory, souvenez-vous :
factoryType
- The expected signature of theCallSite
. The parameter types represent the types of capture variables; the return type is the interface to implement.
Le constructeur de la classe générée joue donc le rôle de factory. Quelque part c’est logique 🙂
Dans l’instruction :
duck.rename("John "::concat)
La factory prend un paramètre (la String “John “).
Voici la classe générée :
import java.util.function.UnaryOperator;
final class Duck$$Lambda implements UnaryOperator {
private final String arg$1;
private Duck$$Lambda(String var1) {
this.arg$1 = var1;
}
public Object apply(Object var1) {
return this.arg$1.concat((String)var1);
}
}
A chaque fois que le CallSite
sera appelé une nouvelle instance de cette classe sera crée avec la variable capturée.
Dans ces instructions le nombre de paramètres de la factory est zéro :
duck.rename(aName -> "M. " + aName)
duck.rename(String::toUpperCase)
Voilà les classes générées :
import java.util.function.UnaryOperator;
final class Duck$$Lambda implements UnaryOperator {
private Duck$$Lambda() {}
public Object apply(Object var1) {
return Duck.lambda$main$0((String)var1);
}
}
et
import java.util.function.UnaryOperator;
final class Duck$$Lambda implements UnaryOperator {
private Duck$$Lambda() {}
public Object apply(Object var1) {
return ((String)var1).toUpperCase();
}
}
Dans ce cas une seule instance de chaque classe est crée puis appelée à chaque appel du CallSite
.
Conclusion
La question était :
Comment le compilateur (javac) se débrouille-t-il pour transformer des expressions aussi variées que
new Renamer()
"John "::concat
aName -> "M. " + aName
String::toUpperCase
En instances de UnaryOperator<String>
?
Réponse :
Le compilateur ne se casse pas la tête. Si l’objet passé est une lambda ou une référence de méthode, il passe la main à une metafactory fournie par le JDK.
Cette metafactory génère à la volée des factories (d’où le nom) de UnaryOperator
dont les instances exécuteront le code indiqué par les références de méthodes ou les corps des lambdas2
Encore une fois Baeldung a un excellent article sous le coude qui résume tout ça.
Comment est compilé le body d’une lambda ? Voir cet article.