Interfaces
Héritage et Sous-Type
On a vu l’intérêt de l’héritage et du polymorphisme pour le sous-typage.
Animal a1 = Math.random() < 0.5 ? new Dog() : new Cat();
a1.speak();
Ce qui permet l’utilisation de variables de type générique, pouvant stocker des objets qu’on utilise quelque soit leur type.
Ceci est rendu possible par la redéfinition des méthodes et le polymorphisme.
Héritage et Sous-Type
C’est parfois l’utilité principale de l’héritage, plus que la “réutilisation du code de la classe de base”.
Héritage et Sous-Type
C’est tellement utile qu’on aimerais parfois qu’un objet hérite de plusieurs classe, afin de lui associé plusieurs types.
Impossible car on ne peut hériter que d’une classe.
Interfaces
Les interfaces permettent de créer des sous-types mais sans créer de classe de base.
On à la même relation “EST UN” entre un objet et une interface.
Par exemple on pourrait dire “Un objet est Inflammable s’il propose une méthode enflammer()”
Les objets inflammables n’ont pas vraiment de choses en commun que l’on pourrait centraliser dans une classe Inflammable
. Il n’hérite pas de propriété avec cette relation (à part l’existence de cette fonction).
Interfaces
Une interface est une liste de noms de méthodes (uniquement les signatures des méthodes).
Une interface est un prototype de classe. Elle définit la signature des méthodes qui doivent être implémentées dans les classes construites à partir de ce prototype.
Interfaces
On dit qu’une classe implémente une interface, si elle définit les méthodes de l’interface.
En java on déclare qu’une classe implémente une interface avec le mot clé implements
.
Une interface définit un type (comme une classe) et les classes qui implémentent cette interface sont donc des sous-types.
Exemple
interface Inflammable {
void enflammer();
}
class Bois implements Inflammable {
public void enflammer() {
System.out.println("Je brule et fais des braises");
}
}
class Dancefloor implements Inflammable {
public void enflammer() {
System.out.println("♪ ♫ Youhouhou ♬ ♫ ");
}
}
public class Main {
static public void main(String[] args) {
Inflammable[] tab = { new Bois(), new Dancefloor() };
for(Inflammable i : tab)
i.enflammer();
}
}
Le mot clé public
est implicite dans une interface.
Interfaces
Avantage :
- Une classe peut implémenter plusieurs interfaces.
- On a tous les avantages du sous-typage comme avec l’héritage classique (notamment le polymorphisme).
Exemple 2
public interface Comparable {
boolean greaterThan(Object o);
}
class Maximize {
static public Comparable max(Comparable a, Comparable b) {
if(a.greaterThan(b)) {
return a;
}
return b;
}
}
class Person implements Comparable {
private int size;
Person(int size) { this.size = size; }
@Override
public String toString() { return "size: "+size; }
public boolean greaterThan(Object o) {
return this.size > ((Person)o).size;
}
}
public class Main {
static public void main(String[] args) {
Person p1 = new Person(157);
Person p2 = new Person(173);
System.out.println(Maximize.max(p1, p2));
}
}
Interface
Une interface peut remplacer une classe pour déclarer :
- un attribut
- une variable
- un paramètre
- une valeur de retour
Attention, on ne peut pas instancier une interface.
Comparable c = new Comparable(); // NON
Exemple 3
interface Comparable {
boolean greaterThan(Object o);
}
class Maximize {
static public Comparable max(Comparable a, Comparable b) {
if(a.greaterThan(b)) {
return a;
}
return b;
}
}
class ListElements {
Comparable[] tab;
int nbElements = 0;
public ListElements(int maxSize) {
tab = new Comparable[maxSize];
}
public void add(Comparable e) {
tab[nbElements] = e;
nbElements++ ;
}
public boolean isIncreasing() {
for (int i = 1; i < nbElements ; i++) {
if (tab[i-1].greaterThan(tab[i])) return false;
}
return true;
}
}
class Person implements Comparable {
private int size;
Person(int size) { this.size = size; }
@Override
public String toString() { return "size: "+size; }
public boolean greaterThan(Object o) {
return this.size > ((Person)o).size;
}
}
public class Main {
static public void main(String[] args) {
Person p1 = new Person(157);
Person p2 = new Person(173);
Person p3 = new Person(175);
ListElements l = new ListElements(10);
l.add(p1);
l.add(p2);
l.add(p3);
System.out.println(l.isIncreasing());
l.add(p2);
System.out.println(l.isIncreasing());
}
}
Exemple 4 – Java 8 Interfaces
public interface InterfaceA {
public default void foo() {
System.out.println("A -> foo()");
}
}
public interface InterfaceB {
public default void foo1() {
System.out.println("B -> foo()");
}
}
private class Test implements InterfaceA, InterfaceB {
.....
}
Les méthodes “foo” et “foo1” par défaut sont appelables dans la classe Test.
Exemple 4 – Java 8 Interfaces
public interface InterfaceA {
public default void foo() {
System.out.println("A -> foo()");
}
}
public interface InterfaceB {
public default void foo() {
System.out.println("B -> foo()");
}
}
private class Test implements InterfaceA, InterfaceB {
// Erreur de compilation : "class Test inherits unrelated defaults for foo() from types InterfaceA and InterfaceB"
}
Exemple 4 – Java 8 Interfaces – Solution
public class Test implements InterfaceA, InterfaceB {
public void foo() {
System.out.println("Test -> foo()");
}
}
La méthode “foo” par défaut n’est plus appelable directement. L’appel de cette méthode se réalise en utilisant Interface.super.méthode.
public class Test implements InterfaceA, InterfaceB {
public void foo() {
InterfaceB.super.foo();
}
}
Méthodes et Classes Abstraites
Méthodes Abstraites
Lorsqu’on crée une hiérarchie de classes (pensez aux animaux), il y a certaines méthodes difficiles à définir dans les classes trop générales.
Dans la classe Animal
, par exemple, comment définir la méthode manger
.
public class Animal {
...
public void manger(Nourriture n) {
// Que faire?
}
}
Méthodes Abstraites
Dans ce cas, on peut définir une méthode abstraite avec le mot clé abstract
.
public abstract class Animal {
...
public abstract void manger(Nourriture n);
// pas de code associé
}
Une méthode abstraite doit obligatoirement être redéfinie dans les classes dérivées.
c’est exactement comme pour les méthodes définies par une interface. Les méthodes d’une interface sont d’ailleurs aussi appelées “méthodes abstraites”.
Classe Abstraites
Une classe qui contient une méthode abstraite, est aussi abstraite, et doit être déclarée comme telle.
public abstract class Animal {
...
public abstract void manger(Nourriture n);
// pas de code associé
}
Comme pour les interfaces, on ne peut pas instancier une classe abstraite
Classe Abstraite
public class Lion extends Mammifère {
...
@Override
public void manger(Nourriture n) {
// code spécifique aux lions
}
}
Classe Abstraite, Remarques
A la différence d’une interface, une classe abstraite peut contenir des méthodes concrètes (non-abstraites).
Une classe abstraite peut ne pas contenir de méthodes abstraites.
Instanciation et Spécialisation
Comme avec une interface, une classe abstraite constitue un type à part entière, mais qui ne peut pas être instanciée :
Animal unAnimal; // OK
unAnimal = new Animal (...); // ERREUR
Une sous-classe d’une classe abstraite peut :
- implémenter toutes les méthodes abstraites. Elle pourra alors être déclarée comme concrète et donc instanciée.
- ne pas implémenter toutes ces méthodes abstraite. Elle reste alors nécessairement abstraite et ne pourra être instanciée.
- ajouter d’autre(s) méthode(s) abstraite(s). Elle reste alors nécessairement abstraite et ne pourra être instanciée.
Classe Abstraite
Les classes abstraites sont entre les classes concrètes et les interfaces.
On les utilise :
- lorsqu’on a besoin de l’héritage (réutilisation du code ; méthodes concrètes dans la classe de base qui fonctionnent de la même manière quelque soit les classes dérivées).
- mais que certaines méthodes n’ont pas de sense à être définie
Exemple de Classe Abstraite
Exemple
Problème : faire un système permettant de représenter des formes géométriques dans un terminal. Toutes les formes géométriques doivent avoir une couleur de fond, une couleur de bordure et une largeur de bordure.
Solution : Définir une classe Shape
qui permet des gérer tous ce que les figures ont en commun. Shape
doit avoir une méthode abstraite contains(int x, int y)
pour savoir si un pixel appartient à la figure.
La Classe Shape
public abstract class Shape {
private char fillColor, strokeColor;
private int strokeWidth;
public Shape() {
}
public Shape(char fillColor, char strokeColor, int strokeWidth) {
this.fillColor = fillColor;
this.strokeColor = strokeColor;
this.strokeWidth = strokeWidth;
}
public void setFillColor(char b) { fillColor = b; }
public void setStrokeColor(char b) { strokeColor = b; }
public void setStrokeWidth(int w) { strokeWidth = w; }
public char getFillColor() { return fillColor; }
public char getStrokeColor() { return strokeColor; }
public int getStrokeWidth() { return strokeWidth; }
abstract public boolean contains(int x, int y);
public boolean onStroke(int x, int y) {
if(contains(x, y)) return false;
for(int dx = -strokeWidth; dx <= strokeWidth; ++dx) {
for(int dy = -strokeWidth; dy <= strokeWidth; ++dy) {
if(contains(x + dx, y + dy)) return true;
}
}
return false;
}
@Override
public String toString() { return "Shape"; }
}
La Classe Rectangle
public class Rectangle extends Shape {
private int x, y, w, h;
public Rectangle(int x, int y, int w, int h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
@Override
public boolean contains(int x, int y) {
return x >= this.x && x < this.x + this.w &&
y >= this.y && y < this.y + this.h;
}
}
La Classe Circle
public class Circle extends Shape {
private int x, y, radius;
public Circle(int x, int y, int radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public boolean contains(int x, int y) {
return (x-this.x)*(x-this.x) + (y - this.y)*(y - this.y) < radius*radius;
}
}
Exemples de Hiérarchies
Problème de Figures avec Aires
Problème : on veut créer un programme qui calcule les aires d’un ensemble de figures. On veut pouvoir créer de nouvelles classes représentant des figures données. On veut donc que chaque figure possède une méthode getArea()
qui retourne cette aire.
Solution : ici, inutile de créer une classe Shape
car les figures n’auront rien en commun à part une méthode getArea()
. Une interface Measurable
par exemple est suffisante.
Classe et Interface
Parfois il est intéressant de proposer à la fois une classe abstraite et une interface pour modéliser un comportement.
On le retrouve souvent dans l’API standart de Java. Runnable Collection
Héritage et Composition
Parfois il est aussi préférable d’utiliser la composition. (exemple du copieur/imprimante/scanner).
Héritage et Composition
Supposons qu’on veut enrichir une classe ListProduct
(qui stocke des produits), afin qu’elle garde en mémoire la somme des prix des produits stockés.
On suppose que cette classe est proposée par une bibliothèque et qu’on n’a pas accès au code source.
class Product {
public int getPrice() { return 1; }
}
class ListProduct {
...
public ListProduct() {
...
}
public void addAll(Product[] tab) {
...
}
public void add(Product e) {
...
}
}
class AwesomeListProduct extends ListProduct {
private int totalPrice;
public void addAll(Product[] tab) {
super.addAll(tab);
for(Product p : tab) {
totalPrice += p.getPrice();
}
}
public void add(Product p) {
super.add(p);
totalPrice += p.getPrice();
}
public int getTotalPrice() { return totalPrice; }
}
Héritage et Composition
class Main {
public static void main(String[] args) {
AwesomeListProduct list = new AwesomeListProduct();
Product[] tab = {new Product(), new Product()};
list.addAll(tab);
System.out.println(list.getTotalPrice());
}
}
Qu’affiche se programme?
2
Héritage et Composition
En effet on ne sais pas comment est implémentée la méthode addAll
. Dans ce cas, elle appelle la méthode add()
.
class ListProduct {
private Product[] products;
private int nbProduct;
public ListProduct() {
products = new Product[10];
}
public void addAll(Product[] tab) {
for(Product e : tab) {
add(e);
}
}
public void add(Product e) {
products[nbProduct] = e;
nbProduct++;
}
}
Héritage et Composition
En voyant ça, on peut se dire qu’on n’a juste à enlever la redéfinition de la méthode addAll()
Cependant, notre classe va alors dépendre de la manière dont la classe ListProduct
est implémentée.
Si la librairie modifie l’implémentation de ces méthodes, par exemple pour optimiser la méthode addAll()
. Notre classe pourrait ne plus fonctionner.
Héritage et Composition
Il ne faut pas que notre programme dépendent de l’implémentation interne des libraries utilisées.
Si possible les différentes parties de notre programme doivent aussi être indépendantes. Il faut éviter les dépendances sur l’implémentation entre composants.
Héritage et Composition
Comment faire alors?
Dans ce cas on peut utiliser la composition.
Héritage et Composition
class AwesomeListProduct {
private ListProduct listProduct;
private int totalPrice;
public AwesomeListProduct() {
listProduct = new ListProduct();
}
public void addAll(Product[] tab) {
listProduct.addAll(tab);
for(Product p : tab) {
totalPrice += p.getPrice();
}
}
public void add(Product p) {
listProduct.add(p);
totalPrice += p.getPrice();
}
public int getTotalPrice() { return totalPrice; }
}
Enumérations
Utilité
Prenons l’exemple du jeu du robot. Il serait intéressant d’avoir une fonction qui permet de connaitre la direction du robot (haut, bas, gauche ou droite).
Problème comment représenter cette direction?
public ??? getDirection() {
...
}
Utilité
Problème comment représenter cette direction?
- Se mettre d’accord pour dire que : 1 signifie haut, 2 signifie bas, etc.
public int getDirection() {
if(....)
return 1; // return UP
...
}
if(robot.getDirection() == 1) {
...
}
Pas forcément facile de se rappeler des valeurs ; c’est source d’erreur.
Utilité
Problème comment représenter cette direction?
- Utiliser une chaine de caractères : “UP”, “DOWN”, “LEFT”, “RIGHT”
public String getDirection() {
if(....)
return "LEFT";
...
}
if(robot.getDirection() == "LEFT") {
...
}
Difficile de comprendre en regardant la signature de la méthode ce qu’elle renvoie exactement.
Utilité
Problème comment représenter cette direction?
- Utiliser une énumération.
public Direction getDirection() {
if(....)
return Direction.LEFT;
...
}
if(robot.getDirection() == Direction.LEFT) {
...
}
Définir une Enumération
On crée une énumération comme une classe
public enum Direction {
UP,
RIGHT,
DOWN,
LEFT
}
Comme une classe, ça définit un type!
C’est un peu comme une classe mais qui ne contient des attributs static
et public
.
On ne peut pas l’instancier et il y a un nombre fini d’objets qui existent déjà et qu’on peut utiliser.
Utiliser une Enumération
public enum Direction {
UP,
RIGHT,
DOWN,
LEFT
}
Direction d = Direction.UP;
System.out.println(d); // affiche UP
System.out.println(d == Direction.UP);
switch(d) {
case UP: d = Direction.LEFT; break;
case LEFT: d = Direction.DOWN; break;
case DOWN: d = Direction.RIGHT; break;
case RIGHT: d = Direction.UP; break;
}
System.out.println(d);
Lister les valeurs possibles
la méthode values()
permet de lister les valeurs possibles :
for(Direction d : Direction.values()){
System.out.println(d);
}
Conversion depuis une chaine de caractères
Scanner sc = new Scanner(System.in);
String text = sc.next(); // lit une direction au clavier, ex: UP
Direction d = Direction.valueOf(text);
System.out.println(d); // affiche UP
Pour convertir une chaine de caractère en valeur d’énumération, il faut utiliser la méthode valueOf()
.
Enumération plus complexes
Comme pour les classes, on peut aussi ajouter des attributs et des méthodes.
enum Direction {
UP("↑"),
RIGHT("→"),
DOWN("↓"),
LEFT("←");
private String arrow;
Direction(String arrow) {
this.arrow = arrow;
}
public String toArrow() { return arrow; }
}
Si on définit un constructeur nécessitant des arguments, ces-derniers doivent être fournit lors de l’énumération des valeurs possibles.
Retour sur l’exemple des figures
public enum ConsoleColor {
BLACK('0'),
RED('1'),
GREEN('2'),
WHITE('7');
private char code = '0';
ConsoleColor(char code){
this.code = code;
}
public char code(){
return code;
}
public String apply(String s) {
return "\u001B[3" + code() + "m" + s + "\u001B[0m";
}
}
Package et Jar
Package et Jar
Vous avez vu qu’un package dans un dossier n’est pas facile à distribuer.
C’est pourquoi on peut créer, à partir d’un dossier qui contient un package, un fichier Jar
(Java Archive). Plus facile à distribuer et à utiliser (notamment avec eclipse).
Package et Jar
On créee un Jar
avec la commande jar
.
Cette commande accepte les opérations c
, t
, et x
ainsi que les options v
(pour afficher les détails) et f
(pour indiquer le nom du fichier Jar).
Package et Jar
c
: Pour créer une archive. On indique le dossier contenant le package
jar cvf RobotGame.jar -C dossier/contenant/les/classes .
Package et Jar
c
: Pour créer une archive. On indique le dossier contenant le dossier contenant le packaget
: Pour afficher le contenu d’une archive
jar tf RobotGame.jar
Package et Jar
c
: Pour créer une archive. On indique le dossier contenant le dossier contenant le packaget
: Pour afficher le contenu d’une archivex
: Pour extraire une archive
jar xf RobotGame.jar
Comment organiser un Projet?
Le mieux est de séparer les sources et les fichiers compilés.
Dans un dossier vide, on crée un dossier src
qui va contenir nos fichiers sources, et un dossier bin
qui va contenir nos fichiers compilés.
Organisation d’un projet
Pour mon package fr.dant.robotgame
, j’ai mes sources dans un dossier src/fr/dant/robotgame
Je peux les compiler et placer les fichiers compilés dans le répertoire donné.
javac -d ./bin -cp ./bin src/fr/dant/robotgame/*.java
Et finalement créer le fichier Jar
:
jar cvf RobotGame.jar -C bin .
MANIFEST
Le fichier MANIFEST.MF contient l’ensemble des métadonnées d’une archive jar sous forme d’un unique fichier texte stocké dans le répertoire META-INF.
Il peut contenir entre autre :
Manifest-Version : numéro de version
Created-By : nom de l’auteur
Class-Path : nom d’autre archive contenant des dépendances
Main-Class : nom de la classe contenant la main à exécuter.
MANIFEST
Exemple :
Manifest-Version: 1.0
Created-By: Quentin Bramas
Main-Class: RobotGame
Attention : Le fichier MANIFEST doit obligatoirement :
être encodé en UTF8 ;
se terminer par un retour à la ligne (dernière ligne vide) ;
n’avoir aucun espace à la fin des différentes lignes.
MANIFEST
L’option m
permet de fournir un MANIFEST.MF à la création du jar :
jar cvmf src/MANIFEST.MF RobotGame.jar -C bin .