Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Descubre millones de libros electrónicos, audiolibros y mucho más con una prueba gratuita

Desde $11.99 al mes después de la prueba. Puedes cancelar en cualquier momento.

Java Curso Práctico
Java Curso Práctico
Java Curso Práctico
Libro electrónico889 páginas6 horas

Java Curso Práctico

Calificación: 0 de 5 estrellas

()

Leer vista previa

Información de este libro electrónico

Este libro recoge conocimientos necesarios para desarrollar aplicaciones profesionales con Java, siendo necesaria como mínimo la versión 8 del JDK.De forma práctica y didáctica se explican los conceptos de la programación y el diseño orientado a objetos, explicando a continuación cómo se aplica un enfoque moderno en el estudio de la estructuras de
IdiomaEspañol
Fecha de lanzamiento20 oct 2024
ISBN9788418551314
Java Curso Práctico

Relacionado con Java Curso Práctico

Libros electrónicos relacionados

Programación para usted

Ver más

Comentarios para Java Curso Práctico

Calificación: 0 de 5 estrellas
0 calificaciones

0 clasificaciones0 comentarios

¿Qué te pareció?

Toca para calificar

Los comentarios deben tener al menos 10 palabras

    Vista previa del libro

    Java Curso Práctico - José María Vegas Gertrudix

    Acerca del autor

    José María Vegas Gertrudix es ingeniero técnico en informática de sistemas y máster en desarrollo de videojuegos por la facultad de informática de la Universidad Complutense de Madrid.

    Con más de 15 años de experiencia, actualmente trabaja como Ingeniero de Software en Xeridia para CecaBank. A lo largo de su carrera, ha compaginado el ejercicio del magisterio privado personalizado para lograr que los alumnos alcancen sus metas con su labor en la empresa privada. Experto en banca electrónica y comercio electrónico, ha realizado aplicaciones para la inmensa mayoría de los grandes bancos mundiales.

    Escritor de artículos:

    En febrero de 2007, en la revista Sólo Programadores, titulado ¿Es viable una aplicación de escritorio con Java?.

    En agosto de 2011, en la página web de javaHispano, titulado Multitarea en Swing.

    En agosto de 2011, en la página web de javaHispano, titulado Tipos Abstractos de Datos y Diseño por Contrato.

    Prefacio

    El hardware es la parte física de un computador, mientras que el software es la parte lógica del mismo.

    Un Ingeniero de Software es aquella persona que construye un sistema software correcto y eficiente para ser ejecutado por el hardware de un computador.

    Construir software es divertido y gratificante. En cierto modo, nos convierte en creadores de mundos nuevos. Pero también es difícil, porque los computadores tienen la mala costumbre de hacer lo que les decimos que hagan, no lo que queremos que hagan.

    El camino para aprender a construir software correcto y eficiente es duro, pero estoy seguro de que libros como el que estás leyendo ahora mismo pueden ayudarte a hacerlo más fácil.

    El libro está totalmente actualizado para Java 8, se dirige a aquellos programadores que quieren poner a trabajar la tecnología Java en proyectos reales. Se exige de los lectores una experiencia mínima en programación; por ejemplo, no se va a explicar qué es una variable o un bucle. No obstante, no es necesario que el lector tenga conocimiento alguno de Programación Orientada a Objetos, Programación Funcional ni Programación Concurrente.

    El libro está pensado para ser leído secuencialmente, porque cada capítulo se construye sobre los conceptos aprendidos en los capítulos previos. No obstante, el lector que ya conozca determinados contenidos puede saltar directamente a los capítulos que le resulten desconocidos.

    Es preciso tener en cuenta que lo normal no es entender todos los conceptos presentados en el libro simplemente leyendo el texto. Es importante estudiar el código, escribirlo, ejecutarlo e incluso depurarlo para llegar a entenderlo.

    Aunque la obra aborda estructuras de programación modernas y actuales del lenguaje Java, no por ello olvida problemas clásicos de la programación, pero enfocados desde una óptica actual, como los métodos algorítmicos de backtracking y divide y vencerás, los problemas concurrentes del productor-consumidor o de la cena de los filósofos, las estructuras de datos clásicas, el patrón de diseño Observador o el patrón de arquitectura Modelo-Vista-Controlador.

    El código del proyecto está disponible para descargar en la web del libro en:

    www.ra-ma.es

    Se trata de un proyecto Maven que puede ser importado en Eclipse. Cualquier versión de este entorno de desarrollo integrado debería servir, siempre y cuando soporte Java 8.

    Me encantaría tener noticia de cualquier errata que exista en el libro o en el código. Para informar de una errata, está disponible el siguiente correo electrónico:

    jomaveger@gmail.com

    Al final de la presente obra, hay una sección dedicada a la bibliografía consultada para la elaboración de este manual. Quiero expresar mi más profundo agradecimiento tanto a los autores de dichas obras de consulta, como a las numerosas fuentes de internet que han ayudado a que este libro sea una realidad, tan amplias que son de difícil enumeración.

    1

    Programación Orientada a Objetos

    Tipos Abstractos de Datos, Clases y Objetos

    Cuando uno estudia programación, inevitablemente encuentra el término tipo de datos. Una triste confusión habitual en la mayoría de libros y manuales es considerar que el concepto matemático de conjunto es el más indicado para explicar qué es un tipo de datos; sin embargo, no es así, puesto que el concepto matemático que corresponde a la naturaleza de un tipo de datos es el de álgebra.

    Un tipo de datos es un álgebra, es decir, un conjunto de valores, llamado dominio, caracterizados por el conjunto de operaciones que sobre ellos se pueden aplicar y por el conjunto de propiedades que dichas operaciones poseen y que determinan inequívocamente su comportamiento. Un valor perteneciente al dominio de un tipo de datos se denomina instancia o ejemplar del tipo de datos.

    En programación, un tipo abstracto de datos es simplemente un tipo de datos que se define mediante una especificación que es independiente de cualquier implementación. Por tanto, la especificación del tipo abstracto de datos es la definición del mismo. Para acortar, con frecuencia abreviaremos el término tipo abstracto de datos con el acrónimo tad. En inglés el acrónimo es adt, de abstract data type.

    Si queremos construir un tad, lo primero que hay que hacer es definirlo, por tanto especificarlo. Y para ello no se puede usar el lenguaje informal. Porque es impreciso, ambiguo y redundante. Se emplea, en cambio, un lenguaje matemático, formal, que no vamos a estudiar.

    Lo que nos interesa remarcar es que una especificación de un tad es independiente de cualquier implementación. Cuando se habla de especificación y de implementación de un tad, parece que el mismo tipo abstracto de datos se divide en dos partes diferenciadas: Por un lado su especificación y por otro su implementación. Esto no es así. Lo que ocurre realmente es que no estamos ante un único tipo de datos sino ante dos tipos de datos distintos pero relacionados entre sí. El tipo abstracto de datos viene definido por su especificación y se llama tipo implementado, mientras que una implementación de ese tad es un tipo de datos distinto a dicho tad y se llama tipo implementador.

    Como es lógico, es necesario que el tipo implementador implemente correctamente el tipo implementado. Ahora bien, lo que ocurre es que el tipo implementado, el tad, se define mediante una especificación matemática propia, mientras que el tipo implementador se define mediante un módulo software. Eso complica la comparación entre ambos porque se trata de diferentes lenguajes matemáticos.

    Para que una implementación de un tad sea correcta, debe conservar las propiedades de la especificación que dice implementar. Para ello, se requieren ciertas condiciones matemáticas que en esencia dicen que el tipo implementador simula adecuadamente el tipo implementado; o, lo que es lo mismo, que el módulo software implementador define un álgebra que simula adecuadamente el álgebra definida por la especificación matemática.

    Esto implica que implementar un tad consiste intuitivamente en:

    Simular o representar los valores del tipo a implementar, el tad, por medio de valores del tipo implementador.

    Simular las operaciones del tipo a implementar mediante operaciones del tipo implementador.

    En consecuencia, una implementación es correcta cuando el usuario del tipo implementador no es capaz de notar la diferencia entre el tipo de datos especificado, el tad, y el tipo de datos implementador que lo simula.

    ¿Y toda esta información qué relación tiene con la Programación Orientada a Objetos? Es lo que vamos a estudiar a continuación.

    La clase es el concepto básico en el método orientado a objetos. Una clase es un módulo software que define, puede que de forma parcial, un tipo implementador; o, lo que es lo mismo, una clase es un módulo software que define una implementación, que puede ser parcial, de un tipo abstracto de datos.

    Dado que puede haber varias implementaciones distintas de un mismo tad, puede haber entonces varias clases distintas que implementen el mismo tad.

    Una clase que está completamente implementada define una implementación completa de un tipo abstracto de datos y se denomina efectiva. Una clase que está implementada sólo parcialmente define una implementación parcial de un tipo abstracto de datos y se llama abstracta o diferida; en el caso más extremo, una clase diferida puede carecer completamente de implementación alguna.

    Con estos conceptos en mente, podemos definir un programa, aplicación o sistema software orientado a objetos como una colección de tipos abstractos de datos parcial o totalmente implementados que interactúan entre sí.

    De modo análogo, se puede definir el método orientado a objetos como el método de desarrollo de software que basa la arquitectura de cualquier aplicación en una colección estructurada de implementaciones, que pueden ser parciales, de tipos abstractos de datos. La colección de estas implementaciones, las clases, que conforman un programa orientado a objetos es estructurada gracias a dos relaciones entre ellas: clientelismo y herencia.

    La clase fusiona dos conceptos, el de módulo software y el de tipo de datos. Por un lado, toda clase puede ser vista como un módulo de tal modo que el nombre del módulo es el de la clase. Por otro, toda clase puede ser vista como un tipo de datos de tal modo que el nombre del tipo de datos es el de la clase. La clase, vista como módulo, es la unidad básica de descomposición de un sistema software orientado a objetos y proporciona unas facilidades relacionadas que constituyen a su vez los componentes del álgebra que define la clase, vista como tipo de datos.

    El objeto es el segundo concepto básico en el método orientado a objetos. Un objeto es un ejemplar o instancia de una clase, es decir, una estructura de datos o representación física en la memoria del computador, y que por tanto puede ser creada y manipulada por un sistema software durante su ejecución, de un valor del dominio del tipo abstracto de datos que implementa la clase de ese objeto.

    Lo habitual en la inmensa mayoría de casos es no disponer de la especificación matemática de un tad. Por ello, se suele trabajar directamente tratando de implementar el tad a partir de una idea intuitiva o de una descripción en lenguaje informal del mismo. Como ayuda, podemos contar con distintas herramientas que proporcionan una serie de heurísticas que permiten escribir implementaciones correctas de un tad partiendo de la información que tenemos del mismo.

    La Estructura Estática: Las Clases

    Recordemos que una variable es un espacio de la memoria del computador, a cuyo contenido se accede mediante un identificador conocido como nombre de la variable.

    Java es un lenguaje con comprobación estática de tipos, porque comprueba el tipo de cada variable en tiempo de compilación. Los lenguajes de programación con comprobación dinámica de tipos comprueban el tipo de cada variable en tiempo de ejecución.

    Java es un lenguaje con comprobación estricta de tipos o fuertemente tipado, porque todas las variables tienen que tener un tipo declarado de forma explícita. Los lenguajes de programación con comprobación laxa de tipos o débilmente tipados no comprueban el tipo de cada variable o bien no obligan a definirlo.

    Java es un lenguaje orientado a objetos puro, pero su sistema de tipos no es uniforme. ¿Qué significa esta frase? Significa que, por un lado, Java es un lenguaje orientado a objetos puro en el sentido de que cada nuevo tipo que se crea en el lenguaje es una clase; pero, por otro lado, su sistema de tipos no es uniforme porque tiene una serie de tipos de datos primitivos predefinidos que no son clases.

    Por si el lector se lo está preguntando, es verdad que, en Java, es posible crear un nuevo tipo que no sea una clase; se trataría de una interfaz. Lo que ocurre es que una interfaz no es más que un caso extremo de clase completamente abstracta.

    Los tipos de datos primitivos predefinidos en Java son byte, short, int, long, float, double, char y boolean. Tienen semántica de valor, lo que significa que una variable de un tipo de datos primitivo contiene un valor de dicho tipo de datos.

    Toda clase tiene la necesidad de dos tipos de características:

    Algunas características se representarán mediante espacio de memoria, es decir, asociando un cierto elemento de información a cada objeto de la clase. Se denominarán atributos.

    Algunas características se representarán en el tiempo, es decir, definiendo un cierto algoritmo aplicable a todos los objetos de la clase. Se denominarán métodos.

    Los atributos construyen la estructura de datos de cada objeto de la clase.

    El estado de un objeto en un instante determinado viene dado por el valor de sus atributos en ese instante concreto.

    Los métodos son las operaciones aplicables a los objetos de una clase, y pueden acceder y manipular los atributos.

    La signatura de un método está formada por el nombre del método y su lista de parámetros; en la lista de parámetros es importante tanto el número de parámetros como los tipos de datos de los mismos como, finalmente, su orden dentro de la lista.

    La cabecera de un método está formada por la signatura del método más el tipo de retorno del mismo.

    Una clase en Java se define como pública, en un fichero de código fuente que tiene el mismo nombre de la clase, y con el modificador de visibilidad public. En un fichero de código fuente, sólo puede haber una clase pública, pero se pueden tener tantas clases no públicas como se desee.

    Es recomendable que cada fichero de código fuente del proyecto contenga sólo una clase.

    Si una clase no es pública, se dice que tiene visibilidad de paquete. Java permite agrupar las clases en una colección denominada paquete. Los paquetes son una forma cómoda de organizar nuestro trabajo, y también sirven para separar nuestro código de las bibliotecas de código que proporcionan otras personas. Además, garantizan la exclusividad de los nombres de las clases.

    Cuando una clase se define como pública, cualquier otra clase, sea del mismo paquete o no, puede acceder a ella. Sin embargo, cuando una clase no se define como pública, sólo las demás clases del mismo paquete pueden acceder a ella.

    La biblioteca estándar de Java se distribuye en un cierto número de paquetes, denominados java.lang, java.util, y así sucesivamente. Los paquetes estándar de Java son ejemplos de paquetes jerárquicos. Del mismo modo que se tienen subdirectorios anidados en el disco duro, es posible organizar los paquetes empleando niveles de anidamiento.

    Para ubicar una clase dentro de un paquete, es preciso poner el nombre del paquete en la parte superior del fichero de código fuente:

    package org.jomaveger.examples.chapter1;

    Si no se pone una sentencia package en el código fuente, entonces la clase de ese fichero pertenece al paquete predeterminado. El paquete predeterminado carece de nombre.

    Es recomendable que ninguna clase del proyecto pertenezca al paquete predeterminado.

    Los ficheros de un paquete se ubican en un subdirectorio que coincide con el nombre completo del paquete. Para anidar un nuevo paquete, se crearía un nuevo subdirectorio dentro de aquél.

    Las clases pueden utilizar todas las clases de su propio paquete, así como todas las clases públicas de otros paquetes. Para acceder a las clases públicas de otros paquetes, se hace uso de la sentencia import. Se puede importar una clase concreta o bien todo el paquete. Las sentencias import se ponen al principio de los ficheros de código fuente, pero por debajo de la sentencia package.

    Vamos a estudiar el aspecto que tiene una clase en Java. Nuestro primer ejemplo modela un contador.

    package org.jomaveger.examples.chapter1;

    public class Counter {

        private Integer count;

        public Counter() {

            this.count = 0;

        }

        public Integer currentCount() {

            return this.count;

        }

        public void incrementCount() {

            this.count = this.count + 1;

        }

        public void reset() {

            this.count = 0;

        }

    }

    Como es de esperar, esta clase se almacenará en un fichero de código fuente llamado Counter.java.

    En la definición de una clase, tanto los atributos como los métodos pueden llevar asociado también un modificador de visibilidad. Si el modificador de visibilidad de una característica de una determinada clase es privado (mediante la palabra reservada private), significa que sólo las demás características de la clase pueden acceder a ella. Si el modificador de visibilidad de una característica de una determinada clase es público (mediante la palabra reservada public), significa que cualquier clase de cualquier paquete puede acceder a ella. Si el modificador de visibilidad de una característica de una determinada clase está ausente, se dice que tiene visibilidad de paquete, lo que significa que cualquier clase puede acceder a ella, pero sólo si está en el mismo paquete.

    Es recomendable no emplear visibilidad de paquete para las características de las clases del proyecto.

    El conjunto de métodos públicos de una clase se denomina interfaz de la clase.

    Principio de encapsulamiento

    El único mecanismo de acceso a los atributos de una clase debe ser la interfaz de dicha clase, es decir, mediante el conjunto de métodos públicos de la misma.

    Principio de ocultamiento de la información

    Los atributos de una clase deben estar ocultos o, lo que es lo mismo, deben ser privados.

    Principio de diseño de separación de métodos en consultas y órdenes

    Los métodos de una clase se organizan en consultas y órdenes. Las consultas devuelven un resultado que representa información asociada con el estado del objeto, pero no modifican dicho estado. Las órdenes pueden cambiar el estado del objeto pero no devuelven resultado alguno.

    En nuestra clase de ejemplo, los métodos incrementCount() y reset() son órdenes, mientras que currentCount() es una consulta.

    El método Counter() es un constructor y se utiliza únicamente para construir objetos de la clase. Los constructores no se consideran características de la clase.

    En todos los métodos está disponible el parámetro implícito this, que hace referencia al objeto actual de la clase que lo engloba. El parámetro implícito this puede acceder a todas las características de la clase, sean privadas o no. También es posible ver this como un puntero al objeto al que pertenece el método que se está ejecutando.

    En nuestro ejemplo de clase Counter, el tipo del atributo count es la clase Integer. Se podía haber definido también count con el tipo primitivo int, pero de este modo, al evitar utilizar dicho tipo primitivo, se diseña un código fuente más uniforme, lo que es recomendable.

    Integer es lo que se denomina una clase de envoltorio del tipo primitivo int. Todos los tipos primitivos poseen sus contrapartidas en forma de clases, que suelen denominarse envoltorios. Las clases de envoltorio tienen nombres bastante evidentes: Integer, Long, Float, Double, Short, Byte, Character, Void y Boolean. Las clases envoltorio son inmutables: No se puede modificar el valor que contienen una vez que se ha construido el envoltorio.

    Una característica notable de Java es que el compilador inserta automáticamente instrucciones para empaquetar (en inglés autoboxing) un tipo primitivo en su respectiva clase de envoltorio cuando se realiza una asignación. El empaquetado se produce, por ejemplo, de un número entero int en un objeto de la clase Integer en la siguiente instrucción de asignación de nuestra clase:

    this.count = 0;

    De forma semejante, el compilador inserta automáticamente instrucciones para desempaquetar (en inglés autounboxing) el objeto, operar con el valor resultante y volver a empaquetarlo, cuando hay involucradas clases de envoltorio. Por ejemplo, este proceso ocurre en la siguiente instrucción de nuestra clase:

    this.count = this.count + 1;

    El empaquetado automático tiene una penalización en el rendimiento por lo que, en aplicaciones que requieran una computación numérica intensiva, puede ser necesario emplear los tipos primitivos, en lugar de sus clases de envoltorio correspondientes, para alcanzar el objetivo deseado de rendimiento de la aplicación.

    Sea S una clase. Una clase C que contiene una declaración de la forma

    S a;

    Se dice que es cliente de S. A su vez, se dice que S es un proveedor de C.

    En nuestra clase de ejemplo, Counter es un cliente de Integer e Integer es un proveedor de Counter. Entre ambos se establece una relación de clientelismo.

    El mecanismo básico de la computación orientada a objetos es la invocación de una característica. En la ejecución de un sistema software orientado a objetos, toda computación es llevada a cabo a partir de la invocación de ciertas características en ciertos objetos. De manera general, una llamada a una característica puede manifestarse de dos formas:

    x.a donde x es el receptor de la llamada, una variable que en tiempo de ejecución será vinculada a un cierto objeto; x tiene un cierto tipo, dado por una clase C, luego a tiene que ser una característica de C, en concreto un atributo de C.

    x.m(u, v, ...) donde x es el receptor de la llamada, una variable que en tiempo de ejecución será vinculada a un cierto objeto; x tiene un cierto tipo, dado por una clase C, luego m tiene que ser una característica de C. Más precisamente, m es un método que puede tener cero, uno o más parámetros y u, v, ..., son los parámetros reales de la llamada, que deben igualar en tipo y número a los parámetros formales declarados para m en C.

    El efecto en tiempo de ejecución de invocar una característica en un receptor es aplicar dicha característica al objeto vinculado al mencionado receptor, después de haber iniciado cada parámetro formal (si lo hay y si la característica es un método) con el valor del correspondiente parámetro real.

    Toda operación en la computación orientada a objetos es relativa a un cierto objeto, la instancia actual en el momento de ejecutar esa operación. Es this el receptor de la llamada actual. En una llamada subsiguiente, this representará el receptor de esa nueva llamada.

    Por tanto:

    Ningún elemento software es ejecutado salvo como parte de la llamada a un método.

    Toda llamada tiene su receptor.

    En ocasiones, este receptor está implícito y no aparece en la invocación de una característica. Así, por ejemplo, el método incrementCount() podía haberse escrito correctamente también del siguiente modo:

    count = count + 1;

    La Estructura Dinámica: Los Objetos

    En Java, se utiliza un tipo especial de método, llamado constructor, para crear una nuevo objeto de una clase y darle un valor inicial. Un constructor tiene el mismo nombre que la clase. Una clase puede tener más de un constructor. Un constructor puede tener cero, uno o más parámetros.

    La capacidad de que una clase pueda tener más de un constructor se denomina sobrecarga de métodos. Java permite sobrecargar cualquier método, no sólo los métodos constructores. La sobrecarga se produce cuando varios métodos poseen el mismo nombre pero distintos parámetros. Dos métodos poseen distintos parámetros si el número de parámetros de entrada es distinto, o bien si es igual pero el tipo de datos de alguno de los parámetros de entrada es diferente. El compilador tiene que averiguar cuál de los métodos debe ser invocado. El método correcto se determina comparando el número y tipos de los parámetros formales que aparecen en las declaraciones de los diferentes métodos con el número y tipos de los valores de los parámetros reales empleados en la llamada al método. Se produce un error de compilación si el compilador no localiza los parámetros o si se halla más de una solución posible.

    Si no se le da valor explícitamente a un atributo en un constructor, ese atributo recibirá automáticamente un valor predeterminado: Los números se ponen a 0, los caracteres reciben el carácter nulo ('\u0000'), los booleanos reciben el valor falso (false) y las variables referencia reciben el valor nulo (null).

    Ésta es una diferencia importante entre los atributos y las variables locales. Las variables locales de un método siempre deben recibir un valor inicial explícito.

    Se denomina constructor predeterminado al constructor sin parámetros. Si se escribe una clase que carezca por completo de constructores, Java proporciona un constructor predeterminado, que da a todos los atributos sus valores predeterminados.

    Es recomendable no emplear la iniciación predeterminada de atributos en las clases del proyecto.

    Es recomendable no escribir clases que carezcan por completo de constructores.

    Para iniciar un atributo, podemos limitarnos a asignar explícitamente un valor a cualquier atributo durante la definición de la clase. Esta asignación se lleva a cabo antes de que se ejecute el constructor, y es útil si todos los constructores de una clase deben asignar el mismo valor a un determinado atributo.

    Si la primera sentencia de un constructor posee la forma this(...), entonces el constructor llama a otro constructor de la misma clase. Resulta útil emplear la palabra reservada this de esta forma; así, sólo es preciso escribir una vez el código común de construcción.

    Si deseamos construir un objeto de nuestra clase Counter, combinamos el constructor con el operador new, de la siguiente manera:

    new Counter();

    Esta expresión construye un nuevo objeto. El atributo del objeto es iniciado con el valor cero.

    Es posible aplicar un método a un objeto recién construido. Así, podríamos hacer lo siguiente:

    new Counter().incrementCount();

    En este caso, se crea un nuevo objeto, cuyo atributo acaba teniendo el valor uno.

    En estos dos ejemplos, el objeto construido es usado una sola vez. Normalmente, se desea mantener una conexión con los objetos que se construyen para poder seguir usándolos. La manera de hacerlo es almacenando el objeto en una variable referencia:

    Counter counter = new Counter();

    La variable counter es una variable referencia. Una variable referencia es aquella cuyo tipo de datos es una clase y almacena una referencia o puntero al objeto o, lo que es lo mismo, una dirección de la posición de memoria en la que se almacena el objeto.

    Recordemos que los tipos de datos predefinidos tienen semántica de valor, lo que significa que una variable de un tipo de datos primitivo contiene un valor de dicho tipo de datos. Por el contrario, las clases tienen semántica de referencia, lo que significa que una variable de una clase contiene la dirección de la posición de memoria en la que se almacena un objeto de esa clase, y no el propio objeto.

    Se dice que los tipos de datos predefinidos son tipos valor, mientras que las clases son tipos referencia.

    Hay una diferencia importante entre los objetos y las variables referencia. Por ejemplo, la sentencia:

    Counter myCounter;

    Define una variable referencia, myCounter, que puede apuntar a objetos de tipo Counter. Es importante darse cuenta de que la variable myCounter no es un objeto y, de hecho, ni siquiera apunta todavía a un objeto. En este punto, no se puede invocar método alguno de la clase Counter sobre esta variable referencia. La sentencia:

    myCounter.incrementCount();

    Provocaría un error en tiempo de compilación.

    Es necesario primero iniciar la variable referencia. Hay tres opciones. La primera es iniciar la variable con un objeto recién construido:

    myCounter = new Counter();

    La segunda es asignar a la variable un objeto existente:

    myCounter = counter;

    En este segundo caso, ambas variables referencia apuntan al mismo objeto. Esta segunda opción tiene una implicación especial.

    Si x e y son dos variables referencia e y no es nulo, la asignación x = y da lugar a que x e y estén conectados al mismo objeto. El resultado es ligar x e y de una forma duradera, hasta que se produzca alguna asignación posterior a cualquiera de ellos. La conexión de x al mismo objeto que y se conoce como alias dinámico (en inglés dynamic aliasing): Alias porque la asignación hace que se pueda acceder y manipular un objeto a través de dos variables referencia; dinámico porque el alias ocurre en tiempo de ejecución.

    Es importante volver a remarcar que una variable referencia no contiene realmente un objeto, sólo apunta a un objeto. En Java, el valor de cualquier variable referencia es un puntero a un objeto que está almacenado en otro lugar.

    El valor de retorno del operador new es, por tanto, una referencia o puntero. Una sentencia como la siguiente:

    Counter anotherCounter = new Counter();

    Tiene dos partes. La expresión new Counter() construye un objeto de tipo Counter, y su valor es una referencia al objeto recién creado. Esa referencia (o dirección de memoria) es entonces almacenada en la variable anotherCounter.

    La tercera opción es establecer explícitamente una variable referencia al valor nulo (null) para indicar que actualmente no se refiere a objeto alguno.

    Counter yourCounter = null;

    Si se invoca un método sobre una variable referencia que tiene el valor nulo, se obtiene un error en tiempo de ejecución.

    Como ya se explicó, las variables locales de referencia no son automáticamente iniciadas al valor nulo, así que es obligatorio iniciarlas, bien invocando a new, bien asignándoles null o un objeto ya creado.

    Una referencia es entonces un valor en tiempo de ejecución que o bien es nulo o bien está vinculado. Si está vinculada, una referencia identifica un único objeto (se dice entonces que está vinculada a ese objeto en particular).

    El concepto de referencia trae consigo el concepto de identidad de un objeto. Todo objeto creado durante la ejecución de un sistema orientado a objetos tiene una identidad única, independientemente del estado del objeto. En particular:

    Dos objetos con diferentes identidades pueden tener idéntico estado.

    A la inversa, el estado de un objeto puede variar durante la ejecución de un sistema; pero este hecho no afecta a la identidad del objeto.

    La identidad de un objeto es, por tanto, la propiedad del objeto que lo distingue de todos los demás. Esta unicidad de un objeto se consigue a través de un identificador de objeto, que es implementado a través de punteros de forma independiente de los atributos que definen el estado del objeto. Generalmente, un identificador de objeto es generado por el sistema y no puede ser modificado por el programador.

    Podemos detallar en este momento qué etapas sigue la construcción de un objeto en Java cuando se invoca a un constructor de una clase:

    Primero, los atributos reciben el valor por defecto que les corresponde.

    Segundo, se ejecutan las sentencias de iniciación de los atributos, en el orden en el que aparecen en la definición de la clase.

    Tercero, si la primera sentencia del constructor llama a un segundo constructor con this(...), entonces se ejecuta el cuerpo de este segundo constructor.

    Cuarto, se ejecuta el cuerpo del constructor. Si algún atributo no ha sido iniciado explícitamente, conservará al final de este paso su valor por defecto.

    El lenguaje de programación Java utiliza siempre como mecanismo de paso de parámetros en los métodos el llamado paso por valor; es decir, cuando se realiza la llamada a un método, cada parámetro formal (es decir, cada parámetro declarado en la definición del método) reserva un espacio de memoria independiente y recibe una copia del valor del correspondiente parámetro real (es decir, del correspondiente parámetro indicado en la llamada al método).

    Hay, no obstante, dos tipos de parámetros de métodos:

    Si el parámetro es de un tipo predefinido, el paso por valor significa que el parámetro formal recibe un valor nuevo copia del original y, por tanto, no se puede modificar el parámetro real durante la ejecución del método.

    Si el parámetro es de un tipo referencia, el paso por valor significa que el parámetro formal recibe una copia de una dirección de memoria y, por tanto, la referencia original no se puede modificar, durante la ejecución del método, logrando que el parámetro real referencie un objeto distinto. Lo que sí es posible es modificar el estado del parámetro real a través de la interfaz de su clase; para ello, se invocan a los métodos correspondientes a través de su parámetro formal.

    Si un parámetro formal de un método tiene el mismo nombre que un atributo, entonces el nombre del parámetro ensombrece al nombre del atributo dentro del método. Para diferenciar entre el acceso al parámetro y el acceso al atributo, sería necesario utilizar this para acceder al atributo dentro de dicho método.

    En el momento de retornar un valor, los métodos simplemente devuelven el valor de la variable que retornan, bien una referencia a un objeto o bien un valor de un tipo primitivo.

    Java tiene recolección de basura, un mecanismo automático que permite recuperar como disponible el espacio de memoria ocupado por aquellos objetos de un programa en ejecución que son inalcanzables, es decir, aquellos objetos que existen en memoria pero que no pueden ser recuperados a partir de las variables referencia que existen en el programa. Gracias a la recolección de basura, no es necesario liberar memoria explícitamente cuando ya no necesitamos seguir trabajando con un objeto determinado. Los objetos se crean y se destruyen en una zona de memoria denominada el montículo (o heap, en inglés).

    Hemos estudiado que el mecanismo básico de la computación orientada a objetos es la invocación de una característica. Implica que los objetos interactúan entre sí mediante el intercambio de mensajes. Los mensajes incluyen la petición para la realización de una acción acompañada de información adicional (parámetros) necesaria para realizar esa acción. Si un objeto acepta un mensaje, entonces acepta la responsabilidad de llevar a cabo la acción que indica ese mensaje. Esta acción es llevada a cabo mediante la ejecución de un método.

    El comportamiento de un objeto está determinado por los métodos que implementa como respuesta a los mensajes que recibe. Es lo que hemos denominado interfaz de la clase ya que, debido al principio de encapsulamiento, los métodos públicos son el único interfaz mediante el cual podemos comunicarnos con el objeto.

    Mensajes y métodos no son equivalentes ya que, ante el mismo mensaje, un objeto puede ejecutar distintos métodos debido a diferentes factores. Uno de estos factores es la sobrecarga de métodos.

    Características Constantes y Globales

    Es posible definir un atributo de una clase como final (mediante la palabra reservada final), lo que significa que dicho atributo debe ser iniciado cuando el objeto es construido, pero después no puede ser modificado de nuevo.

    Un atributo final implica que se garantiza que:

    Su valor ha sido establecido al finalizar la ejecución de cada constructor.

    Si es de un tipo predefinido, su valor no se puede cambiar; si es de un tipo referencia, es imposible reasignar la variable referencia a un objeto diferente, es decir, la variable referencia apunta siempre al mismo objeto.

    Es posible definir cada parámetro formal de un método como final. Si bien el paso por valor impide que se pueda modificar el parámetro real, establecer el parámetro formal como final impide además que, dentro del método, la variable de dicho parámetro formal pueda ser reasignada.

    Es recomendable que los parámetros formales de los métodos sean finales.

    Cada objeto tiene su propia copia de todos los atributos. Si se define un atributo como estático (mediante la palabra reservada static), hay sólo un atributo por clase; es más, el atributo existirá incluso aunque no exista objeto alguno de la clase, debido a que pertenece a la clase y no a un objeto individual concreto.

    Un atributo puede ser final y estático, y entonces se trata de una constante.

    Es recomendable que los atributos constantes de una clase sean públicos.

    Un método estático es aquel método que no opera sobre un objeto; es decir, carece del parámetro implícito this. Un método estático de una clase no puede acceder a los atributos de esa clase dado que no opera sobre objeto alguno, pero sí puede acceder a los atributos estáticos que tenga dicha clase.

    Se puede usar un método estático en alguna de las dos posibles situaciones siguientes:

    Cuando no necesita acceder al estado del objeto porque todos los parámetros que necesita le son suministrados como parámetros explícitos.

    Cuando sólo necesita acceder a atributos estáticos de la clase.

    Para acceder a una característica estática de una clase, se utiliza el nombre de la clase, no un objeto de la misma.

    Al igual que ocurre con los atributos no estáticos, los atributos estáticos toman el valor predefinido cero, carácter nulo, falso o nulo, en función de su tipo de datos, a menos que se les asigne uno distinto. Para iniciar un atributo estático, o bien se proporciona un valor inicial en la definición del mismo, o bien se emplea un bloque de iniciación estática. Este proceso de iniciación estática tiene lugar cuando la clase es cargada por primera vez y, por tanto, antes de la invocación a constructor alguno; es más, es independiente de que más tarde se invoque o no a algún constructor de la clase. En este proceso, las asignaciones de valores iniciales a atributos estáticos y los bloques de iniciación estática se ejecutan en el orden en el que aparecen en la definición de la clase. Si algún atributo estático no ha sido iniciado explícitamente, conservará al final de este paso su valor por defecto.

    La sentencia import (mediante import static) también permite importar un método o atributo estático específico de una clase, o bien todos los métodos y atributos estáticos de la clase; en ambos casos, gracias a dicha importación, las características estáticas podrían utilizarse en el código sin utilizar como prefijo el nombre de la clase.

    Como hemos comentado, se pueden invocar a métodos estáticos de una clase sin tener objeto alguno de la misma. El método estático por excelencia en Java es el método predefinido main(). De hecho, cuando un programa en Java empieza a ejecutarse, no existe objeto alguno; para que la máquina virtual pueda empezar la ejecución de la aplicación, el método main() debe estar definido de la siguiente forma en al menos una de las clases del proyecto:

    public static void main(String[] args) {

    ...

    }

    El método main() es el encargado de crear los primeros objetos que la aplicación necesita. Según la lógica de la aplicación, es de esperar que estos objetos a su vez creen otros objetos, y así sucesivamente.

    Toda clase puede tener su método main(). Simplemente, a la hora de ejecutar la aplicación, hay que seleccionar cuál de las clases de la aplicación se selecciona como principal, lo que significa que la máquina virtual ejecutará el método main() de dicha clase.

    Nuestro siguiente clase, Point2D, que modela un punto bidimensional o vector de posición, muestra muchos de los conceptos que hemos visto. Esta clase se almacenará en un fichero de código fuente llamado Point2D.java.

    package org.jomaveger.examples.chapter1;

    import static java.lang.Math.*;

    public class Point2D {

        private Double x;

        private Double y;

        private static Integer numberOfPoints;

        static {

            Point2D.numberOfPoints = 0;

        }

        public Point2D() {

            this(0.0, 0.0);

        }

        public Point2D(final Double x, final Double y) {

            this.x = x;

            this.y = y;

            Point2D.numberOfPoints++;

        }

        public Double getX() {

            return this.x;

        }

        public Double getY() {

            return this.y;

        }

        public static Integer getNumberOfPoints() {

            return Point2D.numberOfPoints;

        }

        public Double getRho() {

            return sqrt(pow(x, 2) + pow(y, 2));

        }

        public Double getTheta() {

            Double angrad = atan2(y, x);

            return toDegrees(angrad);

        }

        public Double getDistance(final Point2D p) {

            return sqrt(pow(x - p.x, 2) + pow(y - p.y, 2));

        }

        public void translate(final Double a, final Double b) {

            x += a;

            y += b;

        }

        public void scale(final Double factor) {

            x *= factor;

            y *= factor;

        }

        public void rotate(final Point2D p, final Double angle) {

            Double angrad = toRadians(angle);

            Double x1 = this.x - p.x;

            Double y1 = this.y - p.y;

            Double x2 = x1 * cos(angrad)

                        - y1 * sin(angrad);

            Double y2 = x1 * sin(angrad)

                        + y1 * cos(angrad);

            this.x = x2 + p.x;

            this.y = y2 + p.y;

        }

    }

    El atributo estático numberOfPoints permite llevar la cuenta del número de puntos que creamos en nuestro programa.

    El método getRho() devuelve el radio de las coordenadas polares del punto.

    El método getTheta() devuelve el ángulo en grados de las coordenadas polares del punto. Por convenio, el ángulo es positivo en sentido contrario a las agujas del reloj, y negativo en sentido de las agujas del reloj.

    El método getDistance() devuelve la distancia entre dos puntos.

    El método translate() traslada un punto, mientras que el método scale()lo escala.

    Finalmente, el método rotate() rota el punto alrededor de otro punto un determinado ángulo expresado en grados.

    A continuación, mostramos un ejemplo de clase principal que hace uso de las clases Counter y Point2D:

    package org.jomaveger.examples.chapter1;

    public class Main {

        public static void main(String[] args) {

            Counter count = new Counter();

            System.out.println(Counter value: +  count.currentCount());

            count.incrementCount();

            count.incrementCount();

            count.incrementCount();

            System.out.println(Counter value: + count.currentCount());

            System.out.println(Number of Points: + Point2D.getNumberOfPoints());

            Point2D point = new Point2D(12.0, 5.0);

            System.out.println(Number of Points: + Point2D.getNumberOfPoints());

            System.out.println(Radio de Coordenadas Polares: + point.getRho());

            System.out.println(Angulo en Grados de Coordenadas Polares:

                + point.getTheta());

            Point2D pointA = new Point2D(-1.0 , -1.0);

            Point2D pointB = new Point2D(2.0 , 3.0);

            System.out.println(Distancia del punto (-1.0, -1.0) +

                    al punto (2.0, 3.0): + pointA.getDistance(pointB));

            Point2D pointC = new Point2D(1.0 , 3.0);

            Point2D pointD = new Point2D(3.0 , 6.0);

            pointD.rotate(pointC, 270.0);

            System.out.println(Resultado de rotar el punto (3.0, 6.0) +

                    alrededor del punto (1.0, 3.0) un angulo de 270 grados:

                    + pointD.getX() + - + pointD.getY());

            System.out.println(Number of Points: + Point2D.getNumberOfPoints());

        }

    }

    La ejecución de esta clase principal produciría la siguiente salida:

    Counter value: 0

    Counter value: 3

    Number of Points: 0

    Number of Points: 1

    Radio de Coordenadas Polares: 13.0

    Angulo en Grados de Coordenadas Polares: 22.619864948040426

    Distancia del punto (-1.0, -1.0) al punto (2.0, 3.0): 5.0

    Resultado de rotar el punto (3.0, 6.0) alrededor del punto (1.0, 3.0) un angulo de

    270 grados: 3.9999999999999996 - 0.9999999999999996

    Number of Points: 5

    Process finished with exit code 0

    Herencia

    La herencia es el mecanismo por el cual una clase X puede heredar características de una clase Y (se dice que X hereda de Y), de modo que los objetos de la clase X tienen acceso a los atributos y métodos públicos y protegidos de la clase Y, sin necesidad de volver a definirlos.

    La palabra reservada extends indica que vamos a crear una nueva clase a partir de una clase existente. La clase existente se denomina superclase, supertipo, clase base o clase padre. La nueva clase se denomina subclase, subtipo, clase derivada o clase hija.

    Un descendiente de una clase C es cualquier clase que herede directa o indirectamente de C, incluyendo el propio C. Un descendiente propio de C es otro descendiente de C que no sea el propio C.

    Desde un punto de vista más abstracto, existe una relación es-un entre la subclase y la superclase, de modo que todo objeto de la subclase es-un objeto de la superclase. Pero, además, las subclases tienen más funcionalidad que sus superclases.

    La razón del empleo de los prefijos super y sub viene de la teoría de conjuntos; ocurre que el conjunto de todos los objetos de la superclase es un superconjunto, es decir, contiene al conjunto de todos los objetos de la subclase; o, lo que es lo mismo, el conjunto de todos los objetos de la subclase es un subconjunto, es decir, es contenido por el conjunto de todos los objetos de la superclase.

    La forma general de definición de una clase que hereda de una superclase es la siguiente:

    public class subclase extends superclase {

        // cuerpo de la clase

    }

    Java no soporta herencia múltiple, por lo que sólo es posible especificar una única superclase para cualquier subclase que se defina. Sí es posible crear una jerarquía de herencia en la que una subclase es a su vez una superclase de otra subclase; no obstante, una clase no puede convertirse en superclase de sí misma.

    Aunque una subclase incluye todas las características de su superclase, no puede tener acceso a aquellas características de la superclase que han sido definidas como privadas. Por ejemplo, la única manera de acceder a un atributo privado de la superclase es a través del método público apropiado de la misma. No obstante, existe otra solución a este problema. La superclase podría declarar el atributo como protegido (mediante la palabra reservada protected); así, en general, si el modificador de visibilidad de una característica de una determinada clase es protegido, significa que las subclases, además de las características de la propia clase, pueden acceder a ella. Sin embargo, los atributos protegidos presentan algunos problemas:

    El diseñador de la superclase no tiene control alguno sobre los autores de las subclases, de modo que cualquier método de las subclases puede corromper la información almacenada en los atributos de la superclase.

    ¿Disfrutas la vista previa?
    Página 1 de 1