viernes, 28 de agosto de 2015

Base de Datos Orientada a Objetos

En una base de datos orientada a objetos, la información se representa mediante objetos como los presentes en la programación orientada a objetos. Cuando se integra las características de una base de datos con las de un lenguaje de programación orientado a objetos, el resultado es un sistema gestor de base de datos orientada a objetos (ODBMS, object database management system). Un ODBMS hace que los objetos de la base de datos aparezcan como objetos de un lenguaje de programación en uno o más lenguajes de programación a los que dé soporte. Un ODBMS extiende los lenguajes con datos persistentes de forma transparente, control de concurrencia, recuperación de datos, consultas asociativas y otras capacidades.

Las bases de datos orientadas a objetos se diseñan para trabajar bien en conjunción con lenguajes de programación orientados a objetos como Java, C#, Visual Basic.NET y C++. Los ODBMS usan exactamente el mismo modelo que estos lenguajes de programación.

Los ODBMS son una buena elección para aquellos sistemas que necesitan un buen rendimiento en la manipulación de tipos de dato complejos.

Los ODBMS proporcionan los costes de desarrollo más bajos y el mejor rendimiento cuando se usan objetos gracias a que almacenan objetos en disco y tienen una integración transparente con el programa escrito en un lenguaje de programación orientado a objetos, al almacenar exactamente el modelo de objeto usado a nivel aplicativo, lo que reduce los costes de desarrollo y mantenimiento.

Una base de datos es una colección de datos que puede constituirse de forma que sus contenidos puedan permitirse el encapsular, tramitarse y renovarse sencillamente, elementos de datos, sus características, atributos y el código que opera sobre ellos en elementos complejos llamados objetos. Las base de datos están constituida por objetos, que pueden ser de muy diversos tipos, y sobre los cuales se encuentran definidas unas operaciones donde interactúan y se integran con las de un lenguaje de programación orientado a objetos, es decir, que los componentes de la base de datos son objetos de los lenguajes de programación además que este tipo de base de datos están diseñadas para trabajar con lenguajes orientados a objetos también manipulan datos complejos de forma rápida y segura.
Las bases de datos orientadas a objetos se crearon para tratar de satisfacer las necesidades de estas nuevas aplicaciones. La orientación a objetos ofrece flexibilidad para manejar algunos de estos requisitos y no esta limitada por los tipos de datos y los lenguajes de consulta de los sistemas de bases de datos tradicionales.
Los objetos estructurados se agrupan en clases. Las clases utilizadas en un determinado lenguaje de programación orientado a objetos son las mismas clases que serán utilizadas en una base de datos; de tal manera, que no es necesaria una transformación del modelo de objetos para ser utilizado. De forma contraria, el modelo relacional requiere abstraerse lo suficiente como para adaptar los objetos del mundo real a tablas. El conjunto de las clases se estructuran en subclases y superclases, los valores de los datos también son objetos.
Muchas organizaciones que actualmente usan tecnología orientada a objetos también desean los beneficios de los sistemas de gestión de base de datos orientados a objetos. En otras palabras, se desea la migración de bases de datos y aplicaciones de bases de datos relacionales a orientadas a objetos. La migración a la tecnología de objetos consiste de la ingeniería reversa de los programas de aplicación y la migración de la base de datos. El objetivo de la migración de la base de datos es tener un esquema equivalente y la base de datos disponibles. Esto desde luego puede ser logrado por medio de la transformación manual del código de los programas lo cual resulta demasiado complicado. Para esto existen tres enfoques que hacen uso de la tecnología de objetos para bases de datos relacionales.
a.- Construir una interface orientada a objetos sobre el sistema de base de datos relacional.
b.- La migración a un sistema de base de datos relacional/objetos.
c.- Conversión del esquema de base de datos relacional a uno orientado a objetos.
El primer enfoque retiene la base de datos relacional y crea una interface orientada a objetos encima de ésta. Este enfoque es el más fácil; no existe interrupción del sistema para la migración de datos y no existe perdida semántica de la información. Por otro lado el rendimiento disminuye debido que no existe un buen acoplamiento entre los dos paradigmas en el tiempo de ejecución.
En el segundo enfoque, los datos deben ser migrados de acuerdo con el motor de base de datos (por ejemplo Oracle 7 a 8), y las características orientadas a objetos solo pueden ser explotadas con la modificación o extensión del esquema.
El tercer enfoque es la migración de la base de datos en donde un nuevo esquema bajo el OODBMS es creado y los datos son migrados de la base de datos relacional a la orientada a objetos.
Una base orientada a objetos es una base de datos que incorpora todos los conceptos importantes del paradigma de objetos:
Encapsulación: Propiedad que permite ocultar información al resto de los objetos, impidiendo así accesos incorrectos o conflictos.
Herencia: Propiedad a través de la cual los objetos heredan comportamientos dentro de una jerarquía de clases.
Polimorfismo: Propiedad de una operación mediante la cual puede ser aplicada a distintos tipos de objetos.
En bases de datos orientadas a objetos, los usuarios pueden definir operaciones sobre los datos como parte de la definición de la base de datos. Una operación (llamada función) se especifica en dos partes. La interfaz (o signatura) de una operación incluye el nombre de la operación y los tipos de datos de sus argumentos (o parámetros). La implementación (o método) de la operación se especifica separadamente y puede modificarse sin afectar la interfaz. Los programas de aplicación de los usuarios pueden operar sobre los datos invocando a dichas operaciones a través de sus nombres y argumentos, sea cual sea la forma en la que se han implementado. Esto podría denominarse independencia entre programas y operaciones.


