Java/.NET : Types de données

Dans ce second billet de la série "Java/.NET", nous allons parler de toutes les différences touchant aux types de données.

Fichiers sources

En Java, le compilateur n’autorise qu’une seule et unique classe publique par fichier source. De plus, la classe publique d’un fichier source doit posséder exactement le même nom que le fichier dans lequel elle se trouve.

En C#, les restrictions comme celles-ci n’existent pas. Il est possible de déclarer plusieurs classes publiques au sein du même fichier, et aucune vérification n’est faite quand au nom du fichier contenant les classes.

Types primitifs - types valeurs

En Java comme en C#, il existe un type de données particulier : leur valeur est directement stocké dans une zone mémoire appelée la pile. De plus, ces différents types de données sont directement passés par valeur.

Ils peuvent représenter une valeur "basique", à savoir :
  • Un nombre (entier ou à virgule flottante)
  • Un caractère
  • Un booléen

En Java, ces types de données qui sont appelés des types primitifs. Il s’agit des seuls types de données qui ne sont pas des objets, c'est-à-dire qu’ils ne possèdent aucune méthode. Pour palier à cet inconvénient, il existe des types dits wrappers, qui sont des équivalents objets de ces types primitifs.

En C#, ces types sont appelés les types valeurs. Ils résident également sur la pile, mais ce sont cependant des objets. Chaque type valeur possède un alias, qui permet de faciliter l’appel de ces types dans le code. Par exemple, le type valeur Int32 possède un alias int. Ces alias seront convertis au moment de la compilation.

A noter également qu’en C#, les chaînes de caractères et les énumérations ont la particularité d’être des types valeur. Au contraire, les chaînes de caractères et les énumérations en Java sont des objets, et résident sur le tas.

Types références

Chacun des deux langages définit un autre type de variables : les types références.

Contrairement aux types primitifs (ou types valeurs), ils sont passés par référence lors des différents appels de méthodes. Ils représentent en général des types complexes, que nous allons définir nous même. De plus, la valeur de ces types de données est stockée sur le tas, et la pile ne contient qu’une référence vers ces objets.

En Java, chaque type primitif possède un équivalent en type référence. Ce sont les types wrappers. Ces objets sont des objets comme tous les autres objets Java. Leur valeur réside sur le tas et ils sont passés par référence lors des différents appels de méthodes. Depuis Java 1.5, la conversion entre les types primitifs et types wrapper est automatique.

En C# comme en Java, les classes sont des types références.

Structures

Le langage C# introduit un type de données qui n’existe pas en Java : le type structure. Les structures sont un type de données semblable aux classes, sauf qu’elles sont de types valeur, et non de type référence.

Cette différence implique que les structures sont stockées sur la pile (et non sur le tas, contrairement aux objets), et qu’elles sont passées par valeur en paramètre aux méthodes.

De par leur statut particulier, les structures ont cependant quelques fonctionnalités en moins par rapport aux classes. Il est impossible par exemple d’hériter d’une structure en C#.

Enumérations

En Java, les énumérations sont des objets. Comme pour les objets, il est possible de leur définir des attributs, des méthodes, et même des constructeurs.

Contrairement à cela, en C#, les énumérations sont de type valeur. Les énumérations correspondent à des entiers codés sur 8, 16, 32 ou 64 bits. Il est également possible d’assigner n’importe quelle valeur numérique à une énumération (y compris des valeurs non définies dans l’énumération), ainsi que de combiner plusieurs valeurs pour une même énumération grâce à l’opérateur | (bitwise or).

Espaces de nommages

Lors du développement d’applications, il arrive que plusieurs structures de données (classes, interfaces, énumérations...) possèdent le même nom. Afin d’éviter les collisions de noms ainsi générées, chaque langage à mis en place sa propre solution : les espaces de nommages.

En Java, un espace de nommage s’appelle un package. Un package doit être déclaré en début de tout fichier source grâce au mot clé package suivi du nom du package, et s’applique à tout le fichier. Il est possible d’accéder à des classes appartenant à d’autres packages grâce au mot clé import, suivi du nom de la classe à importer. Il est également possible d’importer toutes les classes d’un package en utilisant l’étoile (*).

En C#, un espace de nommage s’appelle un namespace. Un namespace est un bloc de code, noté grâce au mot clé namespace, suivi du nom du namespace. Il est possible d’utiliser des classes appartenant à d’autres espaces de noms grâce au mot clé using, suivi du nom de l’espace de nom à utiliser.

Voici par exemple la déclaration de deux classes dans un espace de nommage, en Java (à gauche) et en C# (à droite) :
package ns;

class Toto {
}

class Tata {
}

namespace Ns
{
    class Toto
    {
    }
    class Tata
    {
    }
}

Types génériques

Java et C# implémentent tous les deux les types génériques. Si l’utilisation est similaire dans les deux langages, leurs implémentations sont cependant très différentes.

Premier point, les types génériques en Java n’acceptent pas les types primitifs, alors que l’implémentation en C# accepte les types valeurs.

Second point, les génériques en Java sont implémentés selon une technique dite du type erasure. Derrière ce nom effrayant se cache un concept simple : les génériques en Java sont évalués à la compilation, toute trace de type générique n’existe plus à l’exécution. C’est le compilateur qui va se charger de transformer les types génériques en leurs opérations équivalentes (principalement du transtypage et des vérifications de types), et supprimer toute trace de leur passage.

Contrairement à cela, les génériques en C# persistent à l’exécution, et sont interprétés par l’environnement d’exécution. Il est donc possible de connaître le type générique d’une classe ou d’une méthode lors de l’exécution.

