Greg's Devblog Par un développeur, pour les développeurs

20nov/100

Java : lenteurs inexpliquées ?

On aurait du utiliser un profiler. Voilà la conclusion à laquelle je suis arrivé après qu'on ait cherché à améliorer la vitesse et la réactivité de notre application au boulot (pour des appareils à encre électronique, donc en gros le même hardware que sur des smartphones).

Date.toString()

Sur certains écran, l'affichage prenait 6 secondes, alors que sur des écrans équivalents on était largement en-dessous de la seconde. On n'avait pas de profiler disponible sur l'appareil, donc on a commencé à mettre des logs un peu partout pour afficher les différents temps, et là, surprise : ce qui prenait autant de temps, c'est l'affichage des dates ! Un simple date.toString() peut ainsi prendre  plus d'1 seconde, parce que par défaut ça va aller charger tout et n'importe quoi pour trouver les locales et créer de quoi formatter la date comme il faut ! La solution, qui était déjà utilisée, a été de cacher les DateFormatter histoire qu'on ne se prenne pas le surcout à chaque fois et de bien convertir les dates en String en utilisant ce qui est en cache.

Requêtes à 2 à l'heure...

C'était déjà mieux, pas encore parfait. On avait encore des lenteurs assez bizarres sur des requêtes pourtant simples : de bêtes SELECT, UPDATE, ou INSERT pouvaient prendre 1 à 3 secondes, alors que d'autres prenaient à peine quelques millisecondes...

On a donc mis des logs autour de toutes les requêtes, en séparant les moindre lignes, pour essayer d'évaluer au mieux là où on passe trop de temps. Et surprise, l'exécution des requêtes est rapide, le problème n'est pas là ! En fait si on a une méthode qui ne fait que la requête, la ligne qui exécute la requête dans la méthode est rapide, alors que l'appel de la méthode en entier est long... Pas super logique :p

Bon bah c'est simple, il faut virer tout ce qui ne concerne pas directement la requête, à savoir juste des logger.trace qui ne sont même pas affichés, puisqu'on avait mis le mode de log à "info". Et là, gain de vitesse monstrueux !

Et oui, car afin d'avoir un maximum d'informations de debug, on a des logger.trace un peu partout dans le code des requêtes, qui permettent d'afficher toutes les valeurs injectées dans la base ou récupérée depuis celle-ci. Alors certes, rien n'est affiché, mais les arguments passés aux méthodes sont tout de même évalués, et parmi ceux-si on trouve... des dates ! Le retour du foireux Date.toString()...

On a été assez étonné car au final depuis le début on était persuadé d'avoir un problème de lenteur avec la base, alors que le problème était tout autre (et dans le fond venait de nous, et non de SQLite ^^). Ca explique pourquoi on ne trouvait pas trop de monde avec le même souci sur le net.

Régler le problème des logs

J'ai trouvé ce problème un peu con, en fait. C'est un souci qu'on n'aurait pas eu en C/C++ : les méthodes de log sont des macros, et quand on ne veut pas de log, la macro ne défini plus rien, et le log n'est tout simplement plus présent dans le code compilé.

Bon, à part commenter tous les logs (un peu chiant), que peut-on faire en Java pour ne pas avoir ce souci ? On n'a actuellement pas de préprocesseur, mais peut-être faudrait-il envisager d'en mettre un petit, au moins pour les logs... L'autre solution peut être de passer à un autre logger dont un collègue m'a parlé : au lieu de prendre juste un String, il reprend en fait la syntaxe du printf avec les "Date : %s..." et l'utilisation de la date comme argument. Et là, pour le coup, si le logger n'affiche rien, les arguments sont passés mais il n'y a virtuellement rien à évaluer (les dates resteront des Object tant que le logger ne les affiche pas), aucune concaténation de String à faire, c'est beaucoup plus rapide.

Alors certes, c'est mieux, mais conceptuellement ça reste du code qui pourrait être présent dans l'exécutable final, qui ne ferait rien, et qui ne serait pourtant pas 100% gratuit. Et j'ai un peu du mal à me dire que l'exécutable finale pourrait être légèrement (à peine, mais bon) plus lent que ce qu'il pourrait être juste parce qu'on a laissé trainé des debugs dedans...

Si vous avez des solutions alternatives, je suis preneur !