Origen de las base de datos orientadas a objetos
El origen se encuentra básicamente en las siguientes razones:
La existencia de problemas para representar cierta información y modelar ciertos aspectos del "mundo real", puesto que los modelos clásicos permiten representar gran cantidad de datos, pero las operaciones y representaciones que se pueden realizar sobre ellos son bastante simples.
El paso del modelo de objetos al modelo relacional genera dificultades que en el caso no surgen ya que el modelo es el mismo.Por lo tanto, las bases de datos orientadas a objetos surgen básicamente para tratar de paliar las deficiencias de los modelos anteriores y para proporcionar eficiencia y sencillez a las aplicaciones.
Las debilidades y limitaciones de los Sistema Gestor de Bases de Datos Orientadas a Objetos son:
Pobre representación de las entidades del "mundo real".
Sobrecarga y poca riqueza semánticas.
Soporte inadecuado para las restricciones de integridad y empresariales
Estructura de datos homogénea
Operaciones limitadas
Dificultades para gestionar las consultas recursivas
Desadaptación de impedancias
Problemas asociados a la concurrencia, cambios en los esquemas y el inadecuado acceso navegacional.
No ofrecen soporte para tipos definidos por el usuario (sólo dominios)
Mientras que las necesidades de las aplicaciones actuales con respecto a las bases de datos son:
Soporte para objetos complejos y datos multimedia
Identificadores únicos
Soporte a referencias e interrelaciones
Manipulación navegacional y de conjunto de registros
Jerarquías de objetos o tipos y herencia
Integración de los datos con sus procedimientos asociados
Modelos extensibles mediante tipos de datos definidos por el usuario
Gestión de versiones
Facilidades de evolución
Transacciones de larga duración
Interconexión e interoperabilidad
Debido a las limitaciones anteriormente expuestas, su uso es más ventajoso si se presenta en alguno de los siguientes escenarios:
Un gran número de tipos de datos diferentes
Un gran número de relaciones entre los objetos
Objetos con comportamientos complejos
Se puede encontrar este tipo de complejidad acerca de tipos de datos, relaciones entre objetos y comportamiento de los objetos principalmente en aplicaciones de ingeniería, manufacturación, simulaciones, automatización de oficina y en numerosos sistemas de información. No obstante, las BDOO no están restringidas a estas áreas. Ya que al ofrecer la misma funcionalidad que su precursoras relacionales, el resto de campos de aplicación tiene la posibilidad de aprovechar completamente la potencia que las BDOO ofrecen para modelar situaciones del mundo real.
Características
Una de las características mandatorias de o reglas son:
1.-Debe tener un motor de base de datos.
2.-Debe ser un sistema orientado a objetos.
Mandatorias.- Son las que el Sistema debe satisfacer a orden de tener un sistema de base de datos orientadas a objetos y estos son: Objetos complejos, Identidad de objetos, Encapsulación, Tipos ó Clases, Sobre paso combinado con unión retardada, Extensibilidad, Completación Computacional, Persistencia y Manejador de almacenamiento secundario, Concurrencia, Recuperación y Facilidad de Query.
Opcional.- Son las que pueden ser añadidas para hacer el sistema mejor pero que no son Mandatorias estas son de: herencia múltiple, chequeo de tipos e inferencia distribución y diseño de transacciones y versiones.
Abiertas.- Son los puntos donde el diseñador puede hacer un número de opciones y estas son el paradigma de la programación la representación del sistema ó el tipo de sistema y su uniformidad.
El modelo orientado a objetos se basa en encapsular código y datos en una única unidad, llamada objeto. El interfaz entre un objeto y el resto del sistema se define mediante un conjunto de mensajes. El término mensaje en un contexto orientado a objetos, no implica el uso de un mensaje físico en una red de computadoras, si no que se refiere al paso de solicitudes entre objetos sin tener en cuenta detalles específicos de implementación. El modelo de datos orientado a objetos es una extensión del paradigma de programación orientado a objetos. Los objetos entidad que se utilizan en los programas orientados a objetos son análogos a las entidades que se utilizan en las bases de datos orientadas a objetos puros, pero con una gran diferencia: los objetos del programa desaparecen cuando el programa termina su ejecución, mientras que los objetos de la base de datos permanecen. A esto se le denomina persistencia.
Ventajas e inconvenientes de las bas e de datos orientadas a objetos
Aunque los Sistema Gestor de Bases de Datos Orientadas a Objetos pueden proporcionar soluciones apropiadas para muchos tipos de aplicaciones avanzadas de bases de datos, también tienen sus desventajas.
Las ventajas de un Sistema Gestor de Bases de Datos Orientadas a Objetos son:
Mayor capacidad de modelado. El modelado de datos orientado a objetos permite modelar el "mundo real" de una manera mucho más fiel. Esto se debe a:
1. un objeto permite encapsular tanto un estado como un comportamiento
2. un objeto puede almacenar todas las relaciones que tenga con otros objetos
3. los objetos pueden agruparse para formar objetos complejos (herencia).
Ampliabilidad. Esto se debe a:
1. Se pueden construir nuevos tipos de datos a partir de los ya existentes.
2. Agrupación de propiedades comunes de diversas clases e incluirlas en una superclase, lo que reduce la redundancia.
3. Reusabilidad de clases, lo que repercute en una mayor facilidad de mantenimiento y un menor tiempo de desarrollo.
Lenguaje de consulta más expresivo. El acceso navegacional desde un objeto al siguiente es la forma más común de acceso a datos en un Sistema Gestor de Bases de Datos Orientadas a Objetos. Mientras que SQL utiliza el acceso asociativo. El acceso navegacional es más adecuado para gestionar operaciones como los despieces, consultas recursivas, etc.
Adecuación a las aplicaciones avanzadas de base de datos. Hay muchas áreas en las que los SGBD tradicionales no han tenido excesivo éxito como el CAD, CASE, OIS, sistemas multimedia, etc. en los que las capacidades de modelado de los Sistema Gestor de Bases de Datos Orientadas a Objetos han hecho que esos sistemas sí resulten efectivos para este tipo de aplicaciones.
Mayores prestaciones. Los Sistema Gestor de Bases de Datos Orientadas a Objetos proporcionan mejoras significativas de rendimiento con respecto a los Sistema Gestor de Bases de Datos Orientadas a Objetos relacionales. Aunque hay autores que han argumentado que los bancos de prueba usados están dirigidos a aplicaciones de ingeniería donde los Sistema Gestor de Bases de Datos Orientadas a Objetos son más adecuados. También está demostrado que los SGBDR tienen un rendimiento mejor que los Sistema Gestor de Bases de Datos Orientadas a Objetos en las aplicaciones tradicionales de bases de datos como el procesamiento de transacciones en línea (OLTP).
Los inconvenientes de un Sistema Gestor de Bases de Datos Orientadas a Objetos son:
Carencia de un modelo de datos universal. No hay ningún modelo de datos que esté universalmente aceptado para los SGBDOO y la mayoría de los modelos carecen una base teórica.
Carencia de experiencia. Todavía no se dispone del nivel de experiencia del que se dispone para los sistemas tradicionales.
Carencia de estándares. Existe una carencia de estándares general para los Sistema Gestor de Bases de Datos Orientadas a Objetos.
Competencia. Con respecto a los SGBDR y los SGBDOR. Estos productos tienen una experiencia de uso considerable. SQL es un estándar aprobado y ODBC es un estándar de facto. Además, el modelo relacional tiene una sólida base teórica y los productos relacionales disponen de muchas herramientas de soporte que sirven tanto para desarrolladores como para usuarios finales.
La optimización de consultas compromete la encapsulación. La optimización de consultas requiere una compresión de la implementación de los objetos, para poder acceder a la base de datos de manera eficiente. Sin embargo, esto compromete el concepto de encapsulación.
El modelo de objetos aún no tiene una teoría matemática coherente que le sirva de base.



lunes, 17 de agosto de 2015

La clase Fraccion

definir una clase denominada Fracción 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 claseFraccion) 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 Fracción.

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,
  • En la primera iteración, se halla el resto r de dividir el primero u entre el segundo v.  Se asigna a u el divisor v, y se asigna a v el resto r.
  • En la segunda iteracción, se halla el resto r de dividir u entre v. Se asigna a u el divisor v, y se asigna a v el resto r.
  • Se repite el proceso hasta que el resto r sea cero. El máximo común divisor será el último valor de v.
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 Fracciona 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.
  • Crear un objeto de la clase Fraccion o una fracción
     Fraccion x=new Fraccion(2,3);
  • Mostrar una fracción
     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());
  • Suma de dos fracciones
     Fraccion x=new Fraccion(2,3);
     Fraccion y=new Fraccion(4,3);
     System.out.println("x+y= "+Fraccion.sumar(x, y));
  • Producto de dos fracciones
     Fraccion x=new Fraccion(2,3);
     Fraccion y=new Fraccion(4,3);
     System.out.println("x*y= "+Fraccion.multiplicar(x, y));
  • Operaciones combinadas
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);
  • Simplificar una fracción
     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.
  • Miembros públicos