Les conséquences sont alors multiples. Par exemple, en C#, il est possible de créer une instance d’un type générique, et des tableaux de types génériques, choses qui sont impossibles à faire en Java, principalement à cause du type erasure.

Autre différence importante, il est possible d’effectuer des vérifications plus poussées sur les types utilisés en tant que types génériques dans le langage C#. Par exemple, en C#, il est possible de définir une contrainte « Cette classe utilise un type T, qui doit posséder un constructeur par défaut », contrainte qui est impossible en Java.


Voilà donc la fin de cette liste de différences entre Java et C# en ce qui concerne les types de données.

Le prochain billet portera sur les opérateurs, et devrait parler de deux fonctionnalités présentes en C# et inexistantes en Java : la surcharge des opérateurs et les indexeurs.

PS : Ceci était de 42ème billet de ce blog ;-)

Permalink  |  Commentaires (0)

Astuce Java #6 : Enum et toString

Depuis Java 5, le langage Java dispose d'une nouvelle fonctionnalité : les énumérations.

Les énumérations sont des structures permettant de représenter une variable ayant un nombre fini de valeurs, comme par exemple le sexe d'une personne, qui sera soit "homme", soit "femme".

Les énumérations sont mises en place de la manière suivante :
enum Sex {
    MALE,
    FEMALE;
}

Il est ensuite possible de créer des variables du type de l'énumération, et de les utiliser :
Sex sex = Sex.MALE;
System.out.println(sex);

Et c'est là qu'apparaît notre problème : la console nous affiche "MALE". Certes, il s'agit de la bonne valeur, mais la présentation n'est pas très esthétique. Heureusement pour nous, en Java une énumération est également un objet. Il nous est possible de définir nos propres méthodes, et aussi de récrire certaines méthodes de la classe Object. Ainsi, on peut avoir le code suivant :
enum Sex {
    MALE,
    FEMALE;
    
    @Override
    public String toString() {
        switch (this) {
            case MALE:
                return ("Male");
            case FEMALE:
                return ("Female");
        }
        throw new RuntimeException("Invalid value for this");
    }
}

L'exécution du même code nous donne ainsi "Male" au lieu de "MALE". On obtient le résultat souhaité, à savoir une chaîne de caractères plus facilement lisible par l'utilisateur.

Le soucis que l'on rencontre maintenant viens du switch dans la méthode toString. Nous devons gérer toutes les valeurs possible dans cette méthode, sous peine de lancer une RuntimeException. Une des solutions possibles (que j'ai trouvé dans un commentaire d'un (très) vieux billet de blog), et que je trouve de loin ma préférée, est de fournir un constructeur à notre énumération. Ce constructeur prend en paramètre une chaîne de caractères qui sera utilisée en tant que valeur de retour de la méthode toString :
enum Sex {
    MALE("Male"),
    FEMALE("Female");
    
    private String value;
    
    private Sex(String value) {
        this.value = value;
    }
    
    @Override
    public String toString() {
        return (value);
    }
}

Ainsi, nous obtenons on comportement optimal :
  • Chaque valeur de l'énumération renverra une chaîne de caractères "esthétique" représentant sa valeur
  • Chaque valeur devra posséder une chaîne de caractères (au risque de générer une erreur de compilation)

Le problème majeur qui est ainsi rencontré concerne la conversion énumération -> chaîne de caractères -> énumération. La chaîne de caractères renvoyait de base par toString était le nom de la constante dans l'énumération (MALE ou FEMALE dans notre cas), qui pouvait ensuite être reconverti en énumération via la méthode valueOf. Dans notre cas, la conversion ne se ferra plus puisque toString ne renverra plus une valeur compréhensible par valueOf. Cependant, la méthode name, définie dans toute énumération, renvoie toujours le nom de la constante tel que définit dans le code.

Permalink  |  Commentaires (3)

Astuce Java #2 : abstract et final vs interface et enum

Nouvelle question existentielle (je commence à croire que les questions nous viennent au rythme de une par jour en ce moment...).

Ce fois ci : Quelles sont les combinaisons possible en mélangeant les mots clés abstract et final avec les mots clés interface et enum ?

On pourrait résumer l'intégralité de cette question de cette manière :
Lesquelles de ces différentes combinaisons de mots clés sont autorisées pour déclarer un type ?
abstract final
interface ? ?
enum ? ?

Voici ce à quoi on aurait pu s'attendre :
abstract final
interface Illégal Légal
enum Légal Illégal

Je m'explique :
  • Une interface est par définition abstraite, abstract serait donc bien absurde dans ce cas.
  • Une interface pourrait être finale comme les classes le sont pour interdire l'héritage. On pourrait donc continuer à implémenter une interface, mais on ne pourrait pas en hériter (pour rappel, une interface hérite d'une autre).
  • Une énumération peut contenir des méthodes. Donc pourquoi pas des méthodes abstraites ?
  • Les types "enum" se sont pas extensibles (on ne peut pas les hériter), donc un final ici serait absurde une fois de plus.

Et bien voilà, après tests, ce que l'on obtient :
abstract final
interface Légal Illégal
enum Illégal Illégal

Trois surprises :
  • Il est possible de rendre une interface abstraite. Le comportement de l'interface n'est en rien modifié. D'après la Java Language Specification (Third edition), § 9.1.1.1 :
    Every interface is implicitly abstract. This modifier is obsolete and should not be used in new programs.
    The Java Language Specification, Third Edition, § 9.1.1.1
  • Il n'est pas possible de rendre une interface finale.
  • Il n'est pas possible de mettre des méthodes abstraites dans une énumération.

Je m'attendais pas vraiment à ces résultats, je vais retourner traîner à la recherche d'une explication sur ces points (car explication il doit y avoir).

Permalink  |  Commentaires (1)