Moral ?

Ce qu'on a eu là comme souci illustre aussi quelques règles de programmation qu'on a parfois tendance à négliger :

  1. Pas d'optimisation à l'aveugle : quand on cherche à optimiser, il est important d'avoir des benchs pour savoir si l'optimisation est mieux ou pas que le code d'origine, et surtout savoir si on en train d'optimiser une partie pertinente du code. J'ai ainsi fait des optimisations sur le code d'affichage qui ont eu finalement un impact minime sur les performances globales, puisque 90% de la perte de temps était ailleurs. Et c'est comme ça qu'une fois sur PocketPC j'ai passé quelques heures à faire un code d'affichage beaucoup plus optimal pour me rendre compte que je n'avais rien gagné : GCC faisait aussi bien sur le code d'origine...
  2. Se remettre en question avant de douter des autres : ça fait plusieurs mois qu'on sait que les accès à la base sont lents, et depuis le début on a rejeté le faute sur SQLite... Pourtant les bonnes pratiques veulent qu'on vérifie que son propre code n'est pas la cause des lenteurs/bugs avant de dire que c'est dans une librairie qu'on utilise.
11nov/100

javaz : les sources !

J'ai décidé de me bouger le cul et de livrer les sources de javaz (compilateur Java pour Zam, le bytecode Caml). Il n'y a à l'heure actuelle pas de licence, pas de warning, pas d'instructions, rien :)

Vous pouvez compiler (nécessite d'avoir de quoi compiler du caml sur votre machine...) et tester, et un certain nombre de jeux de test sont déjà inclus et testable (avec "make tree", "make peano", etc...).

Je ferai probablement un petit billet sur les entrailles de la bête le jour où j'aurai le temps et si ça intéresse quelqu'un. Mais pour l'instant, c'est livré "as is".

Have fun !

Taggé comme: , , Aucun commentaire
7nov/100

Java : variable arity

Je ne sais plus où j'ai vu ça pour la première fois, mais en tout cas depuis Java 1.5 on peut faire des méthodes à arité variable, et j'ai trouvé la façon de faire plutôt pas mal.

Version C/C++

En C++, pour faire varié l'arité, on a en gros 2 façon de faire (en tout cas que j'utilise) :

void truc(int bidule, int blabla, float machin=0.0f, int ok = 0);

Ici, c'est particulier, car au final c'est juste qu'on donne des arguments par défaut. En Java, on peut obtenir le même résultat en surchargeant la méthode avec moins d'arguments, et en la faisant appeler l'autre méthode en mettant soi-même les valeurs par défaut.

int printf ( const char * format, ... );

Je prends volontairement printf, puisque c'est le meilleur exemple du genre, mais voilà, on peut aussi utiliser les "va_args". J'ai toujours trouvé ça un peu galère à utiliser, et la version Java me parait au final bien plus propre...

Version Java

public void myMethod(int arg0, int arg1, Object... objects){...}

