3. Clases e interfaces
3.1. El objeto a través de ejemplos
3.1.1. Aspectos 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 (denominados atributos o propiedades) y funciones (denominadas métodos). Un objeto se crea según un modelo denominado 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 anterior C1, se pueden crear numerosos objetos O1, O2,… Todos ellos 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 designa la propiedad p1 de O1 y O1.m1 designa el 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. Una clase es, por tanto, 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, esté o no definida 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).
Por lo 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 initialize
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
son ilegales. Tenemos que inicializar un objeto de tipo personne mediante un método público. Esa es la función del método initialise. Escribiremos:
La sintaxis p1.initialise es válida, ya que initialise es de acceso público.
3.1.4. El operador «new»
La secuencia de instrucciones
es incorrecta. La instrucción
declara 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 se escribiera:
donde se indica explícitamente, mediante la palabra clave null, que la variable p1 aún no hace referencia a ningún objeto.
Cuando a continuación se escribe
se invoca el método initialise del objeto al que hace referencia p1. Sin embargo, este objeto aún no existe y el compilador señalará el error. Para que p1 haga referencia a un objeto, hay que escribir:
Esto tiene como efecto la creación de un objeto de tipo personne aún sin inicializar: 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, se produce una inicialización por defecto. Ahora que p1 hace referencia a un objeto, la instrucción de inicialización de dicho objeto
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 designa el 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:
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 se podría haber 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 dicho objeto, la escritura this.A es implícita. Debe utilizarse de forma explícita cuando exista 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
Resulta sorprendente que el programa test1.java no importe la clase personne con una instrucción:
Cuando el compilador encuentra en el código fuente una referencia a una clase no 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 ha iniciado el compilador
En nuestro ejemplo, el compilador se ha ejecutado 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 sin olvidar que hay que importar la clase «persona», en el futuro escribiremos al principio del programa:
Ahora podemos ejecutar el archivo test1.class:
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 pasa a llamarse test2 para tener en cuenta el cambio de 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 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:
A partir de ahora, 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
Sigamos considerando la clase personne y añadá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 que admitan parámetros diferentes. Este 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 sigue siendo cierto: 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:
3.1.8. Constructores de la clase «persona»
Un constructor es un método que lleva el nombre de la clase y al que se recurre al crear el objeto. Se suele utilizar para inicializarlo. Es un método que puede aceptar argumentos, pero que no devuelve ningún resultado. Ni su prototipo ni su definición van precedidos de ningún tipo (ni siquiera void).
Si una clase tiene un constructor que admite n argumentos argi, la declaración y la inicialización de un objeto de dicha clase se pueden realizar de la siguiente forma:
classe objet =new classe(arg1,arg2, ... argn);
o
classe objet;
…
objet=new classe(arg1,arg2, ... argn);
Cuando una clase tiene uno o varios constructores, es obligatorio utilizar uno de ellos para crear un objeto de dicha clase. Si una clase C no tiene ningún constructor, dispone de 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 ocurría cuando, en los programas anteriores, escribíamos:
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á creando.
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:
3.1.9. Las referencias de los objetos
Seguimos utilizando 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 de 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
p1 hace referencia al objeto personne("Jean","Dupont",30), pero no es el objeto en sí. En C, se diría que es un puntero, c.a.d, a la dirección del objeto creado. Si a continuación escribimos:
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 no hay ninguna otra variable que haga referencia a él.
Cuando se escribe:
se inicializa el puntero p2: «apunta» al mismo objeto (designa al mismo objeto) que el puntero p1. Así pues, si se modifica el objeto «apuntado» (o referenciado) por p1, se modifica el que está referenciado por p2.
Cuando se escribe:
se crea un nuevo objeto, que es una copia del objeto al que hace referencia p1. Este nuevo objeto será referenciado por p3. Si se modifica el objeto «apuntado» (o al que hace referencia) por p1, no se modifica en absoluto el objeto al que hace referencia p3. Así lo demuestran 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 (para modificarlo, por ejemplo). Este objeto temporal se crea para evaluar la expresión y, a continuación, se descarta. El espacio de memoria que ocupaba será recuperado automáticamente más adelante por un programa denominado «reciclador 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.
Veamos el siguiente ejemplo:
// import persona;
public class test1{
public static void main(String arg[]){
new personne(new personne("Jean","Dupont",30)).identifie();
}
}
y modificamos 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:
lo que muestra la construcción sucesiva de los dos objetos temporales.
3.1.11. Métodos de lectura y escritura de los atributos privados
Añadimos 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:
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 gestionar nosotros mismos un contador, 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 a 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 propia clase y no de un objeto concreto de dicha clase, se declara de forma diferente con la palabra clave static:
Para hacer referencia a él, se escribe personne.nbPersonnes para indicar que es un atributo de la propia clase personne. En este caso, hemos creado un atributo privado al que no se podrá acceder directamente 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 concreto: de hecho, nbPersonnes no es el atributo de un objeto concreto, sino el atributo de toda una clase. Por lo tanto, necesitamos un método de clase declarado también como static:
que, desde el exterior, se invocará con la sintaxis personne.getNbPersonnes(). A continuación se muestra 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:
3.1.13. Pasar un objeto a una función
Ya hemos dicho 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 da 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 dicho 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 ningún objeto nuevo.
Si se pasa una referencia de objeto R1 a una función, se copiará en el parámetro formal correspondiente R2. Por lo tanto, las referencias R2 y R1 hacen referencia al mismo objeto. Si la función modifica el objeto al que apunta R2, evidentemente modifica también el objeto al que hace referencia R1, ya que se trata del mismo.

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 dicho objeto ha sido 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, en Java no es posible escribir 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 tiene 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:
3.1.15. Una tabla de personas
Un objeto es un dato como cualquier otro y, como tal, 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, por lo que no hacen referencia a ningún objeto. Una vez más, por un uso incorrecto del lenguaje, se habla de «matriz de objetos», cuando en realidad se trata únicamente de una matriz de referencias a objetos. La creación de la matriz de objetos —matriz que es en sí misma un objeto (presencia de new)— no crea, por lo tanto, por sí misma ningún objeto del tipo de sus elementos: hay que hacerlo a continuación.
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 del ejemplo
3.2.1. Aspectos generales
Aquí abordamos el concepto de herencia. El objetivo de la herencia es «personalizar» una clase existente para que se adapte a nuestras necesidades. Supongamos que queremos crear una clase enseignant: un profesor es una persona concreta. Tiene atributos que otra persona no tendrá: la asignatura que imparte, por ejemplo. Pero también tiene los atributos propios de cualquier persona: nombre, apellidos y edad. Por lo tanto, un profesor forma parte plenamente de la clase personne, pero tiene atributos adicionales. En lugar de crear una clase enseignant partiendo de cero, preferiríamos aprovechar lo ya establecido en la clase personne y adaptarlo al carácter particular de los profesores. El concepto de herencia es lo que nos permite hacerlo.
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: basta con indicar 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 denomina identite. En este caso, la clase enseignant añade 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 asignatura)
- un nuevo constructor que permite inicializar todos los atributos de un profesor
3.2.2. Creación de un objeto «profesor»
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 «prénom», «nom» y «age» del objeto «personne» contenido dentro del objeto «étudiant». Esto parece bastante complicado y quizá sea preferible 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 recurrir a métodos públicos para acceder a ellos. La situación habría sido diferente si la clase personne hubiera declarado protegidos (protected) los tres campos: en ese caso, habría permitido a las clases derivadas tener acceso directo a los tres campos. En nuestro ejemplo, utilizar el constructor de la clase padre era, por tanto, la solución correcta y es el método habitual: al crear un objeto hijo, primero se llama al constructor del objeto padre y, a continuación, se completan las inicializaciones propias del objeto hijo (section en nuestro ejemplo).
Probemos con un primer programa:
// importar persona;
// import profesor;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
Este programa se limita a crear un objeto enseignant (nuevo) y a identificarlo. La clase enseignant no tiene ningún 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 agrupan en un mismo directorio y, a continuación, 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:
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, a continuación, se completa con el campo section, propio de la clase enseignant.
La clase enseignant dispone ahora de 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 padre está «sobrecargado» por el método identite de la clase hija. En general, si O es un objeto y M 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 padre, si la tiene
- en la clase padre de su clase padre, si existe
- etc…
La herencia permite, por tanto, sobrecargar 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 sobrecarga 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:
3.2.4. El polimorfismo
Consideremos una línea de clases: C0 C1 C2 … Cn
donde Ci Cj indica que la clase Cj 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:
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
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:
La clase Object es la «clase madre» de todas las clases de Java. Por lo tanto, cuando escribimos:
se escribe implícitamente:
Por lo tanto, todo objeto Java contiene en su interior una parte de tipo Object. Así, se podrá escribir:
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:
El método obj.toString() devuelve una cadena de caracteres que identifica el objeto obj con el formato nom_de_la_classe@adresse_de_l'objeto. ¿Qué ocurre en el caso de nuestro ejemplo anterior:
El sistema deberá ejecutar la instrucción System.out.println(e.toString()), donde e es un objeto profesor. Busca un método toString en la jerarquía de clases que conduce a la clase enseignant, empezando 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:
Es decir, el objeto «nom_de_la_classe@adresse_de_l». Como no resulta muy explícito, nos sentimos tentados a definir un método toString para las clases personne y etudiant que sobrecargaría el método toString de la clase padre 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 toString estos métodos identite:
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 pruebas que antes, los resultados obtenidos son los siguientes:
3.3. Clases internas
Una clase puede contener la definición de otra clase. Veamos 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 resultar ú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 ha generado un archivo test1$article.class para la clase article, interna a la clase test1. Si se ejecuta el programa anterior, se obtienen los siguientes resultados:
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 el que verifica dicha 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
Los métodos hasMoreElements() y nextElement() deberán definirse en la clase C.
Veamos el siguiente código, que define una clase élève en la que se especifica el nombre de un alumno y su nota en una asignatura:
// una clase «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 agrupa las notas de todos los alumnos en una asignatura:
// clases importadas
// importación de alumno
// clase de notas
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 asignaturas
matière=MATIERE;
élèves=ELEVES;
}//notas
// toString
public String toString(){
String valeur="matière="+matière +", notes=(";
int i;
// se concatenan todas las notas
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: la media y la desviación estándar de las notas:
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 denominados moyenne y écartType con la firma indicada en la interfaz Istats. La clase notesStats es la siguiente:
// clases importadas
// importar notas;
// importar 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 de la media de las notas
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 la media de las puntuaciones
return _moyenne;
}//media
public double écartType(){
// devuelve la desviación estándar
return _écartType;
}//écartType
}//clase
La media _moyenne y la desviación típica _ecartType se calculan en el 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;
// importación de Istats;
// importar notas;
// importar notesStats;
// clase de prueba
public class test{
public static void main(String[] args){
// algunos alumnos y notas
é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 «notas»
notes anglais=new notes("anglais",ELEVES);
// y que se muestran
System.out.println(""+anglais);
// lo mismo con la media y la desviación típica
anglais=new notesStats("anglais",ELEVES);
System.out.println(""+anglais);
}//mano
}//clase
da como resultado:
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
Las diferentes clases de este ejemplo se encuentran todas en un archivo fuente distinto:
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 podría perfectamente haber implementado los métodos moyenne y écartType por sí misma sin indicar que implementaba la interfaz Istats. ¿Cuál es, pues, la utilidad de las interfaces? Es la siguiente: una función puede admitir 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 dicha 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
// import 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 nada más que implementar la interfaz Iexemple. Veamos ahora el siguiente ejemplo:
// clases importadas
// import clase1;
// importar 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, podrá recibir para este parámetro 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:
Se observa, por tanto, que se trata de una propiedad similar al polimorfismo visto en las clases. Así pues, si un conjunto de clases Ci no relacionadas entre sí por herencia (por lo que no se puede utilizar el polimorfismo de la herencia) presenta un conjunto de métodos con la misma firma, puede resultar 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, es decir, funciones que solo utilizan los métodos de los objetos Ci definidos en la interfaz I y no los atributos ni los métodos específicos de las diferentes clases Ci.
En el ejemplo anterior, cada clase o interfaz era objeto de un archivo fuente independiente:
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 que se puede escribir como
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. Numerosos 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, podríamos sentir la tentación de escribir:
Iexemple i1=new Iexemple()
Sin embargo, una interfaz no se puede instanciar. Solo se puede instanciar una clase que implemente dicha interfaz. En este caso, se define dicha clase «sobre la marcha» en el propio 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 análogo 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 a menudo para instanciar objetos cuya única función es implementar una interfaz.
La ejecución del programa anterior da los siguientes resultados:
El ejemplo anterior utilizaba clases anónimas para implementar una interfaz. Estas también pueden utilizarse para derivar clases que no tengan constructores con parámetros. Veamos 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 derivada 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
En ella 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, en este caso, el compilador detecta que classe3 no es una interfaz, sino una clase. Por lo tanto, para él se trata de una derivación de clase. Todos los métodos que encuentre en el cuerpo de la clase anónima sustituirán a los métodos del mismo nombre de la clase base.
La ejecución del programa anterior da los siguientes resultados:
3.6. Los paquetes
3.6.1. Crear clases en un paquete
Para escribir una línea en pantalla, utilizamos la instrucción
Si observamos la definición de la clase System, descubrimos que, en realidad, se llama java.lang.System:

Comprobémoslo con 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 forma implícita, en todo programa Java se importa sistemáticamente el «paquete» java.lang. Así pues, es como si al principio de cada programa hubiera la instrucción:
¿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 sobre ello más adelante. Para crear una clase en un paquete, se escribe:
A modo de ejemplo, creemos en un paquete nuestra clase personne, que hemos estudiado 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, apellidos, 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, a continuación, se coloca en un directorio istia\st 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 istia\st\personne.class. Por eso hemos colocado el archivo personne.class en el directorio istia\st. Compilemos y 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. 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 volvamos a compilar 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 hemos movido. 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 resultar útil la opción -classpath. Permite indicar al compilador dónde buscar sus clases y paquetes. Probémoslo. Compilemos indicando al compilador que el paquete istia.st se encuentra ahora 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 puede observar que JVM también tiene 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. El motivo es el siguiente: al no estar presente la palabra clave classpath, el directorio actual se explora sistemáticamente durante la búsqueda de clases, pero no cuando está presente. Por lo tanto, no se encuentra la clase test2.class, que se encuentra en el directorio actual. ¿La solución? Añadir 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 adquiere estos dos conjuntos de clases, en los que ambas empresas han definido una clase denominada 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
para acceder a todas las clases del paquete java.util, ¿dónde se encuentra este? Ya hemos dicho 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:

En este ejemplo, se explorarán los árboles de directorios jdk14\lib y jdk14\jre\lib para buscar en ellos 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:

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

Un método sencillo para gestionar los paquetes consiste, por tanto, en 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útalo 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:

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)
Observamos que solo hemos tenido que indicar el nombre del archivo que se va 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 estudiado en el capítulo anterior y lo procesamos utilizando una clase. Recordemos el problema:
Nos situamos en el caso simplificado de un contribuyente que solo tiene que declarar su salario:
- se calcula el número de participaciones del empleado nbParts = nbEnfants/2 + 1 si no está casado, nbEnfants/2 + 2 si está casado, donde nbEnfants es el número de hijos que tiene.
- si tiene al menos tres hijos, tiene media parte más
- se calcula su base 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:
12 620,0 | 0 | 0 |
13 190 | 0,05 | 631 |
15 640 | 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 | 12 106 |
127 860 | 0,45 | 16 754,5 |
151 250 | 0,50 | 23 147,5 |
172 040 | 0,55 | 30 710 |
195 000 | 0,60 | 39 312 |
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
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:
lo que da como resultado el impuesto I = 0,65 * R - 49 062 * nbParts.
La clase «impots» se definirá de la siguiente manera:
// creación de una clase «impuestos»
public class impots{
// 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 comprueba que las tres 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 correcto
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 base imponible y del 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++;
// Devolución del resultado
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//calcular
}//Clase
Se crea un objeto «impots» con los datos necesarios para calcular los impuestos de un contribuyente. Esta es la parte estable del objeto. Una vez creado este objeto, se puede llamar repetidamente a su método «calcular», que calcula los impuestos 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 introduce tres datos mediante el teclado: casado nbEnfants salario
// a continuación, el programa muestra 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 de los impuestos
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 que hacer algo?
if(paramètres==null || paramètres.equals("")) break;
// comprobación del número de argumentos en la línea introducida
String[] args=paramètres.split("\\s+");
int nbParamètres=args.length;
if (nbParamètres!=3){
System.err.println(syntaxe);
continue;
}//if
// Comprobació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;
}//if
// 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
}//principal
}//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 :