Skip to content

3. Clases e interfaces

3.1. El objeto a través de ejemplos

3.1.1. Conceptos generales

Ahora abordaremos, a través de ejemplos, la programación orientada a objetos. Un objeto es una entidad que contiene datos que definen su estado (llamados atributos o propiedades) y funciones (llamadas métodos). Un objeto se crea según un modelo que se denomina clase:

public class C1{
    type1 p1;            // propiedad p1
    type2 p2;            // propiedad p2
    
    type3 m3(){        // método m3
        
    }
    type4 m4(){        // método m4
        
    }
    
}

A partir de la clase C1 anterior, se pueden crear numerosos objetos O1, O2,… Todos tendrán las propiedades p1, p2, … y los métodos m3, m4, … Tendrán valores diferentes para sus propiedades pi, por lo que cada uno tendrá un estado propio.

Si O1 es un objeto de tipo C1, O1.p1 se refiere a la propiedad p1 de O1 y O1.m1 al método m1 de O1.

Consideremos un primer modelo de objeto: la clase personne.

3.1.2. Definición de la clase «persona»

La definición de la clase personne será la siguiente:


import java.io.*;

public class personne{
  // atributos
  private String prenom;
  private String nom;
  private int age;
  
  // método
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

  // método
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

Aquí tenemos la definición de una clase, es decir, un tipo de datos. Cuando creemos variables de este tipo, las llamaremos objetos. Por lo tanto, una clase es un molde a partir del cual se construyen los objetos.

Los miembros o campos de una clase pueden ser datos o métodos (funciones). Estos campos pueden tener uno de los tres atributos siguientes:

privé: Un campo privado (private) solo es accesible mediante los métodos internos de la clase

public: Un campo público es accesible desde cualquier función, ya sea definida o no dentro de la clase

protégé: Un campo protegido (protected) solo es accesible mediante los métodos internos de la clase o de un objeto derivado (véase más adelante el concepto de herencia).

En general, los datos de una clase se declaran privados, mientras que sus métodos se declaran públicos. Esto significa que el usuario de un objeto (el programador)

a: no tendrá acceso directo a los datos privados del objeto

b: podrá invocar los métodos públicos del objeto y, en particular, aquellos que le den acceso a sus datos privados.

La sintaxis para declarar un objeto es la siguiente:


public class nomClasse{
    private  donnée ou méthode privée
    public  donnée ou méthode publique
    protected  donnée ou méthode protégée
}

Remarques

  • El orden de declaración de los atributos private, protected y public es arbitrario.

3.1.3. El método inicializa

Volvamos a nuestra clase «persona» declarada como:


import java.io.*;

public class personne{
  // atributos
  private String prenom;
  private String nom;
  private int age;
  
  // método
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

  // método
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

¿Cuál es la función del método initialise? Dado que el apellido, el nombre y la edad son datos privados de la clase «persona», las instrucciones

personne p1;
p1.prenom="Jean";
p1.nom="Dupont";
p1.age=30;

son ilegales. Debemos inicializar un objeto de tipo personne mediante un método público. Esa es la función del método initialise. Escribiremos:

personne p1;
p1.initialise("Jean","Dupont",30);

La escritura p1.initialise es válida porque initialise es de acceso público.

3.1.4. El operador new

La secuencia de instrucciones

personne p1;
p1.initialise("Jean","Dupont",30);

es incorrecta. La instrucción

    personne p1;

declara a p1 como una referencia a un objeto de tipo personne. Este objeto aún no existe y, por lo tanto, p1 no está inicializado. Es como si escribiéramos:

personne p1=null;

donde se indica explícitamente con la palabra clave null que la variable p1 aún no hace referencia a ningún objeto.

Cuando luego se escribe

p1.initialise("Jean","Dupont",30);

se invoca el método initialise del objeto al que hace referencia p1. Sin embargo, este objeto aún no existe y el compilador reportará el error. Para que p1 haga referencia a un objeto, hay que escribir:

personne p1=new personne();

Esto tiene como efecto la creación de un objeto de tipo personne que aún no se ha inicializado: los atributos nom y prenom, que son referencias a objetos de tipo String, tendrán el valor null, y age tendrá el valor 0. Por lo tanto, existe una inicialización por defecto. Ahora que p1 hace referencia a un objeto, la instrucción de inicialización de este objeto

p1.initialise("Jean","Dupont",30);

es válida.

3.1.5. La palabra clave this

Veamos el código del método initialise:


public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

La instrucción this.prenom=P significa que el atributo prenom del objeto actual (this) recibe el valor P. La palabra clave this se refiere al objeto actual: aquel en el que se encuentra el método que se está ejecutando. ¿Cómo lo sabemos? Veamos cómo se inicializa el objeto al que hace referencia p1 en el programa que lo llama:

p1.initialise("Jean","Dupont",30);

Se llama al método initialise del objeto p1. Cuando en este método se hace referencia al objeto this, en realidad se hace referencia al objeto p1. El método initialise también podría haberse escrito de la siguiente manera:


public void initialise(String P, String N, int age){
   prenom=P;
   nom=N;
   this.age=age;
  }

Cuando un método de un objeto hace referencia a un atributo A de ese objeto, la notación this.A es implícita. Se debe utilizar explícitamente cuando haya un conflicto de identificadores. Este es el caso de la instrucción:


this.age=age;

donde age designa un atributo del objeto actual, así como el parámetro age recibido por el método. En ese caso, hay que resolver la ambigüedad designando el atributo age como this.age.

3.1.6. Un programa de prueba

A continuación se muestra un programa de prueba:


public class test1{
  public static void main(String arg[]){
    personne p1=new personne();
    p1.initialise("Jean","Dupont",30);
    p1.identifie();
  }
}

La clase personne se define en el archivo fuente personne.java y se compila:

E:\data\serge\JAVA\BASES\OBJETS\2>javac personne.java

E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002  09:21                  473 personne.java
10/06/2002  09:22                  835 personne.class
10/06/2002  09:23                  165 test1.java

Hacemos lo mismo con el programa de prueba:

E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java

E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002  09:21                  473 personne.java
10/06/2002  09:22                  835 personne.class
10/06/2002  09:23                  165 test1.java
10/06/2002  09:25                  418 test1.class

Es sorprendente que el programa test1.java no importe la clase personne con una instrucción:

import personne;

Cuando el compilador encuentra en el código fuente una referencia a una clase que no está definida en ese mismo archivo fuente, busca la clase en varios lugares:

  • en los paquetes importados mediante las instrucciones import
  • en el directorio desde el que se inició el compilador

En nuestro ejemplo, el compilador se ejecutó desde el directorio que contiene el archivo personne.class, lo que explica que haya encontrado la definición de la clase personne. En este caso, incluir una instrucción import provoca un error de compilación:

E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
test1.java:1: '.' expected
import personne;
               ^
1 error

Para evitar este error, pero para recordar que se debe importar la clase «persona», en el futuro escribiremos al inicio del programa:

// clases importadas
// import persona;

Ahora podemos ejecutar el archivo test1.class:

E:\data\serge\JAVA\BASES\OBJETS\2>java test1
Jean,Dupont,30

Es posible agrupar varias clases en un mismo archivo fuente. Agrupemos, pues, las clases personne y test1 en el archivo fuente test2.java. La clase test1 se renombra como test2 para reflejar el cambio en el nombre del archivo fuente:

// paquetes importados
import java.io.*;

class personne{
   // atributos
  private String prenom;    // nombre de mi persona
  private String nom;            // su apellido
  private int age;                // su edad

   // método
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }//inicializa

   // método
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }//identifica
}//clase
public class test2{
  public static void main(String arg[]){
    personne p1=new personne();
    p1.initialise("Jean","Dupont",30);
    p1.identifie();
  }
}

Cabe señalar que la clase personne ya no tiene el atributo public. De hecho, en un archivo fuente de Java, solo una clase puede tener el atributo public. Es aquella que tiene la función main. Además, el archivo fuente debe llevar el nombre de esta última. Compilemos el archivo test2.java:

E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002  09:36                  633 test2.java

E:\data\serge\JAVA\BASES\OBJETS\3>javac test2.java

E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002  09:36                  633 test2.java
10/06/2002  09:41                  832 personne.class
10/06/2002  09:41                  418 test2.class

Cabe destacar que se ha generado un archivo .class para cada una de las clases presentes en el archivo fuente. Ahora ejecutemos el archivo test2.class:

E:\data\serge\JAVA\BASES\OBJETS\2>java test2
Jean,Dupont,30

De ahora en adelante, utilizaremos indistintamente los dos métodos:

  • clases agrupadas en un único archivo fuente
  • una clase por archivo fuente

3.1.7. Otro método de inicialización

Consideremos nuevamente la clase personne y agreguémosle el siguiente método:


