Remise à niveau en programmation objets en Java

Test unitaire

Depuis le début nous faisons des essais en modifiant la méthode main. Pour faciliter ce processus, nous allons mettre en place des tests unitaires. Suivre les étapes ci-dessous :

  • Ajouter la classe ci-dessous
    package fr.univamu.ran;
    
    import org.junit.jupiter.api.Test;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    public class TestRan {
    
        @Test
        public void firstTest() {
            System.out.println("My first test...");
            int x = 10 + 20;
            assertEquals(30, x);
            assertTrue(x >= 30);
        }
    
    }
    
  • IntelliJ va détecter une erreur. Une ampoule rouge vous propose d'ajouter Junit en version 5.x.
  • Utiliser l'option Run all test dans le menu contextuel du projet.
Travail à faire : Créer un test afin de tester les getters/setters de la classe Person.

POO, la suite

Héritage

L'héritage est un mécanisme qui permet de définir un classe par extension d'une classe existante. Cette extension consiste à ajouter des méthodes, des variables d'instance et/ou de classe. Tester l'exemple ci-dessous

public class Student extends Person {
    String formation;
}
Travail à faire : Avec le menu contextuel et l'option Generate... ajouter les getter/setter.
Travail à faire : Avec le même mécanisme, ajouter un constructeur.
Travail à faire : Créer le nouveau constructeur c-dessous. Il utilise explicitement le constructeur de la classe Person.
public Student(String formation, String name) {
    super(0, 0.0f, name);
    this.formation = formation;
}
Note : Une instance de Student est une instance de Person. Créer le test unitaire qui valide cette affirmation avec
assertTrue( student instanceof Person );
Essayer d'affecter uns instance de Student dans une variable typée. Person.
Note : Une classe qui n'utilise pas l'héritage, hérite automatiquement de la classe Object. Consulter la documentation et utiliser les méthodes disponibles (comme toString()).

Polymorphisme

Travail à faire : Ajouter la méthode ci-dessous à la classe Student :
public String toString() {
    return "Student";
}
Travail à faire : Vérifier (avec un TU) que c'est bien la méthode ci-dessus qui est utilisée, même si la référence à l'instance de Student est stockée dans une variable typée Person, voir Object (toutes instances est une instance de Object). Ajouter ensuite une méthode toString à Person.
Note : Le polymorphisme est donc la capacité à utiliser plusieurs classes (ou types si nous généralisons) comme si il s'agissait de la même classe.

Surcharge

Travail à faire : Modifier la méthode ci-dessous de la classe Student :
public String toString() {
    return "Student " + super.toString();
}
Dans ce cas, nous définissons toString() en surchargeant (et donc utilisant) la version toString de Person.
Travail à faire : Modifier le TU en conséquence.

Les classes et méthodes abstraites

Lire les trois premières sections de cette documentation et tester les exemples proposés.

Les interfaces

Une interface est un moyen de spécifier des comportements qui devront être implémentés par des classes pour être utilisables. Ajouter l'interface ci-dessous

package fr.univamu.ran;

public interface INamed { // débute par un "I" pour indiquer sa nature

    String getName();

    void setName(String newName);

}

Elle définie les comportements attendus d'un objet qui possède un nom. Vous pouvez maintenant indiquer que la classe Person implémente cette interface :

package fr.univamu.ran;

public class Person implements INamed {
    ...
}
Travail à faire : Vérifier (dans un TU) qu'une instance de Person (et donc de Student) est une instance de INamed.
Travail à faire : Vous pouvez vous intéresser seulement aux comportements et ignorer l'implémentation en utilisant uniquement l'interface. Modifier le TU en conséquence.
INamed s = new Student("M1", "John");
Note : Définir l'interface IAged dans le même esprit. Nous pouvons définir une interface par héritage (voir ci-dessous). De même une classe peut implémenter plusieurs interfaces. Modifier le TU en conséquence.
package fr.univamu.ran;

public interface INamedAndAged extends INamed, IAged {

}
Note : Bien que nous soyons sur les spécifications, une interface peut proposer une implémentation par défaut qui peut éventuellement être surchargée par la classe.
// à ajouter à INamed
default String getUpperName() {
    return getName().toUpperCase();
}
Modifier le TU pour utiliser getUpperName().
Travail à faire : Vous pouvez aller plus loin avec cette documentation.

Les types génériques

Nous avons souvent besoin de parler de quelque chose sans le connaître précisément ou sans le connaître du tout. Par exemple, la classe ci-dessous représente un bus qui transporte quelque chose identifié par un type T générique inconnu (nous profitons de l'occasion pour illustrer le cas d'une variable d'instance privée et final donc forcément affectée par le constructeur) :

package fr.univamu.ran;

public class Bus<T> {
   final private T charge;

    public Bus(T charge) {
        this.charge = charge;
    }

    public T getCharge() {
        return charge;
    }
}

Pout utiliser ce bus, nous devons choisir la nature de la charge :

var p = new Person("John");
var bus = new Bus<Person>(p);
Travail à faire : Réaliser un TU pour concrétiser ces essais. Vérifier qu'un bus de personne peut transporter des étudiants. Utiliser ensuite INamed à la pace de Person.

Nous pouvons imposer des contraintes sur le type générique comme le montre l'exemple ci-dessous :

public class Bus<T extends INamed> {
   ...
}
Travail à faire : Ajouter à Bus une méthode toString() qui utilise le nom de la charge. Modifier le TU en conséquence.
Travail à faire : Créer, par héritage, une classe PersonBus qui est une spécialisation de la classe Bus pour les personnes.
Note : Les types génériques sont également utilisables dans les interfaces.
Travail à faire : Les types génériques sont également applicables aux méthodes :
    public <T extends INamed> T analyse(T object) {
        System.out.println("value = " + object);
        System.out.println("class = " + object.getClass());
        System.out.println("name = " + object.getName());
        return object;
    }
Utiliser cette méthode dans un test unitaire.

Les lambda expressions

C'est un point particulièrement important. Suivre ce tutoriel avec attention (éventuellement en créant un nouveau package et une classe de TU).

Les exceptions

Lire et tester les exemples de ce cours sur les exceptions Java.

Les classes Integer, Float, etc.

Java prévoit une classe pour chaque type primitif (int, long, ...). Découvrir dans cette documentation comment choisir entre les deux représentations.