Los miembros públicos son aquellos que tienen delante la palabra public, y se puede acceder a ellos sin ninguna restricción.
  • Miembros privados
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 mcdcomo privada y simplificar como pública.
  • Por defecto (a nivel de paquete)
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


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 valorMayorvalorMenor eimprimir.
    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;
    }
}

Una clase con funciones estaticas

El propósito de este capítulo es definir un conjunto de funciones estáticas dentro de una clase denominada Matematicas. Al definir el concepto de clase estudiamos las funciones miembro o métodos. En esta página, vamos a ver cómo se define una función para realizar una determinada tarea.
Se deberá tener en cuenta que no se puede usar una variable de instancia por una función estática. Por ejemplo
class UnaClase{
    int i=0;
    static void funcion(){
        System.out.println(i);
    }
//otras funciones miembro
}
Al compilar se produce un error ya que la variable i es de instancia y no se puede usar en un contexto estático. El lector puede probar esto con cualquier clase que describe una aplicación. Declara una variable de instancia en la clase y la trata de usar en la función estática main.

El valor absoluto de un número.

  • Nombre de la función: absoluto
  • Parámetros: un entero
  • Devuelve: un número entero positivo.
El valor absoluto de un número x es x si el número es positivo y -x si el número es negativo. Una función absoluto que calcula el valor absoluto de un número recibirá el dato del número, y devolverá el valor absoluto del mismo. Supongamos que dicho dato es entero, la definición de la función será:
int absoluto(int x){
 int abs;
 if(x>0) abs=x;
 else abs=-x;
 return abs;
}
Podemos simplificar el código sin necesidad de declarar una variable temporal abs.
int absoluto(int x){
 if(x>0) return x;
 else return -x;
}
O bien,
int absoluto(int x){
 if(x>0) return x;
 return -x;
}
Una función similar nos hallará el valor absoluto de un dato de tipo double.
    double absoluto(double x){
        if(x<0) return -x;
        return x;
    }


La potencia de exponente entero de un número entero

  • Nombre de la función: potencia
  • Parámetros: la base y el exponente, ambos enteros
  • Devuelve: el resultado, un número del tipo long.
Para hallar la potencia de un número se multiplica tantas veces la base como indica el exponente. Por ejemplo, para hallar la quinta potencia de 3, se escribirá
35=3*3*3*3*3
Podemos realizar este producto en un bucle for, de manera que en la variable resultado se vaya acumulando el resultado de los sucesivos productos, tal como se recoge en la Tabla, entre paréntesis figura el valor de resultado en la iteración previa, el valor inicial es 1.
iteraciónvalor de resultado
(1)*3
(1*3)*3
(1*3*3)*3
(1*3*3*3)*3
(1*3*3*3*3)*3
 long resultado=1;
 for(int i=0; i<5; i++)
  resultado*=3;
El siguiente paso es la generalización del proceso a un exponente positivo cualquiera y a una base entera cualesquiera. La variable resultado es de tipo long, porque al hallar la potencia de un número entero se puede sobrepasar el rango de dichos números.
 long resultado=1;
 for(int i=0; i<exponente; i++)
  resultado*=base;
Por último, le pondremos una etiqueta a esta tarea, asociaremos esta rutina al nombre de una función.
long potencia(int base, int exponente){
 long resultado=1;
 for(int i=0; i<exponente; i++){
  resultado*=base;
        }
 return resultado;
}


El factorial de un número

  • Nombre de la función: factorial
  • Parámetros: un entero int
  • Devuelve: el resultado, un número positivo del tipo long.
Como ejemplos de los sentencias iterativas for y while ya tuvimos la ocasión de calcular el factorial de un número. Ahora se trata de poner las líneas de código dentro de una función para que pueda ser reutilizada.
        long resultado=1;
        while(numero>0){
            resultado*=numero;
            numero--;
        }
Es mucho más lógico definir una función que calcule el factorial del número num,
    long factorial(int num){
        long resultado=1;
        while(num>0){
            resultado*=num;
            num--;
        }
        return resultado;
    }
que repetir tres veces el código del cálculo del factorial para hallar el número combinatorio m sobre n, de acuerdo a la siguiente fórmula.

El numerador y el denominador del número combinatorio m sobre n se obtiene del siguiente modo:
 long num=factorial(m);
 long den=factorial(n)*factorial(m-n);


Determinar si un número es o no primo

  • Nombre de la función: esPrimo
  • Parámetros: un entero int
  • Devuelve: true o false, un dato del tipo boolean
Como ejemplos de los sentencias iterativas for y break ya tuvimos la ocasión de calcular los números primos comprendidos entre 3 y 100. Recuérdese que
  • un número par no es primo, excepto el 2
  • un número impar es primo si no es divisible por los números impares comprendidos entre 1 y numero/2
            boolean bPrimo=true;
            for(int i=3; i<numero/2; i+=2){
                if(numero%i==0){
                    bPrimo=false;
                    break;
                }
            }
Ahora, se trata de poner las líneas de código dentro de una función que determine si un número es primo (devuelve true) o no es primo (devuelve false), para que pueda ser reutilizada.  El código de la función esPrimo es la siguiente
    boolean esPrimo(int numero){
        if(numero==2)     return true;
        if(numero%2==0)     return false;
        boolean bPrimo=true;
        for(int i=3; i<numero/2; i+=2){
            if(numero%i==0){
                bPrimo=false;
                break;
            }
        }
        return bPrimo;
    }
Podemos ahorrarnos la variable bPrimo, escribiendo la función esPrimo de la siguiente forma equivalente
    boolean esPrimo(int numero){
        if((numero!=2)&&(numero%2==0))     return false;
        for(int i=3; i<numero/2; i+=2){
            if(numero%i==0){
                return false;
            }
        }
        return true;
    }


La clase Matematicas

Ahora solamente nos queda poner todas las funciones en el interior de una clase que le llamaremos Matematicas, anteponiéndole a cada una de las funciones la palabra reservada static.
public class Matematicas {
    static long factorial(int num){
 //...
    }
    static long potencia(int base, int exponente){
 //...
    }
    static boolean esPrimo(int numero){
 //...
    }
    static double absoluto(double x){
 //...
    }
    static int absoluto(int x){
 //...
    }
}

Llamada a las funciones miembro

Para llamar a las funciones miembro se escribe
 Nombre_de_la_clase.función_miembro(...);
  • Hallar el número combinatorio m sobre n
    int m=8, n=3;
    System.out.print("El número combinatorio "+m +" sobre "+ n);
    long numerador=Matematicas.factorial(m);
    long denominador=Matematicas.factorial(m-n)*Matematicas.factorial(n);
    System.out.println(" vale "+numerador +" / "+ denominador);
  • Encontrar los números primos comprendidos entre 100 y 200
        for(int num=100; num<200; num++){
            if(Matematicas.esPrimo(num)){
                System.out.print(num+" - ");
            }
        }
  • Hallar la potencia de un número entero
        System.out.print("La potencia 5 elevado a 4 ");
        System.out.print("vale "+Matematicas.potencia(5, 4));
  • Hallar el valor absoluto de un número
        double y=-2.5;
        System.out.print("El valor absoluto de "+y);
        System.out.println(" vale "+Matematicas.absoluto(y));