public void initialise(personne P){
    prenom=P.prenom;
    nom=P.nom;
    this.age=P.age;
  }

Ahora tenemos dos métodos con el nombre initialise: esto es válido siempre y cuando acepten parámetros diferentes. Ese es el caso aquí. El parámetro es ahora una referencia P a una persona. Los atributos de la persona P se asignan entonces al objeto actual (this). Cabe señalar que el método initialise tiene acceso directo a los atributos del objeto P, aunque estos sean de tipo private. Esto siempre es así: los métodos de un objeto O1 de una clase C siempre tienen acceso a los atributos privados de los demás objetos de la misma clase C.

A continuación se muestra una prueba de la nueva clase personne:


// import persona;
import java.io.*;

public class test1{
  public static void main(String arg[]){
    personne p1=new personne();
    p1.initialise("Jean","Dupont",30);
    System.out.print("p1=");
    p1.identifie();
    personne p2=new personne();
    p2.initialise(p1);
    System.out.print("p2=");
    p2.identifie();
  }
}

y sus resultados:

p1=Jean,Dupont,30
p2=Jean,Dupont,30

3.1.8. Constructores de la clase persona

Un constructor es un método que lleva el nombre de la clase y que se invoca al crear el objeto. Por lo general, se utiliza para inicializarlo. Es un método que puede aceptar argumentos, pero que no devuelve ningún resultado. Su prototipo o definición no va precedido de ningún tipo (ni siquiera void).

Si una clase tiene un constructor que acepta n argumentos argi, la declaración e inicialización de un objeto de esa clase se puede realizar de la siguiente manera:


        classe objet =new classe(arg1,arg2, ... argn);

o


        classe objet;

        objet=new classe(arg1,arg2, ... argn);

Cuando una clase tiene uno o más constructores, es obligatorio utilizar uno de ellos para crear un objeto de esa clase. Si una clase C no tiene ningún constructor, cuenta con uno por defecto, que es el constructor sin parámetros: public C(). Los atributos del objeto se inicializan entonces con valores por defecto. Esto es lo que sucedió cuando, en los programas anteriores, escribimos:

    personne p1;
    p1=new personne();

Creemos dos constructores para nuestra clase personne:


public class personne{
  // atributos
  private String prenom;
  private String nom;
  private int age;
  
  // constructores
  public personne(String P, String N, int age){
    initialise(P,N,age);
  }
  public personne(personne P){
    initialise(P);
  }

  // método
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }
  public void initialise(personne P){
    this.prenom=P.prenom;
    this.nom=P.nom;
    this.age=P.age;
  }

  // método
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

Nuestros dos constructores se limitan a invocar los métodos initialise correspondientes. Recordemos que cuando en un constructor aparece, por ejemplo, la notación initialise(P), el compilador la traduce como this.initialise(P). Por lo tanto, en el constructor se invoca el método initialise para trabajar con el objeto al que hace referencia this, es decir, el objeto actual, el que se está construyendo.

A continuación se muestra un programa de prueba:


// import persona;
import java.io.*;

public class test1{
  public static void main(String arg[]){
    personne p1=new personne("Jean","Dupont",30);
    System.out.print("p1=");
    p1.identifie();
    personne p2=new personne(p1);
    System.out.print("p2=");
    p2.identifie();
  }
}

y los resultados obtenidos:

p1=Jean,Dupont,30
p2=Jean,Dupont,30

3.1.9. Las referencias de los objetos

Siempre utilizamos la misma clase personne. El programa de prueba queda así:


// import persona;
import java.io.*;

public class test1{
  public static void main(String arg[]){
    // p1
    personne p1=new personne("Jean","Dupont",30);
    System.out.print("p1=");  p1.identifie();
    // p2 hace referencia al mismo objeto que p1
    personne p2=p1;
    System.out.print("p2="); p2.identifie();
    // p3 hace referencia a un objeto que será una copia del objeto al que hace referencia p1
    personne p3=new personne(p1);
    System.out.print("p3=");  p3.identifie();
    // se cambia el estado del objeto al que hace referencia p1
    p1.initialise("Micheline","Benoît",67);
    System.out.print("p1=");  p1.identifie();
    // como p2 = p1, el objeto al que hace referencia p2 debe haber cambiado de estado
    System.out.print("p2=");  p2.identifie();
    // como p3 no hace referencia al mismo objeto que p1, el objeto al que hace referencia p3 no debe haber cambiado
    System.out.print("p3=");  p3.identifie();
  }
}

Los resultados obtenidos son los siguientes:

p1=Jean,Dupont,30
p2=Jean,Dupont,30
p3=Jean,Dupont,30
p1=Micheline,Benoît,67
p2=Micheline,Benoît,67
p3=Jean,Dupont,30

Al declarar la variable p1 mediante

personne p1=new personne("Jean","Dupont",30);

p1 hace referencia al objeto personne("Jean","Dupont",30), pero no es el objeto en sí. En C, diríamos que es un puntero, c.a.d, a la dirección del objeto creado. Si luego escribimos:

    p1=null

No es el objeto personne("Jean","Dupont",30) el que se modifica, sino que es la referencia p1 la que cambia de valor. El objeto persona("Jean","Dupont",30) se «perderá» si ninguna otra variable hace referencia a él.

Cuando se escribe:

personne p2=p1;

se inicializa el puntero p2: este «apunta» al mismo objeto (designa al mismo objeto) que el puntero p1. Por lo tanto, si se modifica el objeto al que «apunta» (o al que hace referencia) p1, se modifica el objeto al que hace referencia p2.

Cuando se escribe:

personne p3=new personne(p1);

se crea un nuevo objeto, que es una copia del objeto al que hace referencia p1. Este nuevo objeto será referido por p3. Si se modifica el objeto al que apunta (o al que hace referencia) p1, no se modifica en absoluto el objeto al que hace referencia p3. Esto es lo que muestran los resultados obtenidos.

3.1.10. Los objetos temporales

En una expresión, se puede invocar explícitamente al constructor de un objeto: este se crea, pero no tenemos acceso a él (por ejemplo, para modificarlo). Este objeto temporal se crea para evaluar la expresión y luego se desecha. El espacio de memoria que ocupaba será recuperado automáticamente más adelante por un programa llamado «recolector de basura», cuya función es recuperar el espacio de memoria ocupado por objetos a los que ya no hacen referencia los datos del programa.

Consideremos el siguiente ejemplo:


// import persona;

public class test1{
  public static void main(String arg[]){
    new personne(new personne("Jean","Dupont",30)).identifie();
  }
}

y modifiquemos los constructores de la clase personne para que muestren un mensaje:


// fabricantes
  public personne(String P, String N, int age){
    System.out.println("Constructeur personne(String, String, int)");
    initialise(P,N,age);
  }
  public personne(personne P){
    System.out.println("Constructeur personne(personne)");
    initialise(P);
  }

Obtenemos los siguientes resultados:

Constructeur personne(String, String, int)
Constructeur personne(personne)
Jean,Dupont,30

lo que muestra la construcción sucesiva de los dos objetos temporales.

3.1.11. Métodos para leer y escribir los atributos privados

Agregamos a la clase personne los métodos necesarios para leer o modificar el estado de los atributos de los objetos:


public class personne{
  private String prenom;
  private String nom;
  private int age;
  
  public personne(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

  public personne(personne P){
    this.prenom=P.prenom;
    this.nom=P.nom;
    this.age=P.age;
  }

  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }

  // accesorios
  public String getPrenom(){
    return prenom;
  }
  public String getNom(){
    return nom;
  }
  public int getAge(){
    return age;
  }

  //modificadores
  public void setPrenom(String P){
    this.prenom=P;
  }
  public void setNom(String N){
    this.nom=N;
  }
  public void setAge(int age){
    this.age=age;
  }
}

Probamos la nueva clase con el siguiente programa:


// importar persona;

public class test1{
  public static void main(String[] arg){
    personne P=new personne("Jean","Michelin",34);
    System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
    P.setAge(56);
    System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
  }
}

y obtenemos los siguientes resultados:

P=(Jean,Michelin,34)
P=(Jean,Michelin,56)

3.1.12. Los métodos y atributos de clase

Supongamos que queremos contar el número de objetos personne creados en una aplicación. Podemos manejar un contador por nuestra cuenta, pero corremos el riesgo de olvidarnos de los objetos temporales que se crean aquí y allá. Parecería más seguro incluir en los constructores de la clase personne una instrucción que incremente un contador. El problema es pasar una referencia de este contador para que el constructor pueda incrementarlo: hay que pasarles un nuevo parámetro. También se puede incluir el contador en la definición de la clase. Como se trata de un atributo de la clase misma y no de un objeto específico de esa clase, se declara de manera diferente con la palabra clave static:

    private static long nbPersonnes;        // número de personas creadas

