Las clases
En Java la unidad de programación es la clase. No hay funciones sueltas: todo el código vive dentro de una clase, y a partir de ella se instancian los objetos. Las clases contienen atributos —los datos, también llamados campos— y métodos —las operaciones o funciones—.
La comparación que mejor funciona es la del plano de construcción: las clases son para los objetos lo que el plano es para las casas. De un mismo plano se construyen muchas casas, y de una misma clase se instancian muchos objetos. Y así como no se puede vivir en un plano —hay que construir la casa primero—, no se puede «hacer funcionar» una clase: hay que crear un objeto de esa clase e invocar sus métodos.
Al empaquetar el software en forma de clases, el programador puede reutilizarlas en desarrollos posteriores. Los grupos de clases relacionadas forman paquetes, que son componentes reutilizables. Reutilizar ahorra tiempo y esfuerzo, y además hace sistemas más confiables: las clases que ya existen pasaron por un proceso largo de pruebas, depuración y optimización.
Clase y objeto, con un ejemplo
Una clase es la generalización de un tipo específico de objetos: el conjunto de características y comportamientos de todos los objetos que la componen. La clase Marcador, por ejemplo, tiene todas las características —tamaño, color, grosor del punto— y todos los métodos —pintar, marcar, subrayar— que pueden tener los marcadores que existen en la realidad. Y un marcador en particular, el permanente negro de punta fina que está arriba del escritorio, es un objeto de la clase Marcador.
Con una clase que represente una cuenta bancaria pasa lo mismo: tendría un método para depositar dinero, otro para retirar y otro para consultar el saldo. Esa clase describe lo que cualquier cuenta puede hacer; cada cuenta concreta es un objeto.
Los objetos se manejan con mensajes
Cuando se aprieta el pedal del acelerador de un auto, se le está enviando un mensaje al auto para que realice la tarea de aumentar la velocidad. En programación pasa exactamente lo mismo: se le envían mensajes a un objeto llamando a sus métodos, para que hagan las tareas que la clase describe.
Y un auto, además de las tareas que puede hacer, tiene atributos: su color, la cantidad de puertas, la nafta que queda en el tanque, los kilómetros recorridos. Todos esos atributos también están en el plano de diseño, y están asociados a cada auto. Lo mismo con las clases: tienen atributos, y los objetos de esas clases tienen sus propios valores para esos atributos.
En el ejemplo del banco: la clase cuenta bancaria tiene un atributo llamado saldo, y cada objeto de esa clase conoce el saldo que le corresponde. Los atributos se especifican mediante las variables de instancia de la clase.
La sintaxis
Por convención los nombres de las clases empiezan con mayúscula. La declaración tiene esta forma:
[Modificador] class NombreClase [extends NombreClasePadre] [implements interface] {
Declaración de variables;
Declaración de métodos;
}
Los corchetes indican que esas partes son optativas. extends se usa cuando la clase hereda de otra y implements cuando implementa una interfaz; las dos cosas se ven más abajo.
Los objetos
Un objeto es una abstracción encapsulada de datos y de los procedimientos que los manipulan: una entidad con atributos —sus propiedades— y formas de operar sobre ellos, que son los métodos. Todos los objetos tienen características —un auto tiene tamaño, peso, volumen, color— y todos tienen uno o varios comportamientos: acelera, frena, dobla.
Y así como las personas se envían mensajes unas a otras, los objetos también se comunican mediante mensajes. Un objeto cuenta bancaria puede recibir el mensaje de disminuir su saldo en cierta cantidad, porque el cliente retiró ese dinero.
Los objetos pueden modelar cosas muy distintas entre sí: dispositivos, roles, organizaciones, sucesos, ventanas, íconos.
Variables de instancia y estado
Los atributos se representan como variables en la declaración de la clase. Esas variables se llaman campos y se declaran dentro de la clase pero fuera de los cuerpos de los métodos. Cuando cada objeto de la clase mantiene su propia copia de un atributo, ese campo se conoce como variable de instancia: cada instancia de la clase tiene su copia separada en memoria.
El estado de un objeto está representado por sus variables de instancia. Es la definición de la lección 1, ahora con una ubicación concreta en el código.
El ocultamiento de datos
La mayoría de las declaraciones de variables de instancia van precedidas por la palabra clave private. Los miembros declarados private son accesibles solo para los métodos de la clase en la que se declaran. Ese proceso se conoce como ocultamiento de datos u ocultamiento de información.
Cuando el programa instancia un objeto, la variable queda encapsulada dentro del objeto y solo es accesible para los métodos de su clase. Eso evita que una clase en otra parte del programa modifique la variable por accidente.
Hay que poner un modificador de acceso delante de cada declaración de campo y de cada método. La regla práctica: las variables de instancia se declaran private y los métodos public. Y es válido declarar algunos métodos como private cuando solo los van a usar otros métodos de la misma clase: son los métodos internos, los que no forman parte de la interfaz.
Control del acceso a los miembros
Los miembros de una clase son sus atributos y sus métodos, y su nivel de protección determina quién puede acceder a ellos. Se pueden establecer distintos niveles de encapsulamiento según desde dónde se quiera permitir el acceso:
| Visibilidad | Significado | En Java | En UML |
|---|---|---|---|
| Pública | Se puede acceder al miembro desde cualquier lugar. | public |
+ |
| Protegida | Solo desde la propia clase o desde una clase que herede de ella. | protected |
# |
| Privada | Solo desde la propia clase. | private |
- |
El propósito principal de los métodos públicos es presentarle a quien usa la clase una vista de los servicios que ofrece: la interfaz pública de la clase. Quien la usa no necesita saber cómo hace sus tareas. Por eso las variables y los métodos privados —los detalles de la implementación— no son accesibles desde afuera: si se intenta, el compilador da un error.
Sobre protected hay una recomendación concreta que conviene tomar desde el principio: se aplica a métodos y constructores, y preferentemente no a los campos. Un campo protegido lo pueden tocar todas las subclases directamente, y eso debilita el encapsulamiento que se acaba de explicar.
Para encapsular por completo el estado de un objeto, todos sus atributos se declaran privados. A un objeto se accede siempre a través de sus métodos públicos, y para usarlo no hace falta conocer ni los algoritmos de sus métodos ni los tipos de datos con los que guarda su estado.
Herencia
En Java solo existe la herencia simple: cada clase tiene una sola clase padre, que se llama superclase. La clase hija se llama subclase. Una subclase hereda las variables y los métodos de su superclase, aunque puede anular algunos o agregar otros.
La herencia organiza las clases en una estructura jerárquica de árbol, y todas las relaciones entre clases se ajustan a esa estructura. En Java, además, todas las clases tienen como primer padre a la clase Object: es la raíz del árbol, y por eso cualquier objeto hereda métodos como toString() o equals().
Cómo se implementa: extends
Para declarar una subclase se usa la palabra clave extends:
[Modificador] class NombreDeSubclase extends NombreDeSuperclase {
}
Una subclase hereda todos los métodos de su superclase, salvo los que sobrescriba. Y así como this apunta al objeto actual, existe la variable super, que apunta a la clase de la cual deriva la clase actual: sirve para llamar al método original de la superclase cuando la subclase lo redefine.
Sobrescribir un método
Se dice que una subclase sobrescribe un método de su superclase cuando crea un método con las mismas características —nombre, número y tipo de argumentos—. Y se usa para agregar, quitar o modificar la funcionalidad del método heredado. Es el mecanismo con el que se implementa el polimorfismo de la lección 1:
class Animal {
protected String nombre;
void metodoUno(String var) {
// Cuerpo del metodoUno
}
void metodoDos() {
// Cuerpo del metodoDos
}
}
public class Herbivoro extends Animal {
private String tipoEstomago; // atributo propio de Herbivoro
// Este método sobrescribe al de la clase padre
void metodoUno(String var) {
// Cuerpo del metodoUno, modificado en la subclase
}
void metodoTres(int i, String dato) { // método nuevo, no estaba en Animal
// Cuerpo del metodoTres
}
}
Mirando ese código se ve todo junto: Herbivoro hereda metodoDos() sin escribirlo, sobrescribe metodoUno(), agrega metodoTres() y suma un atributo propio que Animal no tenía. Ese es el mecanismo de reutilización del que se hablaba: se parte de la estructura que ya está y se agrega o modifica lo que haga falta.
Preguntar de qué tipo es un objeto: instanceof
A veces no alcanza con tener una referencia: hay que saber qué objeto hay del otro lado. Para eso existe el operador instanceof, que devuelve true o false:
Animal animal = new Hiena(); // la referencia es Animal; el objeto, una Hiena
System.out.println(animal instanceof Hiena); // true — el objeto es una Hiena
System.out.println(animal instanceof Animal); // true — una Hiena es un Animal
System.out.println(animal instanceof Object); // true — toda clase desciende de Object
System.out.println(animal instanceof Leon); // false — una hiena no es un león
La clave está en la primera línea: la referencia dice Animal, pero el objeto es una Hiena. El operador instanceof no mira el tipo de la variable: mira el tipo real del objeto, el que quedó fijado cuando se lo creó con new. Y contesta que sí tanto para la clase propia del objeto como para todas sus superclases, para las interfaces que implementa y para Object, que es la raíz de todas.
Tres detalles que conviene tener presentes:
- Con
nullsiempre dafalse. Preguntar de qué tipo es «nada» no tiene respuesta, así que la respuesta es que no es de ningún tipo. Es una forma cómoda y segura de comprobar si una referencia trae un objeto. - Si los tipos son incompatibles, no compila. Preguntar si un
Stringes unIntegeres un error de compilación (incompatible types), no unfalse: el compilador sabe que esa respuesta nunca podría ser verdadera y lo avisa. - Sirve para las interfaces. Como las interfaces son tipos, también se puede preguntar por una capacidad en lugar de por una clase:
animal instanceof Aulladatruesolo si el objeto implementa esa interfaz, sin importar de qué clase sea.
Lo que se hace después de preguntar es convertir la referencia para poder usar los métodos de la subclase, y desde Java 16 las dos cosas se escriben juntas con el binding de patrón:
// Forma clásica: preguntar y después convertir
if (animal instanceof Hiena) {
Hiena h = (Hiena) animal;
h.aullar();
}
// Desde Java 16: la variable queda declarada si la respuesta fue true
if (animal instanceof Hiena h) {
h.aullar();
}
instanceof. Un programa lleno de preguntas por el tipo suele ser un síntoma: si hay que averiguar de qué clase es cada objeto para decidir qué hacer, lo que faltaba era un método declarado en la superclase y sobrescrito en cada subclase —o sea, polimorfismo, que es lo que hace que la decisión la tome el objeto y no el que lo usa (está explicado en la lección 1)—. instanceof queda para los casos legítimos: comprobar si un objeto tiene cierta capacidad, o el comienzo de un equals(). Y si el JDK es anterior a la versión 16, la forma con la variable adentro del if no compila: hay que usar la versión clásica con la conversión en la línea siguiente.
Abstractas, comunes y finales
Las clases del árbol se pueden clasificar en tres tipos, según en qué nivel estén y qué se pueda hacer con ellas:
- Abstracta — es la más general. Ejemplo:
Animal. - Común — es intermedia. Ejemplo:
Herbívoro. - Final — es la más específica. Ejemplo:
Conejo.
La clase abstracta
La raíz del árbol es la clase abstracta: es la clase que declara la existencia de métodos pero no su implementación. Es decir, los nombra y dice qué reciben y qué devuelven, pero no lleva las llaves { } con las sentencias adentro.
En una clase abstracta, por lo menos uno de los métodos tiene que ser abstracto. Se usa la palabra clave abstract tanto para la clase como para el método:
abstract class A {
abstract void metodoUno(String var1); // sin llaves: solo la firma
void metodoDos() {
// Cuerpo del metodoDos: este sí está implementado
}
}
Las dos reglas de una clase abstracta:
- No se pueden crear objetos de ella. Tiene sentido: si le falta la implementación de un método, un objeto suyo no sabría qué hacer cuando se lo llame.
- Sí se puede heredar de ella. Las subclases agregan la funcionalidad de los métodos abstractos. Si alguna no lo hace, esa subclase queda también abstracta.
La clase común y la clase final
La clase común está en los niveles intermedios del árbol: tiene ascendencia y descendencia, se puede heredar de ella y además se pueden crear objetos. Es la clase de trabajo habitual.
La clase final es el nivel de abajo: tiene ascendencia pero no descendencia, porque no se puede heredar de ella. Se declara con la palabra clave final:
final class Conejo {
// Cuerpo de la clase
}
¿Para qué sirve prohibir la herencia? Para garantizar que el comportamiento de esa clase no se pueda alterar en ningún lado. Es una decisión de diseño, y en Java la clase String es un buen ejemplo de por qué: si alguien pudiera heredar de String y redefinir sus métodos, dejaría de ser confiable el uso de las cadenas en todo el lenguaje.
Interfaces
Java no soporta la herencia múltiple: una clase no puede heredar las propiedades de varias clases padres. La decisión fue deliberada, y la razón que dieron sus creadores es que la herencia múltiple agrega demasiada complejidad al código. Si dos clases base definen un método con el mismo nombre, ¿con cuál se queda la subclase? Y ese problema, que parece de laboratorio, aparece en cualquier proyecto real.
Pero la herencia múltiple resolvía cosas útiles, así que Java introdujo otro concepto en su lugar: la interfaz.
Qué es una interfaz
Formalmente, una interfaz es como una clase, con dos diferencias:
- Sus métodos están vacíos: se declaran sin cuerpo, no hacen nada.
- Para definirla no se usa
classsino la palabra claveinterface.
interface Animal {
public int edad = 10;
public String nombre = "Lanudo";
public void nace();
public void getNombre();
public void getNombre(int i);
}
Vale preguntarse para qué sirve una interfaz si sus métodos están vacíos. La respuesta es que una interfaz es un contrato: cuando una clase la implementa, está haciendo la promesa de que va a implementar todos sus métodos. La interfaz no dice cómo hacer las cosas, dice qué hay que saber hacer.
Implementar una interfaz
Si la clase que implementa la interfaz no sobrescribe alguno de sus métodos, esa clase se convierte automáticamente en abstracta y no se pueden crear objetos de ella. Y hay una condición que produce errores difíciles de ver: para que un método cuente como sobrescrito tiene que tener el mismo nombre, los mismos datos, el mismo tipo de dato de retorno y el mismo modificador. Si el modificador no coincide, el compilador da error y no deja seguir.
public class Perro3 implements Animal {
// Ojo: si el método se llamara nacio() en lugar de nace(),
// no compila, porque no se habría sobrescrito el de la interfaz.
public void nace() {
System.out.println("hola mundo");
}
public void getNombre() {
System.out.println(nombre);
}
public void getNombre(int i) {
System.out.println(nombre + " " + i);
}
public static void main(String[] args) {
Perro3 dog = new Perro3();
// Esta línea da error al compilar: las variables de una
// interfaz son final y no se pueden modificar.
dog.edad = 8;
}
}
Las variables de una interfaz
Las variables que se definen en una interfaz llevan implícitamente el atributo final, y es obligatorio darles un valor dentro del cuerpo de la interfaz. Además no pueden llevar los modificadores private ni protected: solo public. Su función es ser una especie de constantes para todos los objetos que implementen esa interfaz. Es lo que muestra el último ejemplo: dog.edad = 8 no compila, porque edad es una constante.
Y la conclusión que recupera lo que se había perdido: aunque una clase solo puede heredar de una clase, puede implementar cuantas interfaces quiera. Ahí se recupera buena parte de la potencia de la herencia múltiple, sin el problema de los métodos duplicados: las interfaces no traen implementación, así que no hay nada que se pise.
Protección de clases
Hasta acá se habló de la protección de los miembros. La protección de una clase es otra cosa: determina la relación que tiene con las clases de otros paquetes. Hay dos niveles:
- De paquete. Una clase con este nivel solo puede ser usada por las clases de su propio paquete.
- Pública. Una clase
publicpuede ser usada por cualquier clase de cualquier paquete: se pueden crear objetos de ella y manipularlos.
Por omisión, todas las clases tienen nivel de paquete, a menos que se escriba la palabra reservada public delante. De ahí viene la costumbre de declarar public class casi siempre: si no, la clase queda encerrada en su paquete.
Cómo se usa una clase de otro paquete
Hay dos formas de referirse a una clase que está en otro paquete:
- Con su nombre completo, en todas las partes del código donde haya que nombrarla:
java.lang.System.out.println("Hola Lucía"); - Importándola, lo que permite referirse a ella por su nombre simple:
Puede haber muchas sentenciasimport java.lang.System; // se puede usar el comodín * // para importar todo: java.lang.* System.out.println("Hola Lucía");importen un programa, y siempre van antes de cualquier definición de clase.
Un detalle que se entiende recién cuando se programa en serio: el import no agranda el programa. Solo le dice al compilador dónde buscar el código que necesita.
Composición: «tiene un»
Una clase puede tener referencias a objetos de otras clases como miembros. A esa capacidad se la conoce como composición, y es la relación «tiene un» de la lección 1.
El ejemplo del apunte es claro: un objeto de la clase RelojAlarma necesita saber la hora actual y la hora en la que se supone que va a sonar la alarma. Entonces lo razonable es que RelojAlarma tenga dos referencias a objetos Tiempo como miembros:
public class RelojAlarma {
private Tiempo horaActual; // tiene un Tiempo
private Tiempo horaAlarma; // tiene otro Tiempo
public void configurarAlarma(Tiempo alarma) {
this.horaAlarma = alarma;
}
}
La diferencia con la herencia queda clara comparando las frases: un RelojAlarma no es un Tiempo, tiene tiempos. Si se hubiera usado herencia ahí, el diseño estaría mal: se estaría diciendo que el reloj es una hora.
Miembros static
Cada objeto tiene su propia copia de todas las variables de instancia de la clase. Pero hay casos en los que conviene que haya una sola copia compartida por todos los objetos. Para eso existe el campo static, que se llama variable de clase.
static representa información de toda la clase: si un objeto la modifica, los demás ven el cambio.Existen aunque no haya objetos
Los miembros static de una clase existen aunque no exista ningún objeto de esa clase: están disponibles apenas la clase se carga en memoria, en tiempo de ejecución. Tienen alcance a nivel de clase.
Para acceder a un miembro public static —haya objetos o no— se antepone el nombre de la clase y un punto: Math.PI, Integer.parseInt("7"). Y si el miembro es private static, hace falta un método public static que lo exponga, y ese método también se llama con el nombre de la clase adelante.
Las dos prohibiciones del static
Un método static no puede acceder a los miembros no estáticos de la clase. La razón es lógica: se lo puede invocar sin que se haya creado ninguna instancia, así que no hay ningún objeto cuyos atributos leer. Por lo mismo, no se puede usar this adentro de un método static: this se refiere a un objeto específico, y en ese momento puede no haber ninguno. Hacerlo es un error de compilación.
Estos métodos se conocen como métodos de clase, por oposición a los métodos de instancia. Y es exactamente la razón por la que main es static, como se vio en la lección 2.
Static import
Para no tener que escribir el nombre de la clase y el punto cada vez, existe la declaración static import, que permite importar los miembros estáticos de una clase o interfaz y usarlos por su nombre simple. Tiene dos formas:
// Importa un miembro estático específico
import static java.lang.Math.PI;
import static java.lang.Math.abs;
// Importa todos los miembros estáticos de la clase ("sobre demanda")
import static java.lang.Math.*;
Ojo con el alcance: las declaraciones static import solo importan miembros estáticos. Las sentencias import comunes siguen siendo necesarias para las clases que use el programa.
Variables de instancia final
La palabra clave final sirve para indicar que una variable no se puede modificar: es una constante, y cualquier intento de cambiarla es un error.
private final int INCREMENTO;
Esa línea declara una variable de instancia final llamada INCREMENTO, de tipo int. Y acá aparece una regla que sorprende: si no se inicializa en la declaración, hay que inicializarla en cada uno de los constructores. Si una variable final no queda inicializada ni de una forma ni de la otra, el compilador da error.
Inicializarla en el constructor tiene una consecuencia interesante: cada objeto puede tener un valor distinto para esa constante. Sigue siendo constante para ese objeto —no cambia nunca después de construido— pero puede diferir entre objetos.
Constantes de clase
Si además de final se la declara static, la constante pasa a ser de la clase y todos los objetos la comparten:
public class Circulo {
private float radio;
private final static float pi = 3.1415f;
public float perimetro() {
return 2 * pi * this.radio;
}
}
Acá pi es una constante que, al ser static, la comparten todos los objetos de Circulo. Si no fuera static, cada objeto tendría su propia copia de pi, que no tiene ningún sentido: el valor es el mismo para todos y no hay motivo para repetirlo en memoria.
La referencia this
Cada objeto puede acceder a una referencia a sí mismo mediante la palabra clave this. Cuando se llama a un método no estático de un objeto, el cuerpo del método usa ese this en forma implícita para referirse a las variables de instancia y a los otros métodos. Eso es lo que permite que el mismo código de la clase sepa qué objeto está manipulando.
Por eso hay un dato de implementación que aclara todo: Java mantiene una sola copia de cada método por clase —todos los objetos invocan el mismo código— y en cambio cada objeto tiene su propia copia de las variables de instancia. El this implícito es lo que distingue un objeto de otro dentro del mismo método.
Y el uso más frecuente, ya visto en la lección 2: cuando un método tiene un parámetro o una variable local con el mismo nombre que un campo de la clase. En ese caso hay que usar this para llegar al campo; si no, se referencia al parámetro. Es un error lógico que no da error de compilación, y por eso es de los más molestos de encontrar.
public class CuentaBancaria {
private double saldo;
private double limite;
public void depositar(double importe) {
this.saldo = this.saldo + importe;
}
}
En depositar el this no hace falta —no hay ningún parámetro llamado saldo— pero se escribe igual, y está bien: deja explícito que ese saldo es el del objeto. Es una convención que evita confusiones.
Métodos
Un método es una colección de sentencias que ejecutan una tarea específica. En Java, un método siempre pertenece a una clase, y es un comportamiento de los objetos que se creen de esa clase. Su definición tiene una cabecera y un cuerpo entre llaves:
[modificador] tipo_resultado nombreMetodo ([lista de parámetros]) {
declaraciones de variables locales;
sentencias;
[return [expresion]];
}
Las variables declaradas en el cuerpo del método son locales al método.
Una recomendación para promover la reutilización, que se ve en todos los libros de estilo: cada método debería realizar una sola tarea bien definida, y su nombre debería expresar esa tarea. Un método que hace tres cosas no se puede reutilizar, no se puede probar por separado y no se puede entender.
Los argumentos pueden ser constantes, variables o expresiones. Y un método puede devolver a lo sumo un valor —pero ese valor puede ser una referencia a un objeto que contenga muchos valores, que es la forma de devolver varias cosas a la vez.
Las tres formas de llamar a un método
- Con el nombre del método solo, para llamar a otro método de la misma clase.
- Con una variable que contiene la referencia a un objeto, un punto y el nombre del método, para llamar a un método no estático de ese objeto.
- Con el nombre de la clase y un punto, para llamar a un método
static.
Y una consecuencia de las reglas del static que conviene tener presente: un método static solo puede llamar directamente a otros métodos static de la misma clase, y solo puede manipular directamente variables static. Para tocar un miembro no estático necesita una referencia a un objeto de esa clase. Los métodos de clase se relacionan con la clase como un todo; los de instancia, con un objeto específico y sus variables.
void y return
El hecho de que un método devuelva un valor o no es una decisión de diseño. Cuando no devuelve nada, se usa la palabra reservada void, que le indica al compilador que su valor de retorno será «vacío», y la sentencia return se omite:
void nombreMetodo([lista de parámetros]) {
sentencia1;
sentencia2;
sentenciaN;
}
El control vuelve a quien llamó de dos maneras: si el método no devuelve resultado, al llegar a la llave de cierre; si devuelve, cuando se ejecuta return expresión;, que evalúa la expresión y devuelve el resultado.
Dos ejemplos
Un método que no devuelve nada, con un parámetro, y cómo se lo invoca desde el main:
public static void saludoUsuario(String nombre) {
String cadena_Saludo = "Hola " + nombre;
System.out.println(cadena_Saludo);
System.out.println("Mensaje impreso desde un método estático");
}
public static void main(String[] args) {
String nombre = "Lucía Fernández";
saludoUsuario(nombre);
}
Y uno que devuelve un valor de tipo double:
public double obtenerPotencia(double base, double potencia) {
double resultado = Math.pow(base, potencia);
return resultado;
}
public static void main(String[] args) {
double b = 3;
double p = 2;
double valor_resultante;
valor_resultante = obtenerPotencia(b, p);
System.out.println(b + " a la " + p + " = " + valor_resultante);
}
Fijate en el detalle de la invocación: al llamar al método no se pone el tipo de dato de los argumentos —eso se hace solo en la declaración—, aunque los valores que se pasan tienen que coincidir con el tipo que el método espera.
Paso de parámetros
En Java, los parámetros de tipo primitivo —int, long, double— siempre se pasan por valor. Las variables de tipo objeto y los arreglos se pasan por referencia: lo que viaja al método es la dirección de memoria donde está el objeto. Ya se vio con los vectores en la lección 2 y vale para cualquier objeto: el método puede modificar el estado del objeto que recibe, pero no puede cambiar cuál es el objeto que tiene el que llamó.
Cantidad variable de argumentos
Un parámetro precedido por tres puntos (…) indica que el método recibe una cantidad variable de argumentos en la llamada. Hay dos reglas: puede haber un solo parámetro de ese tipo, y tiene que ser el último de la lista.
Java trata esa lista como un arreglo cuyos elementos son todos del mismo tipo. La sintaxis completa es:
[modificador] tipo_resultado nombreMetodo([lista de parámetros], [tipo … nombre_parametro])
donde la lista de parámetros anterior puede estar vacía. Un ejemplo que sirve para entenderlo: un método que calcule el promedio de una cantidad variable de notas.
public class PruebaCantVariableArg {
public static double promedio(double... notas) {
double sum = 0.0;
for (double val : notas) {
sum += val;
}
return sum / notas.length;
}
public static void main(String[] args) {
System.out.printf("El promedio entre 8 y 5 es %.2f%n", promedio(8, 5));
System.out.printf("El promedio entre 8, 5 y 7 es %.2f%n", promedio(8, 5, 7));
System.out.printf("El promedio entre 8, 5, 7, 2 y 9 es %.2f%n", promedio(8, 5, 7, 2, 9));
}
}
Mirá lo que pasa adentro del método: notas se usa como un vector —se recorre con un for mejorado y se le pide length— porque eso es lo que es. Lo cómodo está del lado del que llama: puede pasar dos, tres o cinco valores sin que exista una versión distinta del método para cada cantidad.
Constructores
Una clase puede proporcionar un método especial llamado constructor, que se usa para inicializar el objeto en el momento en que se lo crea. Java exige una llamada al constructor para cada objeto. Lo que hace el operador new es pedirle memoria al sistema para almacenar el objeto y después llamar al constructor correspondiente para inicializarlo. Un constructor tiene el mismo nombre que la clase.
Toda clase tiene al menos un constructor: si no se escribe ninguno, el compilador crea un constructor predeterminado sin argumentos, que inicializa las variables de instancia con los valores indicados en sus declaraciones o con sus valores por omisión —cero, false, null—.
new CuentaBancaria() deje de compilar después de agregarle un constructor con parámetros.
Otras tres reglas de los constructores:
- No devuelven valores, así que no se les especifica tipo de retorno —ni siquiera
void—. - Por lo general se declaran
public. - Se pueden sobrecargar: varias declaraciones con distintas firmas permiten inicializar los objetos de distintas maneras.
Un constructor puede llamar a los métodos de la clase, pero con cuidado: las variables de instancia pueden no estar inicializadas todavía, porque el constructor está justamente en el proceso de inicializar el objeto. Usarlas antes de darles un valor es un error lógico. Y si un programa intenta crear un objeto pasando una cantidad o un tipo de argumentos que no corresponde a ningún constructor, hay error de compilación.
El ejemplo completo
Esta clase junta casi todo lo de la lección: una constante de clase, dos variables de instancia privadas, tres constructores sobrecargados, métodos de trabajo y un main que prueba los tres:
public class CuentaBancaria {
// Constante de clase
public static final double LIMITE_NORMAL = 3000.00;
// Variables de instancia
private double saldo;
private double limite;
// Constructor sin parámetros
public CuentaBancaria() {
this.saldo = 0;
this.limite = LIMITE_NORMAL;
}
// Constructor con un parámetro: cambia el límite
public CuentaBancaria(double limit) {
this.saldo = 0;
this.limite = limit;
}
// Constructor con dos parámetros: saldo inicial y límite
public CuentaBancaria(double saldoInicial, double limit) {
this.saldo = saldoInicial;
this.limite = limit;
}
public void depositar(double importe) {
this.saldo = this.saldo + importe;
}
public void mostrarDatosCuenta() {
System.out.println("Saldo de cuenta " + this.saldo);
System.out.println("Limite de cuenta " + this.limite);
}
public static void main(String[] args) {
// Llama al constructor sin parámetros
CuentaBancaria CuentaNormal1 = new CuentaBancaria();
CuentaNormal1.mostrarDatosCuenta();
// Llama al constructor que modifica el límite
CuentaBancaria CuentaEspecial1 = new CuentaBancaria(5000);
CuentaEspecial1.mostrarDatosCuenta();
// Llama al constructor que modifica saldo y límite
CuentaBancaria CuentaConSaldoInicial1 = new CuentaBancaria(2500, 8000);
CuentaConSaldoInicial1.mostrarDatosCuenta();
}
}
Los tres objetos se crean con la misma clase y con la misma instrucción new, y quedan en estados iniciales distintos. Eso es exactamente para lo que sirve sobrecargar constructores.
Los métodos set y get
Los campos privados solo pueden ser manipulados por los métodos de la clase. Por lo tanto, quien usa un objeto —cualquier clase que llame a sus métodos— necesita métodos públicos para establecer (asignar valores) u obtener (leer los valores de) las variables de instancia privadas.
Los nombres no están obligados a empezar con set o get, pero la convención es muy recomendada y además es obligatoria para algunos componentes de Java —los JavaBeans— que simplifican la programación en los entornos de desarrollo.
Por qué no son un adorno
La diferencia entre un atributo público y un par de set/get tiene tres consecuencias concretas:
- El
getpuede controlar el formato de los datos que devuelve, y de ese modo protege al código que los usa de la representación actual de los datos. Si mañana cambia cómo se guarda por dentro, elgetsigue devolviendo lo mismo. - El
setpuede —y debe— revisar los intentos de modificar el valor y lanzar una excepción si el valor no tiene sentido. Un día del mes en 37 se rechaza; un peso negativo, se niega; una edad de -5, se rechaza. - El código que usa la clase queda desacoplado de la implementación: no necesita saber si adentro hay un
double, unStringo un objeto.
La convención
- El nombre del
setes la palabrasetseguida del nombre del atributo con su primera letra en mayúscula. Tiene un parámetro del mismo tipo que el atributo y no devuelve nada. - El nombre del
getsigue la misma idea. No tiene parámetros y devuelve un valor del tipo del atributo.
public double getSaldo() {
return saldo;
}
public void setSaldo(double saldo) {
this.saldo = saldo;
}
Ojo con el detalle de setSaldo: el this es obligatorio. Sin él, la sentencia saldo = saldo asignaría el parámetro a sí mismo y el atributo quedaría como estaba. Y esa línea compila sin chistar, que es lo peor que puede pasar con un error.
Recolección de basura
Todo objeto usa recursos del sistema, como la memoria, y hace falta una manera disciplinada de devolverlos cuando ya no se necesitan. Si no, aparecen las fugas de recursos: memoria que se va acumulando sin uso hasta que el programa —o el sistema— se queda sin ella.
La JVM hace la recolección de basura de forma automática para reclamar la memoria de los objetos que ya no se usan. Cuando ya no quedan referencias a un objeto, ese objeto pasa a ser candidato para la recolección, que en general ocurre cuando la JVM ejecuta su recolector. Las fugas de memoria típicas de C y C++ —donde la memoria se libera a mano— son mucho menos probables en Java, aunque no imposibles.
Las fugas que la basura no arregla
La recolección de basura resuelve la memoria, y nada más. Hay otros recursos que un objeto puede retener y que el recolector no devuelve:
- Archivos abiertos. Si una aplicación abre un archivo y no lo cierra, ninguna otra aplicación puede usarlo hasta que termine la que lo abrió.
- Conexiones de red o de base de datos. Ocupan recursos del otro lado, y ese servidor tiene un límite de conexiones simultáneas.
El método finalize, y por qué no se usa
El recolector de basura llama al método finalize() para hacer las tareas de preparación antes de reclamar la memoria del objeto. No recibe parámetros y devuelve void. Pero tiene un problema de fondo: no está garantizado que el recolector se ejecute en un tiempo determinado. Puede no ejecutarse nunca antes de que el programa termine. Entonces no se sabe ni si se va a llamar a finalize ni cuándo. Por esa razón, la mayoría de los programadores debería evitar usarlo.
Lo que corresponde hacer, en cambio, es que la clase ofrezca un método para liberar los recursos que quien la usa pueda llamar cuando ya no los necesita. Muchas clases de la API de Java traen métodos close() o dispose() para eso, y los bloques try-with-resources que se ven en la lección 4 hacen que ese cierre se llame solo.
Creación de paquetes
Un paquete es un conjunto de clases e interfaces relacionadas entre sí, agrupadas bajo un nombre. El paquete java.io, por ejemplo, agrupa las clases que permiten a un programa hacer entrada y salida de información. Un paquete puede contener a otros paquetes.
Y resuelven cuatro cosas a la vez:
- Organizan las clases en grupos, lo que ayuda a administrar la complejidad de una aplicación grande.
- Facilitan la reutilización: permiten que los programas importen clases de otros paquetes, en vez de copiarlas en cada programa que las usa.
- Evitan conflictos de nombres: proporcionan una convención para que los nombres de clase sean únicos. Se pueden llamar
Cuentaa dos clases distintas si están en paquetes distintos. - Controlan el acceso: los miembros sin modificador son accesibles solo dentro del paquete.
Los pasos para crear una clase reutilizable
- Declarar la clase
public. Si no, solo la van a poder usar las clases del mismo paquete. - Elegir un nombre único para el paquete y agregar la declaración
packageal principio del archivo. Solo puede haber una declaraciónpackagepor archivo, y va antes que todas las demás declaraciones e instrucciones. Si no se pone ninguna, la clase queda en el paquete predeterminado y solo es accesible para las otras clases del paquete predeterminado que estén en el mismo directorio. - Compilar la clase de manera que quede ubicada en la estructura de directorios del paquete.
- Importarla en el programa donde se la quiera usar.
El cargador de clases y el classpath
El compilador usa un objeto especial, el cargador de clases, para encontrar las clases que necesita. Las busca en este orden:
- Las clases estándar de Java que vienen con el JDK.
- Los paquetes opcionales, a través del mecanismo de extensión del lenguaje.
- La ruta de clases (classpath), que es una lista de ubicaciones donde se almacenan clases.
La ruta de clases consiste en una lista de directorios o de archivos JAR, separados por punto y coma (;) en Windows o por dos puntos (:) en GNU/Linux y macOS. Los archivos JAR son archivos individuales que contienen otros archivos, generalmente comprimidos: las clases estándar de Java, por ejemplo, viven en el archivo rt.jar, que se instala junto con el JDK.
De manera predeterminada, la ruta de clases es el directorio actual, y se puede modificar de dos maneras:
- Con la opción
-classpathdel compiladorjavac. - Con la variable de entorno
CLASSPATH, que el sistema operativo mantiene para que las aplicaciones busquen clases en las ubicaciones indicadas.
Todos los archivos .java de un paquete que usen clases de otros paquetes tienen que llevar la directiva import con el nombre del paquete, el punto y la clase. Si son muchas clases del mismo paquete, se usa el * para referirse a todas.
La API de Java
Para escribir programas en Java se combinan los métodos y clases que escribe el programador con los que ya vienen predefinidos en la API —la Interfaz de Programación de Aplicaciones, o biblioteca de clases—. Las clases relacionadas están agrupadas en paquetes, para poder importarlas y reutilizarlas.
La API proporciona una colección enorme de clases predefinidas: cálculos matemáticos, manipulación de cadenas y de caracteres, entrada y salida, bases de datos, redes, procesamiento de archivos, comprobación de errores. Para cada clase nueva que se use hay que saber en qué paquete está.
| Paquete | Qué trae |
|---|---|
java.lang | Las clases con las que trabaja el programa principal. Se importa solo, sin declararlo. |
java.util | Clases especializadas, como los calendarios y las colecciones. |
java.io | Servicios de entrada y salida, independientes del dispositivo. |
java.awt | La mayoría de las clases dedicadas a los gráficos. |
java.net | Clases para trabajar con programas de bajo nivel en Internet y la WWW. |
java.applet | La clase para trabajar con applets, hoy en desuso. |
Una observación sobre los import que ahorra errores de compilación: System y String están en java.lang, que se importa implícitamente en todo programa. La mayoría de las demás clases hay que importarlas explícitamente. Y las clases de un mismo paquete se importan entre sí solas, así que no hace falta el import entre clases del mismo paquete.
La documentación de la API es el material de consulta permanente de cualquier programador de Java, y está publicada por Oracle en docs.oracle.com/en/java/javase.
UML
UML (Unified Modeling Language, lenguaje unificado de modelado) es un lenguaje que permite modelar, especificar, construir y documentar los elementos que forman un sistema de software orientado a objetos. Y un modelo, como se vio en la lección 1, es una simplificación de la realidad.
Modelar es esencial al construir software porque permite:
- Comunicar la estructura de un sistema complejo.
- Especificar el comportamiento deseado del sistema.
- Comprender mejor lo que se está construyendo.
- Descubrir oportunidades de simplificación y de reutilización.
Un modelo proporciona «los planos» del sistema y puede ser más o menos detallado según qué sea relevante en cada momento; lo que tiene que capturar es lo esencial. Y todo sistema se puede describir desde distintos puntos de vista: los modelos estructurales —la organización del sistema— y los modelos de comportamiento —su dinámica—.
Por qué UML es el estándar
Se convirtió en el estándar de facto de la industria porque lo crearon los autores de los tres métodos más usados de orientación a objetos: Grady Booch, Ivar Jacobson y Jim Rumbaugh. Los tres fueron contratados por Rational Software para crear una notación unificada sobre la que basar sus herramientas CASE, y en el proceso participaron además empresas como Microsoft, Hewlett-Packard, Oracle e IBM, junto con analistas y desarrolladores.
Para qué sirve en la práctica: los diagramas UML son documentos que plasman el trabajo de capturar la idea, para la evolución posterior del proyecto. El cliente puede entender el plan de trabajo y señalar cambios si algo no se captó bien; y como el desarrollo se hace en equipo, cada integrante puede ubicar su trabajo particular y el general a partir de los diagramas. Es importante tener claro que un modelo UML describe qué hará el sistema, no cómo se implementa.
Los diagramas más usados
| Diagrama | Qué muestra | Ejemplo |
|---|---|---|
| De clases | Las clases y cómo se relacionan en un escenario o sistema. Es el que se usa para diseñar el sistema antes de programarlo. Varios entornos lo incorporan —Eclipse con el plugin Papyrus, NetBeans con easyUML— y algunos generan el código fuente a partir del diagrama. | La clase Cuenta con sus atributos, sus métodos y sus relaciones con otras clases. |
| De casos de uso | Las acciones del sistema desde el punto de vista del usuario. Si el sistema lo va a usar gente en general, es el diagrama que traduce los requerimientos a algo que el analista y el programador entienden. | «Un alumno se inscribe a un curso». |
| De estados | Las transiciones de un objeto entre sus estados posibles. | Una lavadora pasa por remojo, lavado, enjuague, centrifugado o apagada. Un ascensor sube, baja o está en reposo. |
| De secuencias | Información dinámica: cómo interactúan los objetos mientras el tiempo transcurre. Visualiza la mecánica de las interacciones con base en tiempos. | En la lavadora, las mangueras, el tambor y el drenaje interactúan a lo largo del tiempo de funcionamiento. |
| De actividades | La secuencia de acciones que desarrollan los objetos, sin importar el tiempo. Se parece a un diagrama de flujo. | Los pasos del proceso de inscripción, en orden. |
Cómo se dibuja una clase
Una clase se representa con un rectángulo dividido en tres partes: el nombre de la clase arriba, sus atributos en el medio y sus métodos abajo. La notación de cada parte es esta:
Atributos [visibilidad] nombre [multiplicidad] [: tipo [= valor_por_defecto]]
Métodos [visibilidad] nombre ([[in|out] parametro : tipo [, ...]]) [: tipo_devuelto]
Los corchetes indican partes opcionales. La visibilidad se escribe con los signos que ya se vieron: - privada, # protegida, + pública. La multiplicidad va entre corchetes —[2], [0..2], [*], [3..*]— y los parámetros pueden marcarse como de entrada (in) o de salida (out).
+ y el - son la visibilidad pública y privada.Las relaciones entre clases
Una relación describe el vínculo entre dos clases, y hay que precisar tres cosas: el tipo de relación, la cardinalidad o multiplicidad —cuántos objetos de cada clase intervienen— y la navegabilidad —qué objeto puede ver a cuál—. Las relaciones indican cómo se comunican los objetos de esas clases: los mensajes navegan por las relaciones.
| Relación | Qué significa | Cómo se dibuja |
|---|---|---|
| Generalización | Los elementos comunes de dos o más clases se generalizan en una clase superior: es la herencia. Al instanciar la clase derivada se heredan las propiedades y los métodos de la superior, que pueden ser abstractas para aprovechar los métodos polimórficos. | Una línea con una flecha de triángulo hueco apuntando a la superclase. |
| Asociación | Una relación estructural que describe una conexión entre objetos. Dos o más clases pueden estar asociadas de distintas maneras. | Una línea entre las dos clases. Si es unidireccional, termina en punta de flecha que indica el sentido. |
| Agregación | Una asociación que indica que una clase es parte de otra —composición débil—. Los componentes pueden ser compartidos por varios compuestos y la destrucción del compuesto no destruye los componentes. Es la relación más frecuente de las dos. | Un rombo blanco del lado del «todo». |
| Composición | Una forma fuerte: la vida de la clase contenida coincide con la de la contenedora. Los componentes no se comparten, y si se destruye el compuesto se destruyen las partes. | Un rombo negro del lado del «todo». |
El ejemplo del apunte para distinguir las dos últimas: un instituto está compuesto por una o varias aulas —si el instituto desapareciera, el aula también—, mientras que un instituto es concurrido por alumnos: un alumno concurre a uno o varios institutos y no deja de existir porque el instituto cierre.
| Característica | Agregación | Composición |
|---|---|---|
| Varias asociaciones comparten los componentes | Sí | No |
| Se destruyen los componentes al destruir el compuesto | No | Sí |
| Cardinalidad a nivel del compuesto | Cualquiera | 0..1 o 1 |
| Representación | Rombo transparente | Rombo negro |
La multiplicidad
La multiplicidad —o cardinalidad— de una asociación determina cuántos objetos de cada tipo intervienen en la relación: es el número de instancias de una clase que se relacionan con una instancia de la otra. Cada asociación tiene dos multiplicidades, una en cada extremo, y se especifica indicando la mínima y la máxima separadas por dos puntos.
| Multiplicidad | Significado |
|---|---|
1 | Uno y solo uno |
0..1 | Cero o uno |
N..M | Desde N hasta M |
* | Cero o varios |
0..* | Cero o varios |
1..* | Al menos uno, o varios |
Con eso se pueden leer los ejemplos del apunte: un curso es dictado por un único profesor, y un profesor puede dictar uno o varios cursos; todo profesor pertenece a un departamento, y un departamento tiene varios profesores; un alumno puede estar inscripto a ningún curso o a varios, y un curso puede tener entre 1 y 30 alumnos inscriptos —ese 1..30 es una multiplicidad perfectamente válida—.
Ejercicios
1. Clase abstracta o común
Para esta jerarquía: Figura → Polígono → Triángulo, Cuadrado. Decidir cuál debería ser abstracta, cuál común y cuál final, y justificar cada una. Después escribir la declaración de las tres en Java.
2. static o de instancia
Decidir si cada dato conviene como variable de instancia o como variable static de la clase, y explicar por qué:
- El saldo de una cuenta bancaria.
- La cantidad total de cuentas creadas.
- El domicilio de un alumno.
- La nota mínima para aprobar, en una clase
Materia. - El nombre del profesor a cargo del curso.
3. Interfaces
Explicar por qué el siguiente código no compila y qué habría que hacer para arreglarlo sin escribir el método que falta:
interface Vehiculo {
void arrancar();
void frenar();
}
public class Bicicleta implements Vehiculo {
public void arrancar() {
System.out.println("pedaleando");
}
}
4. Constructores
Escribir una clase Alumno con los atributos privados nombre, apellido y edad, con tres constructores sobrecargados —uno que no reciba nada, uno que reciba nombre y apellido, y uno completo— y con los set y get correspondientes. El setEdad tiene que rechazar valores fuera del rango 0 a 120.
5. Agregación o composición
Decidir qué relación corresponde —agregación o composición— y dibujar el rombo donde va:
- Una factura y sus ítems.
- Un curso y sus alumnos.
- Una casa y sus habitaciones.
- Un equipo de fútbol y sus jugadores.
6. varargs y sobrecarga
Escribir un método promedio que reciba una cantidad variable de números decimales y devuelva su promedio. Después explicar qué pasa si se le pasa como único argumento un vector ya armado, y por qué.
7. La jerarquía de los mamíferos
El ejercicio que junta todo lo de esta lección. Escribir estas clases:
- Una interfaz
IMamiferoscon un métodocomunicarse()que devuelva unString. - Una clase abstracta
Mamifero, que implementeIMamiferos, con los atributosnombre,razayfechaNacimiento(los tresString) ypeso(float), un métodocomer()y un constructor que reciba los cuatro datos. Perro, con el atributo propiolugarDeEntrenamiento, yGato, conalturaDeSalto(double). El perro se comunica con «guau guau» y el gato, con «miau miau».- Un
mainque cree un perro y un gato y los recorra con un vector de tipoMamifero, mostrando cómo come y cómo se comunica cada uno.
Después responder: ¿por qué Mamifero no implementa comunicarse()?
Respuestas sugeridas
1. Figura debería ser abstracta: es la raíz de la jerarquía y no tiene sentido calcular el área de «una figura» sin saber de qué figura se trata, así que declara el método area() sin implementación. Polígono sería una clase común: es intermedia, tiene ascendencia y descendencia, y se puede instanciar. Triángulo y Cuadrado son clases de trabajo, y podrían ser final si no se quiere que nadie las herede.
abstract class Figura {
abstract double area();
}
class Poligono extends Figura {
protected int lados;
double area() {
return 0; // cada subclase la define en serio
}
}
final class Triangulo extends Poligono {
private double base;
private double altura;
double area() {
return base * altura / 2;
}
}
2. (a) De instancia: cada cuenta tiene su propio saldo. (b) static: es un dato de la clase como conjunto, no de cada cuenta; conviene incrementarlo en el constructor. (c) De instancia: cada alumno vive en un domicilio distinto. (d) static final: es una constante de la clase, la misma para todas las materias y para todos los objetos. (e) De instancia: cada curso tiene su profesor —salvo que en el sistema haya uno solo, y entonces sería static—. El criterio para decidir es siempre el mismo: si el valor puede ser distinto entre dos objetos de la misma clase, va como variable de instancia.
3. No compila porque Bicicleta declara implementar Vehiculo pero no implementa todos sus métodos: falta frenar(). Ese es el contrato de la interfaz. Para arreglarlo sin escribir frenar() hay que declarar la clase como abstracta: public abstract class Bicicleta implements Vehiculo. Con eso el compilador lo acepta, pero a cambio no se pueden crear objetos new Bicicleta() hasta que alguna subclase implemente el método que falta.
4.
public class Alumno {
private String nombre;
private String apellido;
private int edad;
public Alumno() {
this.nombre = "";
this.apellido = "";
this.edad = 0;
}
public Alumno(String nombre, String apellido) {
this.nombre = nombre;
this.apellido = apellido;
this.edad = 0;
}
public Alumno(String nombre, String apellido, int edad) {
this.nombre = nombre;
this.apellido = apellido;
setEdad(edad);
}
public String getNombre() { return nombre; }
public String getApellido() { return apellido; }
public int getEdad() { return edad; }
public void setNombre(String nombre) {
this.nombre = nombre;
}
public void setApellido(String apellido) {
this.apellido = apellido;
}
public void setEdad(int edad) {
if (edad < 0 || edad > 120) {
throw new IllegalArgumentException("Edad inválida: " + edad);
}
this.edad = edad;
}
}
Fijate que la validación se escribe una sola vez, en setEdad, y el constructor completo la reutiliza llamándolo. Es la forma de que no se pueda crear un objeto inválido ni desde afuera ni desde adentro de la clase. Nota: throw new IllegalArgumentException se ve en la lección 4; acá alcanza con saber que corta la ejecución con un mensaje.
5. (a) Composición: los ítems no existen sin la factura; si se anula la factura, los ítems desaparecen. Rombo negro del lado de la factura. (b) Agregación: los alumnos existen antes y después del curso; si el curso se cierra, siguen existiendo. Rombo blanco del lado del curso. (c) Composición: las habitaciones no se entienden sin la casa. Rombo negro. (d) Agregación: los jugadores existen sin ese equipo —pueden pasar a otro—. Rombo blanco.
6.
public static double promedio(double... notas) {
double sum = 0.0;
for (double val : notas) {
sum += val;
}
return sum / notas.length;
}
Si se le pasa un vector ya armado funciona igual, porque el parámetro variable es un vector:
double[] notas = { 8, 5, 7 };
System.out.println(promedio(notas)); // 6.666…
System.out.println(promedio(8, 5, 7)); // lo mismo
Es la razón por la que adentro del método se puede usar notas.length: la lista de argumentos se empaqueta en un arreglo. Cuidado con un caso que confunde: promedio(notas) pasa un argumento, no tres, y por eso da el mismo resultado que pasar los tres valores sueltos.
7.
interface IMamiferos {
String comunicarse();
}
abstract class Mamifero implements IMamiferos {
protected String nombre;
protected String raza;
protected String fechaNacimiento;
protected float peso;
public Mamifero(String nombre, String raza, String fechaNacimiento, float peso) {
this.nombre = nombre;
this.raza = raza;
this.fechaNacimiento = fechaNacimiento;
this.peso = peso;
}
public void comer() {
System.out.println(nombre + " está comiendo");
}
// No implementa comunicarse(): el contrato de la interfaz queda pendiente
// y lo cumplen las subclases. Por eso la clase tiene que ser abstracta.
}
class Perro extends Mamifero {
private String lugarDeEntrenamiento;
public Perro(String nombre, String raza, String fechaNacimiento, float peso,
String lugarDeEntrenamiento) {
super(nombre, raza, fechaNacimiento, peso); // primero se construye el padre
this.lugarDeEntrenamiento = lugarDeEntrenamiento;
}
public String comunicarse() {
return "guau guau";
}
}
class Gato extends Mamifero {
private double alturaDeSalto;
public Gato(String nombre, String raza, String fechaNacimiento, float peso,
double alturaDeSalto) {
super(nombre, raza, fechaNacimiento, peso);
this.alturaDeSalto = alturaDeSalto;
}
public String comunicarse() {
return "miau miau";
}
}
public class Guarderia {
public static void main(String[] args) {
Mamifero[] animales = new Mamifero[2];
animales[0] = new Perro("Rocky", "Labrador", "12/03/2019", 28.5f, "Patio de la escuela");
animales[1] = new Gato("Michi", "Siamés", "02/07/2021", 4.2f, 1.8);
for (Mamifero animal : animales) {
animal.comer();
System.out.println(animal.nombre + " dice " + animal.comunicarse());
}
}
}
La salida del programa:
Rocky está comiendo
Rocky dice guau guau
Michi está comiendo
Michi dice miau miau
Mamifero no implementa comunicarse() porque no hay una sola respuesta para un mamífero: un perro ladra y un gato maúlla, así que escribir cualquiera de las dos en la clase padre sería mentir para la otra. Es exactamente el caso de la clase Figura del ejercicio 1: lo que una clase general puede prometer es que el método existe; lo que hace cada uno lo decide cada subclase. Y como el contrato de la interfaz queda sin cumplir adentro de la clase, esa clase tiene que declararse abstract —igual que en el ejercicio 3—, con lo cual tampoco se puede hacer new Mamifero(...). Tampoco tendría sentido: ¿un mamífero, de qué tipo?
Tres cosas del código que vale la pena mirar:
super(...)va primero, siempre. Adentro del constructor dePerrono se puede tocar nada propio antes de construir la parte del padre: el compilador corta conreference to x may only appear after an explicit constructor invocationsi una asignación intenta ir antes.- Los constructores no se heredan.
Perrono hereda el constructor deMamifero: escribe el suyo y llama al del padre consuper(...). Es la diferencia con los métodos, que sí se heredan. - El vector de tipo
Mamiferoes lo que hace visible el polimorfismo. La llamadaanimal.comunicarse()está escrita una sola vez en el código y sin embargo da dos resultados distintos, porque la decide el objeto que está en cada posición del vector. Es el polimorfismo de la lección 1, ahora con la mecánica concreta de Java.
De paso, una observación de diseño para discutir en clase: en esta jerarquía la interfaz IMamiferos no es imprescindible —con el método declarado en la clase abstracta alcanzaría—. Sirve para marcar la idea de capacidad: cualquier clase que sepa comunicarse puede implementar IMamiferos aunque no sea un mamífero, y entonces el mismo código la acepta. Una clase puede heredar de un solo padre, pero puede implementar todas las interfaces que quiera.
Resumen
- En Java la unidad de programación es la clase: contiene atributos y métodos, y de ella se instancian los objetos. El estado de un objeto son sus variables de instancia; los mensajes son las llamadas a sus métodos.
- El control de acceso tiene tres niveles:
public(+),protected(#) yprivate(-), más el nivel de paquete cuando no se escribe ninguno. Los atributos van privados y los métodos públicos: eso es el encapsulamiento. - La herencia es simple en Java: cada clase tiene una sola superclase, y todas descienden de
Object. Se escribe conextends;superapunta a la clase base y sobrescribir es redefinir un método heredado con la misma firma. - Hay tres tipos de clases: abstracta —no se instancia, se hereda—, común —se instancia y se hereda— y final —no se hereda—. Una clase abstracta declara métodos sin implementación.
- Las interfaces reemplazan a la herencia múltiple que Java no tiene: son contratos con métodos vacíos y constantes implícitamente
final. Una clase puede implementar varias interfaces, y si deja un método sin implementar queda abstracta. - La composición es la relación «tiene un»: una clase con atributos que son objetos de otra clase.
- Los miembros
staticson de la clase, no del objeto: existe una sola copia, están disponibles aunque no haya objetos creados, y no pueden tocar miembros de instancia ni usarthis.static importpermite usarlos por su nombre simple. finalconvierte una variable en constante: se inicializa en la declaración o en cada constructor. Constatices una constante de clase.thises la referencia al objeto actual, y sirve sobre todo para distinguir un atributo de un parámetro que se llama igual.- Los métodos pueden devolver un valor o nada (
void), se llaman de tres formas y los primitivos se pasan por valor mientras los objetos se pasan por referencia. Los varargs permiten una cantidad variable de argumentos, que adentro se ve como un arreglo. - Los constructores inicializan el objeto, llevan el nombre de la clase, no devuelven nada y se pueden sobrecargar. Si se declara uno, desaparece el predeterminado.
- Los
setygetson la puerta de entrada a los atributos privados, y son la oportunidad de validar y de ocultar la representación interna. - La recolección de basura libera la memoria sola, pero no libera archivos ni conexiones: esos recursos los tiene que cerrar el programa.
finalize()no es confiable. - Los paquetes agrupan clases relacionadas, evitan conflictos de nombres y controlan el acceso. La API de Java es el conjunto de paquetes que ya vienen con el lenguaje, y
java.langse importa solo. - UML es el lenguaje de modelado estándar: la clase se dibuja en tres partes y las relaciones se distinguen por el tipo de línea y por el rombo —blanco para la agregación, negro para la composición—.
Con esto ya se puede escribir cualquier clase: con sus atributos, sus métodos, su constructor, su herencia, sus interfaces y sus relaciones con otras clases. Lo que falta es que el programa resista los errores y que pueda guardar y recuperar información. Eso es la lección 4.
Licencia. Obra derivada de los apuntes de clase del Prof. Matías E. García —Clase Teórica 2—, publicada en profmatiasgarcia.com.ar bajo CC BY-NC-SA 2.5 Argentina; el desarrollo del texto, los diagramas y los ejercicios son de Prof Mauro Bobyk.