La syntaxe est proche, sauf que cette fois-ci on précise le type des paramètres (en mettant Object on est super large, mais on met ce qu'on veut...). Ce qu'on récupère, au final, c'est juste un tableau "objects" contenant tous les Object passés en argument. Simple, efficace, on peut connaitre leur nombre, on peut boucler dessus avec une boucle foreach, et on peut aussi utiliser Arrays.asList(objects) si on veut en faire une liste.

Je m'en suis servi au boulot justement à un endroit où on voulait passer l'équivalent d'une liste d'éléments à utiliser dans une interface graphique. Je ne sais pas si niveau performance c'est bien (a priori pas forcément, car d'un côté on créé un array qu'il faut remplir, et de l'autre je converti l'array en liste), mais en tout cas au niveau code je pense qu'on a gagné en lisibilité. Ca évite de créer une liste, d'ajouter tous les éléments dedans, puis de passer cette liste en argument. Là on fait juste ça :

setKeyboards(KeyboardType.Alpha, KeyboardType.Num, KeyboardType.Symbols);

Bien ou pas ? Je dirais que ce genre d'ajout à un langage relève plus du sucre syntaxique qu'autre chose, puisque a priori il n'y a aucune nouvelle "fonctionnalité", juste une façon plus pratique d'écrire. Mais ça reste une façon simple et efficace d'enrichir un langage :)
(si ça se trouve dans 2 semaines je ferai un billet sur pourquoi c'est mal d'utiliser les méthodes à arité variable, mais pas grave ^^)

4nov/100

javaz : coming soon

Je fais un article rapide, la suite arrivera d'ici quelques jours ^^ Juste pour dire qu'hier avec Benoit (mon binome sur ce projet) on a rendu le premier jet de javaz, notre projet de compilateur Java pour zam (bytecode caml).

Le projet n'est pas encore très abouti, mais permet déjà de tester quelques trucs. On a de l'héritage, des appels de méthodes, des champs, après il manque des conneries genre les méthodes statiques, les génériques, les tableaux, et autres bétises.

Pour les tests, on a mis au point 2 exemples, un qui cherche des nombres premiers, et un exemple où on représente les entiers par des objets foireux : un objet de type Zero qui représente 0 (WOW !), et un objet de type Succ qui possède un objet (Zero ou Succ) et qui a pour valeur 1 + cet objet. Donc en gros pour représenter les calculs, c'est le bordel, et pour faire de simples additions ça coute super cher, mais au moins pour faire du bench sur les appels de méthodes et les accès aux champs, c'est marrant ^^

Je posterai tout ça avec quelques instructions d'utilisation demain. Mais pour résumer, en gros, niveau performances on tourne entre 3.5 et 10 fois plus lent que la JVM, ce qui est correct compte tenu du fait que le bytecode caml est exécuté dans une VM sans JIT, contrairement à la JVM :)

Projet intéressant, la suite demain !

Taggé comme: , , , Aucun commentaire
21oct/100

Performances en Java

java

J'ai souvent des a priori sur Java et sur ce qu'il faut faire ou éviter. J'avais notamment en tête qu'il vaut mieux éviter d'utiliser de bons gros codes bien lourd avec de la récurrence à gogo, et en fait l'autre jour j'ai lu un test qui tend à prouver le contraire...

Quantifying Recursion on the Java Platform

Je ne vais pas faire un gros résumé de l'article puisqu'il est déjà court et très clair, mais en gros, j'en retiens 2 choses :

  • Un code récursif peut être plus rapide que son homologue itératif, en Java
  • Quoi qu'on fasse, il est important de savoir comment tourne le code derrière... Je pense notamment à l'utilisation du "foreach" (oui, je sais, c'est un simple "for" en Java), qui n'est pas forcément gratuite...

A propos de ce for spécial, même s'il rend le code particulièrement clair et lisible, ce n'est pas forcément gratuit pour autant. Cet article met ce phénomène en avant, car entre une boucle for "à la main" (avec un compteur et tout) et une boucle for "jolie", la consommation de mémoire est très différente, ainsi que le temps d'exécution.

Bon, ça ne va par contre pas dire qu'il vaut mieux éviter ce for, car en fait il peut y avoir des cas où ça sera l'inverse : ce qui se passe en pratique est que Java génère pour ces boucles des Iterators. Dans le cas d'un ArrayList, où on peut accéder à n'importe quel élément de la liste en O(1), ce n'est pas vraiment utile, voire même contre-productif. Par contre, si on utilise un autre type de liste, et là je pense tout particulièrement aux listes chainées, alors l'Iterator sera bien plus rapide et efficace (puisque l'accès à l'élément suivant est rapide) que si on utilise le "children.get(i)", qui lui va enclencher un parcours de la liste jusqu'à arriver au "i"ème élément à chaque fois...

J'ai pu constater ce genre de phénomène "à la con" à la fac l'année dernière, où les étudiants qui ne codent qu'en Java ont pris la fâcheuse tendance d'utiliser tout et n'importe quoi sans chercher à savoir en quoi ça consiste réellement. Quand ils ont voulu coder un Huffman en Java, ils ont eu un gain de l'ordre de 50x à un moment en changeant simplement le type de List utilisé !!!

Méfiance, donc :)

17oct/102

Evaluation non paresseuse en Java

A ma grande surprise, il est possible en Java d'utiliser les opérateurs binaires '&' et '|' sur des booléens, alors que j'étais persuadés qu'ils étaient limités aux opérations binaires sur les entiers...

En fait, Ces symboles font presque la même chose que '&&' et '||', mais sans évaluation paresseuse. Etudions ce que fait le code suivant :

if (checkFoo() && checkBar()){
	...
}

Ici, si checkFoo retour FALSE, il n'est pas utile d'évalué checkBar, puisqu'on sait par avance que le résultat du test dans le if sera FALSE. Par évaluation paresseuse, checkBar n'est donc pas appelé... Or, que se passe-t-il si dans checkBar on avait un effet de bord ? Car comme celui-ci n'est pas effectué, cela pourrait changer l'execution du programme.

Avec le code suivant, on passe outre l'évaluation paresseuse :

if (checkFoo() & checkBar()){
	...
}

Le résultat du test sera inchangé, mais par contre cette fois-ci checkBar sera appelée.

Je pense que dans l'ensemble il est rare d'avoir besoin de ce genre de subtilité, et j'utilise toujours l'évaluation paresseuse (qui, par définition, est sensé être plus rapide...) ; mais on ne sait jamais, ça pourrait servir un jour :)