Para hacer referencia a él, se escribe personne.nbPersonnes para indicar que es un atributo de la propia clase personne. Aquí hemos creado un atributo privado al que no se tendrá acceso directo desde fuera de la clase. Por lo tanto, creamos un método público para dar acceso al atributo de clase nbPersonnes. Para obtener el valor de nbPersonnes, el método no necesita un objeto específico: de hecho, nbPersonnes no es el atributo de un objeto específico, sino el atributo de toda una clase. Por lo tanto, se necesita un método de clase, también declarado como static:

public static long getNbPersonnes(){
    return nbPersonnes;
}

que desde el exterior se llamará con la sintaxis personne.getNbPersonnes(). A continuación, un ejemplo.

La clase personne queda así:


public class personne{
  
  // atributo de clase
  private static long nbPersonnes=0;

  // atributos de objetos

  
  // constructores
  public personne(String P, String N, int age){
    initialise(P,N,age);
    nbPersonnes++;
  }
  public personne(personne P){
    initialise(P);
    nbPersonnes++;
  }

  // método


  // método de clase
  public static long getNbPersonnes(){
    return nbPersonnes;
  }

}// clase

Con el siguiente programa:


// import persona;

public class test1{
  public static void main(String arg[]){
    personne p1=new personne("Jean","Dupont",30);
    personne p2=new personne(p1);
    new personne(p1);
    System.out.println("Nombre de personnes créées : "+personne.getNbPersonnes());
  }// main
}//prueba1

se obtienen los siguientes resultados:

    Nombre de personnes créées : 3

3.1.13. Paso de un objeto a una función

Ya hemos mencionado que Java pasa los parámetros efectivos de una función por valor: los valores de los parámetros efectivos se copian en los parámetros formales. Por lo tanto, una función no puede modificar los parámetros efectivos.

En el caso de un objeto, no hay que dejarse engañar por el uso incorrecto del lenguaje que se hace sistemáticamente al hablar de «objeto» en lugar de «referencia de objeto». Un objeto solo se manipula a través de una referencia (un puntero) a él. Por lo tanto, lo que se pasa a una función no es el objeto en sí, sino una referencia a ese objeto. Así pues, es el valor de la referencia —y no el valor del objeto en sí— el que se copia en el parámetro formal: no se crea un nuevo objeto.

Si se pasa una referencia de objeto R1 a una función, esta se copiará en el parámetro formal correspondiente R2. Por lo tanto, las referencias R2 y R1 apuntan al mismo objeto. Si la función modifica el objeto al que apunta R2, obviamente modifica también el objeto al que hace referencia R1, ya que se trata del mismo.

Image

Esto es lo que muestra el siguiente ejemplo:


// import persona;

public class test1{
  public static void main(String arg[]){
    personne p1=new personne("Jean","Dupont",30);
    System.out.print("Paramètre effectif avant modification : ");
    p1.identifie();
    modifie(p1);
    System.out.print("Paramètre effectif après modification : ");
    p1.identifie();
  }// main

  private static void modifie(personne P){
    System.out.print("Paramètre formel avant modification : ");
    P.identifie();
    P.initialise("Sylvie","Vartan",52);
    System.out.print("Paramètre formel après modification : ");
    P.identifie();
  }// modifica
}// clase

El método modifie se declara como static porque es un método de clase: no es necesario anteponerle un objeto para llamarlo. Los resultados obtenidos son los siguientes:

Constructeur personne(String, String, int)
Paramètre effectif avant modification : Jean,Dupont,30
Paramètre formel avant modification : Jean,Dupont,30
Paramètre formel après modification : Sylvie,Vartan,52
Paramètre effectif après modification : Sylvie,Vartan,52

Se observa que solo se crea un objeto: el de la persona p1 de la función main, y que el objeto sí fue modificado por la función modifie.

3.1.14. Encapsular los parámetros de salida de una función en un objeto

Debido al paso de parámetros por valor, no es posible escribir en Java una función que tenga parámetros de salida de tipo int, por ejemplo, ya que no se puede pasar la referencia de un tipo int que no es un objeto. Por lo tanto, se puede crear una clase que encapsule el tipo int:


public class entieres{
  private int valeur;

  public entieres(int valeur){
    this.valeur=valeur;
  }

  public void setValue(int valeur){
    this.valeur=valeur;
  }

  public int getValue(){
    return valeur;
  }
}

La clase anterior cuenta con un constructor que permite inicializar un entero y dos métodos que permiten leer y modificar el valor de dicho entero. Probamos esta clase con el siguiente programa:


// import enteros;

public class test2{
  public static void main(String[] arg){
    entieres I=new entieres(12);
    System.out.println("I="+I.getValue());
    change(I);
    System.out.println("I="+I.getValue());
  }
  private static void change(entieres entier){
    entier.setValue(15);
  }
}

y se obtienen los siguientes resultados:

I=12
I=15

3.1.15. Una tabla de personas

Un objeto es un dato como cualquier otro y, por lo tanto, se pueden agrupar varios objetos en una tabla:


// import persona;

public class test1{
  public static void main(String arg[]){
    personne[] amis=new personne[3];
    System.out.println("----------------");
    amis[0]=new personne("Jean","Dupont",30);
    amis[1]=new personne("Sylvie","Vartan",52);
    amis[2]=new personne("Neil","Armstrong",66);
    int i;
    for(i=0;i<amis.length;i++)
      amis[i].identifie();
  }
}

La instrucción persona[] amigos = new persona[3]; crea un array de 3 elementos de tipo personne. Estos tres elementos se inicializan aquí con los valores null y c.a.d, ya que no hacen referencia a ningún objeto. Una vez más, por un uso incorrecto del lenguaje, se habla de una matriz de objetos cuando en realidad se trata solo de una matriz de referencias a objetos. La creación del array de objetos —el cual es en sí mismo un objeto (por la presencia de new)— no crea por sí misma ningún objeto del tipo de sus elementos: esto debe hacerse posteriormente.

Se obtienen los siguientes resultados:

----------------
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Jean,Dupont,30
Sylvie,Vartan,52
Neil,Armstrong,66

3.2. El legado a través del ejemplo

3.2.1. Generalidades

Aquí abordamos el concepto de herencia. El objetivo de la herencia es «personalizar» una clase existente para que satisfaga nuestras necesidades. Supongamos que queremos crear una clase enseignant: un maestro es una persona particular. Tiene atributos que otra persona no tendrá: la materia que imparte, por ejemplo. Pero también tiene los atributos de cualquier persona: nombre, apellido y edad. Por lo tanto, un maestro forma parte plenamente de la clase personne, pero tiene atributos adicionales. En lugar de crear una clase enseignant desde cero, preferiríamos aprovechar lo que ya tenemos de la clase personne y adaptarla a las características específicas de los maestros. El concepto de herencia nos permite hacer esto.

Para expresar que la clase enseignant hereda las propiedades de la clase personne, escribiremos:


    public class enseignant extends personne

personne se denomina clase padre (o madre) y enseignant, clase derivada (o hija). Un objeto enseignant tiene todas las características de un objeto personne: cuenta con los mismos atributos y los mismos métodos. Estos atributos y métodos de la clase padre no se repiten en la definición de la clase hija; solo se indican los atributos y métodos añadidos por la clase hija:


class enseignant extends personne{
// atributos
  private int section;

// constructor
  public enseignant(String P, String N, int age,int section){
    super(P,N,age);
    this.section=section;
  }
}

Suponemos que la clase personne está definida de la siguiente manera:


public class personne{
  private String prenom;
  private String nom;
  private int age;
  
  public personne(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

  public personne(personne P){
    this.prenom=P.prenom;
    this.nom=P.nom;
    this.age=P.age;
  }

  public String identite(){
    return "personne("+prenom+","+nom+","+age+")";
  }

  // accesores
  public String getPrenom(){
    return prenom;
  }
  public String getNom(){
    return nom;
  }
  public int getAge(){
    return age;
  }

  //modificadores
  public void setPrenom(String P){
    this.prenom=P;
  }
  public void setNom(String N){
    this.nom=N;
  }
  public void setAge(int age){
    this.age=age;
  }
}

El método identifie se ha modificado ligeramente para generar una cadena de caracteres que identifica a la persona y ahora se llama identite. Aquí, la clase enseignant agrega a los métodos y atributos de la clase personne:

  • un atributo section, que es el número de sección a la que pertenece el profesor dentro del cuerpo docente (básicamente, una sección por disciplina)
  • un nuevo constructor que permite inicializar todos los atributos de un profesor

3.2.2. Creación de un objeto «docente»

El constructor de la clase enseignant es el siguiente:


// constructor
  public enseignant(String P, String N, int age,int section){
    super(P,N,age);
    this.section=section;
  }

La instrucción super(P,N,age) es una llamada al constructor de la clase padre, en este caso la clase personne. Sabemos que este constructor inicializa los campos nombre, apellido y age del objeto personne contenido dentro del objeto étudiant. Esto parece muy complicado y tal vez preferiríamos escribir:


// constructor
  public enseignant(String P, String N, int age,int section){
    this.prenom=P;
        this.nom=N
        this.age=age
      this.section=section;
  }

Es imposible. La clase personne ha declarado como privados (private) sus tres campos prenom, nom y age. Solo los objetos de la misma clase tienen acceso directo a estos campos. Todos los demás objetos, incluidos los objetos derivados como en este caso, deben utilizar métodos públicos para acceder a ellos. Esto habría sido diferente si la clase personne hubiera declarado como protegidos (protected) los tres campos: en ese caso, habría permitido que las clases derivadas tuvieran acceso directo a los tres campos. En nuestro ejemplo, utilizar el constructor de la clase padre era, por lo tanto, la solución correcta y es el método habitual: al construir un objeto hijo, primero se llama al constructor del objeto padre y luego se completan las inicializaciones propias del objeto hijo (section en nuestro ejemplo).

Probemos con un primer programa:


// importar persona;
// importar profesor;

public class test1{
  public static void main(String arg[]){
    System.out.println(new enseignant("Jean","Dupont",30,27).identite());
  }
}

Este programa simplemente crea un objeto enseignant (nuevo) y lo identifica. La clase enseignant no tiene un método identité, pero su clase padre sí tiene uno que, además, es público: por herencia, se convierte en un método público de la clase enseignant.

Los archivos fuente de las clases se reúnen en un mismo directorio y luego se compilan:

E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002  10:00                  765 personne.java
10/06/2002  10:00                  212 enseignant.java
10/06/2002  10:01                  192 test1.java

E:\data\serge\JAVA\BASES\OBJETS\4>javac *.java

E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002  10:00                  765 personne.java
10/06/2002  10:00                  212 enseignant.java
10/06/2002  10:01                  192 test1.java
10/06/2002  10:02                  316 enseignant.class
10/06/2002  10:02                1 146 personne.class
10/06/2002  10:02                  550 test1.class

Se ejecuta el archivo test1.class:

E:\data\serge\JAVA\BASES\OBJETS\4>java test1
personne(Jean,Dupont,30)

3.2.3. Sobrecarga de un método

En el ejemplo anterior, obtuvimos la identidad de la parte personne del profesor, pero falta cierta información específica de la clase enseignant (la sección). Por lo tanto, debemos escribir un método que permita identificar al profesor:


class enseignant extends personne{
  int section;

  public enseignant(String P, String N, int age,int section){
    super(P,N,age);
    this.section=section;
  }

  public String identite(){
    return "enseignant("+super.identite()+","+section+")";    
  }    
}

El método identite de la clase enseignant se basa en el método identite de su clase padre (super.identite) para mostrar su parte «personne» y luego la complementa con el campo section, que es propio de la clase enseignant.

La clase enseignant cuenta ahora con dos métodos identite:

  • el heredado de la clase padre personne
  • el propio

Si E es un objeto enseignant, E.identite hace referencia al método identite de la clase enseignant. Se dice que el método identite de la clase madre está «sobrecargado» por el método identite de la clase hija. En general, si O es un objeto y M es un método, para ejecutar el método O.M, el sistema busca un método M en el siguiente orden:

  • en la clase del objeto O
  • en su clase madre, si la tiene
  • en la clase madre de su clase madre, si existe
  • etc…

Por lo tanto, la herencia permite sobreescribir en la clase hija los métodos del mismo nombre que existen en la clase madre. Esto es lo que permite adaptar la clase hija a sus propias necesidades. Junto con el polimorfismo, que veremos más adelante, la sobreescritura de métodos es el principal interés de la herencia.

Consideremos el mismo ejemplo que antes:


// import persona;
// importar profesor;

public class test1{
  public static void main(String arg[]){
    System.out.println(new enseignant("Jean","Dupont",30,27).identite());
  }
}

Los resultados obtenidos en esta ocasión son los siguientes:

    enseignant(personne(Jean,Dupont,30),27)

3.2.4. El polimorfismo

Consideremos una línea de clases: C0 C1 C2 … Cn

donde Ci Cj indica que la clase Cj se deriva de la clase Ci. Esto implica que la clase Cj tiene todas las características de la clase Ci, además de otras. Sean Oi objetos de tipo Ci. Es válido escribir:

    Oi=Oj avec j>i

De hecho, por herencia, la clase Cj tiene todas las características de la clase Ci, además de otras. Por lo tanto, un objeto Oj de tipo Cj contiene en sí mismo un objeto de tipo Ci. La operación

    Oi=Oj

hace que Oi sea una referencia al objeto de tipo Ci contenido en el objeto Oj.

El hecho de queuna variable Oi de la clase Ci pueda, de hecho, hacer referencia no solo a un objeto de la clase Ci, sino a cualquier objeto derivado de la clase Ci se denomina polimorfismo: la capacidad de una variable para hacer referencia a diferentes tipos de objetos.

Veamos un ejemplo y consideremos la siguiente función, independiente de cualquier clase:

    public static void affiche(Object obj){
        ….
    }

La clase Object es la «clase madre» de todas las clases de Java. Por lo tanto, cuando escribimos:

    public class personne

se escribe implícitamente:

    public class personne extends Object

Por lo tanto, todo objeto Java contiene en su interior una parte de tipo Object. Así, se podrá escribir:

    enseignant e;
    affiche(e);

El parámetro formal de tipo Object de la función affiche recibirá un valor de tipo enseignant. Como «enseignant» deriva de Object, esto es válido.

3.2.5. Sobrecarga y polimorfismo

Completemos nuestra función affiche:

    public static void affiche(Object obj){
        System.out.println(obj.toString());
    }

El método obj.toString() devuelve una cadena de caracteres que identifica al objeto obj en el formato nom_de_la_classe@adresse_de_l'objeto. ¿Qué sucede en el caso de nuestro ejemplo anterior:

    enseignant e=new enseignant(...);
    affiche(e);

El sistema deberá ejecutar la instrucción System.out.println(e.toString()), donde e es un objeto maestro. Busca un método toString en la jerarquía de clases que conduce a la clase enseignant, comenzando por la última:

  • en la clase enseignant, no encuentra el método toString()
  • en la clase padre personne, no encuentra el método toString()
  • en la clase padre Object, encuentra el método toString() y lo ejecuta

Esto es lo que muestra el siguiente programa:


// import persona;
// importar profesor;

public class test1{
  public static void main(String arg[]){
    enseignant e=new enseignant("Lucile","Dumas",56,61);
    affiche(e);
    personne p=new personne("Jean","Dupont",30);
    affiche(p);
  }
  public static void affiche(Object obj){
    System.out.println(obj.toString());
  }
}

Los resultados obtenidos son los siguientes:

enseignant@1ee789
personne@1ee770

Es decir, el objeto nom_de_la_classe@adresse_de_l. Como esto no es muy explícito, nos sentimos tentados a definir un método toString para las clases personne y etudiant que sobreescribiría el método toString de la clase madre Object. En lugar de escribir métodos que serían similares a los métodos identite ya existentes en las clases personne y enseignant, limitémonos a renombrar estos métodos identite como toString:


public class personne{
    ...

  public String toString(){
    return "personne("+prenom+","+nom+","+age+")";
  }

    ...
}

class enseignant extends personne{
  int section;



  public String toString(){
    return "enseignant("+super.toString()+","+section+")";    
  }    
}

Con el mismo programa de prueba que antes, los resultados obtenidos son los siguientes:

enseignant(personne(Lucile,Dumas,56),61)
personne(Jean,Dupont,30)

3.3. Clases internas

Una clase puede contener la definición de otra clase. Consideremos el siguiente ejemplo:

// clases importadas
import java.io.*;

public class test1{

    // clase interna
    private class article{
         // se define la estructura
        private String code;
        private String nom;
        private double prix;
        private int stockActuel;
        private int stockMinimum;

     // constructor
    public article(String code, String nom, double prix, int stockActuel, int stockMinimum){
         // inicialización de los atributos
      this.code=code;
      this.nom=nom;
      this.prix=prix;
      this.stockActuel=stockActuel;
      this.stockMinimum=stockMinimum;
    }//constructor

    //toString
    public String toString(){
        return "article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
    }//toString
  }//clase de artículo

   // datos locales
  private article art=null;

   // fabricante
  public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
       // definición de atributo
    art=new article(code, nom, prix, stockActuel,stockMinimum);
  }//prueba1

   // accesor
  public article getArticle(){
      return art;
  }//getArticle

    public static void main(String arg[]){
      // creación de una instancia test1
      test1 t1=new test1("a100","velo",1000,10,5);
    // visualización test1.art
    System.out.println("art="+t1.getArticle());
  }//principal

}// fin de la clase

