declaraciones y control de acceso
TRANSCRIPT
1
Declaraciones y Control de acceso (I)
Identificadores en Java
Un identificador es un nombre asignado a un paquete, clase,
interface, método o variable.
Un identificador en Java debe comenzar por una letra, el signo de
subrayado “_” o símbolo de moneda.
Los siguientes caracteres pueden ser cualquier combinación de
letras, números, caracteres de moneda o conectores.
No tienen longitud máxima.
Se diferencia entre mayúsculas y minúsculas.
Un identificador no puede ser una palabra clave de Java, pero
puede contener una palabra clave como parte de su nombre.
Ejemplo de literales válidos:
2
Ejemplo de literales no válidos:
Palabras clave en Java
Son palabras reservadas aquellas que tienen un significado
especial para el compilador de Java.
Se suelen mostrar en un cuadro alfabéticamente pero creo que es
más fácil recordarlas agrupandolas por categorías.
Tipos de datos (10) : byte, short, int, long, float, double,
boolean, char, enum, void
Flujo de datos (11) : if, else, for, do, while, switch, case,
break, default, continue, return
Excepciones(6) : try, catch, finally, throw, throws, assert
Operador(1) : instanceof
NoUso(2) : const, goto
Objetos(9) : package, import, class, interface,extends,
implements, new, this, super
Modificadores(11) : abstract, final, static, private, protected,
public, strictfp, synchronized, transient, volatile, native
true, false y null son literales, se escriben en minúsculas, y
tampoco pueden ser usados como identificadores.
3
Convenciones de codificación Java
Paquetes: en minúsculas (transporte.objetos, utiles.factura)
Clases e Interfaces: la primera letra en Mayúscula
(FacturaElectronica, Facturable)
Métodos: combinaciones verbo-nombre en camelCase. (getCliente,
setAltura, sumarAsientos)
Variables: nombres en camelCase. (nuevoCliente,
saldoMedioInteranual)
Constantes: en Mayúsculas y si hay que separar usar el subrayado
“_” (IPC, TOTAL_NOMINA)
Estructuras de control: cuando las sentencias forman parte de una
estructura de control de flujo,
escribirlas entre llaves, aunque sean sentencias sencillas.
Espacios: solo debe colocarse una sentencia por línea y utilizar
sangrías de dos o cuatro espacios para facilitar la lectura.
Comentarios: utilizar comentarios para explicar segmentos de
código no obvios.
JavaBeans: son clases de Java que tienen sus atributos privados.
Los métodos para obtener estos atributos se denominan getters y
los métodos para establecer estos atributossetters.
(getLongitud(), getFechaNacimiento(), isEncendido(),
setLongitud(l), setFechaNacimiento(fecha), setEncendido(true))
Declaraciones y control de acceso (II)
Declaración de una clase
Una clase es una plantilla que describe las propiedades y el
comportamiento que van a tener los objetos de esa clase.
La sintaxis para declarar una clase es la siguiente:
[Modificadores] class NombreClase [extends SuperClase]
[implementes Interface]{}
La mínima expresión de una declaración de clase sería:
class MinimaClase{}
Los modificadores y las cláusulas de herencia e interfaces son
opcionales.
4
Reglas de Declaración en el fichero fuente
Solo puede existir una clase pública en un fichero .java
El nombre del fichero debe coincidir con el de la clase
pública.
La sentencia package (si existe) debe ser la primera
sentencia del fichero.
Las sentencias import (si existen) deben seguir a la
sentencia package y preceder a las declaraciones de clases.
Pueden existir más clases en el fichero pero no pueden ser
públicas.
Las sentencias package e import afectarán a todas las clases
declaradas en el fichero.
El nombre de los ficheros que solo tengan declaraciones de
clase no públicas no tiene que coincidir con ninguna de las
clases.
Modificadores de acceso
Existen tres modificadores de acceso: public, protected y private.
Sin embargo, existen cuatro niveles de acceso. Cuando no se
especifica ninguno de los tres modificadores anteriores se tiene
el nivel de acceso por defecto, que es el nivel de paquete.
Para las clases de primer nivel solo se permite:
public
nivel de paquete
Para atributos, métodos, clases anidadas, se permiten todos.Los
explicamos un poco más, desde el más restrictivo al menos
restrictivo:
private: solo es accesible dentro de su clase.
no se especifica (nivel de paquete): es
accesible dentro de su clase y por
todas las clases de su paquete.
protected: es accesible dentro de
su clase, por todas las clases de
su paquete y por las clases hijas
que estén en otro paquete
diferente.
public: es accesible para
cualquier clase Java.
5
Otros Modificadores
Otros modificadores que pueden añadirse despúes de los
modificadores de acceso son:
strictfp
abstract
final
Strictfp
puede modificar una clase o un método, nunca una variable.
Cuando marcamos una clase como strictp significa que todo el
código de la clase sigue la especificación IEEE754 para flotantes.
Cuando marcamos strictfp a un método el código del método sigue la
especificación anterior.
Si cumplimos esta especificación prevenimos que los números
flotantes puedan ser dependientes de la plataforma.
Final
Cuando marcamos una clase como final estamos indicando que esta
clase no puede ser extendida en subclases.
Los métodos marcados como final no pueden sobrescribirse.
Si una variable se marca como final, se convierte en una
constante.
Abstract
Cuando marcamos una clase como abstract estamos indicando que no
se puede instanciar. Su objetivo es ser extendida en subclases.
Una clase abstracta puede tener tanto métodos abstractos como no
abstractos. Un solo método abstracto obliga a declarar la clase
como abstracta.
Este tipo de clases son útiles cuando la implementación queremos
que se concrete en sus clases hijas.
No tiene sentido declarar una clase final y abstracta.
6
Declaraciones y control de acceso (III)
Interfaz
La interfaz pública de una clase es un “contrato” entre el código
cliente y la clase que proporciona el servicio.
Decimos que una clase implementa una interfaz cuando implementa
todos los métodos declarados en ella.
Varias clases pueden implementar la misma interfaz. Una sola clase
puede implementar varias interfaces.
La sintaxis para declarar una interfaz es la siguiente:
[Modificadores] interface NombreInterface [extends
InterfacePadre]{
<public static final atributos>
<public abstract metodos>
}
Todos los métodos declarados en una interfaz son public y
abstract, aunque no se especifique.
Todos los atributos en una interfaz son public, static y
final, aunque no se especifique. Es decir, constantes.
Los métodos no pueden ser static ni final, strictfp, o native.
Una interfaz puede extender solo otras interfaces.
Las interfaces se usan para:
declarar métodos que serán implementados por una o varias
clases.
dar a conocer la interfaz de programación sin revelar la
implementación
identificar similitudes entre clases no relacionadas
simular la herencia múltiple declarando una clase que
implemente varias interfaces
Variables Locales y Modificadores
Los modificadores de acceso no son aplicables a variables locales,
provocaría error de compilación.
El único modificador que podría aplicarse a variables locales
sería final.
Modificadores de acceso para miembros de una clase
Se pueden aplicar: public, protected, paquete (por defecto),
private.
7
Otros modificadores para miembros de una clase
Strictfp
Para miembros de clase sólo se puede aplicar a métodos. Cuando
marcamos strictfp a un método el código del método sigue la
especificación anterior IEEE754 para flotantes.
Final
Los métodos marcados como final no pueden sobrescribirse.
Los argumentos marcados como final no pueden cambiar su valor.
Si una variable se marca como final, se convierte en una
constante.
Abstract
Un método abstracto se declara pero no se implementa. Acaba en ;.
Un solo método abstracto obliga a declarar la clase como
abstracta.
La primera subclase concreta debe implementar todos los métodos
abstractos.
Static
Pueden marcarse static: métodos, variables, clases anidadas y
bloques de inicialización.
Crea miembros que están asociados a la Clase y no necesitan que
exista un objeto instanciado de la Clase para existir.
Synchronized
Sólo métodos pueden marcarse synchronized. Significa que solo un
hilo puede acceder al método a la vez.
Native
Sólo métodos pueden marcarse native. Indica que el método está
implementado en un lenguaje que es dependiente de la plataforma,
usualmente C. Su declaración acabará en ‘;’ como los métodos
abstractos, ya que su implementación no se especifica.
Transient variable
Si marcamos una variable de instancia transient no se añadirá en
la serialización del objeto que la contenga.
Volatile variable
Indica al hilo que accede a la variable que siempre debe
reconciliar su copia privada con la copia en memoria. Solo aplica
a variables de instancia.
8
Argumentos variables
Para especificar un método con un número variable de argumentos,
se define el tipo seguido de ‘…’, un espacio y un nombre para el
array asociado.
El argumento variable debe ser el último en la declaración del
método y solo se permite uno.
[Modificador] ValorDeRetorno NombreMetodo (Tipo… a) {}
Constructores
Un constructor es un conjunto de sentencias para inicializar una
instancia.No se consideran métodos. No tienen valores de retorno
ni se heredan.
El nombre del constructor debe coincidir con el de la clase.Cada
clase tiene al menos un constructor. Si no se escribe ninguno Java
suministra uno por defecto. En ese caso el constructor no tiene
argumentos y su cuerpo está vacío.
En el momento que nosotros escribamos un constructor se pierde el
constructor por defecto.
La sintaxis es:
[Modificadores] nombreClase (<argumentos>){}
Declaraciones y control de acceso (IV)
Variables
Por el tipo se pueden dividir en:
variables de tipos primitivos.
variables de tipos de referencia.
Por el lugar en que se definen:
variables locales (dentro de un método, parámetros del
método)
variables miembro (fuera de un método, en la definición de la
clase)
variable de clase (fuera de un método, en la definición de
una clase, lleva la palabra static)
Tipos Primitivos en Java
Los tipos primitivos son simples valores, no objetos. Los tipos de
referencia se utilizan para tipos más complejos, incluidos los que
declara el propio programador.
9
Java define ocho tipos de datos primitivos, que se dividen en
cuatro categorías:
Lógicos: boolean
Texto: char
Enteros: byte, short, int, long
Reales: float, doublé
Boolean
Los valores lógicos se representan mediante el tipo boolean, que
puede contener uno de estos dos valores: true (verdadero) o false
(falso).
Decimos que el tipo boolean admite dos valores literales: true y
false.
Char
Los caracteres se representan mediante el tipo char. Un valor char
representa un carácter Unicode de 16 bits sin signo. Los literales
char se escriben entre ‘ ‘.
Enteros
Hay cuatro tipos de datos enteros en Java. Cada uno de ellos se
declara usando las palabras clave byte, short, int o long.
Se puede representar los literales de los tipos enteros utilizando
una notación decimal, octal o hexadecimal.
Por ejemplo:
70 sería 70 en decimal, 046 en octal y 0x106 en hexadecimal.
Todos los tipos numéricos en Java representan números con signo.
Por defecto los literales enteros son del tipo int, a menos que
vayan seguidos de la letra L, que indicaría que es un long.
Por ejemplo:
24L indicaría que el valor decimal 24 se representaría como un
entero largo.
La tabla siguiente indica el tamaño en bits de cada tipo y su
rango de valores:
10
Reales
Las variables de tipo real se declaran con las palabras float o
double. Un literal numérico es un número real si incluye un
separador decimal y un exponente, o bien si va seguido de la letra
F o f (float) o de la letra D o d (double).
Los literales reales se consideran double de forma predeterminada.
Es posible declarar literales del tipo float agregando F o f al
valor.
Ejemplos:
5.15 Valor corto de precisión simple, pero por
defecto sería double.
3.35E25 Valor largo double
4.393F Valor corto de precisión simple (float)
76.5E800D Valor largo double
Tabla de los reales:
Declarando variables primitivas
Pueden ser declaradas como variables de clase (static), como
variables miembro o como variables locales.
Se pueden declarar en una misma linea varias variables del mismo
tipo.
Ejemplos:
1
2
3
4
5
6
7
8
9
boolean resultado, existe, estaEncendido; //declaraciones
de variables locales
char nuevaLinea;
byte b;
public class claseX {
public static int contador = 0; //ejemplo de declaración
de variable de clase
private int codigo; //ejemplo de declaración
de variables miembro.
private boolean hayExistencias;
}
11
Declarando variables de referencia
Pueden ser declaradas como variables de clase (static), como
variables miembro o como variables locales.
Se pueden declarar en una misma linea varias variables del mismo
tipo.
Ejemplos:
1
2
3
4
5
6
7
8
9
10
11
String nombre, direccion, provincia; // declaración de
variables de referencia locales
MiObjeto x;
Informe i;
public class Cliente{
public static Cuenta[] cuentasVIP = new Cuenta [100];
//declaración como variable de clase
private String nombre; //declaración de variables de
referencia como miembros
private Domicilio domicilioActual;
private Cuenta cuenta;
...
}
Variables Locales
Las variables que se definen dentro de un método se denominan
locales, aunque algunas veces también reciben el nombre de
variables temporales o de pila.
Es preciso inicializarlas antes de usarlas por primera vez, de lo
contrario el compilador dará error.
Los parámetros de los métodos y los constructores también son
variables locales, pero las inicializa el código de llamada.
Las variables locales se crean cuando el programa empieza a
ejecutar el método y se destruyen cuando finaliza dicha ejecución.
Las variables que se definen dentro de los métodos de una clase
son locales para ese método, por lo que es posible usar el mismo
nombre de variable en distintas funciones miembro para hacer
referencia a distintas variables.
12
El único modificador que pueden usar las variables locales es
static.
Variables Miembro y de Clase
Las variables de clase, marcadas con static, se crean cuando se
carga la clase y siguen existiendo mientras ésta se mantenga
cargada.
Las variables miembro existen mientras exista el objeto asociado.
Las variables de clase y variables miembro se inicializan
automáticamente en el momento de crearse. Si el programador no
asigna un valor predeterminado lo hará el compilador.
Las variables no locales (miembro y de clase) pueden utilizar los
siguientes modificadores: public, protected, private, static,
final, transient, volatile.
Declaración y control de acceso (V)
Declaración de un array
Los arrays se usan normalmente para agrupar objetos del mismo
tipo. Permiten hacer referencia al grupo de objetos a través de un
nombre común.
Es posible declarar arrays de cualquier tipo, ya sea de tipos
primitivos o tipos de referencia.
En cualquier caso, un array en Java es un objeto incluso cuando
esté compuesto de tipos primitivos.
Como en el caso de otros tipos de clase, la declaración del array
no crea el objeto en sí. La declaración de un array crea una
referencia al array. La memoria utilizada por los elementos del
array se asigna de forma dinámica mediante una sentencia new o
mediante el inicializador del array.
13
La sintaxis de declaración es:
tipoElementos [] nombreArray;
tipoElementos nombreArray [];
Las dos opciones son válidas. Aunque se suele recomendar los
corchetes a la izquierda por ser más legible y porque éstos se
aplican a todas las variables a la derecha de ellos.
int [] puntuacion, puntuacionMaxima; //Recomendada
int puntuacion[], puntuacionMaxima [];
En la declaración no se debe indicar el tamaño concreto del array,
dará error de compilación. Veremos esto más adelante en la
construcción e incialización de arrays.
Tipos Enumerados
Una práctica habitual en programación es tener un número finito de
nombres simbólicos que representan los valores de un atributo.
Por ejemplo para representar los colores de un semáforo: ROJO,
AMARILLO, VERDE.
A partir de la versión 5.0, Java SE incluye una modalidad de tipos
enumerados que mantiene la seguridad de los tipos.
Veamos un ejemplo de declaración:
1
2
3
4
5
6
7
public class Semaforo {
enum ColorSemaforo{
ROJO,
AMARILLO,
VERDE
};// el punto y coma es opcional en la declaracion de enums.
...
Habría que pensar en el tipo Semaforo como en una clase con un
conjunto finito de valores que reciben los nombres simbólicos
incluidos en la definición del tipo. Los valores serían realmente
instancias de esa clase. El compilador no permitirá que pueda
asignarse otro valor diferente a los especificados.
El orden en el que definamos los valores importa, pero se verá más
adelante
1 ColorSemaforo colorActual = ColorSemaforo.VERDE; // VERDE es
de tipo ColorSemaforo
14
2 ColorSemaforo colorIncorrecto = ColorSemaforo.AZUL;
//provocará error de compilación.
Los tipos enumerados pueden declararse como miembro de una clase,
como una clase aparte, pero nunca dentro de un método.
Declarando constructores, variables y métodos en un Tipo
Enumerado
La nueva modalidad de tipos enumerados permite usar atributos y
métodos, igual que en otras clases.
Un constructor enum siempre debería usar acceso privado. Los
argumentos del constructor se suministran después de cada valor
declarado. Los tipos enumerados pueden tener cualquier cantidad de
atributos y métodos.
Para nuestro ejemplo, definamos un atributo nombreColor con un
literal más amigable que las mayúsculas y un atributo
segundosColor que indicará el tiempo en segundos que está activado
cada color.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Argumentos para el constructor en la declaración
public enum ColorSemaforo{
ROJO ("Rojo", 60),
AMARILLO ("Amarillo", 25),
VERDE ("Verde", 45); //Se requiere el punto y coma cuando
hay
//mas código en la definición del enum
private String nombreColor; //atributos
private int segundosColor; // atributos
//Constructor
private ColorSemaforo (String nombreColor, int
segundosColor){
this.nombreColor = nombreColor;
this.segundosColor = segundosColor;
}
15
18
19
20
21
22
23
24
//Métodos getters
public String getNombreColor(){
return nombreColor;
}
public int getSegundosColor(){
return segundosColor;
}
}
Recordaremos de los constructores de tipos enumerados:
No se puede invocar un constructor de un enum directamente.
Se llama automáticamente cuando especificamos los argumentos
en la declaración de los valores del tipo enumerado.
Se puede definir más de un argumento para el constructor y se
puede sobrecargar los constructores
Orientación a objetos (I)
Encapsulación
La encapsulación es la forma de ocultar ciertos elementos de la
implementación de una clase y, al mismo tiempo, proporcionar una
interfaz pública para el software cliente.
Es una forma más de ocultar los datos, ya que la información de
los atributos es un elemento significativo de la implementación de
las clases. Forzamos a que el código llamante que usa nuestras
clases utilicen métodos para acceder a los atributos en lugar de
usar los atributos directamente.
¿Cómo hacemos esto?
Protegiendo los atributos de nuestras clases (usando el
modificador private)
Suministrando métodos públicos para acceder a nuestros
atributos privados.
Nombrando estos métodos como se nos indica en las reglas de
convención para JavaBeans (getNombreAtributo,
isNombreAtributo, setNombreAtributo)
16
Herencia
Toda clase en Java hereda de la clase Object. De modo que toda
clase que creemos heredará los métodos de Object (equals, clone,
notify…)
Los motivos más usuales para utilizar la herencia son:
Reutilizar código
Poder hacer uso del Polimorfismo
¿Cómo reutilizamos código en la herencia?
En la herencia, al poner los atributos y comportamiento común en
la clase padre estamos evitando repetir este código en cada clase
hija.
Cada subclase definirá solo los atributos y métodos que la hacen
una clase más especializada y de forma automática heredará los
atributos y métodos de la clase padre.
¿Cómo hacemos uso del polimorfismo en la herencia?
Un objeto sólo tiene una forma (aquella que se le asigna cuando se
construye). Sin embargo, una variable es polimórfica porque puede
hacer referencia a objetos de distintas formas.
Java, como la mayoría de los lenguajes de programación orientado a
objetos, permite hacer referencia a un objeto con una variable que
es uno de los tipos de una superclase. Por lo tanto sería posible:
Superclase variable = new Subclase();
Empleado e1 = new Tecnico();
Las variable e1, puede acceder únicamente a las partes del objeto
que son componentes de Empleado; las partes específicas de Tecnico
están ocultas.
Esto es porque, por lo que respecta al compilador, e1 es un
Empleado, no un Gerente.
Usamos el polimorfismo en la herencia cuando en el código no nos
importa qué subclase es, sino que lo que nos importa es que es una
subclase de la clase padre que nos interesa.
Por ejemplo, supongamos un array con objetos de las distintas
subclases de Empleado.
Podríamos recorrer este array volcando cada objeto en una variable
de la superclase y acceder a un método común, getNombreEmpleado().
1
2
3
4
Empleado[] empleados = new Empleado[4];
empleados[0]=new Tecnico();
empleados[1]=new Secretario();
empleados[2]=new Contable();
17
5
6
7
8
9
10
11
12
13
empleados[3]=new Tecnico();
for (int i=0; i<4; i++){
Empleado e = empleados[i];
System.out.println(e.getNombre());
System.out.println(e.getAntiguedad());
System.out.println(e.getSueldo());
}
Relación “es-un”
El concepto es-un se basa en la herencia o en la implementación de
interfaces. Es una forma de decir que “una cosa es un tipo de otra
cosa”.
En Java estas relaciones se expresan mediante las palabras claves
extends o implements.
En el ejemplo anterior cuando Tecnico hereda de Empleado podemos
decir que el “Tecnico es un Empleado”.
Relación “tiene-un”
El concepto tiene-un se basa en la composición. Es decir se
produce cuando para definir una clase estamos usando otra clase.
Por ejemplo, la clase Tecnico podría tener como atributo
proyectoActual de tipo Proyecto. Podemos decir que el “Tecnico
tiene-un Proyecto”.
Sobrescritura de métodos
En las subclases además de poder añadir nuevos atributos para
ampliar la clase padre, es posible modificar un método de la clase
original.
Si se define un método en una subclase de tal forma que su nombre,
el tipo de retorno y la lista de argumentos son idénticos a los
de la clase padre, se dice que el nuevo método sobrescribe al
antiguo.
A partir de la versión J2SE 5.0, también se permite que el tipo de
retorno pueda ser una subclase del tipo de retorno del método
original.
Los métodos sobrescritos no pueden ser menos accesibles
18
El nuevo método no puede ser menos accesible que el método
original. Por ejemplo, si en la clase padre el método que queremos
sobrescribir era public, el método en la clase hija no puede ser
private.
Llamada a métodos sobrescritos
Un método de una subclase puede llamar a un método de una
superclase usando la palabra clave super.
La palabra super se refiere a la superclase de la clase en la que
estamos haciendo uso de ella.
Se usa para hacer referencia a las variables miembro o a los
métodos de la superclase.
Una llamada a super.metodo() llama al comportamiento completo del
método como si hubiera sido llamado en una clase de nivel
superior. El método no tiene que estar definido en la clase
inmediatamente superior, puede heredarse de alguna clase situada
más arriba en la jerarquía.
Sobrescritura de métodos y excepciones
Cuando se sobrescribe un método que genera excepciones, el método
que sobrescribe puede declarar sólo excepciones que sean la misma
clase o una subclase de las excepciones.
Está permitido declarar métodos de sobrescritura que generen menos
excepciones que el método de la superclase, incluso que no generen
ninguna excepción.
Orientación a objetos (II)
Polimorfismo
Hemos hablado anteriormente del polimorfismo cuando hablabamos
sobre la herencia. Decíamos que una variable es polimórfica porque
puede hacer referencia a objetos de distintas formas.
Y vimos que Java permitía hacer referencia a un objeto con una
variable del tipo de una superclase.
Empleado e = new Tecnico();
Usando la variable e podíamos acceder únicamente a las partes del
objeto que son componentes de Empleado (la superclase). Esto se
debía a que para el compilador la variable e es de tipo Empleado y
no Tecnico.
19
Pero supongamos que ambas clases tienen un método toString().
Sabemos que si declaramos lo siguiente y llamamos a toString()
Empleado e1 = new Empleado();
Tecnico e2 = new Tecnico();
e1.toString();
e2.toString();
e1 llama al método toString() de la clase Empleado y e2 llama al
método toString de la clase Tecnico.
¿Pero que ocurre si hacemos esta llamada?
Empleado e3 = new Tecnico();
e3.toString();
Se obtiene el comportamiento asociado al objeto al que hace
referencia la variable durante el tiempo de ejecución. El
comportamiento no está determinado por el tipo de la variable en
el momento de la compilación. Este es un aspecto del polimorfismo
que a menudo recibe el nombre de llamada a métodos virtuales.
e3.toString() llama al método del tipo real del objeto, Tecnico.
Colecciones hetereogéneas
Es posible crear colecciones de objetos que son de una misma
clase. Estas colecciones se llaman homogéneas. Por ejemplo,
cualquier array que construyamos de una clase es una colección
homogénea:
Clientes[ ] clientesNuevos = new Clientes[100];
clientesNuevos[0] = new Cliente();
clientesNuevos[1] = new Cliente();
clientesNuevos[2] = new Cliente();
…
Sin embargo, Java permite crear colecciones compuestas por objetos
de distintos tipos. Estas colecciones se llaman hetereogéneas.
Por ejemplo, una colección con objetos de las subclases de
Empleado que vimos anteriormente:
Empleado[] empleados = new Empleado[3];
empleados[0] = new Tecnico();
empleados[1] = new Secretario();
empleados[2] = new Contable();
Es incluso posible hacer una colección con objetos de cualquier
clase, ya que en Java todos los objetos heredan de Object. Podemos
tener una colección como esta:
Object [] objetos = new Object[5];
objetos[0] = new Empleado();
20
objetos[1] = new Batman();
objetos[2] = new Libro();
objetos[3] = new String(“Maria”);
objetos[4] = new Bicicleta();
Podríamos recorrer el array y llamar a un método que fuera común,
por ejemplo toString().
Sobrecarga de métodos
En algunos casos podemos necesitar definir varios métodos que
realicen la misma función pero con diferentes parámetros. Java
permite reutilizar un mismo nombre de método para varios métodos
si al llamarlos es posible distinguir unos y otros.
Reglas de los métodos sobrecargados:
Las listas de argumentos deben ser diferentes. Deben poder
determinar sin ambigüedad qué método es el que se quiere
llamar.
Los tipos de retorno pueden ser diferentes. El tipo de
retorno puede ser diferente pero no es suficiente si ésta es
la única diferencia.
El modificador de acceso puede cambiar.
Puede declarar nuevas o más amplias excepciones comprobadas.
Y a tener en cuenta:
Un método se puede sobrecargar en la misma clase o en una
subclase.
Llamadas a métodos sobrecargados
Cuando llamamos a un método sobrecargado, existe más de un método
con el mismo nombre. Decidir qué método es el que se debe llamar
se hace en base a los argumentos.
Si tenemos un método sumar(int a, int b), y otro método
sumar(double a, double b).
La llamada sumar(10, 7) llama a sumar con argumentos enteros y la
llamada sumar(2.7, 2.3) llama a sumar con argumentos double.
¿Pero qué ocurre cuando en lugar de parámetros de tipos primitivos
tenemos parámetros de tipos de referencia?
Supongamos estos métodos sobrecargados de la clase Proyecto.
public void añadirPersonal (Empleado e);
public void añadirPersonal (Tecnico t);
Con estas llamadas:
Empleado e = new Empleado();
21
Tecnico t = new Tecnico();
Empleado tec = new Tecnico();
e.añadirPersonal(e); //Como esperamos e es de tipo Empleado y
llama a añadirPersonal(Empleado e)
e.añadirPersonal(t); //Como es de esperar, t es de tipo Tecnico y
llama a añadirPersonal(Tecnico t)
e.añadirPersonal(tec); //En este caso tec es una referencia de
Empleado pero es en realidad un objeto Tecnico.
En este ultimo caso se llama al método añadirPersonal(Empleado e).
Aunque el objeto en tiempo de ejecución sea un Tecnico, la
decisión de a qué método llamar no se hace dinámicamente en tiempo
de ejecución, asi que el tipo de referencia determina a qué metodo
sobrecargado se llama
22
Orientación a objetos (III)
Sobrecarga Constructores
Sobrecarga de constructores
Una clase puede definir varios constructores para permitir
inicializar un objeto con más o menos atributos.
Podríamos tener un constructor sin argumentos para crear un objeto
con unos atributos por defecto y varios constructores con
argumentos dependiendo de a qué atributos queramos dar valor.
Por ejemplo, para la clase Tecnico:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Tecnico{
public static final double SALARIO_BASE = 600;
private String nombre;
private int departamento;
private float sueldo;
public Tecnico(){ //Constructor sin parámetros.
nombre = "";
departamento = 0;
sueldo = SALARIO_BASE;
}
public Tecnico(String nombre, int departamento, float
sueldo){ //Constructor con todos los parámetros.
this.nombre = nombre;
this.departamento = departamento;
this.sueldo = sueldo;
}
public Tecnico(String nombre, float sueldo){ //Constructor con tres
parámetros, que utiliza this().
this(nombre, 0, sueldo);
}
23
21
22
}
El primer constructor sin parámetros inicializa todos los
atributos con valores por defecto para nombre, departamento y
sueldo.
El segundo constructor tiene parámetros para cada atributo de
Tecnico.
El tercer constructor tiene dos parámetros, nombre y sueldo. La
referencia this se utiliza como forma de envío de llamada a otro
constructor (siempre dentro de la misma clase), en este caso el
segundo constructor.
Es decir en esta llamada:
Tecnico t3 = new Tecnico (“Jose Luis”, 1300.50f);
Estaremos invocando al constructor con tres parámetros
así: Tecnico(“Jose Luis”, 0, 1300.50f)
Los constructores no se heredan
Aunque las subclases heredan todos los métodos y variables de sus
superclases, no heredan sus constructores.
Las clases sólo pueden obtener un constructor de dos formas: debe
escribirlo el programador o, si éste no lo escribe, debe usar el
constructor predeterminado. Siempre se llama al constructor del
nivel superior además de llamar al constructor subordinado.
Llamada a constructores de nivel superior
Puede llamar a un determinado constructor de una superclase como
parte de la inicialización de una subclase utilizando la palabra
clave super en la primera línea del constructor de la subclase. Es
preciso suministrar los argumentos adecuados a super().
Cuando no hay ninguna llamada a super con argumentos, se llama
implícitamente al constructor de la superclase que tenga cero
argumentos. Si en la superclase no hubiera un constructor sin
argumentos se produciría un error de compilación.
La llamada super() puede adoptar cualquier cantidad de argumentos
adecuados para los distintos constructores disponibles en la
superclase, pero debe ser la primera sentencia del constructor.
Recordar:
Si se utilizan, es preciso que super o this sean la primera
línea del constructor.
24
Si un constructor no contiene ninguna llamada a super(…) ni a
this(…), el compilador introduce una llamada al constructor
de la superclase sin argumentos.
Otros constructores pueden llamar también a super(…) o
this(…), con lo que iríamos encadenando llamadas a
constructores. Al final el constructor de la clase de nivel
superior se ejecutará antes que ningún otro constructor
subordinado.
Un constructor nunca puede tener ambas llamadas super(…) y
this(…).
Orientación a objetos (IV) – Conversiones
de tipos
Operador instanceof
Como hemos visto, es posible utilizar objetos mediante referencias
a sus superclases. Por lo tanto, a veces es necesario saber con
qué objetos reales estamos tratando. Para ello tenemos el operador
instanceof.
Por ejemplo, si tenemos la siguiente jerarquía de clases,
utilizaríamos el operador instanceof para realizar unas acciones u
otras dependiendo del objeto que realmente tengamos:
1 public class Contrato{...}
1 public class Deposito extends Contrato{...}
1 public class Prestamo extends Contrato{...}
1
2
3
4
public void finalizarContrato(Contrato c){
if (c instanceof Deposito){
//Devolver intereses
//Poner indicador de contrato vivo a falso
25
5
6
7
8
9
10
}
else if (c instanceof Prestamo){
//Poner importePendiente a 0
//Poner indicador de contrato vivo a falso
}
}
¿Como podemos realizar estas acciones si para nuestro compilador c
es de tipo Contrato y no podríamos acceder a métodos de Deposito y
de Prestamo? Haciendo una conversión de tipos.
Conversión de tipos
En los casos como el anterior, en el que ya hemos comprobado
mediante el operador instanceof que el objeto es una subclase
concreta, es posible acceder a toda la funcionalidad del objeto
convirtiendo la referencia.
Para el ejemplo anterior sería:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void finalizarContrato(Contrato c){
if (c instanceof Deposito){
Deposito d = (Deposito) c;
//Devolver intereses
d.devolverIntereses();
//Poner indicador de contrato vivo a falso
d.setIndContratoVivo(false);
}
else if (c instanceof Prestamo){
Prestamo p = (Prestamo)c;
//Poner importePendiente a 0
p.setImportePendiente(0);
//Poner indicador de contrato vivo a falso
p.setIndContratoVivo(false);
}
}
26
Si no realizamos la conversión, cualquier llamada a un método
provocará error al no localizar tal método en la superclase.
Debemos tener en cuenta:
Las conversiones en dirección ascendente (de un objeto de una
subclase a una referencia a una superclase o interfaz)
siempre están permitidas, y de hecho, no precisan el operador
de conversión. Se pueden hacer mediante una simple
asignación.
1
2
Prestamo pr = new Prestamo();
Contrato c = pr; //"Padre" = "Hijo" siempre posible y no
necesario cast.
Si Prestamo implementa Firmable con el método firmar, también
es legal la asignación directa:
1
2
3
4
Prestamo pr = new Prestamo();
Firmable f = prestamo;
f.firmar(); //podremos llamar a los métodos de la interfaz,
//ya que están implementados en Prestamo.
Y más aún, si tenemos la clase PrestamoHipotecario que hereda
de Prestamo, que implementa a Firmable, también podemos hacer
la asignación directa. Ya que PrestamoHipotecario hereda
también la función Firmar por Prestamo implementar a
Firmable.
1
2
3
PrestamoHipotecario hip = new PrestamoHipotecario();
Firmable f = hip;
f.firmar();
En las conversiones de dirección descendente (de un objeto de
una superclase a una referencia a una subclase), el
compilador debe poder considerar que la conversión es
posible, para esto, la clase destino de la conversión tiene
que ser una subclase del tipo de referencia actual.
1
2
3
Contrato c = new Contrato();
Prestamo p = (Prestamo) c; // "Hijo" = "Padre" necesita
cast.
27
4 //El compilador no dará fallo pues parece posible la
conversión.
//Prestamo es una subclase de Contrato.
1
2
3
4
Empleado e = new Empleado();
Prestamo p = (Prestamo) e; //En este caso el compilador
considera
//que es imposible la conversión. Ni siquiera está en la
jerarquía.
//Provocará un error 'Inconvertible types'
Si el compilador permite la conversión, todavía no podemos
cantar victoria, el tipo de objeto se comprueba durante el
tiempo de ejecución. Si el objeto que se va a convertir no es
realmente del tipo al que se hace la conversión, se producirá
un error en tiempo de ejecución. Será una excepción
ClassCastException.
1
2
3
4
5
Contrato c1 = new Contrato();
Contrato c2 = new Prestamo();
Prestamo p1 = (Prestamo) c1; //Compila pero genera
ClassCastException al ejecutar.
Prestamo p2 = (Prestamo) c2; //Compila y se ejecuta
correctamente.
Orientación a objetos (V) – Implementación de
interfaces y tipos de retorno.
Implementando Interfaces
Decíamos con anterioridad que las interfaces son una especie de
“contrato”, una declaración del conjunto de métodos que podemos
utilizar si una clase implementa dicha interfaz.
Por tanto, una clase al implementar una interfaz debe definir cada
uno de los métodos de ésta. De esta forma, cualquier persona que
instancie un objeto de la clase implementadora tiene la seguridad
de que puede llamar a los métodos de la interfaz.
28
Cuando Java encuentra una clase que dice implementar una interfaz
la comprueba. Si falta algún método por definir nos dará error de
compilación. Basta que tengamos la definición de los métodos
aunque no tengamos ningún código para ellos.
Por ejemplo, esta clase no daría ningún error:
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Cultivable{
public void plantar();
public void regar();
public int cosechar();
}
public class cultivoFresas implements Cultivable {
....
public void plantar() {}
public void regar(){}
public int cosechar(){}
...
}
También es posible que nuestra clase implementadora sea abstracta.
En este caso la clase puede elegir entre implementar algunos
métodos, todos o ninguno. Y sería alguna clase concreta de ésta la
que defina los restantes.
Por ejemplo:
1
2
3
4
5
public abstract class Cultivos implements Cultivable {
public void plantar() { ... } //Ejemplo de clase abstracta
public void regar() { ... } // que define algunos métodos
}
29
6
7
8
public class Maiz extends Cultivos {
public void int cosechar() { ... } //Y el que falta en la
concreta.
}
Las clases que implementan una interfaz deben seguir las misas
normas que las clases que extienden una clase abstracta:
Deben definir todos los métodos de la interfaz.
Seguir todas las normas para las sobrescrituras (overrides).
Declarar sólo las excepciones comprobadas definidas en la
interfaz o subclases de éstas.
Mantener la signatura de los métodos y el tipo devuelto (o un
subtipo de éste).
Hay que recordar también que:
Una clase puede extender a sólo una clase.
Una clase puede implementar varias interfaces.
Una interfaz puede extender una o varias interfaces, pero
nunca puede implementar.
Tipos de Retorno Legales
Vamos a hacer un pequeño resumen de qué podemos declarar como
tipos de retorno:
Métodos sobrecargados: un método sobrecargado es un método
totalmente diferente y nuevo.
Si tenemos un método en una clase y lo sobrecargamos en una
subclase no tiene que regirse por las normas de los métodos
sobrescritos. Podemos declarar cualquier tipo de retorno.Hay
que recordar que la sobrecarga no sólo se puede basar en un
tipo diferente de retorno, debía diferenciarse del método
original por el número o tipo de parámetros.
Métodos sobrescritos: un método sobrescrito consistía en dar
una implementación distinta a un método en una subclase. El
método debía coincidir exactamente. A partir de Java 5, se
permite también que el tipo de retorno en el método
30
sobrescrito sea una subclase del tipo de retorno del método
en la superclase.
Para devolver correctamente un valor para que se ajuste al tipo de
retorno, hay que tener en cuenta:
Sólo podemos devolver null si el tipo de retorno es una
variable de referencia (objeto y no tipo primitivo).
Si el tipo de retorno es un tipo primitivo cualquier valor
que sea del tipo o que se pueda convertir
implícitamente o explícitamente (usando un cast).
Nunca retornar un valor si el tipo de retorno es void.
Si el tipo de retorno es una variable de referencia (objeto y
no tipo primitivo) podemos devolver un objeto de una
subclase, ya que se hace el cast implícitamente.
Si el tipo de retorno es una interfaz podemos devolver
cualquier objeto de una clase que implemente dicha interfaz.
Orientación a Objetos (VI) – Constructores
Constructores
Los constructores son métodos especiales que se utilizan para
inicializar un objeto de una clase.
Los constructores son la única forma de crear un objeto en Java.
El nombre del constructor debe ser idéntico al de la clase y no
tendrá tipo de retorno. Los modificadores de acceso válidos para
los constructores son: public, protected y private.
Una clase puede tener cualquier número de constructores. Todos
tendrán el mismo nombre y al igual que otros métodos
sobrecargados, los constructores se diferenciaran unos de otros en
el número y en el tipo de sus parámetros.
La sintaxis será:
[modificador acceso] nombreClase ([listaParámetros]) {…}
Encadenamiento de Constructores
Para llamar al constructor utilizaremos la palabra clave new.
Además, con esta palabra clave, no sólo estamos invocando al
constructor de la clase de nuestro objeto, sino también a los
constructores de las superclases.
Por ejemplo, imaginemos que tenemos la siguiente clase:
31
1
2
3
public class Solicitante extends Persona extends Object {
...
}
Al crear un objeto con new,
1
2
3
...
Solicitante solAyuda1 = new Solicitante();
...
se llamará al constructor de Solicitante, que llamará al
constructor de Persona, que llamará al constructor de Object.
Después, terminará el constructor de Object, terminará el
constructor de Persona inicializando los atributos que le
correspondan, y terminará el constructor de Solicitante
inicializando los atributos que le correspondan.
El Constructor es Obligatorio
Todas las clases deben tener un constructor, incluso las clases
abstractas.
Si no escribimos un constructor para una clase el compilador añade
uno por defecto.
En este caso, el constructor no tendrá parámetros, su cuerpo
estará vacío y tendrá el mismo modificador de acceso que la clase.
Por ejemplo, para esta clase que no hemos escrito constructor:
1
2
3
4
5
public class Persona{
private String id;
private String nombre;
private String movil;
}
El compilador insertaría:
1
2
3
4
public class Persona{
private String id;
private String nombre;
private String movil;
32
5
6
public Persona(){}
}
En el momento en que escribamos un constructor (con o sin
parámetros) el constructor por defecto se perderá.
A Recordar
Cada constructor debe tener en su primera línea una llamada a
otro constructor sobrecargado, (this) o una llamada al
constructor de la superclase (super).
En el caso de que no escribamos la llamada a super, el
compilador insertará automáticamente una llamada a super sin
parámetros, super().
Sólo es posible llamar a a un constructor desde otro
constructor.
Las interfaces no pueden tener constructores, ya que no son
clases.
La llamada al constructor de la superclase mediante super
puede ser con o sin parámetros.
Si el constructor de la superclase necesita parámetros y en
el constructor de la subclase hacemos una llamada a super sin
parámetros fallará.
Las clases abstractas también tienen constructores. Estos
constructores serán llamados cuando se ejecute el constructor
de las subclases que la concretan.
Orientación a Objetos (VII) – Miembros estáticos
Static
La palabra clave static declara miembros (atributos, métodos y
clases anidadas) que están asociados a la clase en lugar de a una
instancia de la clase.
Utilidad
A veces puede ser útil tener una variable compartida por todos los
objetos de la clase (algo parecido a una variable global). En este
caso marcaríamos esta variable como static.
Si una variable static no se marca como private, es posible
acceder a ella desde fuera de la clase. Para hacerlo, no se
necesita ningún objeto, se hace referencia a ella mediante el
nombre de la clase.
33
Por ejemplo, un caso común es un contador de los objetos
instanciados de una clase:
1
2
3
4
5
6
7
8
9
10
11
12
public class Leccion{
public static int contador; //variable estática, asociada a la
clase
private int idLeccion; //atributos, variables no estáticas
private String desLeccion; //asociadas a los objetos
public Leccion()
{
contador = contador + 1; //podemos usar el contador para el
id de cada objeto Leccion.
idLeccion = contador;
desLeccion = "Sin nombre";
}
}
1
2
3
4
5
6
7
8
9
public class MainLeccion{
public static void main (String[] args){
System.out.println("Contador: " + Leccion.contador);
//Muestra 0
Leccion l1 = new Leccion();
Leccion l2 = new Leccion();
System.out.println("Contador: " + Leccion.contador);
//Muestra 2
}
}
O cuando necesitemos un método cuyo código no dependa de los
atributos de un objeto de la clase. En este caso marcaríamos este
método como static.
34
Por ejemplo, podríamos hacer la variable estática anterior privada
y obtener el número de objetos Leccion creados mediante un método
getContador().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Leccion{
private static int contador; //variable estática, asociada a la
clase
public static int getContador(){ //método estático
return contador;
}
private int idLeccion; //atributos, variables no estáticas
private String desLeccion; //asociadas a los objetos
public Leccion()
{
contador = contador + 1; //podemos usar el contador para el
id de cada objeto Leccion.
idLeccion = contador;
desLeccion = "Sin nombre";
}
}
En la clase principal ahora llamaríamos al método estático:
1
2
3
4
5
6
7
public class MainLeccion{
public static void main (String[] args){
System.out.println("Contador: " +
Leccion.getContador()); //Muestra 0
Leccion l1 = new Leccion();
Leccion l2 = new Leccion();
System.out.println("Contador: " +
Leccion.getContador()); //Muestra 2
}
35
Accediendo a Miembros Estáticos
Anteriormente hemos visto que para acceder a las variables y
métodos estáticos hemos utilizado el nombre de la clase y el
operador punto. Es lo más lógico dado que las variables y métodos
estáticos pertenecen a la clase, pero Java permite también acceder
a los miembros estáticos utilizando un objeto.
Java lo permite pero el compilador realmente lo sustituirá por el
nombre de la clase.
En el ejemplo anterior podíamos haber escrito:
1 System.out.println("Contador: " + l2.getContador());
Esto último solo lo podremos hacer si existe una instancia de la
clase.
Redefinición en Métodos Estáticos
Los métodos estáticos no se pueden sobrescribir (override), sin
embargo si es posible tener el mismo método en una subclase. Esto
se denomina redefinición o ocultación.
Si se llama al método con una referencia de objeto, el método
llamado será el correspondiente a la clase para la que se haya
declarado la variable.
¿Qué diferencia existe entre la sobrescritura y la redefinición?
La sobrescritura (override) está íntimamente ligada al
polimorfismo. Como los métodos estáticos están asociados a la
clase en lugar de a los objetos, el polimorfismo en tiempo de
ejecución no es posible y por lo tanto no es sobrescritura sino
redefinición.
Veamos la diferencia. Creamos una clase Persona y una subclase
Cliente. Creamos un método no estático, mostrar() y un método
estático, mostrarStatic() en cada una de las clases.
1
2
3
4
5
6
public class Persona{
private String nif;
private String nombre;
private int edad;
public Persona(String nif, String nombre, int edad){
36
7
8
9
10
11
12
13
14
15
16
17
18
19
this.nif = nif;
this.nombre = nombre;
this.edad = edad;
}
... //getters, setters
public void mostrar(){
System.out.println("Persona " + nif + ": \n Nombre: " +
nombre + "\n Edad: " + edad);
}
public static void mostrarStatic(){
System.out.println("Clase Persona");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Cliente extends Persona{
private int id;
private int antiguedad;
private int descuento;
public Cliente(String nif, String nombre, int edad, int id, int
antiguedad, int descuento) {
super(nif, nombre, edad);
this.id = id;
this.antiguedad = antiguedad;
this.descuento = descuento;
}
... // getters, setters
public void mostrar(){
37
15
16
17
18
19
20
21
System.out.println("Cliente" + id + ": \n Nombre: " +
getNombre()
+ "\n Antigüedad: " + antiguedad + "\n Descuento: " +
descuento);
}
public static void mostrarStatic(){
System.out.println("Clase Cliente");
}
}
Con esta definición de clases si creamos los siguientes objetos
tendríamos los siguientes resultados:
1
2
3
4
5
6
7
8
9
10
11
public class MainSobrescritura{
public static void main(String[] args){
Persona per = new Persona("18520147L", "Maria
Victoria Rodriguez", 18);
Cliente cli = new Cliente("20415789M", "Secundino
Jimenez", 25, 504, 5, 1);
Persona per2 = new Cliente("41605788R", "Luis
Martin", 30, 905, 3, 2);
per.mostrar(); //llama a mostrar() de Persona
cli.mostrar(); //llama a mostrar() de Cliente
per2.mostrar();//llama a mostrar() de Cliente
}
}
Ahora veamos lo que ocurre con un método estático:
1
2
3
4
5
public class MainRedefinicion{
public static void main(String[] args){
Persona per = new Persona("18520147L", "Maria Victoria
Rodriguez", 18);
38
6
7
8
9
10
11
Cliente cli = new Cliente("20415789M", "Secundino Jimenez",
25, 504, 5, 1);
Persona per2 = new Cliente("41605788R", "Luis Martin", 30,
905, 3, 2);
per.mostrarStatic(); //llama a mostrarStatic de Persona
cli.mostrarStatic(); //llama a mostrarStatic de Cliente
per2.mostrarStatic();//llama a mostrarStatic de Persona y
no de Cliente como arriba.
}
}
Vemos que en los métodos estáticos el método llamado será el de la
clase declarada en tiempo de compilación y no en tiempo de
ejecución como pasa con los métodos no estáticos sobrescritos.
Acceso en un método estático
Un método estático no puede acceder a ninguna variable salvo a las
variables locales, los atributos static y sus parámetros.
Cualquier intento de acceder a atributos que no sean estáticos
directamente (sin crear un objeto) provoca un error de
compilación.
Nota El método main es también un método estático. Podemos
comprobar que no creamos ningún objeto para ejecutarlo.Si necesita
datos miembro se deben crear objetos dentro de main.
Orientación a Objetos (VIII) – Acoplamiento
y Cohesión
Uno de los objetivos más importantes del diseño orientado a
objetos es conseguir una alta cohesión entre clases y un bajo
acoplamiento.
¿Qué es la cohesión?
La medida que indica si una clase tiene una función bien definida
dentro del sistema. El objetivo es enfocar de la forma más precisa
posible el propósito de la clase. Cuanto más enfoquemos el
propósito de la clase, mayor será su cohesión.
39
Una prueba fácil de cohesión consiste en examinar una clase y
decidir si todo su contenido está directamente relacionado con el
nombre de la clase y descrito por el mismo.
Una alta cohesión hace más fácil:
Entender qué hace una clase o método
Usar nombres descriptivos
Reutilizar clases o métodos
¿Qué es el acoplamiento?
El acoplamiento entre clases es una medida de la interconexión o
dependencia entre esas clases.
El acoplamiento fuerte significa que las clases relacionadas
necesitan saber detalles internos unas de otras, los cambios se
propagan por el sistema y el sistema es posiblemente más difícil
de entender.
Por ello deberemos siempre intentar que nuestras clases tengan un
acoplamiento bajo. Cuantas menos cosas conozca la clase A sobre la
clase B, menor será su acoplamiento.
Lo ideal es conseguir que la clase A sólo conozca de la clase B lo
necesario para que pueda hacer uso de los métodos de la clase B,
pero no conozca nada acerca de cómo estos métodos o sus atributos
están implementados.
Los atributos de una clase deberán ser privados y la única forma
de acceder a ellos debe ser a través de los métodos getter y
setter.
Un bajo acoplamiento permite:
Entender una clase sin leer otras
Cambiar una clase sin afectar a otras
Mejora la mantenibilidad del código
Asignaciones – Heap y Stack.
El Heap (Montículo) y el Stack (Pila) son diferentes memorias que
utiliza la Java Virtual Machine de Java.
Mientras que para cada thread en la JVM se tiene un Stack privado,
el Heap es un espacio de memoria dinámica único que se crea al
inicio de la máquina virtual. El administrador del Heap es el
sistema de administración de almacenamiento automático o más
conocido como Garbage Collector.
40
¿Qué se guarda en el Stack?
- variables locales
- variables de referencia
- parámetros y valores de retorno
- resultados parciales
- el control de la invocación y retorno de métodos
¿Qué se guarda en el Heap?
- objetos
- variables de instancia
Ejemplo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class EjemploStackYHeap{
int atributoEntero;
String atributoCadena;
public void setAtributoEntero (int x) {
atributoEntero = x;
}
public void setAtributoCadena(String s) {
atributoCadena = s;
}
public static void main (String[] args){
int enteroLocal = 5;
String cadenaLocal = "atributo";
EjemploStackAndHeap e = new EjemploStackAndHeap();
e.setAtributoEntero (enteroLocal);
e.setAtributoCadena (cadenaLocal);
}
}
Comienza la ejecución con main.
Se declara e inicializa una variable de tipo primitivo,
41
enteroLocal.
Se almacena esta variable y su valor en el Stack.
Se declara e inicializa una variable de tipo de referencia.
La variable se crea en el Stack. El objeto se crea en el Heap.
La variable apunta al objeto String en el Heap.
Se llama al constructor EjemploStackAndHeap(). El constructor crea
en el Heap el espacio para el objeto y sus atributos. Se
inicializan los valores según el tipo de los atributos. En el
Stack se crea la variable de referencia e que apunta al objeto en
Heap.
Se llama a setAtributoEntero con el parámetro enteroLocal. La
variable del método setAtributoEntero x recibe el valor de
enteroLocal. Se le asigna el valor de x a atributoEntero. (El
método tiene una variable this que apunta al objeto, de esta forma
puede acceder a sus atributos).
Se llama a setAtributoCadena con el parámetro cadenaLocal. La
variable del método setAtributoCadena s recibe el valor de
cadenaLocal. Se le asigna el valor de s a atributoCadena. (El
método tiene una variable this que apunta al objeto, de esta forma
puede acceder a sus atributos).
42
Asignaciones – Asignaciones, Widening, Cast
La operación de asignación nos
sirve para dar valor a una
variable.
La forma genérica de una
asignación es la siguiente:
variable = valor;
Donde valor puede ser un literal,
otra variable o una expresión.
El proceso de la asignación se
realiza en dos fases. En primer
lugar, se evalúa la parte derecha y se obtiene un valor. En
segundo lugar, se asigna ese valor a la variable de la parte
izquierda.
Literales
Ya comentamos que tenemos ocho tipos primitivos en Java: byte,
short, int, long, float, double, char y boolean. Un literal es un
valor concreto de un tipo.
Enteros
Se pueden representar utilizando una notación decimal, octal o
hexadecimal.
15 formato decimal.
017 formato octal, el prefijo 0 indica un valor octal.
0xF formato hexadecimal, el prefijo 0x indica un valor
hexadecimal.
Un literal entero se considera por defecto de tipo int. Para
indicar que queremos que sea de tipo long debemos acompañarlo de
‘l’ o ‘L’. Algunos literales enteros: 15, 320, 29999, 130000,
250L.
Reales
Un literal real se considera por defecto de tipo double. Para
indicar que queremos que sea de tipo float debemos acompañarlo de
‘f’ o ‘F’. Algunos literales reales en coma flotante: 3.4,
1293.239, 150.23F.
Carácter
Un literal char se escribe entre comillas simples. Como son
carácteres Unicode también se puede especificar el carácter en
concreto por sus dígitos hexadecimales.
Algunos literales char: ‘b’, ‘z’, ‘\n’, ‘\t’, ‘\u003B’
43
Lógico
Los literales son false y true.
Expresiones
Es frecuente que nos encontremos expresiones en la que participan
diferentes tipos de datos. Algunos tipos de datos pueden ser
compatibles (enteros con caracteres p.e), mientras que otros no
(enteros con reales).
Los booleanos no son compatibles con ningún otro tipo de dato.
En las expresiones con datos enteros el resultado será al menos un
int o el tipo del mayor operando de la expresión.
1
2
3
byte a = 10;
byte b = 20;
byte c = (a + b) * 0.5; //El resultado de (a+b) * 0.5 es de
tipo double.
En este caso el compilador generará un error, se necesita una
conversión.
En Java las conversiones pueden ser implícitas o explícitas.
Conversión Implícita (Widening)
Se produce cuando los tipos son compatibles y Java realiza la
conversión de forma automática.
Siempre es posible asignar un valor de un tipo más pequeño en una
variable de un tipo mayor.
Grande = Pequeño; //OK
1 long a = 100; //100 al ser un literal entero, es de tipo
int.
No hay problema en guardar este valor en una variable long. La
conversión es automática.
Conversión Explícita (Cast)
Cuando los tipos son incompatibles o existe la posibilidad de
perder información en una asignación, el compilador dará un error
pidiendo que se confirme esa asignación con una conversión
explícita.
Pequeño = Grande; //ERROR
44
1
2
int a = 100L; //ERROR 100L es un literal de tipo long y se quiere
asignar a una variable int.
int a = (int) 100L; // OK
El compilador generará error. Como sabemos que en la asignación no
hay peligro de perder información al
caber 100 perfectamente en un int, convertimos explícitamente.
Casos Posibles de Asignaciones
o Asignación de Iguales: Se produce cuando el tipo del
valor a asignar coincide con el tipo de la variable. En
esta situación no hay ningún problema.
1
2
3
4
5
char c = 'a';
int entero = 100;
int suma = entero + 25;
double precio = 950.283;
boolean indCerrado = false;
En estas asignaciones es necesario ningún tipo de conversión.
o Conversión de ampliación (Widening): Ocurre cuando el
tipo del valor a asignar es más pequeño que el tipo de
la variable.
1
2
3
4
//grande = pequeño; //OK
int a = 150; //Declaramos un entero
long var = a; //La variable a se puede asignar al long var, ya que
vamos de pequeño a grande.
double b = 15.3f * var;
Cualquier expresión entera siempre se podrá asignar a una variable
double, ya que es el mayor tipo entero.
Conversión de reducción (Narrowing): Ocurre cuando el tipo
del valor a asignar es más grande que el tipo de la variable.
Es necesaria la conversión explícita.
1
2
//pequeño = grande; //ERROR
//pequeño = (pequeño) grande; //OK
45
3
4
5
6
7
8
9
10
long grande = 5500L;
int pequeño = (int) grande; //Es necesario hacer un casting a int.
byte primero = 5;
byte segundo = 20;
byte suma = (byte) (primero + segundo); //La suma de dos bytes es un
int, por lo tanto necesita
//un casting a byte.
o Excepción:
Cuando el valor a asignar es una expresión onstante de
tipo byte, short, char o int el
compilador hace la conversión automáticamente. Esto se
llama compile-time narrowing of constants:
1 byte auto = 10; //OK, no hace falta hacer byte auto =
(byte) 10;
o Si la variable a asignar es final y “cabe” en el destino
no es necesario casting:
1
2
3
4
5
final short prueba = 100;
byte destino = prueba; //OK
final short prueba = 300; //sobrepasa el valor posible
para los byte.
byte destino = prueba; //ERROR: possible loss of
precision
o Nota:
Aunque tengamos un valor real que pueda caber en un
“float” y lo queremos asignar a un
double, siempre será necesario el sufijo o el casting:
1
2
3
float f = 500.7; //ERROR
float f = 500.7f; //OK
float f = (float) 500.7; //OK
46
Conversión de Tipos de Referencia:Se puede asignar a un tipo
menos especializado un tipo más especializado en la
jerarquía.Padre = Hijo //OK
1 Persona pe = new Programador();
Esto es posible porque un objeto hijo (más especializado)
puede hacer todo lo que haga el
padre (menos especializado) pero no al revés.
Hijo = Padre //ERROR
1 Programador pro = new Persona(); //ERROR
Asignaciones – Paso de Parámetros en Java
El paso de parámetros en Java se hace siempre por valor, ya se
trate de un tipo primitivo o de un tipo de referencia, el método
trabaja con una copia de la variable.
En el caso de una variable de tipo primitivo, si dentro del método
se modifica la variable, estamos modificando la copia y no la
variable original.
En el caso de variables de referencia, si se modifica la variable
dentro del método se está modificando una copia de la referencia y
no la referencia original. Eso sí, al tener una copia de la
referencia que apunta al mismo objeto, sí se puede cambiar el
contenido de éste.
Veámoslo con un ejemplo:
1
2
3
4
5
6
7
8
class PasoParametros{
public static void main(String[] args){
//Para un entero comprobamos que se modifica la copia,
despues vuelve a su valor orig.
int entero = 100;
System.out.println("Antes de modificar: " + entero);
47
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
modificar(entero);
System.out.println("Despues de modificar: " + entero);
//Para un array de un entero, comprobamos que con la copia de
la referencia
//podemos cambiar su contenido.
int[] array = new int[1];
array[0] = 100;
System.out.println("\nAntes de modificar: " + array[0]);
modificarContenido(array);
System.out.println("Despues de modificar: " + array[0]);
//Para el mismo array de entero, comprobamos que no podemos
modificar la referencia.
System.out.println("\nAntes de modificar: " + array[0]);
modificar(array);
System.out.println("Despues de modificar: " + array[0]);
}
static void modificar(int e){
e = 200;
System.out.println("Dentro de modificar: " + e);
}
static void modificarContenido(int[] a){
a[0] = 200;
System.out.println("Dentro de modificarContenido: " +
a[0]);
}
48
38
39
40
41
42
43
44
45
static void modificar (int[] a){
//Creamos un nuevo array "b" y hacemos que "a" apunte a este
nuevo array.
//Funciona solo dentro del metodo, puesto que "a" aquí es una
copia.
int[] b = new int[1];
b[0] = 300;
a = b;
System.out.println("Dentro de modificar: " + a[0]);
}
}
Resultado de la ejecución:
49
Asignaciones – Arrays
Un array es un objeto que nos permite guardar varios elementos de
un mismo tipo. Un array puede guardar valores de tipo primitivo o
de tipo de referencia, pero el array siempre será un objeto y como
tal se almacena en la memoria dinámica, el Heap.
Los arrays pueden ser unidimensionales (vectores) o
multidimensionales (matrices).
En la imagen superior tenemos un ejemplo de un array
unidimensional de longitud n. El índice de los arrays en Java
siempre empiezan en 0, por tanto la última posición será n-1. El
índice nos permite acceder a un elemento determinado del array.
En los arrays multidimensionales tendremos tantos índices como
dimensiones.
Declaración de un array
Un array se declara especificando el tipo del array a la variable:
char[] arrayCaracteres;
char arrayVocales []; //posición de los corchetes legal
pero menos legible.
String[] arrayCadenas;
int[] cuponOnce;
int[][] matriz;
La declaración del array crea una referencia que apuntará al
array.
La declaración nunca debe indicar el tamaño del array.
-
Creación de un array
arrayCaracteres = new char[20];
arrayVocales = new char[5];
arrayCadenas = new String[3];
Después de su creación los arrays se inicializan con el valor
predeterminado de su tipo.
50
Aunque hemos separado la declaración de la construcción del array,
lo más usual es que la declaración y la construcción se hagan en
una misma línea:
int[] notasTrimestres = new int[4];
Java también permite utilizar una forma abreviada para crear
arrays con valores iniciales dando los valores entre llaves:
char[] letras = {‘a’, ‘b’, ‘c’, ‘d’};
String[] nombres = {“María”, “Jose”, “Alberto”}
-
Arrays multidimensionales
Dado que es posible declarar un array de cualquier tipo, los
arrays multidimensionales en Java se crean mediante arrays de
arrays.
Un array bidimensional:
//Crea un array de tres elementos en los que cada uno de ellos es
un array.
int [][] bidimensional = new int [3][];
Ahora podemos dar a cada uno de estos tres arrays dimensiones
diferentes si queremos:
bidimensional[0] = new int[2];
bidimensional[1] = new int[3];
bidimensional[2] = new int[2];
51
Si todos nuestros arrays de arrays van a tener la misma dimensión
podemos ahorrarnos esfuerzo y se permite la siguiente declaración
y construcción:
int [][] rectangular = new int[3][2]
-
Límites
En Java todos los arrays comienzan en 0. El número de elementos de
un array se guarda en el
atributo del array length. Es recomendable utilizar este atributo
cuando iteremos los elementos de un array para evitarnos acceder
fuera de los límites permitidos.
Si intentamos acceder a una posición incorrecta del array se
generará la excepción ArrayIndexOutOfBoundsException.
-
Dando Valores a un Array
Podemos rellenar los arrays dando valor a cada uno de sus
elementos:
1
2
3
4
5
6
7
8
9
10
11
12
public class Persona {
private String nombre;
private int edad;
//... // Constructores, getters, setters
public static void main (String[] args){
Persona[] familia = new Persona[3];
familia[0] = new Persona("Maria", 35);
familia[1] = new Persona("Jose", 30);
familia[2] = new Persona("Ana", 3);
}
}
O bien utilizando un bucle:
1 public class Persona{
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private String nombre;
private int edad;
//...// Constructores, getters, setters
public static void main (String[] args){
String[] nombres = {"Maria", "Jose", "Ana"};
int[] edades = {35, 30, 3};
Persona[] familia = new Persona[3];
for (int i = 0; i< familia.length; i++){
familia[i] = new Persona(nombres[i], edades[i]);
}
}
}
-
Asignaciones Legales en Arrays
De tipos primitivos:
Se puede asignar a una posición del array cualquier variable que
pueda ser promovidos o convertidos al tipo del array.
Por ejemplo, en un array de enteros podemos incluir shorts, bytes,
chars …
53
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class asignaArray{
public static void main(String[] args){
byte a = 3;
short b = 30000;
int c = 450;
long d = 300000L;
float e = 2500.203F;
double f = 3234.25;
char g = 'B';
int [] enteros = new int[10];
enteros[0] = a;
enteros[1] = b;
enteros[2] = c;
enteros[3] = (int) d;
enteros[4] = (int) e;v
enteros[5] = (int) f;
enteros[6] = g;
enteros[7] = c/a;
enteros[8] = (int) d/a;
enteros[9] = (int) e/a;</p>
for (int i = 0; i< enteros.length; i ++){
System.out.println("enteros["+ i + "]: " +
enteros[i]);
}
}
}
54
De tipos de referencia: Se puede asignar cualquier subclase del
tipo. Y si el tipo del array es una interfaz podemos asignar
cualquier objeto que implemente dicha interfaz.
Por ejemplo, podríamos tener un array de Empleado con distintos
tipos de empleado:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//...
Empleado[] empleados = new Empleado[4];
empleados[0]=new Tecnico();
empleados[1]=new Secretario();
empleados[2]=new Contable();
empleados[3]=new Tecnico();
for (int i=0; i<empleados.length; i++){
Empleado e = empleados[i];
System.out.println(e.getNombre());
System.out.println(e.getAntiguedad());
System.out.println(e.getSueldo());
}
//...
Es incluso posible hacer una colección con objetos de cualquier
clase, ya que en Java todos los
objetos heredan de Object:
55
1
2
3
4
5
6
Object [] objetos = new Object[5];
objetos[0] = new Empleado();
objetos[1] = new Batman();
objetos[2] = new Libro();
objetos[3] = new String("Maria");
objetos[4] = new Bicicleta();
Asignaciones – Clases Envoltorio y Boxing
En Java los datos numéricos, de caracteres, lógicos se tratan de
forma primitiva por eficiencia.
Normalmente usamos tipos primitivos y no objetos.
Sin embargo, existe una manera de que estos datos puedan ser
objetos, usando las llamadas clases envoltorio. Cada tipo
primitivo en Java tiene su correspondiente clase envoltorio.
Son las siguientes:
Tipo primitivo Clase envoltorio
boolean Boolean
byte Byte
char Character
short Short
int Integer
long Long
float Float
double Double
Los objetos se construyen pasando el valor al constructor
correspondiente.
56
int entero = 500;
Integer Entero = new Integer(entero); //de primitivo a objeto se
llama boxing.
int aEntero = Entero.intValue(); //de objeto a
primitivo se llama unboxing.
Muchas veces es útil convertir a estas clases ya que tienen muchos
métodos interesantes.
String str = “1000”;
int x = Integer.parseInt(str);
En este último ejemplo hemos podido convertir de String a int
utilizando su clase envoltorio Integer y el
método parseInt.
Constructores:
Cada clase envoltorio (menos Character) tienen dos constructores:
uno que admite el tipo primitivo como parámetro y otro que admite
un String.
Integer a = new Integer(500);
Integer b = new Integer(“500”);
Float c = new Float(7.5f);
Float d = new Float(“7.5f”);
Character e = new Character(‘t’);
Para el constructor Boolean cuando el String es true (sin importar
mayúsculas o minúsculas) será true, cualquier otro valor será
falso.
Boolean f = new Boolean(false);
Boolean g = new Boolean(“TrUe”); //g será true.
Boolean h = new Boolean (“NO”); //h será false.
ValueOf:
Otra forma de construir un objeto de una clase envoltorio es
mediante este método estático, valueOf. Este método puede aceptar
un String, o un String y un parámetro que indique la base
numérica.
Integer nuevo = new Integer.valueOf(“150”);
Integer binario = new Integer.valueOf(“1010”, 2);
Métodos Útiles:
Convierte un Envoltorio en primitivo:
byteValue(), shortValue(), intValue(), longValue(),
floatValue(), doubleValue()
57
Convertir un String en primitivo:
parseByte(), parseShort(), parseInt(), parseLong(),
parseFloat(), parseDouble()
Dar una representación en String: toString()
Convertir números a otras bases. Con objetos Integer y Long
se pueden utilizar: toBinaryString(), toOctalString(),
toHexString():
Autoboxing y unboxing:
Para pasar de un tipo primitivo a su objeto equivalente se
necesita utilizar las clases envoltorio (boxing).
Para obtener de la referencia del objeto su tipo primitivo
(unboxing) se necesitan usar los métodos de las clases envoltorio.
Todas estas operaciones complicaban excesivamente el código. Por
ello a partir de J2SE 5.0 se introdujo una conversión automática
(autoboxing) que permite asignar y obtener los tipos primitivos
sin necesidad de utilizar las clases envoltorio.
Por ejemplo:
int enteroPrimitivo = 420;
Integer Entero = enteroPrimitivo; //Se permite la asignación
directa. Se llama autoboxing.
int otroEnteroPrimitivo = Entero; //Se asigna directamente. Se
llama autounboxing.
De modo que ahora el compilador según el caso se encarga de crear
el objeto envoltorio o de extraer el tipo primitivo.
También se permite en el paso de parámetros y en expresiones
aritméticas.
58
Asignaciones – Sobrecarga de Métodos con
Widening, Boxing y Clases Envoltorio
Vamos a ver algunos casos concretos relacionados con el Widening,
las Clases Envoltorio y los Argumentos-Variables que pueden
hacernos dudar en el momento de saber qué método elije el
compilador cuando tenemos métodos sobrecargados…
Sobrecarga y tipos primitivos:
Escogerá el menor tipo que sea mayor que el argumento en la
llamada.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SobrecargaPrimitivos {
public static void main(String[] args){
byte byteVar = 10;
short shortVar = 10;
char charVar = 10;
long longVar = 10;
float floatVar = 10.0f;
System.out.println ("\nmetodo (byteVar)\u003B");
metodo (byteVar); //Llamada con un argumento byte
System.out.println ("\nmetodo (shortVar)\u003B");
metodo (shortVar); //Llamada con argumento short
System.out.println ("\nmetodo (charVar)\u003B");
metodo (charVar); //Llamada con argumento char
System.out.println ("\nmetodo (longVar)\u003B");
metodo (longVar); //Llamada con argumento long
59
22
23
24
25
26
27
28
29
30
31
System.out.println ("\nmetodo (floatVar)\u003B");
metodo (floatVar); //Llamada con argumento float
}
static void metodo (int v) { System.out.print ("-> amplia a int y ejecuta
metodo (int v) \n");}
static void metodo (long v) { System.out.print ("-> amplia a long y
ejecuta metodo (long v) \n");}
static void metodo (double v){System.out.print ("-> amplia a double y
ejecuta metodo (double v)\n");}
}
Sobrecarga Boxing y tipos primitivos:
Si el compilador tiene que elegir entre hacer un boxing (un método
con un parámetro Envoltorio) y un widening (método con un tipo
primitivo mayor) elegirá el widening.
1
2
3
4
class SobrecargaBoxing_Primitivos {
public static void main (String[] args){
int intVar = 10;
System.out.println ("\nmetodo (intVar)\u003B");
60
5
6
7
8
9
10
metodo (intVar); // A qué método llamará?
}
static void metodo (long v) { System.out.print ("-> amplia a long y ejecuta
metodo (long v)\n");}
static void metodo (Integer v) {System.out.print ("-> boxing a Integer y
ejecuta metodo (Integer v)\n");}
}
Sobrecarga Argumentos-Variables y tipos primitivos:
Si el compilador tiene que elegir entre un método con argumentos
variables y un widening (método con un tipo primitivo mayor)
elegirá el widening.
1
2
3
4
5
6
7
8
9
10
11
12
13
class SobrecargaVarArgs_Primitivos {
public static void main (String[] args){
int intVar = 10;
System.out.println ("\nmetodo (intVar)\u003B");
metodo (intVar);
}
static void metodo (long v) { System.out.print ( "-> amplia a long y
ejecuta metodo (long v) \n");}
61
14
15
static void metodo (int ... v) {System.out.print ( "-> ejecuta metodo
(int.. v) \n");}
}
Sobrecarga Boxing y Argumentos-Variables:
Cuando el compilador tiene que elegir entre hacer boxing y un
método con argumentos variables, se queda con el boxing.
1
2
3
4
5
6
7
8
9
class SobrecargaBoxing_VarArgs {
public static void main (String[] args){
int intVar = 10;
System.out.println ("\nmetodo (intVar)\u003B");
metodo (intVar);
}
static void metodo (Integer v) { System.out.print ( "-> boxing a Integer y
ejecuta metodo (Integer v) \n");}
static void metodo (int ... v) {System.out.print ( "-> ejecuta metodo (int..
v) \n");}
}
Sobrecarga y Tipos de Referencia:
62
El compilador hace widening si encuentra un objeto de una subclase
y el método tiene como parámetro un objeto de la superclase. Esto
es debido a que un objeto hijo se puede utilizar donde se espera
un objeto padre y no al contrario.
Sobrecarga con widening y boxing a la vez:
Veamos qué ocurre en los casos en los que el compilador necesita
hacer estas dos conversiones para llamar al método.
Amplía y luego Boxing: en este caso el compilador no puede
hacer esta operación. Da error de compilación.
1
2
3
4
5
6
7
8
9
10
public class wideAndBox{
public static void main(String[] args){
byte byteVar = 5;
System.out.println ("\nmetodo (byteVar)\u003B");
metodo(byteVar);
}
static void metodo(Long v){
System.out.println("-> ejecuta metodo (Long v) \n");
}
}
El compilador tampoco puede hacer las operaciones cambiando el
orden, primero Boxing y luego ampliando porque un Byte no se
puede ampliar a Long (Un Byte no es un Long, no están relacionados
por herencia).
Boxing y luego amplía: el compilador sí puede hacer estas dos
conversiones si despúes de hacer Boxing se puede ampliar
porque estén relacionados por herencia. Un Byte es un Object.
1 public class BoxAndWide{
63
2
3
4
5
6
7
8
9
10
public static void main(String[] args){
byte byteVar = 5;
System.out.println ("\nmetodo (byteVar)\u003B");
metodo(byteVar);
}
static void metodo(Object v){
System.out.println("-> ejecuta metodo (Object v)
\n");
}
}
Combinando Boxing y Widening con VarArgs:
El compilador no tiene problema cuando un método tiene argumentos
variables ya haga falta hacer widening o boxing.
1
2
3
4
5
6
7
8
9
10
public class ejem{
public static void main(String[] args){
int entero = 10;
metodo1 (entero, entero);
metodo2 (entero, entero);
}
static void metodo1(long... v) {
System.out.println("metodo(Long... )");}
64
11 static void metodo2(Integer... v)
{System.out.println("metodo(Integer... )");}
}
Resumen:
Entre diferentes métodos con tipos primitivos, el compilador
escoge el menor tipo que sea mayor al argumento de llamada.
En los tres siguientes casos gana la característica más antigua en
Java:
Entre Boxing y Widening, gana Widening.
Entre Args Variables y Widening, gana Widening.
Entre Boxing y Argumentos Variables, gana Boxing.
Argumentos Variables con Boxing o Argumentos Variables con
widening, ambos funcionan.
Widening y Boxing, no funciona (int pasaría a long, pero long
no puede convertirse en Long).
Boxing y Widening, funciona (int pasaría a Integer e Integer
puede convertirse en Object).
El recolector de basura
Muchos lenguajes de programación permiten asignar memoria dinámica
en tiempo de ejecución pero es el programador el que tiene la
responsabilidad de liberarla.
65
Sin embargo, Java proporciona un hilo en el nivel del sistema que
administra esta memoria, busca la que está en desuso y la libera.
Es el recolector de basura o Garbage Collector (GC).
Cuándo se ejecuta:
El GC está bajo el control de la JVM y ella decide cuándo
ejecutarlo. Dentro de un programa se puede solicitar la ejecución
pero no hay ninguna garantía de que se ejecute.
Cómo funciona:
Los modelos de reciclaje de la memoria pueden variar
considerablemente en cada implementación de la JVM. Lo que debemos
tener claro es cuándo un objeto es elegible por el GC.
Un objeto es elegible cuando ningún thread vivo puede acceder a
él.
Cómo hacer un objeto elegible:
Estableciendo la referencia a null:
Cuando asignamos la referencia de un objeto a null estamos
perdiendo cualquier posibilidad de acceder a él.
1
2
3
4
5
6
7
public class aNull{
public static void main (String[] args){
Integer a = new Integer(10); //1
a = null; //2
//ahora el objeto Integer(10) es elegible por el GC.
}
}
//1 Creación del objeto.
//2 La referencia ya no apunta al objeto. La referencia es
igual a null. El objeto es ahora elegible por el GC.
66
Reasignando la referencia:
Cuando hacemos que la referencia que apunta a un objeto
apunte a otro distinto dejando el primero inaccesible.
1
2
3
4
5
6
7
8
public class Reasigno{
public static void main (String[] args){
Integer a = new Integer(10); //1
Integer b = new Integer(20); //2
a = b; //3
//ahora el objeto Integer(10) es elegible por el
GC.
}
}
//1 y //2 Creación de los objetos.
//3 La referencia a y b están ahora apuntando al segundo
objeto, siendo el primero elegible para el GC.
Cuando la referencia está aíslada:
Se produce cuando un objeto tiene como miembros otros objetos
de su misma clase y aunque se pierdan las referencias de
estos objetos aún conservan entre ellos referencias vivas que
son elegibles para el GC.
1 public class Aislada{
67
2
3
4
5
6
7
8
9
10
11
12
Aislada vecina;
public static void main (String[] args){
Aislada a = new Aislada(); //1
Aislada b = new Aislada();
a.vecina = b; //2
b.vecina = c;
a = null; //3
b = null; // Aunque las pongamos a null entre los
objetos tienen referencias.
}
}
//1 y //2: Creación de los objetos Aislada y asignación de la
vecina.
//3: Se ponen las referencias a null. Los dos objetos
Aisladas son elegibles para el GC.
Solicitar la ejecución del GC:
Como hemos dicho antes no se puede forzar la ejecución del
recolector de basuras. Se solicita su ejecución mediante la
siguiente instrucción pero no se nos garantiza que se ejecute.
Se puede ejecutar mediante:
System.gc();
U obteniendo una instancia de Runtime:
68
Runtime rt = Runtime.getRuntime();
rt.gc();
Método finalize():
Este método está definido en la clase Object y por tanto todos los
objetos lo heredan. En él
podemos escribir cualquier código que queramos que se ejecute
antes de que el GC borre el objeto.
Como la ejecución del GC no está garantizada que se ejecute la
ejecución de finalize() tampoco está garantizada.
Cada objeto sólo podrá llamar a este método una vez. En este
código se podría hacer inelegible el objeto, por ejemplo,
volviendo a asignarle una referencia.
Los Operadores en Java (I)
Operadores
Los operadores son símbolos especiales que producen un nuevo valor
a partir de uno o varios operandos.
Los valores producidos son, en la mayoría de los casos, booleanos
o numéricos.
En la imagen tendríamos un ejemplo de un operador binario, que a
partir de dos operandos produciría un nuevo valor.
En la siguiente tabla mostramos los operadores en Java, su orden
de precedencia y su asociatividad.
(I-D: de izquierda a derecha, D-I: de derecha a izquierda).
69
Operador de Asignación: =
Cuando asignamos valor a variables de tipo primitivo hay casos en
los que se produce un casting implícito, otros en los que es
necesario un casting explícito y casos en los que se puede
producir un truncamiento.
Cuando asignamos valores a variables de tipos de referencia
debemos tener cuidado con el tipo.
Esto lo podemos ver en los posts del Tema3.
Operadores de Asignación Compuestos: +=, -=, *=, /=
Se tratan en el examen los cuatro anteriores por ser los más
usuales.
Algunos ejemplos:
1
2
3
4
x += 10; //sería igual a x = x + 10;
y -= 50; //sería igual a y = y - 50;
z *= 7; // equivale a z = z * 7;
w /= 100; // equivale a w = w/100;
En los operadores compuestos la expresión de la derecha siempre se
evalúa primero.
70
1 x *= 3 + 5; //sería equilvalente a x *= (3+5), es decir x = x * (3 +
5) y no x = (x * 3) + 5.
Operadores Relacionales: <, <=, >, >=
Siempre devuelven true or false. Se suelen utilizar en sentencias
ifs para comprobar condiciones o en expresiones para asignar el
valor resultante a una variable boolean.
Pueden comparar cualquier combinación de enteros, reales o
carácteres. Cuando se compara con un carácter se utiliza su valor
Unicode numérico.
Ejemplos:
1
2
3
4
5
6
7
8
9
10
boolean resultado;
int x = 200;
if (x<=100)
{
resultado = true;
}
else
{
resultado = false;
}
También se podría hacer directamente:
1
2
boolean resultado;
resultado = (x<=100);
Con caracteres:
1
2
3
char a = 'A';
char b = 'B';
boolean resultado = a>b;
Operadores de Igualdad: ==, !=
71
Se pueden comparar dos números, dos carácteres, dos boolean o dos
variables de referencia.
Cuando se comparan tipos primitivos devolverá true si ambos
valores son iguales.
Cuando se comparan referencias devolverá true si las variables
apuntan al mismo objeto.
(No si dos objetos son iguales).
Ejemplos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Igualdad{
public static void main (String[] args)
{
int a = 100;
char b = 'b';
boolean res = true;
char[] letras1 = {'a','b','c','d'};
char[] letras2 = {'a','b','c','d'};
if (a == 100L)
{ System.out.println("100 y 100L, Iguales");}
if (res != (b>a))
{ System.out.println("res distinto a (b>;a)");}
if (letras1 == letras2) //Imprime distinto aunque ambos
tengan el mismo contenido.
{ System.out.println("Iguales");} //porque cada referencia
apunta a un obj distinto.
else
{ System.out.println("Distintos");}
letras2 = letras1; //hacemos que letras2 apunte a letras1
72
25
26
27
28
29
if (letras1 == letras2) //Imprimirá iguales porque ahora
{ System.out.println("Iguales");} //apuntan al mismo
objeto.
else
{ System.out.println("Distintos");}
}
}
Operador instanceof:
Se utiliza solo con referencias y comprueba si un objeto es de una
clase o interfaz determinada.
Por ejemplo, supongamos la siguiente jerarquía de clases:
La clase Persona tiene una subclase Empleado. Empleado a su vez
tiene una subclase Tecnico.
Además la clase Persona implementa la interfaz Modales.
Con las siguientes declaraciones:
1
2
3
4
5
Persona p1 = new Persona();
Persona p2 = new Empleado();
Empleado e1 = new Empleado();
Tecnico t1 = new Tecnico();
Boolean b;
¿Qué devolvería el operador instanceof en las siguientes líneas de
código?
73
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
b = p1 is instanceof Persona;
b = p1 is instanceof Modales;
b = p1 is instanceof Object;
b = p1 is instanceof Empleado;
b = p1 is instanceof Tecnico;
b = p2 is instanceof Persona;
b = p2 is instanceof Empleado;
b = p2 is instanceof Modales;
b = p2 is instanceof Tecnico;
b = e1 is instanceof Empleado;
b = e1 is instanceof Modales;
b = e1 is instanceof Persona;
b = e1 is instanceof Tecnico;
b = t1 is instanceof Persona;
b = t1 is instanceof Modales;
b = t1 is instanceof Empleado;
b = t1 is instanceof Tecnico;
b = null is instanceof Persona;
b = e1 is instanceof String;
Seguimos con el resto de operadores en la siguiente entrada…
Los Operadores en Java (II)
Operador Resto: %
Devuelve el resto de una división.
74
1
2
3
4
5
6
7
8
9
10
11
class Resto{
public static void main (String[] args){
int dividendo = 15;
int divisor = 2;
int resto = dividendo % divisor;
System.out.println("Resto: " + resto);
int expresion = 3 + dividendo % divisor;
System.out.println("Expresion: " + expresion);
}
}
Hay que recordar que los operadores *, /, % tienen una prioridad
mayor que los operadores + y -, como se puede comprobar en el
ejemplo.
Recordamos también que las expresiones se evalúan por defecto de
izquierda a derecha y que para cambiar la prioridad en las
evaluaciones se pueden utilizar paréntesis.
Operador Concatenador: +
Con este operador se realiza una concatenación de objetos String y
resulta en un nuevo objeto String.
1
2
3
4
String sujeto = "Ellos";
String predicado = "estudiaban Java";
String frase = sujeto + " " + predicado;
System.out.println(frase); //Devuelve "
Si uno de los argumentos del operador + es String el otro
argumento se convierte en objeto String.
Por ejemplo, entre números y String:
1
2
3
class PruebaConcatenador{
public static void main (String[] args){
String nombre = "Virginia";
75
4
5
6
7
8
9
10
11
int a = 10;
int b = 20;
System.out.println(nombre + a + b);
System.out.println(a + b + nombre);
System.out.println(a + b + nombre + a + b);
}
}
Devolvería:
Todos los objetos pueden convertirse en objeto String de forma
automática, pero el resultado puede tener poco significado. Es por
esto que para dar una representación significativa de un objeto en
String se suele sobrecargar el método toString().
Operadores de Incremento y Decremento: ++, –
Estos operadores se pueden colocar delante de la variable
(notación prefjia) o después de la variable (notación sufija) y
realizan un incremento o decremento en 1 en el valor de la
variable.
Cuando tenemos la notación prefija (++i ó –i), primero se realiza
el aumento o decremento en el valor y luego se evalúa.
Cuando tenemos la notaciión sufija (i++ ó i–), primero se evalúa y
luego se realiza el aumento o decremento en el valor.
Por ejemplo:
1
2
3
4
5
class OperadoresIncrDecr{
public static void main (String[] args){
int a = 10;
int b = 10;
76
6
7
8
9
10
11
System.out.println("a++: " + a++); //1º Evalua con a y luego
la incrementa.
System.out.println("++b: " + ++b); //1º Incrementa y luego
evalua con el nuevo valor.
System.out.println(a); //Comprobamos que el valor se ha
incrementado.
System.out.println(b); //Comprobamos que el valor se ha
incrementado.
}
}
Devuelve:
Operador Condicional: ?:
El siguiente operador evalúa una condición y asigna un valor si es
verdadera u otro valor si es falsa.
x = (expresion logica) ? valorSiVerdadero : valorSiFalso
Por ejemplo:
1
2
int nota = 80;
String resultado = (nota >= 65)? "Aprobado!!!" : "Suspenso";
Operadores Lógicos: &, |, &&, ||, ^, !
& y | son los operadores And y Or lógicos. Evalúan siempre ambas
condiciones. No tienen evaluación perezosa.
&& y || son los operadores And y Or lógicos con evaluación
perezosa.
Quiere decir que si pueden dar el resultado evaluando solo la
primera condición no evaluarán la segunda.
Esto se da cuando:
77
La primera condición en || es cierta, independientemente de
la segunda condición el resultado ya es cierto.
La primera condición en && es falsa, independientemente de la
segunda condición el resultado es falso.
Un ejemplo con operadores de incremento para comprobar los
diferentes resultados que podemos obtener si usamos un operador
con evaluación normal o perezosa:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Logicos{
public static void main (String[] args){
int a = 0; //Valores para operadores &, |
int b = 5;
int c = 0; //Valores para operadores perezosos &&, ||
int d = 5;
if ( a == 0 | b++ > 2) //Evalúa las dos condiciones y b se
incrementa.
{ System.out.println("Cierto (a==0 | b++ > 2)-> a: " + a + "
b: " + b); }
if (c == 0 || d++ > 2) //Evalúa solo la 1ª condición al ser
cierta (c==0), b no incrementa.
{ System.out.println("Cierto (c==0 || d++ > 2)-> c: " + c + "
d: " + d); }
System.out.println();
if (a > 0 & b++ < 5) //Evalúa las dos condiciones, b se
incrementa de nuevo.
{ }
else
{ System.out.println("Falso (a>0 & b++ a: " + a + " b: " +
b); }
78
25
26
if (c > 0 && d++ < 5) //Evalúa solo la 1ª condición por ser
falsa (c > 0), d no incrementa.
{ }
else
{ System.out.println("Falso (c>0 && d++ <5) -> c: " + c + "
d: " + d); }
}
}
Devuelve:
^ es el operador Xor. Devueve cierto cuando una de las condiciones
es falsa y la otra verdadera.
Siempre evalúa ambos operandos.
! es el operador Not. Si la condición es cierta devuelve falso y
si es falsa devuelve cierto.
Con esto terminamos el tema de Operadores.
Recordaros que los operadores de desplazamiento de bits no entran
en el examen para la versión 6. Os podéis encontrar preguntas
sobre desplazamiento de bits en cuestionarios por ser de versiones
anteriores.
En este post tenéis algunos ejercicios resueltos sobre Operadores.
Control del Flujo: If y Switch
Las sentencias condicionales permiten ejecutar código de forma
selectiva.
En Java tenemos las sentencias condicionales if y switch.
Sentencia If:
La sintaxis más sencilla de una sentencia if es:
1
2
if (expresionLogica) //Si expresionLogica se evalúa a true
entonces se ejecuta codigo.
codigo
79
Si se tiene una sola sentencia de código no es obligatorio
encerrarla entre llaves pero siempre es recomendable por
legibilidad.
Ejemplo:
1
2
3
if (precio < 100){
comprar(); //Mas legible entre llaves.
}
Si nos encontramos un if sin llaves debemos recordar que solo
aplica a la primera sentencia.
1
2
3
4
if (precio < 100)
comprar(); //Solo esta sentencia forma parte del if.
salirTienda(); //No forma parte del código del if.
Siempre se ejecuta.
volverACasa(); //No forma parte del código del if.
Siempre se ejecuta.
La cláusula else es opcional. Se incluye cuando queremos que se
ejecute código en el caso de que no se cumpla la condición en el
if.
1
2
3
4
5
6
if (precio < 100){
comprar();
}
else{
System.out.println("Yo no soy tonto.");
}
También podemos tener una estructura if, else-if, else. Se da en
los casos en los que queremos comprobar varias condiciones.
1
2
3
4
if (precio < 100){
comprar();
}
else if (price < 2000 && tengoCredito){
80
5
6
7
8
9
comprarConTarjeta();
}
else{
System.out.println("Yo no soy tonto.");
}
En las sentencias If se cumplirá:
Habrá 0 o muchos else-if y precederán a la cláusula else.
Habrá 0 o 1 else e irá a continuación de todas las cláusulas
else-if.
Cuando una condición se cumple el resto no se comprueba.
Si tenemos un else y no sabemos a que if corresponde será el
más cercano sin else.
La asignación en una condición no da error si se trata de un
lógico: if(indicador=true)
Switch
Con esta sentencia vamos a evaluar una expresión y dependiendo de
su valor ejecutaremos un código u otro:
1
2
3
4
5
6
switch (expresion){
case constante1: codigo1
case constante2: codigo2
case constante3: codigo3
default: codigo
}
Si expresion coincide con alguna de las constantes se ejecutará el
código de ese case y se seguirá ejecutando el código de los
siguientes case hacia abajo hasta que se encuentre una sentencia
break.
La sentencia default es opcional y se puede especificar cuando
queremos que se ejecute un código para cualquier valor que sea
distinto a los especificados en los case.
En los siguientes ejemplos podemos comprobar cómo afecta al
resultado la existencia y posición de las sentencias breaks y
default:
81
1. Ejemplo de un Switch Típico.
Switch con breaks en cada case y una clásula default al final.
Recorremos el array niveles. Cada valor entra en su case
correspondiente y para al tener la sentencia break. Al estar la
cláusula default al final no hace falta que tenga break.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class EjemploSwitch1{
public static void main (String[] args){
int[] niveles = {1, 2, 3, 0};
System.out.println("\nEjecucion Switch Tipico (breaks y
default al final): " );
for (int i=0; i < niveles.length; i++){ {
System.out.println("Nivel: " + niveles[i]);
switch (niveles[i]){
case 1: System.out.println("\tNivel Basico.");
break;
case 2: System.out.println("\tNivel
Intermedio.");
break;
case 3: System.out.println("\tNivel Avanzado.");
break;
default: System.out.println("\tNivel no
informado.");
}
}
}
}
Resultado:
82
2. Sentencia Default No al Final.
En este caso hemos colocado la sentencia default sin break después
del primer case. Todos los case tienen break. Cuando niveles[i] es
igual a 0, ejecuta el código asociado a default. Al no tener break
pasa al case 2, ejecuta su código y entonces sí para al tener
break.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EjemploSwitch2{
public static void main (String[] args){
int[] niveles = {1, 2, 3, 0};
System.out.println("\nEjecucion Switch con Breaks Default
enmedio sin break: " );
for (int i=0; i < niveles.length; i++) {
System.out.println("Nivel: " + niveles[i]);
switch (niveles[i]){
case 1: System.out.println("\tNivel Basico.");
break;
default: System.out.println("\tNivel no
informado.");
case 2: System.out.println("\tNivel
Intermedio.");
break;
case 3: System.out.println("\tNivel
Avanzado.");
break;
83
21
22
}
}
}
}
Ejecución:
3. Sentencias Case sin Break (Ejemplo en el que no tiene lógica
aplicarlo).
En este Switch comprobamos qué ocurre si no colocamos ningún
break. Para cada valor de niveles se ejecutan todos los case y
default.
Ejecución:
4. Sentencias Case Sin Break (Ejemplo en el que sí podría ser
útil).
En este caso comprobamos cómo a veces puede tener lógica para
nuestro código dejar los case sin break. Hemos cambiado el orden
de las constantes en los case.
1
2
3
4
public class EjemploSwitch4{
public static void main (String[] args){
int[] niveles = {1, 2, 3, 0};
84
5
6
7
8
9
10
11
12
13
System.out.println("\nEjecucion Switch sin Breaks (con logica que
podria ser correcta): ");
for (int i=0; i < niveles.length; i++)
{
System.out.println("Nivel: " + niveles[i]);
switch (niveles[i])
{
case 3: System.out.println("\tNivel Avanzado.");
case 2: System.out.println("\tNivel Intermedio.");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EjemploSwitch3{
public static void main (String[] args){
int[] niveles = {1, 2, 3, 0};
System.out.println("\nEjecucion Switch sin Breaks (entra en
todos los case cada vez): ");
for (int i=0; i < niveles.length; i++)
{
System.out.println("Nivel: " + niveles[i]);
switch (niveles[i])
{
case 1: System.out.println("\tNivel Basico.");
case 2: System.out.println("\tNivel Intermedio.");
case 3: System.out.println("\tNivel Avanzado.");
default: System.out.println("\tNivel no informado.");
}
}
}
}
85
14
15
16
17
18
19
20
21
22
case 1: System.out.println("\tNivel Basico.");
break;
default: System.out.println("\tNivel no informado.");
}
}
}
}
Ejecución:
En el switch se debe cumplir:
La expresión evaluará a char, byte, short, int o enum.
Las constantes deben ser constantes en tiempo de compilación.
Hay que usar break cuando se necesite parar evitar la
ejecución de los case posteriores.
La cláusula default no tiene por qué ser la última.
No puede haber dos case comprobando el mismo valor. No
compilará.
No se pueden omitir las cláusulas case ni los ‘:’
Control del Flujo: Bucles
Los bucles nos permiten alterar el flujo de ejecución de un
programa ejecutando repetidamente una sentecia o grupo de
sentencias.
86
En Java 6 tenemos tres opciones distintas a la hora de realizar
iteraciones:
while,
do while,
for (normal y mejorado)
Vamos a ver cada una de ellas en detalle.
While
Es el bucle indicado cuando no se sabe de antemano cuántas veces
se va a ejecutar el bucle. Simplemente queremos que se repitan
ciertas acciones mientras la condición del bucle sea cierta.
1
2
3
4
//inicialización de variables
while (condicion){ //La condicion debe evaluarse a boolean
//codigo //Puede no ejecutarse ninguna vez
}
Cómo funciona:
Al entrar en el while se comprueba
la condicion por primera vez.
Si resulta falsa no llega a
ejecutar el código y se prosigue
la ejecución fuera del bucle.
Si es cierta se ejecuta el código
y se vuelve a comprobar la
condición.
Así sucesivamente hasta que la
condición devuelva falso y
salgamos del bucle.
Como veremos la última sentencia
que se ejecutará en un bucle
siempre será la comprobación de la
condición.
Por ejemplo:
En este código la variable i
vuelve a incrementarse en la última comprobación de la condición
del bucle, cuando ésta ya es falsa.
87
1
2
3
4
5
6
7
8
9
10
11
12
13
public class EjemWhile1{
public static void main (String[] args){
int i = -1;
boolean encontrado = false;
int[] medidas = {10, 30, 40, 50, 70};
while (++i < 5 && !encontrado){
if(medidas[i] == 40){
encontrado = true;
}
}
System.out.println(i);
}
}
En los bucles while estar atento a:
Declaración de variables en la condición del while. No se
puede, deben estar declaradas con anterioridad.
Expresiones en la condición del while que no sean boolean.
Por ejemplo: enteros en lugar de boolean o asignaciones en
lugar de comparaciones, darán error de compilación.
Do While
En estos bucles primero se ejecuta el código y luego se comprueba
la condición, por lo que tenemos asegurado que el código se
ejecuta al menos una vez.
1
2
3
4
do{
elem = pila.obtener();
System.out.println("elem: " + elem);
} while (!pila.esVacia());
88
Cómo funciona:
En primer lugar se ejecuta el código del bucle.
A continuación se comprueba la condición. Si esta es falsa se
prosigue la ejecución fuera del bucle. Si es verdadera se ejecuta
el código y se vuelve a comprobar la condición.
Estar atento a las mismas consideraciones que en el while.
For
Es el bucle que solemos utilizar cuando sabemos cuántas veces
queremos ejecutar cierto código.
En su sintaxis podemos destacar tres partes importantes:
- declaración e inicialización de variables
- condición
- incremento.
Aunque ninguna de ellas son obligatorias.
Ejemplo de bucle mínimo for:
1
2
3
for ( ; ; ){ //sin inicializacion, condición ni incremento.
Sería un bucle infinito.
//infinito
89
}
Ejemplo de un bucle for que equivaldría a un while:(sin
inicialización ni incremento)
1
2
3
4
int i = 0;
for ( ; i<10 ; ){
i++;
}
Ejemplo de un bucle for típico:
1
2
3
4
5
6
for (int i=0;i<10;i++){
//declaración e inicializacion "int i=0"
//condicion "i<10"
//incremento "i++"
System.out.println("i: " + i);
}
Cómo funciona:
90
La inicialización de la variable se ejecuta al principio una sola
vez.
A continuación se comprueba la condición y si es falsa se prosigue
la ejecución fuera del bucle.
Si resulta cierta se ejecuta
el código, se incrementa la
variable y se vuelve a
comprobar la condición.
Consideraciones del For.
Declaración e inicialización:
Se pueden declarar 0, 1
o más variables del
mismo tipo. Si hay más
de una se separan por
comas.
Las variables declaradas
en el for pierden su
ámbito al salir del
bucle for.
Esta parte sólo se
ejecuta una vez, al principio de la ejecución del bucle.
Condición:
Sólo se puede comprobar una condición no varias.
Control del Flujo: Bucles II (break, continue, etiquetas)
Una vez que hemos visto los bucles en Java vamos a ver cómo les
afectan las siguientes sentencias: break y continue.
Escribimos un bucle anidado y vemos la diferencia entre break y
continue:
1
2
3
4
5
public class BucleConBreak{
public static void main(String[] args){
for (int i = 0; i< 6; i++){
for (int j=0; j<5; j++){
if (j==3){
91
6
7
8
9
10
11
12
13
14
break;
}
else{
System.out.println("iteracion [i,j]: " +
i + ", " + j);
}
}
}
}
}
El resultado es:
La sentencia break hace que se salga completamente del bucle más
interno y proseguimos con la siguiente iteración del bucle
externo.
El mismo código pero con continue:
1
2
3
4
5
6
public class BucleConContinue{
public static void main(String[] args){
for (int i = 0; i< 6; i++){
for (int j=0; j<5; j++){
if (j==3){
continue;
92
7
8
9
10
11
12
13
14
}
else{
System.out.println("iteracion [i,j]: "
+ i + ", " + j);
}
}
}
}
}
Devuelve:
La sentencia continue hace que pasemos a la siguiente iteración
del bucle más interno.
Etiquetas
Es común utilizar las etiquetas con bucles for o while y en
conjunción con las sentencias break y continue.
Una etiqueta se coloca antes de la sentencia a etiquetar seguida
de ‘:’
Son útiles cuando tenemos bucles anidados y queremos especificar
en cuál de ellos queremos hacer un break o continue.
93
En el siguiente ejemplo tenemos un ejemplo de break etiquetado:
El funcionamiento normal del break sería salir del bucle más
interno, el while en este caso.
Sin embargo, el break etiquetado hace que rompa el bucle
etiquetado, es decir el for.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BucleBreakLabeled{
public static void main (String[] args){
boolean esVerdadero = true;
externo: //etiqueta la siguiente sentencia, es decir el for.
for (int i = 0; i<5; i++){
while (esVerdadero){
System.out.println("Hola!");
break externo; //break con etiqueta, hace que rompa la
sentencia de la etiqueta, el for.
}
System.out.println("Despues del while!");
}
System.out.println("Despues del for!");
}
}
Devuelve:
Las sentencias break y continue con etiquetas deben estar dentro
de un bucle con la misma etiqueta, si no dará error de
compilación.
Control del Flujo: Excepciones
Excepción
Es un evento excepcional que altera el flujo normal del programa.
Cuando esto ocurre decimos que se ha lanzado una excepción.
Una excepción se puede generar:
en la JVM, como respuesta a una condición inesperada (Caso
1).
94
en el código, lanzadas explícitamente por el programador
utilizando la sentencia throw (Caso 2).
Las excepciones del primer caso se llaman no comprobadas
(unchecked) y las del segundo comprobadas (checked).
Ejemplo Caso 1: No comprobadas.
Por ejemplo, al intentar acceder a una posición inválida en un
array.
1
2
3
4
5
6
7
8
9
public class PruebaOutOfBounds {
public static void main (String[] args){
int[] notas = new int[5];
for (int i = 0; i<=5; i++){
notas[i] = i*2;
System.out.println("notas["+i+"]=" + notas[i]);
}
}
}
Otras excepciones no comprobadas son: ArithmeticException,
ClassCastException, NullPointerException, …
En general son aquellas que descienden de la
clase RuntimeException.
Ejemplo Caso 2: Comprobadas.
Cuando lanzamos explícitamente una excepción.
1
2
3
4
5
...
public void reverse() throws Exception{
if (nombre == null){
throw new Exception ("El nombre es nulo.\n Lanzamos
excepcion al intentar hacer
95
6
7
8
9
10
11
12
13
reverse().");
}
else
{
for (int i = nombre.length()-1; i >=0; i --)
System.out.println(nombre.charAt(i));
}
}
...
En general son aquellas que descienden de la clase Exception,
excluyendo RuntimeException.
Regla del Manejo o la Declaración
Las excepciones comprobadas que un método pueda lanzar se deben
declarar.
Si se produce una excepción comprobada en el código, el método
debe:
manejar la excepción utilizando el bloque try-catch-finally
ó declarar la excepción y pasar la responsabilidad de
declaración o gestión al método llamante.
Para declarar la excepción se utiliza la sentencia throws.
Manejando Excepciones
El manejo de excepciones en Java se lleva a cabo principalmente
mediante las sentencias: try, catch y finally.
1
2
try{
.... //Código que puede provocar una excepcion
96
3
4
5
6
7
8
9
10
}
catch (Excepcion1){ //Puede haber 0, 1 o muchos bloques
catch.
//En caso de haber 0 debe existir bloque finally.
....
}
finally{ //Puede haber 0 o 1 bloque finally.
.... //En caso de haber 0 debe existir bloque catch.
}
En el bloque try encerramos el código que puede lanzar una
excepción.
En el bloque catch definimos qué excepción y con qué código
tratarla.
En el bloque finally escribimos código que siempre se ejecuta se
produzca o no la excepción.
Ejemplo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Alumno{
//...
public static void main (String[] args){
try{
Alumno a = new Alumno("Ana", 12, 6);
System.out.println(a);
a.reverse();
Alumno c = new Alumno();
System.out.println(c);
c.reverse();
}
catch (Exception e) {
97
16
17
18
19
20
21
System.out.println("--> Hemos capturado la
excepcion!!.");
}
finally {
System.out.println("--> Siempre se ejecuta se
produzca o no excepcion.");
}
}
}
Restricciones de los bloques try-catch-finally:
Un bloque try debe tener un bloque catch o un bloque finally
asociado.
Entre un bloque try y catch no puede escribirse código.
Si hay varios bloques catch las excepciones más específicas
deben colocarse antes que las más generales.
Assertions: Introducción y Ejemplo
Una aserción es una expresión lógica que el programador supone que
siempre será verdadera cuando se ejecute. Es una forma de
confirmar que esta suposición es cierta durante el desarrollo y
así evitar tener que programar código que compruebe o controle
casos que no se producirán.
Tenemos dos formas de utilizar las assertions:
assert(expresion_logica);
assert(expresion_logica): expresion_valor;
La segunda sintaxis nos permite además establecer una expresión
que nos dará más información sobre el error.
Esta expresion_valor será cualquier expresión que pueda ser
convertida a String, es decir puede ser un valor primitivo, una
cadena, un objeto, una función que devuelva alguna de las
anteriores, etcétera.
Características:
98
Se incorporaron en la versión J2SE 1.4
Siempre se afirma que algo será verdadero.
Las assertions están deshabilitadas por defecto.
Para habilitar las assertions añadir al comando java el
parámetro -ea (ó -enable assertions).
Para deshabilitar las assertions añadir al comando java el
parámetro -da (ó -disableassertions).
La clase java.lang.AssertionError desciende de
java.lang.Error y ésta a su vez de java.lang.Throwable.
Ejemplo:
En este programa vamos a construir una clase Triangulo. Tendremos
un constructor con los lados como parámetros. En la función
esRectangulo que devuelve true si el triángulo es rectángulo y
false en caso contrario, colocamos una aserción sobre los lados
del triángulo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.Math;
public class Triangulo {
private float ladoa;
private float ladob;
private float ladoc;
public Triangulo(float ladoa, float ladob, float ladoc){
this.ladoa = ladoa;
this.ladob = ladob;
this.ladoc = ladoc;
}
public boolean esRectangulo(){
//Sabemos un lado de un triangulo debe ser menor que
la suma
//de los otros dos lados.
99
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
assert(ladoa < ladob + ladoc): "\nladoa: " + ladoa
+ " no es < que: "
+ ladob + " + " +
ladoc;
return(Math.pow(ladoc,2) == Math.pow(ladoa,2) +
Math.pow(ladob,2));
}
public static void main (String[] args){
//Triangulo con valores de lados correctos.
Triangulo t1 = new Triangulo(6,8,10)
If t1.esRectangulo(){
System.out.println("El triangulo t1 es
rectangulo");
}
//Creamos un triangulo cuyos lados harán saltar la
assertion.
Triangulo t2 = new Triangulo(10,3,2);
if t2.esRectangulo(){
System.out.println("El triangulo t2 es
rectangulo");
}
}
}
Resultado:
Vemos en la captura que primero hemos compilado el fichero fuente.
En segundo lugar, hemos ejecutado el programa, pero al estar
deshabilitadas las assertion por defecto no ocurre ningún error.
Por último ejecutamos el programa habilitando las assertions y
vemos que la condición no se cumple al comprobar los lados del
segundo triángulo.
Nos añade a la traza del error la cadena “ladoa: 10.0 no es < que:
3.0 + 2.0″
100
Las clases de cadena: String, StringBuffer y StringBuilder
Java proporciona tres clases diferentes para tratar con cadenas de
caracteres:
java.lang.String
java.lang.StringBuffer
java.lang.StringBuilder
Los objetos String son cadenas de caracteres estáticas, al crearse
e inicializarse no pueden cambiar su valor. Es porque son
inmutables que pueden ser compartidas por varias referencias.
Los objetos StringBuffer y StringBuilder, por el contrario, son
cadenas de caracteres dinámicas, pueden cambiar su contenido y su
capacidad.
La diferencia entre los objetos StringBuffer y StringBuilder es
que los primeros pueden ser utilizados de forma segura por varios
hilos, siendo sus métodos sincronizados mientras que los objetos
StringBuilder no.
La clase StringBuilder se añadió en la versión J2SE 1.5.
Inmutabilidad en la clase String
Con el siguiente ejemplo, puede parecer que el String saludo sí
cambia, pero lo que en realidad sucede es que la referencia se
asigna a una nueva cadena creada:
1 public class Saludo{
101
2
3
4
5
6
7
public static void main (String[] args){
String saludo = "Hola ";
saludo = saludo + "Rosa";
System.out.println(saludo);
}
}
Si no se reasigna la referencia no estamos apuntando a una nueva
cadena. Las cadenas que se crean se quedan sin referencia alguna.
Por ejemplo:
1
2
3
4
5
6
7
8
public class Saludo2{
public static void main(String[] args){
String saludo = "Hola ";
saludo.concat("Maria");
saludo.toUpperCase();
System.out.prinltn(saludo);
}
}
102
String Constant Pool
Es una zona especial de memoria en la que se almacena una
colección de referencias a objetos String. Se utiliza para mejorar
la eficiencia. Al ser los objetos String inmutables se pueden
compartir de forma segura por varias referencias y así evitamos
crear varios objetos Strings iguales.
Los Strings son objetos y como tales se crean en el Heap. Las
referencias en la String Constant Pool apuntan a objetos String
vivos en el Heap.
Cuando el compilador se encuentra con un literal String, comprueba
si ya existe en el pool un literal igual. Si se encuentra la nueva
referencia apunta al String existente.
Si no es así, se crea el nuevo objeto String y se incluye en el
pool.
Diferencias al crear un String
Existen diferencias sutiles al crear un String asignando a una
referencia un literal y al crear un objeto String utilizando la
palabra clave new. En ambos casos supondremos que no existe con
anterioridad el String en el pool.
Asignando un literal String:
1 String nombre = "Lola";
Estamos creando un objeto String Lola y una referencia:
103
Sin embargo, utilizando la palabra clave new:
1 String nombre = new String("Lola");
Estamos creando dos objetos y una referencia:
Métodos importantes String
charAt(int i): argumento comienza en cero.
concat(String s): añade la cadena que se pasa como parámetro.
replace(char a, char b): reemplaza el carácter a con el
carácter b.
substring(int ini, int end): devuelve la subcadena que
comienza en ini (empezar en 0) y acaba en end (se cuenta
desde 1).
trim(): devuelve la cadena sin espacios.
length(): devuelve la longitud de la cadena.
toLowerCase(): devuelve la cadena en minúsculas.
toUpperCase(): devuelve la cadena en mayúsculas.
104
equalsIgnoreCase(String s): devuelve si es igual sin importar
mayúscula o minúscula a la cadena que se pasa como parámetro.
Más información sobre la String Constant Pool: Strings, Literally
en JavaRanch.
En la siguiente entrada, las clases StringBuffer, StringBuilder y
los métodos más usuales
Resumen de las clases de Ficheros y E/S
Os dejo este diagrama de las clases que debemos conocer para el
examen.
Incluye los métodos más conocidos y los argumentos que admiten (F:
File, Str: String, Wr: Writer, R: Reader)
Algunas notas rápidas sobre las clases:
File: es una representación abstracta de nombres de ficheros y
directorios.
FileReader: lee ficheros de caracteres. Es una clase que opera a
bajo nivel. El método read lee todo el fichero, devuelve el tamaño
y lo devuelve en un array.
BufferedReader: es la clase que mejora la eficiencia de los
anteriores lectores. Lee largas cantidades de caracteres y guarda
lo datos en buffer. Tiene mejores métodos como readLine().
FileWriter: se usa para escribir en ficheros de caracteres. Su
método write() permite escribir caracteres o cadenas en un
fichero.
BufferedWriter: clase que mejora la eficiencia del anterior
escritor. Escribe grandes cantidades de caracteres en un fichero.
Tiene el método newLine().
105
PrintWriter: clase mejorada desde Java 5. Métodos nuevos como
format(), printf() y append() la hacen flexible y poderosa.
Console: provee métodos para leer y escribir en la consola.
Resumen Interfaz Set y clases implementadoras
En este diagrama os dejo las clases que deberéis conocer sobre la
interfaz Set, los métodos más usados y algunas anotaciones.
También algunos ejemplos para que comprobéis sus características.
1. Ejemplo HashSet
1
2
3
4
5
6
7
8
9
10
11
12
import java.util.*;
public class EjemploHashSet {
public static void main (String[] args)
{
HashSet<String> s1 = new HashSet <String>();
s1.add("Hola");
s1.add("Hola amigo");
s1.add("Encantado de conocerte");
s1.add("Se me hace tarde");
s1.add("Espero verte pronto");
s1.add("Adios");
106
13
14
15
16
17
18
19
20
21
22
23
s1.add("Adios"); //Intentamos añadir un duplicado.
System.out.println("Numero de elementos: " +
s1.size() + "\n");
Iterator i = s1.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
}
}
2. Ejemplo LinkedHashSet
1
2
3
4
5
6
7
8
9
10
11
import java.util.*;
public class EjemploLinkedHashSet {
public static void main (String[] args)
{
LinkedHashSet<String> s2 = new LinkedHashSet
<String>();
s2.add("Hola");
s2.add("Hola amigo");
s2.add("Encantado de conocerte");
s2.add("Se me hace tarde");
s2.add("Espero verte pronto");
107
12
13
14
15
16
17
18
19
20
21
22
23
s2.add("Adios");
s2.add("Adios"); //Intentamos añadir un duplicado.
System.out.println("Numero de elementos: " +
s2.size() + "\n");
Iterator i = s2.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
}
}
3. Ejemplo TreeSet
1
2
3
4
5
6
7
8
9
10
11
import java.util.*;
public class EjemploTreeSet {
public static void main (String[] args)
{
TreeSet<String> s3 = new TreeSet<String>();
s3.add("Hola");
s3.add("Hola amigo");
s3.add("Encantado de conocerte");
s3.add("Se me hace tarde");
s3.add("Espero verte pronto");
108
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
s3.add("Adios");
s3.add("Adios"); //Intentamos añadir un duplicado.
System.out.println("Numero de elementos: " +
s3.size() + "\n");
Iterator i = s3.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
System.out.println ("\nlower(\"Encantado de
conocerte\"): "
+ s3.lower("Encantado de conocerte"));
System.out.println("floor(\"Encantado de
conocerte\"): "
+ s3.floor("Encantado de conocerte"));
System.out.println("higher(\"Encantado de
conocerte\"): "
+ s3.higher("Encantado de conocerte"));
System.out.println("ceiling(\"Encantado de
conocerte\"): "
+ s3.ceiling("Encantado de conocerte"));
// Devolvemos el set en orden descendente
NavigableSet<String> reverse = s3.descendingSet();
//Lo mostramos
Iterator r1 = reverse.iterator();
while (r1.hasNext()){
System.out.println(r1.next());
109
41
42
43
44
45
46
47
48
49
50
51
52
53
}
//Hacemos un pollFirst y pollLast
//Devuelve y elimina el primer elemento
System.out.println("\nreverse.pollFirst: " +
reverse.pollFirst());
//Devuelve y elimina el ultimo elemento
System.out.println("reverse.pollLast: " +
reverse.pollLast() + "\n");
//Vemos cómo queda el set
Iterator r2 = reverse.iterator();
while (r2.hasNext()){
System.out.println(r2.next());
}
}
}
110
Resumen Interfaz List y clases implementadoras
En este diagrama os dejo las clases que deberéis conocer sobre la
interfaz List, los métodos más usados y algunas anotaciones sobre
ellas.
1. Ejemplo ArrayList
1
2
3
import java.util.*;
public class EjemArrayList{
public static void main (String[] args){
111
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ArrayList nombres = new ArrayList();
System.out.println("Elementos al inicio: " +
nombres.size());
nombres.add("Ana");
nombres.add("Bea");
nombres.add("Dalia");
nombres.add("Pedro");
nombres.add("Bea"); //comprobar que admite
duplicados
System.out.println("Contenido: " + nombres);
System.out.println("Elementos: " + nombres.size());
System.out.println(nombres.contains("Ana"));
nombres.remove("Ana"); //eliminamos un elemento
System.out.println("Contenido: " + nombres);
System.out.println("Elementos: " + nombres.size());
System.out.println(nombres.contains("Ana"));
nombres.add(3, "Ana"); //Añadir en posicion x
System.out.println("Contenido: " + nombres);
System.out.println("SubLista(1,4):" +
nombres.subList(1,4));
}
}
Resultado de la ejecución:
112
2. Ejemplo LinkedList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.*;
public class EjemLinkedList {
public static void main (String[] args){
LinkedList nombres = new LinkedList();
nombres.add("Ana");
nombres.add("Maria");
nombres.add("Pedro");
nombres.addFirst("Elena"); //insertamos al ppio.
nombres.addLast("Bea"); //insertamos al final.
nombres.add(2, "Lola"); //insertamos en posicion x
System.out.println("\nContenido: " + nombres);
System.out.println("getFirst(): " +
nombres.getFirst());
System.out.println("getLast(): " +
nombres.getLast());
System.out.println("\nHacemos peek(): " +
nombres.peek());
System.out.println("Contenido: " + nombres);
System.out.println("\nHacemos poll(): " +
nombres.poll());
System.out.println("Contenido: " + nombres);
113
24
25
26
System.out.println("\nHacemos offer(): " +
nombres.offer("Luis"));
System.out.println("Contenido: " + nombres);
}
}
Resultado de la ejecución:
Si queréis consultar algún método en concreto o verlos
todos: ArrayList, LinkedList, List.