La clase Fraccion

prev.gif (997 bytes)chapter.gif (1105 bytes)home.gif (1054 bytes)next.gif (998 bytes)

Clases y objetos

Los miembros dato

Las funciones miembro

La clase Fraccion

Uso de la clase Fraccion

Modificadores de acceso

Mejora de la clase Lista


En esta página vamos a definr una clase denominada Fraccion con dos miembros dato: el numerador y el denominador, y varias funciones miembro que realizan las operaciones entre fracciones. El lenguaje Java no tiene la característica de la sobrecarga de operadores como el lenguaje C++. En este lenguaje es posible sobrecargar los operadores aritméticos, como funciones miembro o como funciones amigas (friend) para que se realicen las operaciones entre entidades definidas por el usuario tal como las pensamos o las escribimos en un papel. Por ejemplo, si a y b son dos fracciones (objetos de la clase Fraccion) podemos escribir

c=a+b;

para obtener la fracción c resultado de la suma de a y b.

Definiremos las operaciones en Java de un modo similar al lenguaje C, pero como en Java no existen funciones que no sean miembros de una clase, definiremos las operaciones como funciones estáticas de una clase que denominamos Fraccion.

disco.gif (1035 bytes)fraccion1: Fraccion.java, FraccionApp1.java

 

Los miembros dato

Consideremos la clase que describe una fracción que denominaremos Fraccion. Consta de dos miembros ambos enteros, el numerador num, y del denominador den.

public class Fraccion {
     int num;
     int den;
//...
}

 

Las funciones miembro

Además de los constructores definiremos varias funciones miembro que codifican las operaciones que se realizan con fracciones: suma de dos fracciones, diferencia de dos fracciones, producto, cociente, fracción inversa de una dada, y simplificar dos fracciones. Finalmente, redefiniremos la función toString para obtener una representación en forma de texto de una fracción.

Los constructores

Definiremos dos constructores, el constructor por defecto, que da al numerdor el valor cero, y al denominador el valor uno, y el constructor explícito.

  public Fraccion() {
     num=0;
     den=1;
  }
  public Fraccion(int x, int y) {
     num=x;
     den=y;
  }

Suma de dos fracciones

Se tratará de definir una función denominada sumar, que realice las operación de sumar dos fracciones. Por tanto, la función sumar tendrá dos parámetros que son dos fracciones a y b, y devolverá una fracción, su declaración será

  	Fraccion sumar(Fraccion a, Fraccion b){
		//...
	}

Para codificar la función plantearemos el procedimiento de sumar dos fracciones a y b, cuyos numeradores son a.num y b.num, y cuyos denominadores son a.den y b.den, respectivamente. El resultado se guarda en la fracción c. El numerador c.num y el denominador c.den se obtienen del siguiente modo:

La suma de dos fracciones es otra fracción c que tiene por numerador c.num.

     c.num=a.num*b.den+b.num*a.den;

y por denominador c.den

     c.den=a.den*b.den;

Una vez efectuada la suma, la función sumar devuelve la fracción c

     return c;

El código completo de la función sumar es

 Fraccion sumar(Fraccion a, Fraccion b){
     Fraccion c=new Fraccion();
     c.num=a.num*b.den+b.num*a.den;
     c.den=a.den*b.den;
     return c;
  }

Diferencia de dos fracciones

La función restar es semejante a la función sumar y no requiere más explicación.

  Fraccion restar(Fraccion a, Fraccion b){
     Fraccion c=new Fraccion();
     c.num=a.num*b.den-b.num*a.den;
     c.den=a.den*b.den;
     return c;
  }

Producto de dos fracciones

Cuando se multiplican dos fracciones a y b, se obtiene otra fracción c cuyo numerador es el producto de los numeradores, y cuyo denominador es el producto de sus denominadores respectivos.

  Fraccion multiplicar(Fraccion a, Fraccion b){
     Fraccion c=new Fraccion();
     c.num=a.num*b.num;
     c.den=a.den*b.den;
     return c;
  }

Podemos ahorrarnos la fracción temporal c, y escribir

  Fraccion multiplicar(Fraccion a, Fraccion b){
     return new Fraccion(a.num*b.num, a.den*b.den);
  }

Inversa de una fracción

La función inversa, recibe una fracción en su único argumento y devuelve una fracción cuyo numerador es el denominador de la fracción argumento, y cuyo denominador es el numerador de dicha fracción.

  public static Fraccion inversa(Fraccion a){
     return new Fraccion(a.den, a.num);
  }

Cociente de dos fracciones

Cuando se dividen dos fracciones a y b, se obtiene otra fracción c cuyo numerador es el producto del numerador de la primera por del denominador de la segunda, y cuyo denominador es el producto del denominador de la primera por el numerador de la segunda.

  Fraccion dividir(Fraccion a, Fraccion b){
     return new Fraccion(a.num*b.den, a.den*b.num);
  }

La operación división de dos fracciones es equivalente a multiplicar la fracción a por la inversa de b, de este modo aprovechamos el código de la función inversa.

 Fraccion dividir(Fraccion a, Fraccion b){
     return multiplicar(a, inversa(b));
  }

Simplificar una fracción

Para simplificar una fracción primero hay que hallar el máximo común divisor del numerador y del denominador. la función mcd se encarga de esta tarea. Para ello emplea el algoritmo de Euclides, cuyo funcionamiento se muestra en el siguiente ejemplo. Sea u=1260 y v=231,

1260=231*5+105

231=105*2+21

105=21*5+0

el máximo común divisor es 21.

Definimos en la clase Fraccion una función mcd que calcula y devuelve el máximo común divisor del numerador y del denominador.

  int mcd(){
     int u=Math.abs(num);
     int v=Math.abs(den);
     if(v==0){
          return u;
     }
     int r;
     while(v!=0){
          r=u%v;
          u=v;
          v=r;
     }
     return u;
  }

A continuación definimos la función simplificar, de modo que al aplicarlo sobre una fracción, dicha fracción se reduzca a la fracción equivalente más simple. Para ello, se divide numerador y denominador por el máximo común divisor de ambos números, y devuelve la fracción simplificada.

  Fraccion simplificar(){
     int dividir=mcd();
     num/=dividir;
     den/=dividir;
     return this;
  }

Aquí tenemos otro ejemplo del uso de la palabra reservada this. Los miembros dato cambian al dividirlos entre el máximo común divisor y la función devuelve el objeto actual, this.

 

La función miembro toString

Para mostrar una fracción podemos definir una función miembro denominada imprimir

public void imprimir(){ 
	System.out.println(num+" / "+den);
} 

Un objeto de la clase Fraccion llama a la función miembro imprimir para mostrar en la consola (una ventana DOS) los valores que guardan sus miembros dato, num y den, el numerador y el denominador. La función imprimir así definida no nos servirá cuando la clase Fraccion se emplee en un contexto gráfico. Ahora bien, como vamos a ver a continuación el lenguaje Java nos proporciona una solución a este problema.

Aunque no se define explícitamente, la clase Fraccion deriva de la clase base Object (la estudiaremos en el siguiente capítulo) y redefine la función miembro pública toString, cuya tarea es la de dar una representación en forma de texto de la fracción.

public String toString(){ 
	String texto=num+" / "+den;
	return texto; 
} 

En la definición de toString vemos que el operador + se usa para concatenar strings (el lenguaje Java convierte automáticamente un dato primitivo en su representación textual cuando se concatena con un string).

Para mostar en la consola el numerador y el denominador de una fracción (objeto de la clase Fraccion) a basta escribir

	System.out.println(a);

Lo que equivale a la llamada explícita

	System.out.println(a.toString());

Si queremos mostrar la fracción a en un contexto gráfico g de un applet o de un canvas escribimos

    	g.drawString("fracción: "+a, 20, 30);

donde 20, 30 son las coordenadoas de la línea base del primer carácter. Esta sentencia equivale a la llamada explícita

    	g.drawString("fracción: "+a.toString(), 20, 30);

La redefinición de la función toString devuelve un string un objeto de la clase String que guarda la representación en forma de texto de los objetos de una determinada clase. De este modo, una clase que redefina toString puede emplearse en cualquier ámbito.

 

La clase Fraccion

Ahora ponemos las funciones miembro dentro de la clase Fraccion, anteponiendo en las funciones que representan operaciones la palabra reservada static.

public class Fraccion {
     private int num;
     private int den;
  public Fraccion() {
     num=0;
     den=1;
  }
  public Fraccion(int x, int y) {
     num=x;
     den=y;
  }
  public static Fraccion sumar(Fraccion a, Fraccion b){
     Fraccion c=new Fraccion();
     c.num=a.num*b.den+b.num*a.den;
     c.den=a.den*b.den;
     return c;
  }
  public static Fraccion restar(Fraccion a, Fraccion b){
     Fraccion c=new Fraccion();
     c.num=a.num*b.den-b.num*a.den;
     c.den=a.den*b.den;
     return c;
  }
  public static Fraccion multiplicar(Fraccion a, Fraccion b){
     return new Fraccion(a.num*b.num, a.den*b.den);
  }
  public static Fraccion inversa(Fraccion a){
     return new Fraccion(a.den, a.num);
  }
  public static Fraccion dividir(Fraccion a, Fraccion b){
     return multiplicar(a, inversa(b));
  }
  private int mcd(){
     int u=Math.abs(num);
     int v=Math.abs(den);
     if(v==0){
          return u;
     }
     int r;
     while(v!=0){
          r=u%v;
          u=v;
          v=r;
     }
     return u;
  }
  public Fraccion simplificar(){
     int dividir=mcd();
     num/=dividir;
     den/=dividir;
     return this;
  }
  public String toString(){
     String texto=num+" / "+den;
     return texto;
  }
}

 

Uso de la clase Fraccion

Como vemos en la definición de la clase Fraccion tenemos funciones estáticas y no estáticas. Vamos a ver la diferencia entre las llamadas a funciones estáticas y no estáticas.

     Fraccion x=new Fraccion(2,3);
     System.out.println("x--> "+x);

Cuando se pone una fracción x como argumento de la función println o se concatena con un string se llama automáticamente a la función miembro toString, lo que equivale a la siguiente llamada

     System.out.println("x--> "+x.toString());
     Fraccion x=new Fraccion(2,3);
     Fraccion y=new Fraccion(4,3);
     System.out.println("x+y= "+Fraccion.sumar(x, y));
     Fraccion x=new Fraccion(2,3);
     Fraccion y=new Fraccion(4,3);
     System.out.println("x*y= "+Fraccion.multiplicar(x, y));

Primero suma las fracicones x e y y luego hace el producto con la fracción z

     Fraccion x=new Fraccion(2,3);
     Fraccion y=new Fraccion(4,3);
     Fraccion z=new Fraccion(1,2);
     Fraccion resultado=Fraccion.multiplicar(Fraccion.sumar(x,y),z);
     System.out.println("(x+y)*z= "+resultado);
     System.out.println(resultado.simplificar());

 

Modificadores de acceso

Este ejemplo ilustra una faceta importante de los lenguajes de Programación Orientada a Objetos denominada encapsulación. El acceso a los miembros de una clase está controlado. Para usar una clase, solamente necesitamos saber que funciones miembro se pueden llamar y a qué datos podemos acceder, no necesitamos saber como está hecha la clase, como son sus detalles internos. Una vez que la clase está depurada y probada, la clase es como una caja negra. Los objetos de dicha clase guardan unos datos, y están caracterizados por una determinada conducta. Este ocultamiento de la información niega a la entidades exteriores el acceso a los miembros privados de un objeto. De este modo, las entidades exteriores acceden a los datos de una manera controlada a través de algunas funciones miembro. Para acceder a un miembro público (dato o función) basta escribir.

objeto_de_la_clase_Fraccion.miembro_público_no_estático
clase_Fraccion.miembro_público_estático

Delante de los miembros dato, como podemos ver en el listado hemos puesto las plabras reservadas public y private.

Los miembros públicos son aquellos que tienen delante la palabra public, y se puede acceder a ellos sin ninguna restricción.

Los miembros privados son aquellos que tienen delante la palabra private, y se puede acceder a ellos solamente dentro del ámbito de la clase.

Los miembros dato num y den son privados, y también la función que calcula el máximo común divisor mcd, que es una función auxiliar de la función miembro publica simplificar. El usuario solamente precisa saber que dispone de una función pública que le permite simplificar una fracción, pero no necesita saber cuál es el procedimiento empleado para simplificar fracciones. Así declaramos la función mcd como privada y simplificar como pública.

Cuando no se pone ningún modificador de acceso delante de los miembros, se dice que son accesibles dentro del mismo paquete (package). Esto es lo que hemos hecho en los ejemplos estudiados hasta esta sección.

package es la primera sentencia que se pone en un archivo .java. El nombre del paquete es el mismo que el nombre del subdirectorio que contiene los archivos .java. Cada archivo .java contiene habitualmente una clase. Si tiene más de una solamente una de ellas es pública. El nombre de dicha clase coincide con el nombre del archivo.

Como el lector se habrá dado cuenta hay una correspondencia entre archivos y clases, entre paquetes y subdirectorios. El Entorno Integrado de Desarrollo (IDE) en el que creamos los programas facilita esta tarea sin que el usuario se aperciba de ello.

 

Mejora de la clase Lista

disco.gif (1035 bytes)list1: Lista.java, ListaApp1.java

Veamos un ejemplo más, la clase Lista, y hagamos uso de la función miembro ordenar para hallar el valorMenor y el valorMayor. Si tenemos una lista ordenada en orden creciente, el valor menor es el primer elemento de la lista x[0], y el valor mayor es el último elemento de la lista x[n-1]. Podemos escribir el siguiente código

    public int valorMayor(){
	ordenar();
        return x[n-1];
    }
    public int valorMenor(){
	ordenar();
        return x[0];
    } 

Podemos llamar una sóla vez a la función miembro ordenar en el constructor, después de haber creado el array, y evitar así la reiteración de llamadas a dicha función en valorMayor, valorMenor e imprimir.

    public Lista(int[] x) {
        this.x=x;
        n=x.length;
	ordenar();
    }
    public int valorMayor(){
        return x[n-1];
    }
    public int valorMenor(){
        return x[0];
    }

La función miembro ordenar, es una función auxiliar de las otras funciones miembro públicas, por tanto, podemos ponerle delante el modificador de acceso private. El usuario solamente está interesado en el valor medio, el valor mayor y menor de un conjunto de datos, pero no está interesado en el procedimiento que permite ordenar el conjunto de datos. Como ocurre en la vida moderna usamos muchos aparatos pero no tenemos por que conocer sus detalles internos y cómo funcionan por dentro. Una clase es como uno de estos aparatos modernos, el usuario solamente tiene que conocer qué hace la clase, a qué miembros tiene acceso, pero no como está implementada en software.

 

La función miembro toString

Si los miembros dato de la clase Lista son privados (private) hemos de definir una función que hemos denominado imprimir para mostrar los valores que guardan los miembros dato de los objetos de la clase Lista.

public class Lista {
    private int[] x;     //array de datos
    private int n;      //dimensión
//...
    public void imprimir(){
        for(int i=0; i<n; i++){
            System.out.print("\t"+x[i]);
        }
        System.out.println("");
    }

La llamada a esta función miembro se efectúa desde un objeto de la clase Lista

        Lista lista=new Lista(new int[]{60, -4, 23, 12, -16});
	System.out.println("Mostrar la lista");
        lista.imprimir();

Sustituímos la función miembro imprimir por la redefinición de toString. Para redefinir una función, tiene que tener el mismo nombre, los mismos modificadores, el mismo tipo de retorno y los mismos parámetros y del mismo tipo en la clase base y en la clase derivada. Para evitar errores, el mejor procedimiento es el de ir al código de la clase base Object, copiar la línea de la declaración de toString, pegarla en la definición de nuestra clase, y a continuación definir dicha función.

public class Lista {
    private int[] x;     //array de datos
    private int n;      //dimensión
//...
    public String toString(){
        String texto="";
        for(int i=0; i<n; i++){
            texto+="\t"+x[i];
        }
        return texto;
    }

La llamada a la función toString se realiza implícitamente en el argumento de la función System.out.println, o bien, al concatenar un string y un objeto de la clase Lista.

    Lista lista=new Lista(new int[]{60, -4, 23, 12, -16});
    System.out.println("Mostrar la lista");
    System.out.println(lista);
public class Lista {
    private int[] x;     //array de datos
    private int n;      //dimensión
    public Lista(int[] x) {
        this.x=x;
        n=x.length;
	ordenar();
    }
    public double valorMedio(){
        int suma=0;
        for(int i=0; i<n; i++){
            suma+=x[i];
        }
        return (double)suma/n;
    }
    public int valorMayor(){
        return x[n-1];
    }
    public int valorMenor(){
        return x[0];
    }
    private void ordenar(){
        int aux;
        for(int i=0; i<n-1; i++){
            for(int j=i+1; j<n; j++){
                if(x[i]>x[j]){
                    aux=x[j];
                    x[j]=x[i];
                    x[i]=aux;
                }
            }
        }
    }
    public String toString(){
        String texto="";
        for(int i=0; i<n; i++){
            texto+="\t"+x[i];
        }
        return texto;
    }
}