La clase test1 contiene la definición de otra clase, la clase article. Se dice que article es una clase interna de la clase test1. Esto puede ser útil cuando la clase interna solo tiene utilidad dentro de la clase que la contiene. Al compilar el código fuente test1.java anterior, se obtienen dos archivos .class:

E:\data\serge\JAVA\classes\interne>dir
05/06/2002  17:26                1 362 test1.java
05/06/2002  17:26                  941 test1$article.class
05/06/2002  17:26                1 020 test1.class

Se generó un archivo test1$article.class para la clase article, que es interna a la clase test1. Al ejecutar el programa anterior, se obtienen los siguientes resultados:

E:\data\serge\JAVA\classes\interne>java test1
art=article(a100,velo,1000.0,10,5)

3.4. Las interfaces

Una interfaz es un conjunto de prototipos de métodos o propiedades que conforman un contrato. Una clase que decide implementar una interfaz se compromete a proporcionar una implementación de todos los métodos definidos en la interfaz. Es el compilador quien verifica esta implementación.

A continuación se muestra, por ejemplo, la definición de la interfaz java.util.Enumeration:

Resumen de métodos
 
boolean
hasMoreElements()
          Comprueba si esta enumeración contiene más elementos.
 
Object
nextElement()
          Devuelve el siguiente elemento de esta enumeración si este objeto de enumeración tiene al menos un elemento más que proporcionar.
 

Cualquier clase que implemente esta interfaz se declarará como

public class C : Enumeration{
    ...
    boolean hasMoreElements(){....}
    Object nextElement(){...}
}

Los métodos hasMoreElements() y nextElement() deberán definirse en la clase C.

Veamos el siguiente código que define una clase élève, la cual almacena el nombre de un alumno y su calificación en una materia:


     // una clase de alumno
public class élève{
     // atributos públicos
    public String nom;
    public double note;
     // constructor
    public élève(String NOM, double NOTE){
        nom=NOM;
        note=NOTE;
    }//constructor
}//alumno  

Definimos una clase notes que reúne las calificaciones de todos los alumnos en una materia:


// clases importadas
// importación de alumno

// clase de calificaciones
public class notes{

     // atributos
    protected String matière;
    protected élève[] élèves;

     // constructor
    public notes (String MATIERE, élève[] ELEVES){
         // almacenamiento de alumnos y materias
        matière=MATIERE;
        élèves=ELEVES;
    }//calificaciones

    // toString
    public String toString(){
        String valeur="matière="+matière +", notes=(";
        int i;
         // se concatenan todas las calificaciones
        for (i=0;i<élèves.length-1;i++){
            valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
        };
         //última nota
        if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";}
        valeur+=")";
         // fin
        return valeur;
    }//toString
}//clase

Los atributos matière y élèves se declaran como protected para que sean accesibles desde una clase derivada. Decidimos derivar la clase notes en una clase notesStats que tendría dos atributos adicionales: el promedio y la desviación estándar de las calificaciones:


public class notesStats extends notes implements Istats {
     // atributos
    private double _moyenne;
    private double _écartType;

La clase notesStats deriva de la clase notes e implementa la siguiente interfaz Istats:


// una interfaz
public interface Istats{
    double moyenne();
    double écartType();
}//

Esto significa que la clase notesStats debe tener dos métodos llamados moyenne y écartType con la firma indicada en la interfaz Istats. La clase notesStats es la siguiente:


// clases importadas
// importar notas;
// importa Istats;
// import alumno;

public class notesStats extends notes implements Istats {
     // atributos
    private double _moyenne;
    private double _écartType;

     // constructor
    public notesStats (String MATIERE, élève[] ELEVES){
         // creación de la clase padre
        super(MATIERE,ELEVES);
         // cálculo del promedio de calificaciones
        double somme=0;
        for (int i=0;i<élèves.length;i++){
            somme+=élèves[i].note;
        }
        if(élèves.length!=0) _moyenne=somme/élèves.length;
        else _moyenne=-1;
         // desviación estándar
        double carrés=0;
        for (int i=0;i<élèves.length;i++){
            carrés+=Math.pow((élèves[i].note-_moyenne),2);
        }//para
        if(élèves.length!=0) _écartType=Math.sqrt(carrés/élèves.length);
        else _écartType=-1;
    }//constructor

    
     // ToString
    public String toString(){
        return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
    }//ToString

     // métodos de la interfaz Istats
    public double moyenne(){
         // calcula el promedio de las calificaciones
        return _moyenne;
    }//promedio
    public double écartType(){
         // devuelve la desviación estándar
        return _écartType;
    }//écartType
}//clase

El promedio _moyenne y la desviación estándar _ecartType se calculan al momento de la creación del objeto. Por lo tanto, los métodos moyenne y écartType solo tienen que devolver el valor de los atributos _moyenne y _ecartType. Ambos métodos devuelven -1 si la tabla de alumnos está vacía.

La siguiente clase de prueba:


// clases importadas
// importar alumno;
// importar Istats;
// importar calificaciones;
// importar notesStats;

// clase de prueba
public class test{
    public static void main(String[] args){
        // algunos alumnos y calificaciones
        élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new élève("jacques",18)};
         // que se guardan en un objeto de notas
        notes anglais=new notes("anglais",ELEVES);
         // y que se muestran
        System.out.println(""+anglais);
         // lo mismo con el promedio y la desviación estándar
        anglais=new notesStats("anglais",ELEVES);
        System.out.println(""+anglais);
    }//mano
}//clase

da los siguientes resultados:


matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0])
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0]),moyenne=16.0,écart-type=1.632993161855452

Todas las clases de este ejemplo se encuentran en un archivo fuente diferente:

E:\data\serge\JAVA\interfaces\notes>dir
06/06/2002  14:06                  707 notes.java
06/06/2002  14:06                  878 notes.class
06/06/2002  14:07                1 160 notesStats.java
06/06/2002  14:02                  101 Istats.java
06/06/2002  14:02                  138 Istats.class
06/06/2002  14:05                  247 élève.java
06/06/2002  14:05                  309 élève.class
06/06/2002  14:07                1 103 notesStats.class
06/06/2002  14:10                  597 test.java
06/06/2002  14:10                  931 test.class

La clase notesStats bien podría haber implementado los métodos moyenne y écartType por sí misma sin indicar que implementaba la interfaz Istats. ¿Cuál es entonces el propósito de las interfaces? Es el siguiente: una función puede aceptar como parámetro un dato del tipo de una interfaz I. Cualquier objeto de una clase C que implemente la interfaz I podrá entonces ser parámetro de esa función. Consideremos la siguiente interfaz:


// una interfaz Iejemplo
public interface Iexemple{
    int ajouter(int i,int j);
    int soustraire(int i,int j);
}//interfaz

La interfaz Iexemple define dos métodos: ajouter y soustraire. Las siguientes clases, classe1 y classe2, implementan esta interfaz.


// clases importadas
// import Iejemplo;

public class classe1 implements Iexemple{
public int ajouter(int a, int b){
        return a+b+10;
    }
    public int soustraire(int a, int b){
        return a-b+20;
    }
}//clase

// clases importadas
// importa Iejemplo;

public class classe2 implements Iexemple{
    public int ajouter(int a, int b){
                    return a+b+100;
                }
    public int soustraire(int a, int b){
        return a-b+200;
    }
}//clase

Para simplificar el ejemplo, las clases no hacen más que implementar la interfaz Iexemple. Ahora consideremos el siguiente ejemplo:


// clases importadas
// import clase1;
// import clase2;

// clase de prueba
public class test{
    // una función estática
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calcular

     // la función main
    public static void main(String[] arg){
        // creación de dos objetos clase1 y clase2
        classe1 c1=new classe1();
        classe2 c2=new classe2();
        // llamadas a la función estática «calcular»
        calculer(4,3,c1);
        calculer(14,13,c2);
    }//main
}//clase test

La función estática calculer admite como parámetro un elemento de tipo Iexemple. Por lo tanto, para este parámetro puede recibir tanto un objeto de tipo classe1 como uno de tipo classe2. Esto es lo que se hace en la función main con los siguientes resultados:

17
21
127
201

Por lo tanto, vemos que aquí tenemos una propiedad similar al polimorfismo que vimos para las clases. Así pues, si un conjunto de clases Ci que no están relacionadas entre sí mediante herencia (por lo que no se puede utilizar el polimorfismo de la herencia) presenta un conjunto de métodos con la misma firma, puede ser interesante agrupar estos métodos en una interfaz I de la que heredarían todas las clases en cuestión. Las instancias de estas clases Ci pueden utilizarse entonces como parámetros de funciones que admiten un parámetro de tipo I, c.a.d. Se trata de funciones que utilizan únicamente los métodos de los objetos Ci definidos en la interfaz I y no los atributos ni los métodos específicos de las distintas clases Ci.