A noter qu'on peut par contre trouver des cas où il est nécessaire d'avoir une évaluation paresseuse pour que le programme tourne correctement. Voici un en C/C++, et un autre en Java :

my_type *p = getThing();
if ((p != NULL) && p->bool_value){
	...
}
MyObj obj = getObject();
if ((obj != null) && obj.isAlive()){
	...
}

Dans les 2 cas, l'évaluation paresseuse va permettre de garantir que l'expression de droite ne sera évaluée que si la première est validée. Dans la version C, on a donc la garanti que le pointeur sera valide (ou en tout cas non null), et de même avec la référence en Java.

13oct/102

Javaz : Compiler du Java en bytecode Caml

Dans le cadre de la matière "Typage et Polymorphisme", à la fac, on avait un choix de sujet (projets ou articles), et avec mon binôme aucun ne nous plaisait. Du coup il a voulu qu'on propose au prof notre propre sujet : compiler des sources Java en bytecode Caml...

Bon, le prof n'était pas trop content, mais il a fini par accepter le projet. On a déjà un petit truc basique qui tourne, et c'est assez intéressant. Le tout est écrit en OCaml, comme ça c'est l'occasion pour moi de m'y remettre un peu (après 1 ou 2 ans sans y avoir touché), et il faut dire que ça se prête particulièrement bien à la tache.

Les différentes étapes du projet sont relativement classiques :

1. Conversion en Ast

On commence donc par convertir les sources en Ast (Arbre de syntaxe abstraite), grâce à Yacc et Lex. En gros, c'est un parcours du fichier source, et on transforme tous les mots-clés et autres éléments repérés sous forme d'arbre.