La clase Math

La clase Math tiene miembros dato y funciones miembro estáticas, vamos a conocer algunas de estas funciones, cómo se llaman y qué tarea realizan.

Miembros dato constantes

La clase Math define dos constantes muy útiles, el número p y el número e.

public final class Math {
    public static final double E = 2.7182818284590452354;
    public static final double PI = 3.14159265358979323846;
    //...
}
El modificador final indica que los valores que guardan no se pueden cambiar, son valores constantes

Se accede a estas constantes desde la clase Math, de la siguiente forma

     System.out.println("Pi es " + Math.PI);    
     System.out.println("e es " + Math.E);    



Funciones miembro

La clase Math define muchas funciones y versiones distintas de cada función.

Por ejemplo, para hallar el valor absoluto de un número define las siguientes funciones. Se llama a una u otra dependiendo del tipo de dato que se le pasa en su único argumento.

public final class Math {
    public static int abs(int a) {
return (a < 0) ? -a : a;
    }
    public static long abs(long a) {
return (a < 0) ? -a : a;
    }  
    public static float abs(float a) {
return (a < 0) ? -a : a;
    }
    public static double abs(double a) {
return (a < 0) ? -a : a;
    }
//...
}
Por ejemplo, hallar el valor absoluto de los siguientes números

    int i = -9;
    double x = 0.3498;  
    System.out.println("|" + i + "| es " + Math.abs(i));
    System.out.println("|" + x + "| es " + Math.abs(x));
Math.abs(i), llama a la primera versión, y Math.abs(x) llama a la última versión.



Funciones trigonométricas

En las funciones trigonométricas los argumentos se expresan en radianes. Por ejemplo, el ángulo 45º se convierte en radianes y luego se halla el seno, el coseno y la tangente

    double angulo = 45.0 * Math.PI/180.0;
    System.out.println("cos(" + angulo + ") es " + Math.cos(angulo));
    System.out.println("sin(" + angulo + ") es " + Math.sin(angulo));
    System.out.println("tan(" + angulo + ") es " + Math.tan(angulo));
Para pasar de coordenadas rectangulares a polares es útil la función atan2, que admite dos argumentos, la ordenada y la abscisa del punto. Devuelve el ángulo en radianes.

    double y=-6.2;  //ordenada
    double x=1.2;   //abscisa
    System.out.println("atan2(" + y+" , "+x + ") es " + Math.atan2(y, x));
Funciones exponencial y logarítmica

La función exponencial exp devuelve el número e elevado a una potencia

    System.out.println("exp(1.0) es " +  Math.exp(1.0));
    System.out.println("exp(10.0) es " + Math.exp(10.0));
    System.out.println("exp(0.0) es " +  Math.exp(0.0));
La función log calcula el logaritmo natural (de base e) de un número

    System.out.println("log(1.0) es " + Math.log(1.0));
    System.out.println("log(10.0) es " + Math.log(10.0));
    System.out.println("log(Math.E) es " + Math.log(Math.E));
Función potencia y raíz cuadrada

Para elevar un número x a la potencia y, se emplea pow(x, y)

   System.out.println("pow(10.0, 3.5) es " +  Math.pow(10.0,3.5));

Para hallar la raíz cuadrada de un número, se emplea la función sqrt

   System.out.println("La raíz cuadrada de " + x + " is " + Math.sqrt(x));

Aproximación de un número decimal

Para expresar un número real con un número especificado de números decimales empleamos la función round. Por ejemplo, para expresar los números x e y con dos cifras decimales escribimos

    double x = 72.3543;
    double y = 0.3498;
    System.out.println(x + " es aprox. " + (double)Math.round(x*100)/100);
    System.out.println(y + " es aprox. " + (double)Math.round(y*100)/100);
Se obtiene 72.35 y 0.35 como cabría esperar. Fijarse que round devuelve un número entero int que es necesario promocionar a double para efectuar la división entre 100.

Si empleamos la función floor en vez de round obtendríamos

     System.out.println(x + " es aprox. " + Math.floor(x*100)/100);
     System.out.println(y + " es aprox. " + Math.floor(y*100)/100);
Se obtiene 72.35 y 0.34. La aproximación del primero es correcta ya que la tercera cifra decimal es 4 inferior a 5. La aproximación del segundo es incorrecta ya que la tercera cifra decimal es 9 mayor que 5. En la mayor parte de los cálculos se cometen errores, por lo que la diferencia entre floor y round no es significativa.

El mayor y el menor de dos números

Para hallar el mayor y el menor de dos números se emplean las funciones min y max que comparan números del mismo tipo.

    int i = 7;
    int j = -9;
    double x = 72.3543;
    double y = 0.3498;
// para hallar el menor de dos número
    System.out.println("min(" + i + "," + j + ") es " + Math.min(i,j));
    System.out.println("min(" + x + "," + y + ") es " + Math.min(x,y));
// Para hallar el mayor de dos números
     System.out.println("max(" + i + "," + j + ") es " + Math.max(i,j));
     System.out.println("max(" + x + "," + y + ") es " + Math.max(x,y));

Números aleatorios

La clase Math define una función denominada random que devuelve un número pseudoaleatorio comprendido en el intervalo [0.0, 1.0). Existe otra alternativa, se pueden generar números pseudoaleatorios a partir de un objeto de la clase Random, que llame a la función miembro nextDouble.

    System.out.println("Número aleatorio: " + Math.random());
    System.out.println("Otro número aleatorio: " + Math.random());


Cálculo del número irracional p

pi.gif (2328 bytes)
Para hallar la longitud de una circunferencia de radio R, primero se calcula el perímetro de un triángulo equilátero (3 lados) inscrito en dicha circunferencia, luego, de un hexágono (6 lados), un dodecágono (12 lados), y así sucesivamente. El límite de la sucesión de perímetros es precisamente la longitud de la circunferencia 2p R.

Si tomamos una circunferencia de radio unidad, al dividir entre dos los valores de los perímetros iremos obteniendo las sucesivas aproximaciones del número irracional p .

A partir de la figura, podemos calcular la longitud del lado an un polígono regular de n lados inscrito en la circunferencia de radio R, (en color rojo).
Del mismo modo, obtenemos la longitud del lado de un polígono regular inscrito de 2n lados (en color azul).
Teniendo en cuanta que 
Establecemos la relación entre an y a2n y por tanto, entre el perímetro Pn del polígono regular de n lados y el perímetro P2n del polígono regular de 2n lados.

Tomando como radio R, la unidad
  • Para un triángulo, n=3, la longitud del lado es a3=2sen60º, y el perímetro 
  • Para un hexágono, n=6, la longitud del lado es a6=2sen30º=1, y el perímetro P6=6.
  • y así sucesivamente.