En el ejemplo anterior, cada clase o interfaz era objeto de un archivo fuente separado:

E:\data\serge\JAVA\interfaces\opérations>dir
06/06/2002  14:33                  128 Iexemple.java
06/06/2002  14:34                  218 classe1.java
06/06/2002  14:32                  220 classe2.java
06/06/2002  14:33                  144 Iexemple.class
06/06/2002  14:34                  325 classe1.class
06/06/2002  14:34                  326 classe2.class
06/06/2002  14:36                  583 test.java
06/06/2002  14:36                  628 test.class

Por último, cabe señalar que la herencia de interfaces puede ser múltiple, c.a.d. Lo cual se puede escribir

public class classeDérivée extends classeDeBase implements i1,i2,..,in{
...
}

donde ij son interfaces.

3.5. Clases anónimas

En el ejemplo anterior, las clases classe1 y classe2 podrían no haberse definido explícitamente. Consideremos el siguiente programa, que hace prácticamente lo mismo que el anterior, pero sin la definición explícita de las clases classe1 y classe2:


// clases importadas
// import Iejemplo;

// clase de prueba
public class test2{

  // una clase interna
  private static class classe3 implements Iexemple{
        public int ajouter(int a, int b){
            return a+b+1000;
        }
        public int soustraire(int a, int b){
            return a-b+2000;
        }
    };//definición de la clase 3

     // una función estática
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calcular

     // la función main
    public static void main(String[] arg){
        // creación de dos objetos que implementan la interfaz Iejemplo
        Iexemple i1=new Iexemple(){
        public int ajouter(int a, int b){
            return a+b+10;
        }
        public int soustraire(int a, int b){
            return a-b+20;
        }
    };//definición de i1

        Iexemple i2=new Iexemple(){
        public int ajouter(int a, int b){
            return a+b+100;
        }
        public int soustraire(int a, int b){
            return a-b+200;
        }
    };//definición de i2
        // otro objeto Iejemplo
    Iexemple i3=new classe3();

        // llamadas a la función estática «calcular»
        calculer(4,3,i1);
        calculer(14,13,i2);
    calculer(24,23,i3);
    }//main
}//clase test

La particularidad está en el código:


         // creación de dos objetos que implementan la interfaz Iejemplo
        Iexemple i1=new Iexemple(){
        public int ajouter(int a, int b){
            return a+b+10;
        }
        public int soustraire(int a, int b){
            return a-b+20;
        }
    };//definición de i1

Se crea un objeto i1 cuya única función es implementar la interfaz Iexemple. Este objeto es de tipo Iexemple. Por lo tanto, se pueden crear objetos de tipo interfaz. Muchos métodos de clases Java devuelven objetos de tipo interfaz c.a.d, es decir, objetos cuya única función es implementar los métodos de una interfaz. Para crear el objeto i1, uno podría sentirse tentado a escribir:


        Iexemple i1=new Iexemple()

Sin embargo, una interfaz no puede ser instanciada. Solo una clase que implemente dicha interfaz puede serlo. Aquí, se define dicha clase «sobre la marcha» en el mismo cuerpo de la definición del objeto i1:


        Iexemple i1=new Iexemple(){
        public int ajouter(int a, int b){
            // definición de «añadir»
        }
        public int soustraire(int a, int b){
            // definición de restar
        }
    };//definición de i1

El significado de una instrucción de este tipo es similar al de la secuencia:

public class test2{
................
// una clase interna
private static class classe1 implements Iexemple{
        public int ajouter(int a, int b){
            // definición de sumar
        }
        public int soustraire(int a, int b){
            // definición de restar
        }
};//definición de clase1
.................
    public static void main(String[] arg){
...........
        Iexemple i1=new classe1();
}//main
}//clase

En el ejemplo anterior, se instancia una clase y no una interfaz. Una clase definida «sobre la marcha» se denomina clase anónima. Es un método que se utiliza con frecuencia para instanciar objetos cuya única función es implementar una interfaz.

La ejecución del programa anterior arroja los siguientes resultados:

17
21
127
201
1047
2001

El ejemplo anterior utilizaba clases anónimas para implementar una interfaz. Estas también pueden utilizarse para derivar clases que no tienen constructores con parámetros. Consideremos el siguiente ejemplo:

// clases importadas
// import Iejemplo;

class classe3 implements Iexemple{
    public int ajouter(int a, int b){
        return a+b+1000;
    }
    public int soustraire(int a, int b){
        return a-b+2000;
    }
};//definición de la clase 3

public class test4{

     // una función estática
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calcular

   // método main
    public static void main(String args[]){
       // definición de una clase anónima que deriva de clase3
     // para redefinir restar
    classe3 i1=new classe3(){
        public int ajouter(int a, int b){
          return a+b+10000;
      }//restar
    };//i1
          // llamadas a la función estática «calcular»
        calculer(4,3,i1);
  }//main
}//clase 

Aquí encontramos una clase classe3 que implementa la interfaz Iexemple. En la función main, definimos una variable i1 cuyo tipo es una clase derivada de classe3. Esta clase derivada se define «sobre la marcha» en una clase anónima y redefine el método ajouter de la clase classe3. La sintaxis es idéntica a la de la clase anónima que implementa una interfaz. Solo que aquí, el compilador detecta que classe3 no es una interfaz, sino una clase. Para él, se trata entonces de una derivación de clase. Todos los métodos que encuentre en el cuerpo de la clase anónima reemplazarán a los métodos del mismo nombre de la clase base.

La ejecución del programa anterior arroja los siguientes resultados:

E:\data\serge\JAVA\classes\anonyme>java test4
10007
2001

3.6. Los paquetes

3.6.1. Crear clases en un paquete

Para escribir una línea en la pantalla, utilizamos la instrucción

System.out.println(...)

Si observamos la definición de la clase System, descubrimos que en realidad se llama java.lang.System:

Image

Veamos un ejemplo:


public class test1{
    public static void main(String[] args){
        java.lang.System.out.println("Coucou");
    }//main
}//clase

Compilemos y ejecutemos este programa:

E:\data\serge\JAVA\classes\paquetages>javac test1.java

E:\data\serge\JAVA\classes\paquetages>dir
06/06/2002  15:40                  127 test1.java
06/06/2002  15:40                  410 test1.class

E:\data\serge\JAVA\classes\paquetages>java test1
Coucou

¿Por qué, entonces, podemos escribir


        System.out.println("Coucou");

en lugar de


        java.lang.System.out.println("Coucou");

Porque, de manera implícita, en todo programa Java se importa sistemáticamente el «paquete» java.lang. Así, todo ocurre como si al inicio de cada programa se tuviera la instrucción:

import java.lang.*;

¿Qué significa esta instrucción? Da acceso a todas las clases del paquete java.lang. El compilador encontrará allí el archivo System.class que define la clase System. Aún no sabemos dónde encontrará el compilador el paquete java.lang ni cómo es un paquete. Volveremos a esto más adelante. Para crear una clase en un paquete, se escribe:

package paquetage;
// definición de la clase
...

A modo de ejemplo, creemos en un paquete nuestra clase personne que analizamos anteriormente. Elegiremos istia.st como nombre del paquete. La clase personne queda así:


// nombre del paquete en el que se creará la clase persona
package istia.st;

// clase persona
public class personne{
    // nombre, apellido, edad
    private String prenom;
    private String nom;
    private int age;
  
    // constructor 1
    public personne(String P, String N, int age){
        this.prenom=P;
        this.nom=N;
        this.age=age;
    }

    // toString
    public String toString(){
        return "personne("+prenom+","+nom+","+age+")";
    }

}//clase

Esta clase se compila y luego se coloca en un directorio llamado istia\st dentro del directorio actual. ¿Por qué istia\st? Porque el paquete se llama istia.st.

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  16:04       <DIR>          istia

E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002  16:04       <DIR>          st

E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002  16:28                  675 personne.class

Ahora utilicemos la clase personne en una primera clase de prueba:


    public class test{
        public static void main(String[] args){
            istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
            System.out.println("p1="+p1);
        }//mano
    }//clase de prueba

Cabe destacar que la clase personne ahora lleva como prefijo el nombre de su paquete: istia.st. ¿Dónde encontrará el compilador la clase istia.st.personne? El compilador busca las clases que necesita en una lista predefinida de directorios y en una estructura de directorios que parte del directorio actual. En este caso, buscará la clase istia.st.personne en un archivo llamado istia\st\personne.class. Por eso hemos colocado el archivo personne.class en el directorio istia\st. Compilemos y luego ejecutemos el programa de prueba:

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  16:06                  246 test.java
06/06/2002  16:04       <DIR>          istia
06/06/2002  16:06                  738 test.class

E:\data\serge\JAVA\classes\paquetages\personne>java test
p1=personne(Jean,Dupont,20)

Para evitar escribir


            istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);

Se puede importar la clase istia.st.personne con una cláusula import:


import istia.st.personne;

Entonces podemos escribir


            personne p1=new personne("Jean","Dupont",20);

y el compilador lo traducirá como


            istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);

El programa de prueba queda entonces así:


// espacios de nombres importados
import istia.st.personne;

public class test2{
    public static void main(String[] args){
        personne p1=new personne("Jean","Dupont",20);
        System.out.println("p1="+p1);
    }//principal
}//clase test2

Compilemos y ejecutemos este nuevo programa:

E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  16:06                  246 test.java
06/06/2002  16:04       <DIR>          istia
06/06/2002  16:06                  738 test.class
06/06/2002  16:47                  236 test2.java
06/06/2002  16:50                  740 test2.class

E:\data\serge\JAVA\classes\paquetages\personne>java test2
p1=personne(Jean,Dupont,20)

Hemos colocado el paquete istia.st en el directorio actual. Esto no es obligatorio. Coloquémoslo en una carpeta llamada mesClasses, también en el directorio actual. Recordemos que las clases del paquete istia.st se encuentran en una carpeta llamada istia\st. La estructura de directorios del directorio actual es la siguiente:

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  16:06                  246 test.java
06/06/2002  16:06                  738 test.class
06/06/2002  16:47                  236 test2.java
06/06/2002  16:50                  740 test2.class
06/06/2002  16:21       <DIR>          mesClasses

E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses
06/06/2002  16:22       <DIR>          istia

E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia
06/06/2002  16:22       <DIR>          st

E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia\st
06/06/2002  16:01                1 153 personne.class

Ahora compilemos nuevamente el programa test2.java:

E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java
test2.java:2: package istia.st does not exist
import istia.st.personne;

El compilador ya no encuentra el paquete istia.st desde que lo movimos. Observemos que lo busca debido a la instrucción import. Por defecto, lo busca desde el directorio actual en una carpeta llamada istia\st que ya no existe. Analicemos las opciones del compilador:

E:\data\serge\JAVA\classes\paquetages\personne>javac
Usage: javac <options> <source files>
where possible options include:
  -g                        Generate all debugging info
  -g:none                   Generate no debugging info
  -g:{lines,vars,source}    Generate only some debugging info
  -O                        Optimize; may hinder debugging or enlarge class file
  -nowarn                   Generate no warnings
  -verbose                  Output messages about what the compiler is doing
  -deprecation              Output source locations where deprecated APIs are used
  -classpath <path>         Specify where to find user class files
  -sourcepath <path>        Specify where to find input source files
  -bootclasspath <path>     Override location of bootstrap class files
  -extdirs <dirs>           Override location of installed extensions
  -d <directory>            Specify where to place generated class files
  -encoding <encoding>      Specify character encoding used by source files
  -source <release>         Provide source compatibility with specified release
  -target <release>         Generate class files for specific VM version
  -help                     Print a synopsis of standard options

Aquí nos puede ser útil la opción -classpath. Permite indicarle al compilador dónde buscar sus clases y paquetes. Probemos. Compilemos indicándole al compilador que el paquete istia.st ahora se encuentra en la carpeta mesClasses:

E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath mesClasses test2.java

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:47                  236 test2.java
06/06/2002  17:03                  740 test2.class
06/06/2002  16:21       <DIR>          mesClasses

Esta vez, la compilación se realiza sin problemas. Ejecutemos el programa test2.class:

E:\data\serge\JAVA\classes\paquetages\personne>java test2
Exception in thread "main" java.lang.NoClassDefFoundError: istia/st/personne
        at test2.main(test2.java:6)

Ahora es el turno de la máquina virtual de Java, que no encuentra la clase istia/st/personne. La busca en el directorio actual, cuando en realidad ahora se encuentra en el directorio mesClasses. Veamos las opciones de la máquina virtual de Java:

E:\data\serge\JAVA\classes\paquetages\personne>java
Usage: java [-options] class [args...]
           (to execute a class)
   or  java -jar [-options] jarfile [args...]
           (to execute a jar file)

where options include:
    -client       to select the "client" VM
    -server       to select the "server" VM
    -hotspot      is a synonym for the "client" VM  [deprecated]
                  The default VM is client.

    -cp -classpath <directories and zip/jar files separated by ;>
                  set search path for application classes and resources
    -D<name>=<value>
                  set a system property
    -verbose[:class|gc|jni]
                  enable verbose output
    -version      print product version and exit
    -showversion  print product version and continue
    -? -help      print this help message
    -X            print help on non-standard options
    -ea[:<packagename>...|:<classname>]
    -enableassertions[:<packagename>...|:<classname>]
                  enable assertions
    -da[:<packagename>...|:<classname>]
    -disableassertions[:<packagename>...|:<classname>]
                  disable assertions
    -esa | -enablesystemassertions
                  enable system assertions
    -dsa | -disablesystemassertions
                  disable system assertions

Se observa que JVM también cuenta con una opción classpath, al igual que el compilador. Utilicémosla para indicarle dónde se encuentra el paquete istia.st:

E:\data\serge\JAVA\classes\paquetages\personne>java.bat -classpath mesClasses test2
Exception in thread "main" java.lang.NoClassDefFoundError: test2

No hemos avanzado mucho. Ahora es la propia clase test2 la que no se encuentra. La razón es la siguiente: al no estar presente la palabra clave classpath, el directorio actual se revisa sistemáticamente al buscar clases, pero no cuando sí está presente. Por lo tanto, no se encuentra la clase test2.class, que se encuentra en el directorio actual. ¿La solución? Agregar el directorio actual a classpath. El directorio actual se representa con el símbolo .

E:\data\serge\JAVA\classes\paquetages\personne>java -classpath mesClasses;. test2
p1=personne(Jean,Dupont,20)

¿Por qué tantas complicaciones? El objetivo de los paquetes es evitar conflictos de nombres entre clases. Consideremos dos empresas, E1 y E2, que distribuyen clases empaquetadas, respectivamente, en los paquetes com.e1 y com.e2. Supongamos que un cliente C compra estos dos conjuntos de clases, en los que ambas empresas han definido una clase llamada personne. El cliente C hará referencia a la clase personne de laempresa E1 como com.e1.personne y la de la empresa E2 como com.e2.personne, evitando así un conflicto de nombres.

3.6.2. Búsqueda de paquetes

Cuando escribimos en un programa

import java.util.*;

para tener acceso a todas las clases del paquete java.util, ¿dónde se encuentra este? Ya mencionamos que, por defecto, los paquetes se buscan en el directorio actual o en la lista de directorios declarados en la opción classpath del compilador o en la JVM, si esta opción está presente. También se buscan en los directorios lib del directorio de instalación de JDK. Consideremos este directorio:

Image

En este ejemplo, se explorarán los árboles de directorios jdk14\lib y jdk14\jre\lib para buscar archivos .class, .jar o .zip que sean archivos comprimidos de clases. Por ejemplo, realicemos una búsqueda de los archivos .jar que se encuentran en el directorio anterior jdk14:

Image

Hay varias decenas. Un archivo .jar se puede abrir con la utilidad winzip. Abramos el archivo rt.jar mencionado anteriormente (rt=RunTime). En él se encuentran varios cientos de archivos .class, entre ellos los que pertenecen al paquete java.util:

Image

Un método sencillo para administrar los paquetes es colocarlos en el directorio <jdk>\jre\lib, donde <jdk> es el directorio de instalación de JDK. Por lo general, un paquete contiene varias clases y resulta práctico agruparlas en un único archivo .jar (JAR = archivo Java ARchive). El ejecutable jar.exe se encuentra en la carpeta <jdk>\bin:

E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\bin\jar.exe"
07/02/2002  12:52               28 752 jar.exe

Para obtener ayuda sobre el uso del programa jar, ejecútelo sin parámetros:

E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar.exe"
Syntaxe : jar {ctxu}[vfm0M] [fichier-jar] [fichier-manifest] [rÚp -C] fichiers ...
Options :
    -c  crÚer un nouveau fichier d''archives
    -t  gÚnÚrer la table des matiÞres du fichier d''archives
    -x  extraire les fichiers nommÚs (ou tous les fichiers) du fichier d''archives
    -u  mettre Ó jour le fichier d''archives existant
    -v  gÚnÚrer des informations verbeuses sur la sortie standard
    -f  spÚcifier le nom du fichier d''archives
    -m  inclure les informations manifest provenant du fichier manifest spÚcifiÚ
    -0  stocker seulement ; ne pas utiliser la compression ZIP
    -M  ne pas crÚer de fichier manifest pour les entrÚes
    -i  gÚnÚrer l''index pour les fichiers jar spÚcifiÚs
    -C  passer au rÚpertoire spÚcifiÚ et inclure le fichier suivant