Ainsi, l'expression (2 + 3) devient un noeud "opération + ", qui a "2" et "3" comme noeuds. Et donc par extension, 2 + 3 * 4 devient "*" avec comme fils gauche "+" (ayant lui-même les fils "2" et "3") et à droite "4". C'est une façon de parser qui revient à mettre le parenthésage implicite qui va bien, et qui permet en fait de calculer le résultat très facilement et récursivement (on applique le méthode de calcul sur le fils gauche, puis sur le fils droit, et fait l'opération correspondante sur le résultat).

La transformation en Ast permet déjà de vérifier si la syntaxe est valide selon la norme...

2. Typage de l'Ast

Une fois qu'on a cet Ast, on va chercher à le typer, ce qui va permettre au passage de vérifier la validité de tout un tas de trucs, et d'appliquer les casts implicites. En Java, le typage est explicite, donc le typeur n'a pas un énorme travail à faire pour "déduire" des types, il se contente de regarder ce qu'il a un peu partout et de valider ou pas. On a ainsi plusieurs opérations d'effectuées :

  • Typage de tous les noeuds de l'Ast : à la fin on obtient un Ast typé, correspondant au fait à tous les noeuds de l'Ast original (plus quelques nouveaux) auquel on ajoute le type résultant. Si on a un noeud "+" qui a comme fils un entier et un floatant, le noeud aura donc comme type "float".
  • Insertion des casts implicites : à chaque fois qu'on a 2 types différents qui se "rencontrent", on regarde comment caster l'un en l'autre de façon implicite. Ainsi, si on a une opération "+" qui prend un entier et un string, on va rajouter le cast de l'entier en string pour faire une concaténation de chaines... De même, si on affecte un entier à une variable qui est de type floatant, on va rajouter le cast. Et en fait on se rend compte qu'on a pas mal de cas, notamment à cause de tous les types d'entiers (byte, char, short, int, long).
  • Vérification de la validité des variables : pendant le parcours, on va "noter" toutes les variables locales que l'on rencontre (ainsi que les champs et paramètres, en fait), et donc quand on utilise une variable dans une méthode, on va tout de suite pouvoir vérifier si elle existe. Le cas échéant, on s'arrête avec un message d'erreur de type "Undeclared Variable XXX". Si elle existe, on va en profiter pour transformer le nom de la variable en une information un peu plus pratique le générateur de code pourra manipuler plus facilement : dans notre cas, on a choisi de représenter une variable locale par un couple d'entiers, le premier représentant le bloc relatif au courant auquel elle appartient, et le second le numéro de la variable dans ce bloc (par ordre de déclaration). Il est donc très facile pour le générateur de code de retrouver la variable dans la pile : soit c'est dans le bloc courant, donc on récupère sa position par rapport au pointeur de pile, soit c'est dans un bloc parent, et dans ce cas il suffit de connaitre le pointeur de pile correspondant à ce parent pour retrouver la position de la variable dans la pile.
  • Vérification des méthodes : on va appliquer le même principe aux méthodes (pas tout à fait fini, mais c'est en cours), vérifier que la méthode existe bien (et est accessible) dans un objet de cette classe, etc...
  • Transformation des boucles for en boucles while, histoire de ne pas se trainer des cas particuliers tout le temps. En cours de route on est tombé sur une petite subtilité : on ne peut pas se contenter de fusioner les instructions du corps avec le code à exécuter à la fin de chaque tour de boucle, car si on fait un "continue" au milieu du bloc, il faut bien exécuter les dernières instructions... Du coup la boucle while possède en fait 2 blocs : un bloc d'instruction pour le corps de la boucle, et une expression à évaluer après l'évaluation du bloc... Pour transformer la boucle for en boucle while, on créé juste un premier bloc d'instructions comprenant l'initialisation de la boucle, ainsi que la boucle while résultante. Ce qui permet de gérer automatiquement les variables déclarées à la volée dans la boucle for :)

Il reste encore pas mal de boulot au niveau du typeur, et c'est notamment lui qui va implémenter une première version de la gestion de surcharges...

3. Interprétation de l'Ast typé (optionnel)

Cette étape est un peu superflue et sert essentiellement pour des raisons de tests, car elle permet de tester le résultat de l'arbre typé en l'exécutant... Comme on tourne dans du code maison, on peut rajouter des affichages de debug un peu partout pour voir ce qui se passe. Et comme l'Ast est typé, l'interpréteur peut en fait vérifier les types à tout moment, sachant que jusqu'à maintenant la seule fois ou un type a été explosé (à savoir, une variable de type boolean contenant la valeur entière "0"), c'est quand j'ai fait péter le pointeur de pile ^^. C'est bien pratique pour repérer les conneries au niveau du typeur, comme des index de variables locales mal mis.

Les performances de l'interpréteur sont bien entendu catastrophique, puisqu'on ne fait que se balader dans un arbre dans un peu tous les sens, en récupérant les fils, puis en remontant, etc...

4. Génération de code

Pour l'instant, seul mon binôme s'est intéressé à cette partie, donc je n'ai pas encore mis les mains dedans. Mais en gros le principe est de prendre chacune des instructions/expressions de l'Ast typé, et de la traduire en code machine correspondante. On écrit ici du bytecode Caml, mais dans le fond c'est un peu la même démarche que quand on génère de l'assembleur (sauf que là ça serait un assembleur de plus haut niveau avec des instructions sympas ^^). Donc voilà, on pousse des valeurs sur la pile, on met le bytecode correspondant à l'addition, etc... C'est un peu long et fastidieux pour l'instant, mais quand on va arriver à la génération du code pour les objets ça devrait être assez sympa :p

Mots de la fin

Le projet n'est pas encore terminé, mais en quelques jours on a déjà bien avancé. C'est vraiment très instructif, car on en apprend à la fois sur Java (avec les spécifications à la con... c'est comme ça que j'ai appris qu'on pouvait mettre un label aux blocs pour ensuite appliquer ce label sur les continue/break !), sur Caml (car on a déjà plusieurs milliers de lignes de code, donc ça fait une bonne base de travail ^^), sur le typage en général, etc... En fait c'est une manière assez rapide de synthétiser tout un ensemble de cours que j'ai eu à la fac depuis 1 an (et d'autres en cours), donc c'est top !

Après, quant à savoir si le projet pourrait avoir un jour un intérêt autre que le délire de l'avoir fait, on est en train de se poser la question, et a priori... non ! Mais techniquement, ça marche déjà : on a fait plusieurs tests, notamment un qui consiste à calculer les nombres premiers dans les 10 000 premiers entiers, et on obtient bien le résultat voulu :)

9oct/100

Java : break label

Parmi les différents projets que j'ai en cours, j'en ai un encore top secret (^^) qui implique de parser du code Java. Pour la beauté de la chose, c'est bien entendu fait en OCaml (amen), mais ce n'est pas du tout le sujet de ce billet.

En fait, parfois je trouve des trucs qui me paraissent bizarres dans la BNF (Backus Normal Form, une notation qui permet de décrire la syntaxe d'un langage), et là je viens de découvrir qu'on peut ajouter un label à un break... Du coup j'ai fait une petite recherche Google pour voir à quoi ça ressemble, et je suis tombé sur un site qui donne un exemple sympa de la chose :

OuterLoop: for (int i = 2;; i++) {
	for (int j = 2; j < i; j++) {
		if (i % j == 0) {
			continue OuterLoop;
		}
	}

	System.out.println(i);

	if (i == 37) {
		break OuterLoop;
	}
}

Dans cet exemple, on a le label qui est utilisé à la fois sur un break et sur un continue. Quel en est le principe : le label permet tout simplement de désigner la boucle sur laquelle on veut que l'action s'effectue. Donc on peut avoir de multiples boucles for/while imbriquées, grâce à ça on peut sortir de tous les niveaux d'un coup, ou juste de quelques uns.

Pas grand chose de plus à ajouter... Ah si, qui sait ce que fait ce code ?



Remplis sous: Java Aucun commentaire
5oct/100

Java : normaliser un String

Dernièrement je fais un ensemble de classes "dictionnaire" en Java pour l'utilisation dans des livres électroniques, et histoire de pouvoir facilement comparer ce qu'on tape avec les mots de la base, il est important que tout soit normalisé... On doit donc retirer tout ce qui est accent, en particulier, et ce n'est pas forcément très intuitif à faire quand on ne l'a jamais fait...

For heureusement, en Java on dispose déjà de tous les outils nécessaires pour gérer cela. Le code que j'utilise effectue en gros 2 passes :

  • Une première pour tout mettre en majuscules (ça simplifie la recherche par la suite). Il suffit d'utiliser mla méthode toUppercase sur la chaine en question.
  • Une deuxième pour retirer les accents. J'utilise pour se faire la méthode suivante :
public static String removeDiacriticalMarks(String string) {
    return Normalizer.normalize(string, Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
}

Avec ça, on récupère tout simplement la chaîne privée de tous ses accents, et on est tranquille !

26sept/105

Unsigned Byte en Java ?

java

L'autre jour, j'ai eu besoin de lire un tableau d'octets qui étaient en non signés... On converti donc les données en bytearray Java qui va bien, et là, quand j'affiche les valeurs : -86, -126, etc... Et oui, en Java un byte est signé ! Et sauf erreur de ma part (après quelques courtes recherches), pas moyen d'avoir de bytes non signés...

La solution existe pourtant, mais je ne la trouve ni élégante, ni pratique : il suffit d'utiliser :

bytearray[i]&0xFF

En effet, pour les opérations les bytes sont castés en int, et avec le ET binaire, on récupère une valeur entre 0 et 255. Du coup ça marche super... Mais ça reste un poil lourd, car au lieu d'avoir un vrai beau type, partout où on veut utiliser des octets non signés, il faut faire cette manip...

18sept/100

Java : classe interne et syntaxe sympa

Un truc que j'aime bien en Java est la possibilité de faire des classes internes, mais surtout de les déclarer à la volée, comme un porc, en ajoutant à l'endroit où on les créé les méthodes à implémenter... En gros, ce genre de chose :

class MyClass
{
	...
	public void init(){
		truc.setListener(new Listener(){
			public void onListenerAction(int action){
				...
			}
		});
	}
	...
}

Ca évite quand même de créer des micro-classes un peu partout, et puis comme ça la classe interne a quand même accès aux champs de la classe d'origine, donc c'est pratique.

Et puis comme je suis un peu un barbare, je me suis demandé comment on faisait si depuis la méthode onListenerAction on voulait utiliser un champs de MyClass qui porte le même nom qu'un des arguments... C'est con, puisque de toute façon dans ce cas il suffit de renommer l'argument, mais pas grave, j'ai quand même tester.

Normalement, on ferait "this.myField", mais comme le this se réfère à la classe interne, ça ne marche pas... Au début, je me suis dit que ça devait être comme en C++, on refile le nom de la classe d'où vient le champs, et ça suffit pour le retrouver. Donc j'ai testé ça :

class MyClass
{
	private int action;
	...
	public void init(){
		truc.setListener(new Listener(){
			public void onListenerAction(int action){
				MyClass.action = action;
			}
		});
	}
	...
}

Sauf que... ca ne marche pas ! Et le message d'erreur est simple : ce champs n'est pas statique, donc on ne peut pas l'utiliser directement sur MyClass... Jusque là, c'est logique, mais alors comment on fait ?

En fait, ce qu'on veut c'est l'instance de la classe MyClass depuis laquelle on a été appelé. Et c'est là que ça devient fun, car du coup on fait "MyClass.this".
Donc au final, on se retrouve à faire

public void onListenerAction(int action){
	MyClass.this.action = action;
}

Et voilà le travail ! (pour l'histoire, en fait en vrai l'exemple du boulot c'était avec un appel de méthode car la classe MyClass implémentait aussi l'interface Listener pour d'obscures raisons, et qu'on avait besoin de centraliser plusieurs Listener dedans.)

Je pense qu'il doit y avoir d'autres façon de faire, comme faire une variable locale finale de type MyClass qui pointe sur "this", et l'utiliser en lieu de place de "MyClass.this", mais c'est tellement moins classe ;)

12sept/100

Java 7 : faut pas être pressé…

java

Dans un précédent billet, je parlais de quelques évolutions qui devraient voir le jour dans Java 7. Toute la question est donc de savoir quand exactement cette nouvelle release sortira...

Sur le blog de Mark Reinhold (via developpez.com), on apprend que la version 7 ne devrait pas sortir tout de suite. Enfin, pour être plus précis, 2 options sont possibles :

=> Sortir une version JDK7 mi-2011, mais amputée de pas mal des nouveautés prévues (dont l'ensemble des trucs sympas dont j'avais parlé), et donc un JDK8 mi-2012 avec le package complet.

=> Sortir le JDK7 mi-2012...

Dans les 2 cas, c'est plutôt nase ^^ Tant pis !

Taggé comme: , Aucun commentaire
30août/100

JNI : trop d’objets tue l’objet

Un charmant petit plantage sur Android tout à l'heure, alors que le même code marchait très bien sur d'autres JVM (celle de Sun Oracle, ainsi que JamVM). Heureusement, le message d'erreur de Dalvik est très explicite, et m'indique que côté JNI je dépasse le nombre autorisé de local refs (qui est de seulement 512).

En fait dans ce bout de code je génère une table des matières en Java à partir d'une table des matières en C++. Le souci, c'est que celle-ci possède genre 300 entrées, et qu'en Java pour chaque entrée je dois créer 1 objet et 1 String, soit 2 objets au total. 300*2 = 600 > 512... Game Over !

La solution est simple, et les explications sur le pourquoi du comment très clairement exposées sur le site de Sun :

(*env)->DeleteLocalRef(env, jstr);

Une fois que l'objet créé a été refilé comme référence à un autre objet Java, on n'a plus vraiment besoin de garder la référence locale (qui in fine ne sert qu'à s'assurer que le GC ne va pas venir libérer des objets qu'on vient d'allouer côté C...), et donc on peut supprimer la LocalRef.

Taggé comme: , , , Aucun commentaire
28août/100

Java 7 et ses évolutions

java

Ce matin j'ai lu un article très intéressant sur Java 7 et les pistes envisagées pour l'évolution du langage. Je ne vais pas tout commenter, mais ça m'a fait plaisir de voir que ça évolue dans le bon sens.

Java est réputé comme étant un langage très (trop) verbeux. Il y a une chose que l'on peut faire "facilement" en C, et de façon moins directe en Java : des pointeurs sur fonction. En Java on se retrouve en fait à utiliser une classe implémentant l'interface "Runnable" (ou autre), et ça devient assez vite moche à lire. Parmi les introductions de Java 7, on pourra donc trouver le type SAM (Single Abstract Method), qui représente en fait une classe n'ayant qu'une méthode abstraite, et les expressions lambda. Je ne vais pas repomper tout l'article car c'est super bien expliqué, mais voici résumé en 1 ligne de code ce que ça pourrait donner de sympa :

Comparator<String> comparator = #(a, b) { a.compareToIgnoreCase(b) }; 

La syntaxe est quand même super compacte, très lisible, et on se paie même le luxe au passage de ne pas préciser les types, car ceux-ci peuvent être déterminés automagiquement (comme on dit chez nous).

Je ne vais pas détailler le reste des potentiels ajouts/modifications, mais ils permettent tous soit de faire de nouvelles choses (pas forcément révolutionnaires, mais qui rendent le développement plus agréable et évitent d'avoir à faire des hacks foireux), soit de faire la même chose qu'avnat mais de façon plus simple et lisible. En somme, que du bon :)

Pour ceux que ça intéresse, je vous refile donc le lien : http://blog.developpez.com/adiguba/p9232/java/java7-projet-lambda/

Taggé comme: , Aucun commentaire
22août/100

JVM : plusieurs implémentations = plusieurs problèmes

L'open source, c'est génial. Java, c'est fantastique, c'est la liberté, tout le monde peut faire ce qu'il veut avec. Mais si chacun est libre de faire sa JVM, ça veut aussi dire qu'il existe un risque accru de problèmes.

C'était déjà le cas avec le Java sur mobiles, à l'époque des "vieux" Nokia et compagnie : chaque téléphone avait son lot de bugs inclus dans la JVM, et les développeurs devaient faire avec. Du coup, au lieu d'avoir une seule version d'un code qui marche partout (puisque c'était le principe du Java...), il fallait à l'époque être capable de patcher les différentes versions pour les différents téléphones... Pratique !

Cette semaine on a eu ce genre de problèmes, mais en utilisant des trucs standards : la JVM JamVM (qu'on utilise pour sa performance et sa compacité en mémoire) et la version JDBC/JNI de SQLite : avec la JVM de Sun, ça marche parfaitement, tandis qu'avec JamVM, on a un segmentation fault. C'est le genre d'erreur un peu gênant quand on fait du Java, puisque ce n'est pas sensé se produire ^^ Mais c'est l'utilisation de JNI, et donc de code natif, qui introduit ce genre de soucis.

Heureusement (façon de parler), le bug ne se produisait pas sur toutes les requêtes, et on s'est rendu compte qu'il ne se produisait qu'en cas de requête qui aurait du renvoyer un String à null... En mettant les mains dans le code de la couche JNI, en remontant les appels successifs, on a rapidement cerné le soucis :

JNIEXPORT jstring JNICALL Java_org_sqlite_NativeDB_column_1text(JNIEnv *env, jobject this, jlong stmt, jint col)
{
    return (*env)->NewStringUTF(env, (const char*)sqlite3_column_text(toref(stmt), col));
}

Ce petit bout de code fait le pont entre la partie purement C de sqlite (qui renvoie donc un (char *)) et la partie JNI (qui doit renvoyer un String) : on créé un String à partir du (char *) en utilisant les routines fournies par la JVM... Et c'est là que tout s'explique : la JVM de Sun autorise qu'on lui file un pointeur à NULL, et renvoie alors null comme String, alors que JamVM ne l'autorise pas.

On a été assez étonné de tomber sur un problème comme ça (à la fois du côté de JamVM qui ne gère pas ça comme la JVM de Sun, et du côté de sqlitejdbc, qui pourrait jouer la sécurité en faisant la vérification à la main). Le patch a été facile, il suffit de rajouter un petit if...

Moralité ? On ne peut pas toujours faire confiance aux libs et outils qu'on utilise, parfois il faut savoir s'en méfier et mettre le nez dedans :-)