Para obtener las sucesivas aproximaciones del número irracional p mediante la fórmula anterior procedemos del siguiente modo
  1. Partimos del valor del perímetro P de un triángulo equilátero inscrito en una circunferencia de radio unidad, el valor de n es 3.
  2. Calculamos el perímetro P de un polígono de 2n lados a partir del valor del perímetro de un polígono regular de n lados.
  3. El valor obtenido P será el valor del perímetro de un polígono regular de n=2n lados.
  4. Se imprime el valor de P dividido entre dos (aproximación de p)
  5. Se vuelve al paso 2.
Ahora, hemos de trasladar las fórmulas matemáticas a código, y aquí es donde podemos llevarnos algunas sorpresas.

En primer lugar, hemos de tener en cuenta que la expresión  es matemáticamente equivalente a  pero no lo es cuando trabajamos con números en el ordenador.

Por ejemplo si n es tipo de dato int. Al evaluar el denominador en la primera expresión obtenemos el cuadrado de n que crece muy rápidamente con n, sobrepasándose (overflow) el valor máximo que puede guardar una variable entera dado por Integer.MAX_VALUE. Integer es la clase que describe los números enteros. Por tanto, al realizar los cálculos en el ordenador es aconsejable emplear la segunda expresión en vez de la primera, incluso si cambiamos el tipo de dato de n de int a long.

El cálculo de p implica un número infinito de iteracciones, ya que como hemos visto no es posible al sobrepasarse el valor máximo que puede guardar una variable entera, nuestra primera intención sería la programar un bucle que realice el máximo número de iteracciones

    double perimetro=3*Math.sqrt(3);      //triángulo equilátero inscrito
    long n=3;
    int i=0; //número de iteracciones
    while(n<Long.MAX_VALUE){
        perimetro=2*n*Math.sqrt(2.0-Math.sqrt(4.0-(perimetro/n)*(perimetro/n)));
        n=2*n;
        i++;
        System.out.println(i+" -- "+perimetro/2);
    }
Con cierta sorpresa observamos la salida del programa cuando se ha completado el bucle, se imprime un cero, en vez de 3.14159265358979323846.

Si observamos las 30 primeras iteracciones vemos, tal como se muestra en la figura inferior, que la valor más próximo a p se obtiene en las iteracciones 13, 14, 15, y 16.

    while(i<30){
//...  
    }

pi1.gif (11311 bytes)

La conclusión final, es que hemos de tener mucho cuidado al trasladar las fórmulas matemáticas a código. Los datos de tipo predefinido solamente pueden guardar valores entre un máximo y un mínimo, tal como hemos visto en su definición.Por otra parte, una variable de tipo double tiene una precisión limitada por lo que no representa a todos los números reales sino a un conjunto finito de éstos.

Miembros estaticos de las variables clases

Variables de instancia, variables de clase

Ya mencionamos la diferencia entre variables de instancia, de clase y locales. Consideremos de nuevo la clase Circulo, con dos miembros dato, el radio específico para cada círculo y el número PI que tiene el mismo valor para todos los círculos. La primera es una variable de instancia y la segunda es una variable de clase.

Para indicar que el miembro PI es una variable de clase se le antepone el modificador static. El modificador final indica que es una constante que no se puede modificar, una vez que la variable PI ha sido inicializada.

Definimos también una función miembro denominada calcularArea que devuelva el área del círculo

public class Circulo{
      static final double PI=3.1416;
      double radio;
public Circulo(double radio){
this.radio=radio;
}
  double calcularArea(){
return (PI*radio*radio);
  }
}
Para calcular el área de un círculo, creamos un objeto circulo de la clase Circulo dando un valor al radio. Desde este objeto llamamos a la función miembro calcularArea.

Circulo circulo=new Circulo(2.3);
System.out.println("área: "+circulo.calcularArea());

Veamos ahora las ventajas que supone declarar la constante PI como miembro estático.

estatica.gif (6955 bytes)


Si PI y radio fuesen variables de instancia

public class Circulo{
      double PI=3.1416;
      double radio;
//....
}
Creamos tres objetos de la clase Circulo, de radios 3.2, 7.0, y 5.4

Circulo circulo1=new Circulo(3.2);
Circulo circulo2=new Circulo(7.0);
Circulo circulo3=new Circulo(5.4);
Al crearse cada objeto se reservaría espacio para el dato radio (64 bits), y para el dato PI (otros 64 bits). Véase la sección tipos de datos primitivos. Como vemos en la parte superior de la figura, se desperdicia la memoria del ordenador, guardando tres veces el mismo dato PI.

public class Circulo{
      static double PI=3.1416;
      double radio;
//....
}
Declarando PI estático (static), la variable PI queda ligada a la clase Circulo, y se reserva espacio en memoria una sóla vez, tal como se indica en la parte inferior de la figura. Si además la variable PI no cambia, es una constante, le podemos anteponer la palabra final.

public class Circulo{
      static final double PI=3.1416;
      double radio;
//....
}


Miembros estáticos

Las variables de clase o miembros estáticos son aquellos a los que se antepone el modificador static. Vamos a comprobar que un miembro dato estático guarda el mismo valor en todos los objetos de dicha clase.

Sea una clase denominada Alumno con dos miembros dato, la nota de selectividad, y un miembro estático denominado nota de corte. La nota es un atributo que tiene un valor distinto para cada uno de los alumnos u objetos de la clase Alumno, mientras que la nota de corte es un atributo que tiene el mismo valor para a un conjunto de alumnos. Se define también en dicha clase una función miembro que determine si está (true) o no (false) admitido.

public class Alumno {
    double nota;
    static double notaCorte=6.0;
    public Alumno(double nota) {
        this.nota=nota;
    }
    boolean estaAdmitido(){
        return (nota>=notaCorte);
    }
}
Creamos ahora un array de cuatro alumnos y asignamos a cada uno de ellos una nota.

        Alumno[] alumnos={new Alumno(5.5), new Alumno(6.3),
new Alumno(7.2), new Alumno(5.0)};
Contamos el número de alumnos que están admitidos

        int numAdmitidos=0;
        for(int i=0; i<alumnos.length; i++){
            if (alumnos[i].estaAdmitido()){
                numAdmitidos++;
            }
        }
        System.out.println("admitidos "+numAdmitidos);
Accedemos al miembro dato notaCorte desde un objeto de la clase Alumno, para cambiarla a 7.0

        alumnos[1].notaCorte=7.0;
Comprobamos que todos los objetos de la clase Alumno tienen dicho miembro dato estático notaCorte cambiado a 7.0

        for(int i=0; i<alumnos.length; i++){
            System.out.println("nota de corte "+alumnos[i].notaCorte);
        }
El miembro dato notaCorte tiene el modificador static y por tanto está ligado a la clase más que a cada uno de los objetos de dicha clase. Se puede acceder a dicho miembro con la siguiente sintaxis

Nombre_de_la_clase.miembro_estático
Si ponemos

        Alumno.notaCorte=6.5;
        for(int i=0; i<alumnos.length; i++){
            System.out.println("nota de corte "+alumnos[i].notaCorte);
        }
Veremos que todos los objetos de la clase Alumno habrán cambiado el valor del miembro dato estático notaCorte a 6.5.

Un miembro dato estático de una clase se puede acceder desde un objeto de la clase, o mejor, desde la clase misma.

Creación de Clases

Crear una clase denominada Lista cuyo miembro dato sea un array de números enteros y cuyas funciones miembro realicen las siguientes tareas:

Hallar y devolver el valor mayor
Hallar y devolver el valor menor
Hallar y devolver el valor medio
Ordenar los números enteros de menor a mayor
Mostrar la lista ordenada separando los elementos por un tabulador
disco.gif (1035 bytes)lista: Lista.java, ListaApp.java


Definición de la clase Lista

Empezamos la definición de la clase escribiendo la palabra reservada class y a continuación el nombre de la clase Lista.
Los miembros dato

Los miembros dato de la clase Lista serán un array de enteros x, y opcionalmente la dimensión del array n.

public class Lista {
    int[] x;     //array de datos
    int n;      //dimensión

El constructor

Al constructor de la clase Lista se le pasará un array de enteros para inicializar los miembros dato

    public Lista(int[] x) {
        this.x=x;
        n=x.length;
    }
Como apreciamos basta una simple asignación para inicializar el miembro dato x que es un array de enteros, con el array de enteros x que se le pasa al constructor. Por otra parte, cuando se le pasa a una función un array se le pasa implícitamente la dimensión del array, que se puede obtener a partir de su miembro dato length.

Las funciones miembro

Las funciones miembro tienen acceso a los miembros dato, el array de enteros x y la dimensión del array n.

El valor medio
Para hallar el valor medio, se suman todos los elementos del array y se divide el resultado por el número de elementos.

    double valorMedio(){
        int suma=0;
        for(int i=0; i<n; i++){
            suma+=x[i];
        }
        return (double)suma/n;
    }
Para codificar esta función se ha de tener algunas precauciones. La suma de todos los elementos del array se guarda en la variable local suma. Dicha variable local ha de ser inicializada a cero, ya que una variable local contrariamente a lo que sucede a los miembros dato o variables de instancia es inicializada con cualquier valor en el momento en que es declarada.

La división de dos enteros suma y n (número de elementos del array) es un número entero. Por tanto, se ha de promocionar el entero suma de int a double para efectuar la división y devolver el resultado de esta operación.

El valor mayor
    int valorMayor(){
        int mayor=x[0];
        for(int i=1; i<n; i++){
            if(x[i]>mayor)  mayor=x[i];
        }
        return  mayor;
    }
Se compara cada elemento del array con el valor de la variable local mayor, que inicialmente tiene el valor del primer elemento del array, si un elemento del array es mayor que dicha variable auxiliar se guarda en ella el valor de dicho elemento del array. Finalmente, se devuelve el valor mayor calculado

El valor menor
    int valorMenor(){
        int menor=x[0];
        for(int i=1; i<n; i++){
            if(x[i]<menor)  menor=x[i];
        }
        return  menor;
    }
El código es similar a la función valorMayor

Ordenar un conjunto de números
intercambio.gif (2714 bytes) En el proceso de ordenación se ha de intercambiar los valores que guardan elementos del array. Veamos como sería el código correspondiente al intercambio de los valores que guardan dos variables x e y.
Para intercambiar el contenido de dos recipientes x e y sin que se mezclen, precisamos de un recipiente auxiliar aux vacío. Se vuelca el contenido del recipiente x en el recipiente aux, el recipiente y se vuelca en x, y por último, el recipiente aux se vuelca en y. Al final del proceso, el recipiente aux vuelve a estar vacío como al principio. En la figura se esquematiza este proceso.

aux=x;
x=y;
y=aux;


ordenar.gif (2050 bytes) Para ordenar una lista de números emplearemos el método de la burbuja, un método tan simple como poco eficaz. Se compara el primer elemento, índice 0, con todos los demás elementos de la lista, si el primer elemento es mayor que el elemento j, se intercambian sus valores, siguiendo el procedimiento explicado en la figura anterior. Se continua este procedimiento con todos los elementos del array menos el último. La figura explica de forma gráfica este procedimiento.
    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;
                }
            }
        }
    }
Caben ahora algunas mejoras en el programa, así la función ordenar la podemos utilizar para hallar el valor mayor, y el valor menor. Si tenemos una lista ordenada en orden ascendente, el último elemento de la lista será el valor mayor y el primero, el valor menor. De este modo, podemos usar una función en otra funciones, lo que resulta en un ahorro de código, y en un aumento de la legibilidad del programa.

    int valorMayor(){
        ordenar();
        return  x[n-1];
    }
    int valorMenor(){
        ordenar();
        return  x[0];
    }
Imprimir la lista ordenada
Imprimimos la lista ordenada separando sus elementos por un tabulador. Primero, se llama a la función ordenar, y despues se imprime un elemento a continuación del otro mediante System.out.print. Recuérdese, que System.out.println imprime y a continuación pasa a la siguiente línea.

    void imprimir(){
        ordenar();
        for(int i=0; i<n; i++){
            System.out.print("\t"+x[i]);
        }
        System.out.println("");
    }
El código completo de la clase Lista, es el siguiente

public class Lista {
    int[] x;     //array de datos
    int n;      //dimensión
    public Lista(int[] x) {
        this.x=x;
        n=x.length;
    }
    double valorMedio(){
        int suma=0;
        for(int i=0; i<n; i++){
            suma+=x[i];
        }
        return (double)suma/n;
    }
    int valorMayor(){
        int mayor=x[0];
        for(int i=1; i<n; i++){
            if(x[i]>mayor)  mayor=x[i];
        }
        return  mayor;
    }
    int valorMenor(){
        int menor=x[0];
        for(int i=1; i<n; i++){
            if(x[i]<menor)  menor=x[i];
        }
        return  menor;
    }
    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;
                }
            }
        }
    }
    void imprimir(){
        ordenar();
        for(int i=0; i<n; i++){
            System.out.print("\t"+x[i]);
        }
        System.out.println("");
    }
}


Los objetos de la clase Lista

A partir de un array de enteros podemos crear un objeto lista de la clase Lista.

        int[] valores={10, -4, 23, 12, 16};
        Lista lista=new Lista(valores);
Estas dos sentencias las podemos convertir en una

        Lista lista=new Lista(new int[] {10, -4, 23, 12, 16});
En el resto del código, el objeto lista llama a las funciones miembro

        System.out.println("Valor mayor "+lista.valorMayor());
        System.out.println("Valor menor "+lista.valorMenor());
        System.out.println("Valor medio "+lista.valorMedio());
        lista.imprimir();

Los números aletorios

La clase Random

disco.gif (1035 bytes)azar: AzarApp.java

Del mismo modo que hemos visualizado el código fuente de la clase String, también la podemos obtener el de la clase Random. Situamos el cursor en el nombre de la clase y seleccionamos el primer elemento del menú flotante Browse symbol at cursor que aparece cuando se pulsa el botón derecho del ratón.

La clase Random proporciona un generador de números aleatorios que es más flexible que la función estática random de la clase Math.

Para crear una secuencia de números aleatorios tenemos que seguir los siguientes pasos:

Proporcionar a nuestro programa información acerca de la clase Random. Al principio del programa  escribiremos la siguiente sentencia.
import java.util.Random;
Crear un objeto de la clase Random
Llamar a una de las funciones miembro que generan un número aleatorio
Usar el número aleatorio.


Constructores