Si un rÚpertoire est spÚcifiÚ, il est traitÚ rÚcursivement.
Les noms des fichiers manifest et d''archives doivent Ûtre spÚcifiÚs
dans l''ordre des indicateurs ''m'' et ''f''.

Exemple 1 : pour archiver deux fichiers de classe dans le fichier d''archives classes.jar :
       jar cvf classes.jar Foo.class Bar.class
Exemple 2 : utilisez le fichier manifest existant ''monmanifest'' pour archiver tous les fichiers du

           rÚpertoire foo/ dans ''classes.jar'':
       jar cvfm classes.jar monmanifest -C foo/ .

Volvamos a la clase personne.class creada anteriormente en un paquete istia.st:

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  17:36                  195 test.java
06/06/2002  16:04       <DIR>          istia
06/06/2002  16:06                  738 test.class
06/06/2002  16:47                  236 test2.java
06/06/2002  18:15                  740 test2.class

E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002  16:04       <DIR>          st

E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002  16:28                  675 personne.class

Creemos un archivo istia.st.jar que contenga todas las clases del paquete istia.st, es decir, todas las clases del árbol istia\st anterior:

E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar" cvf istia.st.jar istia\st\*

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  17:36                  195 test.java
06/06/2002  16:04       <DIR>          istia
06/06/2002  16:06                  738 test.class
06/06/2002  16:47                  236 test2.java
06/06/2002  18:15                  740 test2.class
06/06/2002  18:08                  874 istia.st.jar

Veamos con winzip el contenido del archivo istia.st.jar:

Image

Colocamos el archivo istia.st.jar en el directorio <jdk>\jre\lib\perso:

E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\jre\lib\perso"
06/06/2002  18:08                  874 istia.st.jar

Ahora compilemos el programa test2.java y luego ejecutémoslo:

E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath istia.st.jar test2.java

E:\data\serge\JAVA\classes\paquetages\personne>java -classpath istia.st.jar;. test2
p1=personne(Jean,Dupont,20)

Notamos que solo tuvimos que indicar el nombre del archivo a explorar sin tener que especificar explícitamente dónde se encontraba. Se exploran todos los directorios del árbol <jdk>\jre\lib para encontrar el archivo .jar solicitado.

3.7. El ejemplo IMPÔTS

Retomamos el cálculo del impuesto ya analizado en el capítulo anterior y lo procesamos utilizando una clase. Recordemos el problema:

Consideramos el caso simplificado de un contribuyente que solo tiene que declarar su salario:

  • se calcula el número de partes del empleado nbParts = nbEnfants/2 + 1 si no está casado, nbEnfants/2 + 2 si está casado, donde nbEnfants es su número de hijos.
  • si tiene al menos tres hijos, cuenta con media parte adicional
  • se calcula su ingreso imponible R = 0,72 * S, donde S es su salario anual
  • se calcula su coeficiente familiar QF = R / nbParts
  • Se calcula su impuesto I. Consideremos la siguiente tabla:
12620.0
0
0
13 190
0,05
631
15640
0,1
1290,5
24 740
0,15
2072,5
31 810
0,2
3309,5
39 970
0,25
4900
48 360
0,3
6898,5
55 790
0,35
9316,5
92 970
0,4
12106
127 860
0,45
16 754,5
151 250
0,50
23 147,5
172 040
0,55
30 710
195 000
0,60
39312
0
0,65
49 062

Cada línea tiene 3 campos. Para calcular el impuesto I, se busca la primera línea en la que QF sea menor o igual que el campo 1. Por ejemplo, si QF = 23000, se encontrará la línea

    24740        0.15        2072.5

El impuesto I es entonces igual a 0,15*R - 2072,5*nbParts. Si QF es tal que la relación QF <= campo1 nunca se cumple, entonces se utilizan los coeficientes de la última línea. En este caso:

    0                0.65        49062

lo que da como resultado el impuesto I = 0,65 * R - 49062 * nbParts.

La clase «impots» se definirá de la siguiente manera:


// creación de una clase de impuestos

public class impots{

    // los datos necesarios para el cálculo del impuesto
     // provienen de una fuente externa

    private double[] limites, coeffR, coeffN;

     // fabricante
    public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
         // se verifica que las 3 tablas tengan el mismo tamaño
        boolean OK=LIMITES.length==COEFFR.length && LIMITES.length==COEFFN.length;
        if (! OK) throw new Exception ("Les 3 tableaux fournis n'ont pas la même taille("+
                                LIMITES.length+","+COEFFR.length+","+COEFFN.length+")");
        // Todo está bien
        this.limites=LIMITES;
        this.coeffR=COEFFR;
        this.coeffN=COEFFN;
    }//fabricante

     // cálculo del impuesto
    public long calculer(boolean marié, int nbEnfants, int salaire){
         // cálculo del número de participaciones
        double nbParts;
        if (marié) nbParts=(double)nbEnfants/2+2;
        else nbParts=(double)nbEnfants/2+1;
        if (nbEnfants>=3) nbParts+=0.5;
         // cálculo de la renta imponible y el coeficiente familiar
        double revenu=0.72*salaire;
        double QF=revenu/nbParts;
         // cálculo del impuesto
        limites[limites.length-1]=QF+1;
        int i=0;
        while(QF>limites[i]) i++;
        // resultado
        return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
    }//calcular
}//Clase

Se crea un objeto «impuestos» con los datos necesarios para calcular el impuesto de un contribuyente. Esta es la parte estable del objeto. Una vez creado este objeto, se puede invocar repetidamente su método «calcular», que calcula el impuesto del contribuyente a partir de su estado civil (casado o soltero), el número de hijos y su salario anual.

Un programa de prueba podría ser el siguiente:


//Clases importadas
// importación de impuestos;
import java.io.*;

    public class test
    {
        public static void main(String[] arg) throws IOException
        {
             // programa interactivo de cálculo de impuestos
             // el usuario ingresa tres datos con el teclado: casado nbEnfants salario
             // el programa muestra entonces el impuesto a pagar

            final String syntaxe="syntaxe : marié nbEnfants salaire\n"
                            +"marié : o pour marié, n pour non marié\n"
                            +"nbEnfants : nombre d'enfants\n"
                            +"salaire : salaire annuel en F";

             // tablas de datos necesarias para el cálculo del impuesto
            double[] limites=new double[] {12620,13190,15640,24740,31810,39970,48360,55790,92970,127860,151250,172040,195000,0};
            double[] coeffR=new double[] {0,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.55,0.6,0.65};
            double[] coeffN=new double[] {0,631,1290.5,2072.5,3309.5,4900,6898.5,9316.5,12106,16754.5,23147.5,30710,39312,49062};

       // creación de un flujo de lectura
      BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
            // creación de un objeto de impuestos
            impots objImpôt=null;
            try{
                objImpôt=new impots(limites,coeffR,coeffN);
            }catch (Exception ex){
                System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
                System.exit(1);
            }//try-catch

            // bucle infinito
            while(true){
                 // se solicitan los parámetros para el cálculo del impuesto
                System.out.print("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :");
                String paramètres=IN.readLine().trim();
                // ¿Hay algo que hacer?
                if(paramètres==null || paramètres.equals("")) break;
                // verificación del número de argumentos en la línea ingresada
                String[] args=paramètres.split("\\s+");
                int nbParamètres=args.length;
                if (nbParamètres!=3){
                    System.err.println(syntaxe);
                    continue;
                }//si
                 // verificación de la validez de los parámetros
                // casado
                String marié=args[0].toLowerCase();
                if (! marié.equals("o") && ! marié.equals("n")){
                    System.err.println(syntaxe+"\nArgument marié incorrect : tapez o ou n");
                    continue;
                }//si
                 // nbEnfants
                int nbEnfants=0;
                try{
                    nbEnfants=Integer.parseInt(args[1]);
                    if(nbEnfants<0) throw new Exception();
                }catch (Exception ex){
                    System.err.println(syntaxe+"\nArgument nbEnfants incorrect : tapez un entier positif ou nul");
                    continue;
                }//si
                 // salario
                int salaire=0;
                try{
                    salaire=Integer.parseInt(args[2]);
                    if(salaire<0) throw new Exception();
                }catch (Exception ex){
                    System.err.println(syntaxe+"\nArgument salaire incorrect : tapez un entier positif ou nul");
                    continue;
                }//si
                 // los parámetros son correctos: se calcula el impuesto
                System.out.println("impôt="+objImpôt.calculer(marié.equals("o"),nbEnfants,salaire)+" F");
                 // siguiente contribuyente
            }//mientras
        }//main
    }//clase

A continuación se muestra un ejemplo de ejecución del programa anterior:

E:\data\serge\MSNET\c#\impuestos\3>prueba en Java

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument marié incorrect : tapez o ou n

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument nbEnfants incorrect : tapez un entier positif ou nul

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument salaire incorrect : tapez un entier positif ou nul

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d f
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :