Java Quiz #42 : A string too far

(FR)
Ce code semble totalement inoffensif, mais peut causer des erreurs sous certaines circonstances.
Pouvez-vous détecter le problème ?

(EN)
This code may seem harmless, but will cause serious problems in certain circumstances.
Can you spot the problem ?

  1. public class Quiz42 {
  2.  
  3. public static void main(String[] args) {
  4. new Quiz42();
  5. }
  6.  
  7. private static final int MAX_STRING_LENGTH = 6;
  8. private Map<Integer, String> db = new HashMap<Integer, String>();
  9. private int nextId = 0;
  10.  
  11. public Quiz42() {
  12. // Introductory text
  13. Console console = System.console();
  14. if (console==null) {
  15. System.out.println("Don't cheat in Eclipse :)");
  16. System.exit(0);
  17. }
  18. console.printf(
  19. "FakeDB 1.0%n"+
  20. "'exit': exits FakeDB.%n" +
  21. "'db' : dumps the DB content.%n"+
  22. "Words are limited to %d characters.%n%n> ",MAX_STRING_LENGTH);
  23.  
  24. // Read - Eval loop
  25. String s = null;
  26. while ((s = console.readLine()) != null) {
  27. if ("exit".equals(s)) {
  28. break;
  29. }
  30. if ("db".equals(s)) {
  31. console.printf("DB content : %s%n",db);
  32. continue;
  33. }
  34.  
  35. // Check DB constraints client-side, then save the user's string.
  36. if (s.length() > MAX_STRING_LENGTH) {
  37. console.printf("String too long, please enter another one.%n");
  38. continue;
  39. }
  40. persistInDatabase(s.toUpperCase());
  41. console.printf("Saved '%s'.%n> ",s);
  42. }
  43. }
  44.  
  45. private void persistInDatabase(String s) {
  46. // Simulate a database constraint
  47. if (s.length() > MAX_STRING_LENGTH) {
  48. throw new IllegalArgumentException("DATA TOO LARGE");
  49. }
  50. db.put(nextId++, s);
  51. }
  52.  
  53. }

(FR)
Réponse :
Aussi surprenant que cela puisse paraître, les méthode toUpperCase() et toLowerCase() peuvent changer la taille des chaînes de caractères sur lesquelles elles agissent !
Cela dépend de règles propres à chaque langue. Par exemple, en Allemand, le ß minuscule est transformé en double S majuscules.

Dans notre exemple, le mot "straße" (rue) qui fait 6 caractères sera transformé en "STRASSE", qui fait 7 caractères et provoquera une exception.

A noter, le comparateur String.CASE_INSENSITIVE_ORDER, utilisé par la méthode compareToIgnoreCase(), est obligé d'effectuer deux transformations avant de comparer les caractères des deux chaînes :

  1. private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable {
  2.  
  3. public int compare(String s1, String s2) {
  4. int n1 = s1.length(), n2 = s2.length();
  5. for (int i1 = 0, i2 = 0; i1 < n1 && i2 < n2; i1++, i2++) {
  6. char c1 = s1.charAt(i1);
  7. char c2 = s2.charAt(i2);
  8. if (c1 != c2) {
  9. c1 = Character.toUpperCase(c1);
  10. c2 = Character.toUpperCase(c2);
  11. if (c1 != c2) {
  12. c1 = Character.toLowerCase(c1);
  13. c2 = Character.toLowerCase(c2);
  14. if (c1 != c2) {
  15. return c1 - c2;
  16. }
  17. }
  18. }
  19. }
  20. return n1 - n2;
  21. }
  22. }

(EN)
Answer :
It might seem surprising, but the toUpperCase() and toLowerCase() methods may change the string's length. It depends on rules specific to each language.

For example, in German, the lowercase ß becomes two uppercase S, so the word "straße" (a street), which is 6 characters long, becomes "STRASSE", which is 7 characters long and breaks our database constraint.

This is why the String.CASE_INSENSITIVE_ORDER comparator, used by the compareToIgnoreCase() method, must apply 2 string transformations in a row (toUpperCase() then toLowerCase()) to be able to compare the given strings correctly :

  1. private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable {
  2.  
  3. public int compare(String s1, String s2) {
  4. int n1 = s1.length(), n2 = s2.length();
  5. for (int i1 = 0, i2 = 0; i1 < n1 && i2 < n2; i1++, i2++) {
  6. char c1 = s1.charAt(i1);
  7. char c2 = s2.charAt(i2);
  8. if (c1 != c2) {
  9. c1 = Character.toUpperCase(c1);
  10. c2 = Character.toUpperCase(c2);
  11. if (c1 != c2) {
  12. c1 = Character.toLowerCase(c1);
  13. c2 = Character.toLowerCase(c2);
  14. if (c1 != c2) {
  15. return c1 - c2;
  16. }
  17. }
  18. }
  19. }
  20. return n1 - n2;
  21. }
  22. }

Surprising, isn't it ?


Commentaires

1. Le vendredi 24 septembre 2010, 11:07 par Jérôme

Client-side check : you are checking the length of the initial String.
Database constraint : you are checking the length of the upper cased String, that could be longer => fatal exception thrown.

Also, you are not specifying which Locale to use for toUpperCase().

2. Le vendredi 24 septembre 2010, 11:11 par Jérôme

For those who wonder how a String can get longer when upper cased: the german Eszett ß becomes SS.

3. Le vendredi 24 septembre 2010, 11:19 par Florian

La méthode toUpperCase() peut changer la longueur de la chaîne. Il y a donc incohérence dans le résultat des 2 tests qui testent la longueur de la chaîne.

4. Le vendredi 24 septembre 2010, 14:43 par Brice

I just arrive a bit after the challenge started, but I agree with the previous answers.

By the way I just looked at the toUppercase code, it's funny to see labels. I forgot these constructions are still possible in Java.

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.