La clase dispone de dos constructores, el primero crea un generador de números aleatorios cuya semilla es inicializada en base al instante de tiempo actual.

    Random rnd = new Random();
El segundo, inicializa la semilla con un número del tipo long.

    Random rnd = new Random(3816L);
El sufijo L no es necesario, ya que aunque 3816 es un número int por defecto, es promocionado automáticamente a long.

Aunque no podemos predecir que números se generarán con una semilla particular, podemos sin embargo, duplicar una serie de números aleatorios usando la misma semilla. Es decir, cada vez que creamos un objeto de la clase Random con la misma semilla obtendremos la misma secuencia de números aleatorios. Esto no es útil en el caso de loterias, pero puede ser útil en el caso de juegos,  exámenes basados en una secuencia de preguntas aleatorias, las mismas para cada uno de los estudiantes, simulaciones que se repitan de la misma forma una y otra vez, etc.



Funciones miembro

Podemos cambiar la semilla de los números aleatorios en cualquier momento, llamando a la función miembro setSeed.

    rnd.setSeed(3816);
Podemos generar números aleatorios en cuatro formas diferentes:

rnd.nextInt();
genera un número aleatorio entero de tipo int

rnd.nextLong();
genera un número aleatorio entero de tipo long

rnd.nextFloat();
genera un número aleatorio de tipo float entre 0.0 y 1.0, aunque siempre menor que 1.0

rnd.nextDouble();
genera un número aleatorio de tipo double entre 0.0 y 1.0, aunque siempre menor que 1.0

Casi siempre usaremos esta última versión. Por ejemplo, para generar una secuencia de 10 números aleatorios entre 0.0 y 1.0 escribimos

    for (int i = 0; i < 10; i++) {
        System.out.println(rnd.nextDouble());;
    }
Para crear una secuencia de 10 números aleatorios enteros comprendidos entre 0 y 9 ambos incluídos escribimos

    int x;
for (int i = 0; i < 10; i++) {
        x = (int)(rnd.nextDouble() * 10.0);
        System.out.println(x);
    }
(int) transforma un número decimal double en entero int eliminando la parte decimal.



Comprobación de la uniformidad de los números aleatorios

Podemos comprobar la uniformidad de los números aleatorios generando una secuencia muy grande de números aleatorios enteros comprendios entre 0 y 9 ambos inclusive. Contamos cuantos ceros aparecen en la secuencia, cuantos unos, ... cuantos nueves, y guardamos estos datos en los elementos de un array.

Primero creamos un array ndigitos de 10 de elementos que son enteros.

    int[] ndigitos = new int[10];
Inicializamos los elementos del array a cero.

    for (int i = 0; i < 10; i++) {
        ndigitos[i] = 0;
    }
Creamos una secuencia de 100000 números aleatorios enteros comprendidos entre 0 y 9 ambos inclusive (véase el apartado anterior)

    for (long i=0; i < 100000L; i++) {
        n = (int)(rnd.nextDouble() * 10.0);
        ndigitos[n]++;
    }
Si n sale cero suma una unidad al contador de ceros, ndigitos[0]. Si n sale uno, suma una unidad al contador de unos, ndigitos[1], y así sucesivamente.

Finalmente, se imprime el resultado, los números que guardan cada uno de los elementos del array ndigitos

    for (int i = 0; i < 10; i++) {
        System.out.println(i+": " + ndigitos[i]);
    }
Observaremos en la consola que cada número 0, 1, 2...9 aparece aproximadamente 10000 veces.



Secuencias de números aleatorios

En la siguiente porción de código, se imprime dos secuencias de cinco números aleatorios uniformemente distribuídos entre [0, 1), separando los números de cada una de las secuencias por un carácter tabulador.

    System.out.println("Primera secuencia");
    for (int i = 0; i < 5; i++) {
        System.out.print("\t"+rnd.nextDouble());
    }
    System.out.println("");

    System.out.println("Segunda secuencia");
    for (int i = 0; i < 5; i++) {
        System.out.print("\t"+rnd.nextDouble());
    }
    System.out.println("");
Comprobaremos que los números que aparecen en las dos secuencias son distintos.

En la siguiente porción de código, se imprime dos secuencias iguales de números aleatorios uniformemente distribuídos entre [0, 1). Se establece la semilla de los números aleatorios con la función miembro setSeed.

    rnd.setSeed(3816);
    System.out.println("Primera secuencia");
    for (int i = 0; i < 5; i++) {
        System.out.print("\t"+rnd.nextDouble());
    }
    System.out.println("");

    rnd.setSeed(3816);
    System.out.println("Segunda secuencia");
    for (int i = 0; i < 5; i++) {
        System.out.print("\t"+rnd.nextDouble());
    }
    System.out.println("");
package azar;

import java.util.Random;

public class AzarApp {
  public static void main (String[] args) {
    int[] ndigitos = new int[10];
    int n;
   
    Random rnd = new Random();

// Inicializar el array
    for (int i = 0; i < 10; i++) {
        ndigitos[i] = 0;
    }

// verificar que los números aleatorios están uniformente distribuídos
    for (long i=0; i < 100000L; i++) {
// genera un número aleatorio entre 0 y 9
        n = (int)(rnd.nextDouble() * 10.0);
//Cuenta las veces que aparece un número
        ndigitos[n]++;
    }

// imprime los resultados
    for (int i = 0; i < 10; i++) {
        System.out.println(i+": " + ndigitos[i]);
    }

//Dos secuencias de 5 número (distinta semilla)
    System.out.println("Primera secuencia");
    for (int i = 0; i < 5; i++) {
        System.out.print("\t"+rnd.nextDouble());
    }
    System.out.println("");

    System.out.println("Segunda secuencia");
    for (int i = 0; i < 5; i++) {
        System.out.print("\t"+rnd.nextDouble());
    }
    System.out.println("");

//Dos secuencias de 5 número (misma semilla)
    rnd.setSeed(3816L);
    System.out.println("Primera secuencia");
    for (int i = 0; i < 5; i++) {
        System.out.print("\t"+rnd.nextDouble());
    }
    System.out.println("");

    rnd.setSeed(3816);
    System.out.println("Segunda secuencia");
    for (int i = 0; i < 5; i++) {
        System.out.print("\t"+rnd.nextDouble());
    }
    System.out.println("");
  }
}

Los paquetes

El paquete (package)

  • Los paquetes son una forma de organizar grupos de clases. Un paquete contiene un conjunto de clases relacionadas bien por finalidad, por ámbito o por herencia.
  • Los paquetes resuelven el problema del conflicto entre los nombres de las clases. Al crecer el número de clases crece la probabilidad de designar con el mismo nombre a dos clases diferentes. 
  • Las clases tienen ciertos privilegios de acceso a los miembros dato y a las funciones miembro de otras clases dentro de un mismo paquete.
En el Entorno Integrado de Desarrollo (IDE) JBuilder de Borland, un proyecto nuevo se crea en un subdirectorio que tiene el nombre del proyecto. A continuación, se crea la aplicación, un archivo .java que contiene el código de una clase cuyo nombre es el mismo que el del archivo. Se pueden agregar nuevas clases al proyecto, todas ellas contenidas en archivos .java situadas en el mismo subdirectorio. La primera sentencia que encontramos en el código fuente de las distintas clases que forman el proyecto es package o del nombre del paquete.

//archivo MiApp.java

package nombrePaquete;
public class MiApp{
//miembros dato
//funciones miembro
}
//archivo MiClase.java

package nombrePaquete;
public class MiClase{
//miembros dato
//funciones miembro
}

La palabra reservada import

Para importar clases de un paquete se usa el comando import. Se puede importar una clase individual
 import java.awt.Font;
o bien, se puede importar las clases declaradas públicas de un paquete completo, utilizando un arterisco (*) para reemplazar los nombres de clase individuales.
 import java.awt.*;
Para crear un objeto fuente de la clase Font podemos seguir dos alternativas
 import java.awt.Font;
 Font fuente=new Font("Monospaced", Font.BOLD, 36);
O bien, sin poner la sentencia import
 java.awt.Font fuente=new java.awt.Font("Monospaced", Font.BOLD, 36);
Normalmente, usaremos la primera alternativa, ya que es la más económica en código, si tenemos que crear varias fuentes de texto.
Se pueden combinar ambas formas, por ejemplo, en la definición de la clase BarTexto
import java.awt.*;
public class BarTexto extends Panel implements java.io.Serializable{
//...
}
Panel es una clase que está en el paquete java.awt, y Serializable es un interface que está en el paquete java.io

Los paquetes estándar

PaqueteDescripción
java.appletContiene las clases necesarias para crear applets que se ejecutan en la ventana del navegador
java.awtContiene clases para crear una aplicación GUI independiente de la plataforma
java.ioEntrada/Salida. Clases que definen distintos flujos de datos
java.langContiene clases esenciales, se importa impícitamente sin necesidad de una sentencia import.
java.netSe usa en combinación con las clases del paquete java.io para leer y escribir datos en la red.
java.utilContiene otras clases útiles que ayudan al programador

Los arrays

Un array es un medio de guardar un conjunto de objetos de la misma clase. Se accede a cada elemento individual del array mediante un número entero denominado índice. 0 es el índice del primer elemento y n-1 es el índice del último elemento, siendo n, la dimensión del array. Los arrays son objetos en Java y como tales vamos a ver los pasos que hemos de seguir para usarlos convenientemente:

Declarar el array
Crear el array
Inicializar los elementos del array
Usar el array


Declarar y crear un array

Para declarar un array se escribe

tipo_de_dato[] nombre_del_array;
Para declarar un array de enteros escribimos

int[] numeros;
Para crear un array de 4 número enteros escribimos

numeros=new int[4];
La declaración y la creación del array se puede hacer en una misma línea.

int[] numeros =new int[4];


Inicializar y usar los elementos del array

Para inicializar el array de 4 enteros escribimos

numeros[0]=2;
numeros[1]=-4;
numeros[2]=15;
numeros[3]=-25;
Se pueden inicializar en un bucle for como resultado de alguna operación

for(int i=0; i<4; i++){
numeros[i]=i*i+4;
}
No necesitamos recordar el número de elementos del array, su miembro dato length nos proporciona la dimensión del array. Escribimos de forma equivalente

for(int i=0; i<numeros.length; i++){
numeros[i]=i*i+4;
}
Los arrays se pueden declarar, crear e inicializar en una misma línea, del siguiente modo

int[] numeros={2, -4, 15, -25};
String[] nombres={"Juan", "José", "Miguel", "Antonio"};
Para imprimir a los elementos de array nombres  se escribe

for(int i=0; i<nombres.length; i++){
System.out.println(nombres[i]);
}
Java verifica que el índice no sea mayor o igual que la dimensión del array, lo que facilita mucho el trabajo al programador.

Para crear un array de tres objetos de la clase Rectangulo se escribe

Declarar
    Rectangulo[] rectangulos;
Crear el array
rectangulos=new Rectangulo[3];
Inicializar los elementos del array
rectangulos[0]=new Rectangulo(10, 20, 30, 40);
rectangulos[1]=new Rectangulo(30, 40);
rectangulos[2]=new Rectangulo(50, 80);
O bien, en una sola línea

Rectangulo[] rectangulos={new Rectangulo(10, 20, 30, 40),
new Rectangulo(30, 40), new Rectangulo(50, 80)};
Usar el array
Para calcular y mostrar el área de los rectángulos escribimos

for(int i=0; i<rectangulos.length; i++){
System.out.println(rectangulos[i].calcularArea());
}


Arrays multidimensionales

Una matriz bidimensional puede tener varias filas, y en cada fila no tiene por qué haber el mismo número de elementos o columnas. Por ejemplo, podemos declarar e inicializar la siguiente matriz bidimensional

    double[][] matriz={{1,2,3,4},{5,6},{7,8,9,10,11,12},{13}};
La primer fila tiene cuatro elementos {1,2,3,4}
La segunda fila tiene dos elementos {5,6}
La tercera fila tiene seis elementos {7,8,9,10,11,12}
La cuarta fila tiene un elemento {13}
Para mostrar los elementos de este array bidimensional escribimos el siguiente código

    for (int i=0; i < matriz.length; i++) {
        for (int j=0; j < matriz[i].length; j++) {
            System.out.print(matriz[i][j]+"\t");
        }
        System.out.println("");
    }
Como podemos apreciar, matriz.length nos proporciona el número de filas (cuatro), y matriz[i].length, nos proporciona el número de elementos en cada fila.

Mostramos los elementos de una fila separados por un tabulador usando la función print. Una vez completada una fila se pasa a la siguiente mediante println.

Los arrays bidimensionales nos permiten guardar los elementos de una matriz. Queremos crear y mostrar una matriz cuadrada unidad de dimensión 4. Recordaremos que una matriz unidad es aquella cuyos elementos son ceros excepto los de la diagonal principal i==j, que son unos. Mediante un doble bucle for recorremos los elementos de la matriz especificando su fila i y su columna j. En el siguiente programa

Se crea una matriz cuadrada de dimensión cuatro
Se inicializa los elementos de la matriz (matriz unidad)
Se muestra la matriz una fila debajo de la otra separando los elementos de una fila por tabuladores.
public class MatrizUnidadApp {
  public static void main (String[] args) {
    double[][] mUnidad= new double[4][4];
 
    for (int i=0; i < mUnidad.length; i++) {
        for (int j=0; j < mUnidad[i].length; j++) {
            if (i == j) {
                mUnidad[i][j]=1.0;
            }else {
                mUnidad[i][j] = 0.0;
            }
        }
    }

    for (int i=0; i < mUnidad.length; i++) {
        for (int j=0; j < mUnidad[i].length; j++) {
            System.out.print(mUnidad[i][j]+"\t");
        }
        System.out.println("");
    }
  }
}
Un ejemplo del uso de break con etiqueta y arrays multidimensionales

    int[][] matriz={  {32, 87, 3, 589},
                    {12, -30, 190, 0},
                    {622, 127, 981, -3, -5}};
    int numero=12;
    int i=0, j=0;

    buscado:
        for(i=0; i<matriz.length; i++){
            for(j=0; j<matriz[i].length; j++){
                if(matriz[i][j]==numero){
                    break buscado;
                }
            }
        }
        System.out.println("buscado: matriz("+ i+", "+j+")="+matriz[i][j]);