10 de octubre de 2010

Sun Certified Java Programmer 6, CX-310-065 - Parte 1: Declaraciones y Controles de Acceso

Este es el primer post dedicado a la certificación SCJP 6 (CX-310-065). Definiremos algunos conceptos básicos del lenguaje Java así como sus reglas más básicas de funcionamiento.

¿Por qué es importante esto? Bueno esto es de vital importancia ya que en todo lenguaje de programación, y tecnología en general, es importante para entender sus funciones avanzadas, comenzar entendiendo las partes básicas, así como definir los términos que se usan, para que todos entendamos de lo que estamos hablando.

Pues comencemos con el primer tema con un repaso de los conceptos básicos que debemos tener presentes, a modo de recordatorio.


DECLARACIONES Y CONTROLES DE ACCESO

Conceptos básicos:


Recordemos que Java es un lenguaje orientado a objetos, así que comencemos con algunas definiciones de este paradigma:

  • Clase: Una clase es una especie de plantilla que describe el estado y comportamiento que los objetos de su tipo soportan. Las clases son una forma de encapsular datos y las operaciones relacionadas con esos datos. Son la estructura fundamental del paradigma orientado a objetos.
  • Objeto: Son las instancias de una clase. Tiene su propio estado, y el acceso a todos los comportamientos u operaciones definidas por su clase.
  • Estado: Los valores asignados a las "variables de instancia" de un objeto constituyen el estado del objeto.
  • Comportamiento (Métodos): Es donde la lógica de la clase es almacenada, donde los algoritmos son ejecutados y los datos son manipulados.

Cuando comenzamos a trabajar con el paradigma orientado a objetos algunas veces es difícil entender la diferencia entre clase y objeto. Usando una analogía, podríamos decir que, en vez de ser programadores somos cocineros y estamos haciendo galletas, la clase sería el molde para las galletas y los objetos serían las galletas que se hagan con ese molde.


Identificadores y Palabras Reservadas:


Los nombres de las clases, variables y métodos (en general los nombres de todo lo que podemos declarar) son llamados identificadores. Los programadores de Java (y Sun) han creado convenciones para nombrar los métodos, variables y clases. Estos métodos serán mencionados más adelante en este mismo post.

Como todos los lenguajes de programación Java cuenta con palabras reservadas, tenemos que recordar que estas palabras reservadas no deben ser usadas como identificadores.


Herencia:

El centro de Java y de la programación orientada a objetos. Con la herencia podemos definir que una clase sea reusada en otras clases, podemos definir una clase general (súper-clase) y sub-clases que heredan de esta. La súper-clase no sabe nada de la existencia de las clases que heredan de ella pero todas las sub-clases que la heredan deben declarar explícitamente la relación de herencia que existe. Una sub-clase obtiene automáticamente acceso a las variables y métodos definidos en la súper-clase y también es libre de sobre escribir los métodos (hablaremos sobre sobre-escritura de métodos en un post posterior).


Interfaces:

El compañero perfecto de la herencia es el uso de las interfaces. Las interfaces son clases 100% abstractas que definen los métodos que una subclase debe implementar, pero no como debe hacerlo. Esto nos permite definir el comportamiento que esperamos que tenga una clase, sin decir exactamente cómo debe hacerlo. Gracias a las interfaces podemos lograr uno de los objetivos más buscados dentro del diseño orientado a objetos: bajo acoplamiento (del cual hablaremos en los siguientes posts).


Identificadores y JavaBeans


En java existen reglas y acuerdos para denominar los diferentes tipos de datos y objetos existentes y creados por nosotros mismos a continuación explicamos brevemente cada uno de estos conceptos:

  • Identificador legal: Como dijimos: un identificador es el nombre que se le da a un elemento en Java (ya sea una clase, interface, variable, enumeración, etc). Para que un identificador sea legal se utilizan una serie de reglas explicadas posteriormente. Si un identificador no sigue las reglas necesarias para sea correcto o legal este arrojara un error. Para que el compilador detecte si un identificador es legal o no, utiliza las reglas establecidas para su correcto funcionamiento. Hablaremos de estas reglas en un momento.
  • Convenciones de código Java de Sun: Son recomendaciones de Sun para nombrar a las clases, variables y métodos.
  • Estándar de Nombres JavaBeans: Los requerimientos de nombres de la especificación JavaBeans.

Ahora pasaremos a hablar detalladamente de cada uno de estos:

Identificador legal:

Técnicamente un identificador legal debe estar compuesto por solo caracteres Unicode, números, símbolos de dólar ($) y caracteres de conexión (como el guion bajo "_").

Los identificadores legales deben cumplir con los siguientes requisitos:

  • Un identificador debe empezar con una letra, símbolo de dólar ($) o caracteres de conexión (_), nunca debe empezar con un número.
  • Después del primer carácter él identificador puede contener cualquier combinación de letras, símbolo de dólar ($), caracteres de conexión (_) o números.
  • No hay un límite de caracteres que un identificador pueda tener.
  • No se pueden usar una palabra reservada como identificador.
  • Los identificadores son "Case Sensitive", es decir, una clase "Carro" no es igual que una clase "cARRO".

*Nota: Usaremos el IDE Netbeans para ayudarnos a aclarar algunos ejemplos. Este IDE está actualmente en su versión 6.9, pero usaremos la versión 6.8 con lo cual no debería haber ningún problema.

Ejemplo de identificadores legales:



Como podemos observar, el IDE no marca ningún error, con lo cual deducimos que las declaraciones son correctas aunque algo extrañas.

Ejemplo de identificadores no legales:



Como podemos observar, esta vez el IDE si marca error ya que obviamente no estamos siguiendo las reglas antes mencionadas para que un identificador sea legal.

Las palabras reservadas de Java son 50 (hasta la versión 1.6) y las mencionamos en la siguiente tabla. Recordamos que no podemos llamar a un identificador como uno de estas palabras reservadas.

abstractbooleanbreakbytecasecatch
charclassconstcontinuedefaultdo
doubleelseextendsfinalfinallyfloat
forgotoifimplementsimportinstanceof
intinterfacelongnativenewpackage
privateprotectedpublicreturnshortstatic
strictfpsuperswitchsynchronizedthisthrow
throwstransienttryvoidvolatilewhile
assertenum


Cabe mencionar que aunque todas las palabras de la tabla anterior están reservadas no todas se usan en las versiones actuales de Java, y están "apartadas" por si en algún momento se les da un uso.


Convenciones de código Java de Sun:


Sun ha creado un conjunto de normas o convenciones o recomendaciones de codificación de Java, y se publicaron las normas en un documento titulado ingeniosamente "Convenciones de código Java", que se puede encontrar en la página de convenciones de código de Oracle.

Aquí están las normas de nomenclatura que Sun recomienda:

Clases e interfaces: La primera letra debe ser mayúscula, si varias palabras se unen para formar un nombre cada palabra que lo conforma debe empezar con la primera letra mayúscula a esto se le llama "Camel Case", por ejemplo:
  • Perro
  • Cuenta
  • SistemaDeAdministracionFinanciero

Las interfaces son típicamente adjetivos como:
  • Runnable
  • Serializable
  • Imprimible

Métodos: la primera letra debe ser minúscula y después se puede aplicar la regla Camel Case, típicamente son pares verbo-sustantivo, por ejemplo:
  • calculaPeso
  • getBalance
  • obtenConexion
  • setNombreCliente

Variables: Similar a los métodos, Sun recomienda nombres cortos, significativos y fáciles de recordar.
  • nombreCompleto
  • porcentajeInteres
  • miString

Constantes: Las variables constantes son marcadas como "static" y "final". Estas se deben nombrarse con todas sus letras mayúsculas y separando cada palabra con guion bajo (_), por ejemplo:
  • ESTE_VALOR_NUNCA_CAMBIARA
  • VARIABLE_ESTATICA

Aunque no es necesario seguir estas convenciones para que nuestro código compile, el hacerlo nos ayudará cuando realicemos proyectos en equipo o que otros entiendan más fácilmente nuestro código.

Ahora hablaremos de los estándares JavaBeans que son acuerdos para nombrar a las clases, métodos "set" (establecer un valor) y "get" (obtener un valor), etc. Que tampoco estamos obligados a seguirlas pero es una buena práctica para nosotros y para tener más claro nuestro código.


Estándares JavaBeans


Los JavaBean son clases que contienen propiedades, estas propiedades son nombradas variables de instancia y por lo regular, aunque deberíamos tratar de que fuera siempre, las marcamos como privadas, la única forma de acceder a estas propiedades desde fuera de la clase es a través de métodos, los métodos que se utilizan para establecer valor a estas variables son llamados métodos "setters" y los métodos para obtener el valor son llamados "getters".

Sun establece unas reglas para nombrar a las propiedades, así como a sus métodos "setter" y "getter". Muchos frameworks se basan en que nuestro código sigue estas reglas para poder funcionar correctamente y, como toda regla, si no las seguimos corremos el riesgo de que algo no funcione como lo esperamos.

Reglas para nombrar a las propiedades:
  • Si la propiedad no es de tipo booleano, el prefijo para un método getter es "get", por ejemplo para una variable "nombre" su respectivo método getter será "getNombre" (aquí también se aplica la regla de Camel Case para separar cada palabra). También hay que tener en cuenta que no hay necesidad de que el nombre que sigue después del prefijo "get" sea igual al nombre de la propiedad, el nombre de la propiedad es irrelevante para el método "get" o "set".
  • Si la propiedad es un booleano el método getter para esta propiedad puede llevar el prefijo "get" o "is", para una propiedad booleana llamada "estado" su método get válido puede ser "getEstado" o "isEstado".
  • El prefijo para el método setter de una propiedad es "set", por ejemplo "setNombre" para un atributo "nombre".
  • Como dije anteriormente en las propiedades también se aplica la regla camel case, es decir, después del prefijo "set", "get" o "is" la primera letra seguida es una letra mayúscula.
  • El método "setter" debe ser marcado público con un valor de retorno void (vacio) y con un argumento o parámetro que represente al tipo de la propiedad, por ejemplo: "public void setNombre(String nombre){ … }" para una propiedad "nombre" del tipo String.
  • Los métodos "get" también deben ser marcados públicos con un valor de retorno que represente al tipo de la propiedad y/o con el tipo de argumento o parámetro del método set para esa propiedad, por ejemplo: "public String getNombre(){ return nombre; }" para una propiedad "nombre" del tipo String".

Estas reglas se extieden para proporcionar relgas especiales para los eventos que nuestro sistema pueda lanzar (usualmente en aplicaciones stand alone o de escritorio)

Un evento constituye un método para que una clase notifique a los usuarios de un objeto que algo interesante sucede al objeto, como, por ejemplo, que se ha hecho clic en un control de una interfaz gráfica de usuario. Los eventos JavaBean soportan especificaciones, que permiten a los componentes notificar a otros cuando algo sucede. El modelo de eventos se utiliza a menudo en aplicaciones de interfaz gráfica de usuario (GUI) cuando un evento como un clic del ratón es de multidifusión a muchos otros objetos que pueden tener cosas que hacer cuando se produce el clic del ratón. Los objetos que reciben la información que se produjo un evento se llaman "listeners" (oyentes). Se necesita saber que los métodos que se utilizan para añadir o eliminar los listeners de un evento también debe seguir las normas de nomenclatura JavaBean:

Reglas para nombrar a los listeners:
  • Los nombres de los métodos listeners usados para "registrar" un listener con un evento debe usar el prefijo "add" seguido del tipo de listener, por ejemplo: "addActionListener".
  • Los nombres de los métodos usados para "eliminar" un listener debe usar el prefijo "remove", seguido del tipo de listener.
  • El tipo de listener que se registra o elimina debe ser pasado como argumento al método.
  • Los listeners deben terminar con la palabra "Listener".

Ejemplo de métodos JavaBeans validos:
  • public void setMyValue(int v)
  • public int getMyValue()
  • public boolean isMyStatus()
  • public void addMyListener(MyListener m)
  • public void removeMyListener(MyListener m)

Ejemplo de métodos JavaBeans invalidos:
  • void setNombreCliente(String s) // debe ser publico
  • public void modificarMiValor(int v) // no puede usar 'modificar'
  • public void addXListener(MyListener m) // No coinciden los tipos


Declaraciones de clases:

Cuando escribimos código Java escribimos clases o interfaces, dentro de estas escribimos las variables y métodos. La forma en la que declaremos las clases, variables y métodos tiene implicaciones en el comportamiento de nuestro código, por ejemplo, cuando declaramos una clase como public (publico) esta clase puede ser accedida o vista desde cualquier clase dentro de nuestra aplicación.

Reglas de declaración de archivos de código fuente:
  • Sólo debe haber una clase declarada pública dentro de un archivo de código.
  • Los comentarios pueden ir en cualquier línea del código.
  • Si tenemos una clase declarada publica en un archivo, el nombre del archivo debe ser el mismo de la clase pública, por ejemplo, para la clase "public class Empleado" el archivo debe tener el nombre "Empleado.java".
  • Si la clase forma parte de un paquete, la declaración de este paquete debe ir primero antes que la declaración de la clase. Si no se manejan paquetes y se importan clases, con la palabra reservada "import" (importaciones) estos "import" debe declararse antes que la clase. Si no se tienen ni paquetes ni importaciones la declaración de la clase debe aparecer en la primera línea (aunque pueden ir comentarios antes de esta).
  • Las importaciones y los paquetes se aplican para todas las clases declaradas en un archivo, no hay forma de que declares múltiples clases en un archivo y pertenezcan a diferentes paquetes o usen diferentes importaciones.
  • Un archivo puede tener más de una clase no pública.


Declaración de clases y modificadores


Los modificadores se dividen en dos categorías:
  • Modificadores de acceso: public, private, protected.
  • Modificadores de no acceso: algunos como final, static, abstract, etc.

Primero debemos de tener en mente que con los modificadores de acceso podemos establecer si una clase puede ser accedida o no por un determinado recurso. Existen 4 niveles de acceso pero 3 modificadores de acceso. El cuarto nivel de acceso de control es el usado por defecto, que se establece cuando no declaramos ningún modificador de acceso a nuestra clase, es decir, todas las clases, variables y métodos cuentan con un control de acceso. Los cuatro niveles de acceso funcionan para las variables y métodos pero una clase solo puede ser declarada pública o por defecto.


Acceso a las clases:


Cuando decimos que una clase "A" tiene acceso a una clase "B", esto significa 3 cosas:
  • Que la clase "A" puede crear una instancia de la clase "B".
  • La clase "A" extiende de la clase "B", es decir es una sub clase de la clase "B".
  • La clase "A" puede acceder a ciertos métodos y variables de la clase "B", dependiendo de las controles de acceso que tengas estos en la clase "B".

Acceso por defecto o default:


Una clase con acceso por defecto es la que no tiene ningún modificador de acceso en su declaración. Pensemos en el acceso por defecto como un nivel de acceso de paquete, es decir, una clase con acceso por defecto solo puede ser accedida desde otra clase en el mismo paquete. Por ejemplo si tenemos dos clases "A" y "B", que están en diferentes paquetes y ambas tienen el nivel acceso por defecto (es decir, no tienen declarado ningún modificador de acceso), la clase "B" no puede crear una instancia de la clase "A" (ya que no puede verla), y la clase "A" ni siquiera sabe que la clase "B" existe (ya que tampoco puede verla). Por ejemplo, si tenemos las siguientes clases:

package autos;

class Toyota{ }


Y en un segundo archivo tenemos:

package aviones;

import autos.Toyota;

public class Boing extend Toyota { }


Como podemos, ver la clase "Toyota" y la clase "Boing" se encuentran en diferentes paquetes. La importación en la parte superior de clase "Boing" está tratando de importar a la clase "Toyota", la clase "Toyota" compila bien, pero cuando tratamos de compilar la clase "Boing" tendremos un error como este:

Can't access class autos.Toyota Class or interface must be public, in same package, or an accessible member class. import autos.Toyota;


La clase "Boing" no puede acceder a la clase "Toyota", ya que esta clase esta con el nivel de acceso por defecto y se encuentran en diferentes paquetes. Para lograr nuestro cometido podemos realizar 2 cosas: incluir a las dos clases en el mismo paquete o declarar a la clase "Toyota" como pública.


Acceso público:


Una clase declarada como pública puede ser accedida desde cualquier lugar de nuestra aplicación. No debemos olvidar que para poder acceder a una clase publica desde otro paquete tenemos que importar esta clase pública.


Otros modificadores o modificadores de no acceso (Non Access Modifiers):


Los modificadores de no acceso (non access modifiers) sirven para un propósito distinto al de los modificadores de acceso, sirven para lograr diferentes funcionalidades, en resumen y para mencionar algunos explicaremos primero brevemente algunos de los modificadores de no acceso para dar una idea un poco más claro de estos:
  • El modificador static se aplica a los métodos de clase y las variables para convertirlos en métodos o variables de clases, y no de instancia.
  • El modificador final se aplica a las clases, métodos y variables para evitar que sean extendidos, sobreescritos o cambiados (en resumen modificados de alguna forma).
  • El modificador abstract para crear clases y métodos abstractos.

Hemos mencionado solo algunos de los modificadores de no acceso, mas adelante abarcaremos en más detalle todos estos modificadores. Como podemos ver algunos modificadores solo se pueden aplicar a métodos y clases otros a clases, métodos y variables, todo esto lo veremos a continuación.

Una clase puede tener sólo los siguientes modificadores de no acceso:
  • final
  • abstract
  • strictfp

Estos modificadores son adicionales a los modificadores de acceso y podemos usarlos en combinación con estos. Podemos, por ejemplo, declarar una clase como "public final". Sin embargo no siempre podemos combinar estos los modificadores de no acceso, como por ejemplo nunca debemos declarar una clase "abstract final", ya veremos porque más adelante.


Clases declaradas con el modificador ‘final’:


Una clase declarada con el modificador "final" significa que no se pueden crear subclases a partir de ella. Un ejemplo de una clase declarada "final" es cuando queramos tener la absoluta garantía que los métodos y variables de esta clase no puedan ser sobre escritos por otra clase.

Una de las ventajas de tener clases no "final" es este escenario: imaginen que encontramos un problema con un método en una clase que estamos utilizando, pero no tenemos el código fuente, así que no podemos modificar el código fuente para mejorar el método. Pero se puede extender la clase y reemplazar el método en su nueva subclase, y sustituir en todas partes la superclase original. Si la clase es "final", sin embargo, entonces estamos perdidos.

Modificaremos nuestro ejemplo anterior:

package autos;

public final class Toyota{ }


Si ahora intentamos extender la clase "Toyota", que está marcada como "final", con la siguiente clase que tenemos en un segundo archivo:

package aviones;

import autos.Toyota;

public class Boing extend Toyota { }


Obtendremos un error como el siguiente

Can't subclass final classes: class autos.Toyota class Boing extends Toyota{ 1 error


Tenemos este error ya que la clase "Toyota" tiene el modificador "final" (no puede tener subclases) y aun así la clase "Boing" está tratando de extenderla.


Clases abstractas:


Una clase abstracta nunca puede ser instanciada (osea que no podemos crear instancias de ella usando el operador "new"), su único propósito, misión en la vida, su razón de ser, es ser extendida. Podemos usar una clase abstracta, por ejemplo, si tenemos una clase "Carro" que tiene los métodos que deben ser implementados en todos los tipos de carros que extienden de él.

Un ejemplo de clase abstracta es la siguiente:

abstract class Carro 
{
    private double precio;
    private String modelo;
    private String anio;
    
    public abstract void irRapido();

    public abstract void irLento();
    
    // aquí va codigo adicional, importante y serio 
} 


En donde hemos declarado que la clase "Carro" es una clase "abstract", además tiene dos métodos que hemos marcado también como "abstract": "irRapido" e "irLento".

El ejemplo anterior compila bien, pero si intentamos instanciar esta clase abstracta de la siguiente forma:

Carro x = new Carro();


Obtendremos el siguiente error:

AnotherClass.java:7: class Car is an abstract class. It can't be instantiated.
Carro x = new Carro();
1 error


Notemos que los métodos abstractos declarados terminan en punto seguido (;) en lugar de llaves ({}).

Si un método es declarado como abstracto, tanto el método como la clase deben ser declarados abstractos, o sea que una clase debe ser abstracta si por lo menos uno de sus métodos es abstracto. Si cambiamos los métodos de abstracto a no abstracto no olvidemos de cambiar el punto y como al final por un par de llaves.


Declaración de interfaces:

Cuando declaramos una interface es como si declaráramos un contrato qué debe de hacer la clase sin decir como lo hará. Si, por ejemplo, declaramos una interface "Rebotable" con sus métodos "rebotar" y "setFactorRebote", la clase que implemente esta interface tiene la obligación de implementar los métodos "rebotar" y "setFactorRebote".

Una interface puede ser implementada por cualquier clase. Podemos tener dos clases diferentes que extiendan de clases diferentes y que no estén relacionadas de ninguna forma, pero al decir que estas clases implementan la interfaz "Rebotable" queremos decir que estas dos clases deben de implementar los métodos "rebotar" y "setFactorRebote".

Como dijimos al inicio: una interfaces es una clase 100% abstracta como mostramos en el siguiente ejemplo:

Lo que nosotros declaramos:

interface Rebotable
{
 void rebote();
 void setFactorRebote(int fr);
}


Lo que el compilador ve:

interface Rebotable
{
 public abstract void rebote();
 public  abstract void setFactorRebote(int fr);
}


Lo que la clase que implementa la interface debe hacer

public class Pelota implements Rebotable
{
 public void rebote(){};
 public void setFactorRebote(int fr){};
}


Mientras una clase abstracta puede definir métodos abstractos (que deben ser implementados en la clase que utiliza esta clase abstracta) y métodos no abstractos, una interface solo puede definir métodos abstractos.

Como todas las cosas que vimos anteriormente, las interfaces también cuentan con reglas especificas para su correcta implementación:
  • Todos los métodos de una interfaces son implícitamente "public" y "abstract" (públicos y abstractos), es decir, no es necesario escribir explícitamente estos dos modificadores, pero el método sigue siendo público y abstracto.
  • Todas las variables definidas en una interface deben ser públicas, estáticas y finales ("public", "static", "final"), en otras palabras, las interfaces solo declaran constantes, no variables de instancia.
  • Los métodos de una interface NO deben ser estáticos ("static").
  • Ya que los métodos de una interface son abstractos, no pueden ser marcados con los modificadores "final", "strictfp" y "native".
  • Una interface puede extender a una o más interfaces.
  • Una interface no puede implementar a otras interfaces.
  • Una interfaz debe ser declarada con la palabra reservada "interface".

Escribir una interface con el modificador "abstract" resulta redundante ya que al igual que sus métodos una interface es implícitamente abstracta lo indiquemos o no, por ejemplo:

public interface Rebotable{}

public abstract interface Rebotable{}


Las dos declaraciones anteriores son legales.

Al igual que las declaraciones de las interfaces, los métodos de las mismas no tienen necesidad de ser declarados como abstractos, ya que también resulta redundante porque estos son implícitamente abstractos y públicos, por ejemplo:


void rebote();

public abstract void rebote();


Estas dos declaraciones también son legales.

Ejemplo de declaraciones legales:

void rebote();
 
public void rebote();
 
public abstract rebote();
 
abstract public rebote();


Ejemplo de declaraciones no legales:

final void rebote();

static void rebote();

private void rebote();

protected void rebote();


Declarando constantes en una interfaz:


Una interface permite declarar constantes, la clase que implemente esta interfaz tiene acceso a las variables de la interfaz implementada como si se tratara de una relación de herencia. Hay que recordar una regla para declarar constantes: estas deben de ser públicas, estáticas y finales ("public", "static" y "final"), pero no debemos preocuparnos de escribir explícitamente estos modificadores ya que, al igual que las clases y los métodos, son implícitamente públicos y abstractos, las variables de una interfaz son implícitamente públicos, estáticos y finales, lo escribamos o no.

Pero hay que tener en cuenta lo siguiente: una constante una vez que se le es asignado un valor, este valor no puede ser modificado nunca, por ejemplo:

public interface Rebotable
{
 int NUMERO_CONSTANTE = 5;
}


En otro archivo

public class Pelota implements Rebotable
{
 public void modificaNumeroConstante()
 {
  NUMERO_CONSTANTE = 8;
 }
}


Como dijimos anteriormente una constante una vez asignado el valor no puede cambiar, es decir NUMERO_CONSTANTE = 8; no compilará ya que al declararlo en la interfaz "Rebotable" se convierte en una constante y esta no puede ser modificada como se está tratando de hacer en la clase "Pelota".


Declaración de miembros de clase:


Anteriormente vimos las reglas necesarias y a tener en cuenta para poder declarar correctamente una clase. Ahora veremos lo que se debe hacer para declarar correctamente una variable y los métodos de una clase.


Modificadores de acceso:


Como los métodos y las variables son accedidos de la misma forma, hablaremos de los dos en esta sección.

Recordemos que cuando declaramos una clase solo podemos usar dos de los cuatro niveles de acceso (public y por defecto), con los miembros de clase (métodos y variables) podemos usar los 4 niveles de acceso:
  • public
  • protected
  • private
  • Por defecto

Recordemos que el acceso por defecto es cuando no se escribe explícitamente un modificador de acceso, o sea, no ponemos nada.

Podemos acceder a los miembros de una clase desde otra clase de dos maneras:

La primera consiste en usar el operador punto (.) para invocar a un método o una variable, por ejemplo:

public class Auto
{
 public void muevete()
 {
  System.out.println("Me estoy moviendo!!!");
 }
}

class Perro
{
 public void usandoMueveteDeAuto()
 {
  Auto auto = new Auto();//podemos hacer esto porque tenemos acceso a la clase "Auto"
  
  System.out.println("El perro dice" + auto.muevete());//podemos acceder al método muevete porque este método es publico
 }
}


La segunda forma de acceder a los miembros de una clase es a través de la herencia, por ejemplo:

public class Auto
{
 public void muevete()
 {
  System.out.println("Me estoy moviendo!!!");
 }
}

class Perro extends Auto
{
 public void usandoMueveteDeAuto()
 {
  System.out.println("El perro dice" + this.muevete()); //podemos hacer esto por que heredamos los métodos publicos
 
  Auto auto = new Auto();//podemos hacer esto porque tenemos acceso a la clase Auto

  System.out.println("El perro vuelve a decir" + auto.muevete());//podemos acceder al método muevete porque este método es publico
 }
}



Acceso público:


Cuando una variable o método es declarado con el nivel de acceso público quiere decir que este puede ser accedido desde cualquier clase, independientemente del paquete en que esté (asumiendo que la clase a la cual pertenece es visible). Como se muestra en la siguiente imagen:



También podemos imaginar que la clase "AmoDelPerro" se encuentra en un diferente paquete que las 2 clases anteriores y no tendría ningún problema, siempre y cuando la clase como el método a la cual hace referencia sean públicos (haciendo las importaciones necesarias).


Acceso Privado:


Los miembros de una clase marcados como privados no pueden ser accedidos desde ninguna otra clase que no sea la clase en la cual fue declarado, por ejemplo:

package transporteTerrestre;

public class Auto
{
 private void muevete()
 {  
  //podemos poner cualquier código aquí, pero solo la clase "Auto" la conocerá
 
  System.out.println("Me estoy moviendo!!!");
 }
}


Y en otro archivo declaramos:

package transporteAereo;

import transporteTerrestre.Auto

public class Avion
{
 public  void vuela()
 {
  //Accedemos a la clase Auto, porque esta es publica
  Auto auto = new Auto();

  System.out.println("Primero tiene que moverte" + auto.muevete()); //Error de compilación
 }
}


Si tratamos de ejecutar este código obtendremos un error como el siguiente:

Cannot find Symbol 
Symbol:  method muevete();


Esto sucede ya que el compilador interpreta como si el método "muevete()" no existiera, ya que un método declarado como privado no existe para ninguna clase excepto para su propia clase.

Algo similar sucede con la herencia: si un método en la súper clase es declarado como privado este método no puede ser heredado por la sub-clase; sin embargo, podemos declarar un método con la misma firma en la subclase, no importa cómo se vea, este método no está sobre escribiendo al de la súper clase (la sobre escritura depende de la herencia) es simplemente un método que tiene el mismo nombre que el de la súper clase.

package transporteTerrestre;

public class Auto
{
 private void muevete()
 {
  //podemos poner cualquier código aquí, pero solo la clase //Auto la conocerá
  System.out.println("Me estoy moviendo!!!");
 }
}


Ahora el método "muevete" se limita a la clase "Auto" incluso en el mismo paquete:

package transporteTerrestre;

public class Toyota extends Auto  //Toyota y Auto se encuentran en el mismo paquete y la súper clase Auto es publica  
{   
 private void hazAlgo()
 {
  System.out.println(muevete()); //Error de compilación 
 }
}


Esto se ve más claramente en la siguiente imagen:




Miembros protegidos y por defecto:


Los niveles de acceso protegidos y por defecto son muy parecidos pero tienen una gran diferencia. El miembro de una clase que tiene el nivel de acceso por defecto puede ser accedido por cualquier clase sólo dentro del mismo paquete, los miembros con nivel de acceso protegido ("protected") también pueden ser accedidos desde otro paquete pero solamente por medio de la herencia.

Veamos un ejemplo con las siguientes dos clases que se encuentran en diferente paquete.

package ejemplo;

public  class Auto
{
 void muevete()
 {
  System.out.println("Me estoy moviendo");
 }
}



package  acceso; //Diferente paquete

import  ejemplo.Auto;

public class AccesandoAuto
{
 public static void main (String[] args)
 {
  Auto auto = new Auto();
  auto.muevete();
 }
}


Como podemos apreciar, la clase "Auto" se encuentra en un paquete llamado "ejemplo" y tiene su método llamado "muevete" que esta con el nivel de acceso por defecto (no está declarado explícitamente alguno modificador de acceso), después tenemos la clase "AccesandoAuto" el cual está en un paquete diferente llamado "acceso", como dijimos anteriormente: debemos pensar en el nivel de acceso por defecto como un acceso a nivel de paquete y como el método "muevete" se encuentre en un paquete diferente al tratar de ejecutarlo obtendremos el siguiente error:

ERROR
muevete() is not public in ejemplo.Auto; cannot be accessed from outside package


Los niveles de acceso por defecto y protegidos solo difieren cuando hablamos de herencia (sub clases). Si usamos el nivel de acceso "protected" (protegido) para declarar a un miembro de clase, este miembro de clase puede ser accedido desde cualquier subclase no importando si la súper clase y la sub clase se encuentren en diferente paquete, los miembros de la súper clase siempre son visibles por las sub clases. En contraste con el nivel de acceso por defecto una subclase no puede acceder a los miembros de la súper clase a menos que estén en el mismo paquete, el nivel de acceso por defecto no tiene ninguna consideración para con la herencia.

Para resumir: cuando pienses en acceso por defecto piensa en un acceso a nivel de paquete (sin excepciones) y cuando pienses en el nivel de acceso protegido piensa en un nivel de acceso por paquete y también de padre a hijo cuando hablamos de diferentes paquetes, para aclarar otro punto, si la subclase intenta acceder a la súper clase con el miembro protegido a través de una instancia de la súper clase esto no es posible, aclarando por última vez: Solo es a través de la herencia.


Detalles de acceso a miembros de clase protegido:

package ejemplo;

public  class Auto
{
 protected int numero = 5;
}


El código anterior declara una variable llamada "numero" con nivel de acceso protegido, como dijimos anteriormente esta variable solo puede ser accedido por clases dentro del paquete "ejemplo" y por clases fuera de ese paquete sólo a través de la herencia.

Ahora creemos otra clase en otro paquete:

package acceso;//Diferente paquete

import ejemplo.Auto;

public class AccesandoAuto extends Auto
{
 public void metodo ()
 {
  System.out.println("El valor de numero es: " + numero)
 }
}


El ejemplo anterior compila bien ya que estamos accediendo a un miembro de clase protegido en diferente paquete pero a través de la herencia, pero ¿qué sucede si intentamos acceder al miembro de la subclase a través de una instancia de la súper clase?

package acceso;//Diferente paquete

import ejemplo.Auto;

public class AccesandoAuto extends Auto
{
 public void metodo()
 {
  System.out.println("El valor de numero: " + numero);
  Auto auto = new Auto();
  
  System.out.println("El valor de numero: " + auto.numero); // Error de compilación
 }
}


En código anterior tenemos un error en la línea:

System.out.println("El valor de numero es: " + auto.numero);


El error es el siguiente:

Uncompilable source code - numero has protected access in ejemplo.Auto 
        at acceso. AccesandoAuto.metodo


Ya que a pesar de estar en una clase que hereda a la súper clase con miembro de clase protegido, y espero decirlo por última vez: SOLO ES VISIBLE A TRAVES DE LA HERENCIA.


Detalles de miembros de clase con nivel de acceso por defecto:

Empezaremos con el nivel de acceso por defecto en la herencia, en el siguiente ejemplo modificaremos nuestra variable y le pondremos nivel de acceso por defecto, es decir, no le pondremos explícitamente un modificador de acceso.

package ejemplo;

public class Auto
{
 int numero = 5;
}


¿Qué sucede si la clase padre y la clase hijo se encuentran en diferente paquete y en la clase padre tenemos una variable con nivel de acceso por defecto?

package acceso; //Diferente paquete

import ejemplo.Auto;

public class AccesandoAuto extends Auto
{
 public void metodo()
 {
  System.out.println("El valor de numero es: " + numero); // Error de compilación
  Auto auto = new Auto();
  System.out.println("El valor de numero es: " + auto.numero); // Error de compilación
 }
}


El error es el siguiente

numero is not public in ejemplo.Auto; cannot be accessed from outside package at acceso. AccesandoAuto.metodo
En estos dos casos obtendremos el error descrito anteriormente ya que las variables de clase declaradas con el nivel de acceso por defecto solo son visibles dentro de las clases que se encuentren en el mismo paquete, incluso si hacemos uso de la herencia.


Variables locales y modificadoras de acceso


Antes de todo debemos aclarar que se llaman variables locales a las variables declaradas dentro de un método. Estas variables locales NUNCA deben ser declaradas con algún modificador de acceso, por ejemplo, algo como esto es incorrecto:

package ejemplo;

public class Auto
{
 public void metodo()
 {
  private int numero;
 }
}


El código anterior nos daría el siguiente error

Uncompilable source code - illegal start of expression at ejemplo.Auto.metodo


Para resumir lo visto hasta el momento vamos a mostrar una tabla donde se explica los niveles de acceso:


VisibilidadPublicProtectecDefaultPrivate
Desde la misma claseSiSiSiSi
Desde cualquier clase dentro del mismo paqueteSiSiSiNo
Desde una subclase en el mismo paqueteSiSiSiNo
Desde una subclase desde otro paqueteSiSi (sólo por herencia)NoNo
Desde una clase distinta fuera del paqueteSiNoNoNo



Miembros de clase con modificadores de no acceso:


Primero veremos este tipo de modificadores en los métodos después veremos estos mismos pero aplicados a variables de instancia.


Métodos marcados con el modificador final:


Los métodos marcados con este modificador no pueden ser sobre escritos por una subclase, muchos métodos de la API de Java no puedes ser sobre escritos ya que están marcados con el modificador "final", como ejemplo veamos esta clase:

public class SuperClase 
{
 public final void metodo()
 {
  System.out.println("Esto es un metodo");
 }
}


No hay ningún problema en extender una clase, ya que no está marcada con el modificador final, pero un método marcado con este modificador no puede ser sobre escrito.

public class SubClase extends SuperClase
{
 public void metodo()// Tratamos de sobre escribir el metodo final de la super clase
 { 
  System.out.println("Esto es un metodo");
 }
}


Al intentar ejecutar este código obtendremos un error como este:

The method void metodo () declared in class
SubClass cannot override the final method of the same signature declared in class SuperClass.
Final methods cannot be overridden.
public void metodo () { }
1 error


Como dijimos anteriormente: los métodos marcados con el modificador "final" no pueden ser sobre escritos.


Argumentos marcados con el modificador final


Los argumentos de los métodos son los que aparecen entre los dos paréntesis de un método, por ejemplo:

public void metodo(String argumento){}


Y un método con declarado con múltiples argumentos se ve de la siguiente manera:

public void metodo(String argumentoUno, int argumentoDos){}


Los argumentos de un método son esencialmente lo mismo que las variables locales, nosotros podemos marcar a los argumentos de un método con el modificador "final", por ejemplo:

public void metodo(String argumentoUno, final  int argumentoDos){}


En el ejemplo anterior el argumento de nombre "argumentoDos" está marcado con el modificador "final", lo que quiere decir que este valor no puede ser cambiado dentro del método, en otras palabras, el valor no puede ser otro que el valor que se le paso al método al momento de ser llamado.


Métodos abstractos


Un método abstracto, como aclaramos anteriormente, es un método que debe ser declarado pero no implementado y si recordamos lo dicho anteriormente un método abstracto no tiene llaves (que es donde se coloca la lógica del método), simplemente termina en punto y coma.

Declarando un método como abstracto obligamos a las subclases que heredan de la clase abstracta a la cual pertenece a implementar estos métodos. Por ejemplo si tenemos un método llamado "muevete" en la súper clase "Auto", las sub clases que extiendan a esta clase están obligadas a implementar el método "muevete" en ellas.

También debemos recordar que si un método es definido como abstracto la clase a la cual pertenece también tiene que ser abstracta, es decir, el código siguiente es incorrecto

public class SuperClase // La clase también debería ser marcada como abstracta
{ 
 public abstract void metodo(); // metodo abstracto
}


El código anterior nos arroja el siguiente error:
 
SuperClase is not abstract and does not override abstract method metodo.


También debemos recordar que una clase abstracta puede contar con métodos no abstractos (de los normalitos, llamados concretos) y abstractos, por ejemplo el siguiente código es correcto:
 
public abstract class SuperClase 
{ 
 public abstract void metodo(); // metodo abstracto
 
 public void otroMetodo(){ } // metodo no abstracto
}


No olvidemos que los métodos no abstractos deben llevar llaves.

Ahora prestemos atención a una cosa, si tenemos una clase abstracta que a su vez hereda de otra clase abstracta, esta clase abstracta hija no tiene la necesidad de implementar los métodos abstractos de la clase abstracta padre, pero en algún momento tendremos que declarar una clase concreta y esta clase concreta tendrá que implementar TODOS los métodos abstractos en el árbol de herencia que no han sido implementados entre las clases abstractas, mejor veamos esto en un ejemplo:
 
public abstract class Auto 
{
 private String tipo;
 
 public abstract void acelera(); // Metodo abstracto
 
 public String getTipo() // Metodo no abstracto
 { 
  return tipo;
 }
}

public abstract class Carro extends Auto 
{
 public abstract void acelera(); // Sigue abstracto
 
 public void hazAlgo() 
 {
  // logica del metodo
 }
}

public class Toyota  extends Carro 
{
 public void acelera () 
 {
  // logica del metodo
 }
}


La clase "Toyota" hereda los métodos no abstractos "hazAlgo" y "getTipo" del árbol de herencia y tiene la necesidad de implementar el método "acelera" de la súper clase "Auto" ya que este nunca fue implementado en la clase "Carro" por lo que sigue declarada abstracta en esta clase. Si tenemos métodos abstractos que han sido implementados en las subclases abstractas no tenemos la necesidad de implementar estos métodos, por ejemplo:

public abstract class Auto 
{
 private String tipo;
 
 public abstract void acelera(); // Metodo abstracto
 
 public String getTipo() // Metodo no abstracto
 { 
  return tipo;
 }
 
 public abstract void frena();// Metodo abstracto
}

public abstract class Carro extends Auto 
{
 public abstract void acelera(); // Sigue abstracto

 public void hazAlgo() // Metodo no abstracto
 {
  // logica del metodo
 }

 public void frena()//implementando método abstracto
 { 
  // logica del metodo
 }

 public abstract void atropella();//método abstracto
}

public class Toyota extends Carro 
{
 public void acelera() //implementación obligatoria
 { 
  // logica del metodo
 }

 public void atropella()//implementación obligatoria
 { 
  // logica del metodo
 }
 
 public void frena()// implementación opcional
 { 
  // logica del metodo
 }
}


Como podemos observar, la clase abstracta "Auto" cuenta con dos métodos abstractos ("acelera" y "frena"). La clase abstracta "Carro", que extiende de "Auto", cuenta con un método abstracto propio ("atropella"), además podemos observar que implementa un método abstracto de la súper clase abstracta (el método "frena"), por lo cual la clase concreta "Toyota" tiene la obligación de implementar los métodos abstractos que no han sido implementados en el árbol de herencia (el método abstracto "acelera" en la clase "Auto" y el método abstracto "atropella" en la clase "Carro").

Para aclarar otro punto, el método que implementa un método abstracto heredado tiene que ser exactamente el mismo, veamos esto en otro ejemplo:

public abstract class Carro 
{
 public abstract void acelera(); // metodo abstracto
}

public class Toyota extends Carro 
{
 public void acelera(String mensaje) 
 {
  // logica del metodo
 }
}


Aunque parezca que el método "acelera" en la clase "Toyota" está implementando el método abstracto de la súper clase "Carro", esto no es así, lo que está haciendo el método de la clase "Toyota" es sobrecargar el método, lo cual no cumple con las reglas necesarias para que se considere que el método "acelera" en la clase "Toyota" está implementando el método abstracto de la súper clase "Carro".

Para terminar debemos aclarar que un método abstracto no puede ser marcado como "final" o "private" ya que obviamente, como dijimos anteriormente, un método abstracto está destinado a ser implementado por la sub clase que lo herede, y marcarlo como "final" significa que no puede ser sobre escrito y "private" que no puede ser visto por otra clase, lo cual también impide una posible implementación

También debemos aclarar que un método abstracto no puede ser marcado como "static", a continuación presentamos declaraciones no legales de métodos abstractos:

private abstract void acelera();

final abstract void acelera();

abstract static void acelera();


Al ejecutar el código anteriormente escrito nos dará el siguiente error de compilación:

Illegal combination of modifiers: abstract and static



Métodos sincronizados


Un método marcado con el modificador "synchronized" quiere decir que este método solo puede ser accedido por un hilo (Thread) a la vez, solamente los métodos pueden ser marcados como sincronizados, no variables, no clases SOLO MÉTODOS, un método sincronizado se declara de la siguiente manera:

public synchronized void acelera();


También debemos tener en cuenta que un método marcado como sincronizado puede tener cualquiera de los 4 niveles de acceso


Métodos nativos:


Sólo los métodos pueden ser marcados como nativos ("native"), un método nativo termina en punto y coma como los métodos abstractos indicando que la implementación de dicho método es omitida. Los métodos nativos NO son implementados usando código Java, sino que hacen uso de algún otro lenguaje que genera código "nativo" para el dispositivo en el que se ejecutará nuestra aplicación, como C o C++.


Métodos scrictpf


Con "strictfp", se puede predecir cómo se comportarán los puntos flotantes, independientemente de la plataforma subyacente de la JVM que se está ejecutando. La desventaja es que si la plataforma subyacente es capaz de soportar una mayor precisión, un método "strictfp" no será capaz de tomar ventaja de ello.

Bueno, como dijimos anteriormente: una clase puede ser marcada con este modificador ("scrictpf") y esto también funciona con los métodos, no hay necesidad de marcar la clase con "scrictfp" para que declaremos métodos con este modificador, también debemos recordar que una variable nunca debe ser marcada con este modificador.


Métodos con una lista variable de argumentos (var-args)


Desde la versión 5.0 de Java se permite crear métodos que pueden tener una cantidad variable de argumentos con un mecanismo llamado "var-args", esto es muy útil cuando tenemos métodos que reciben como una cantidad variable o grande de parámetros de un tipo especifico, con var-args solo es necesario definir el tipo de nombre y el nombre del parámetros para que esta pueda recibir una cantidad variable de argumentos del tipo declarado, también sigue un conjunto de reglas que se explicaran a continuación:

Primero vamos a aclarar la diferencia entre argumentos y parámetros:

Argumentos: son las cosas que especificamos entre los paréntesis cuando invocamos a un método, por ejemplo:

mensaje("El mensaje es: ",  "Hola mundo");//invocando al //método mensaje, "El mensaje es:" y  "Hola mundo" son argumentos


Parámetros: Las cosas que ponemos en un método al declararlo, que determina qué es lo que el método debe recibir cuando sea invocado, por ejemplo

mensaje(String palabraUno, String palabraDos){}
//especificamos que el método recibirá 2 Strings cuando sea invocado: palabraUno y palabraDos


Ahora vamos a establecer las reglas para poder declarar correctamente un parámetro var–args:
  • Cuando declaremos un parámetro var-args debemos especificar el tipo de argumento(s) que el método debe recibir, este tipo puede ser un tipo primitivo o un objeto.
  • Para declarar correctamente un var-args primero debemos especificar el tipo que este será seguido de tres puntos (...), un espacio y el nombre que llevará esta lista de parámetros en su conjunto.
  • Es legal tener otros parámetros en un método que usa un var-args.
  • El var-args siempre debe ir como parámetro final y solamente se puede declarar un var-args en un método.
  • Al declarar un var-args decimos que podemos pasar cero o más argumentos del tipo declarado al invocar al método.

Ejemplos de declaraciones correctas de var-args:

public void mensaje(String... mensajes){};

public void mensaje2(int numeroMensajes, String... mensajes){};

public void numeroAutos(Auto... autos){};


Ejemplos de declaraciones incorrectas de var-args:
 
public void mensaje(String mensajes...){};

public void mensaje2(String... mensajes, int numeroMensajes){};

public void mensaje3(String... mensajes, int... numeroMensajes){};


Las declaraciones anteriores son incorrectas porque no siguen las reglas especificadas anteriormente como que los tres puntos seguidos (...) van entre el tipo de dato y el nombre que llevará (primer caso), que los var-args siempre van como parámetro final (segundo caso) y que sólo se puede declarar un var-args en un método, uno y sólo uno (tercer caso).

También es posible usar var-args donde normalmente usamos arreglos (ya que se trabajan de la misma forma) siempre y cuando se sigan las reglas indicadas anteriormente. Por ejemplo, normalmente declaramos nuestro método "main" de la siguiente forma:

public static void main(String[] args)
{
  //codigo
}


Usando var-args podemos declararlo de la siguiente forma:

public static void main(String... args)
{
  //codigo
}


La cual también es una declaráción válida.


Declaración de constructores


Cada vez que creamos un nuevo objeto, usando el operador "new", el constructor del objeto es invocado. Todas las clases tienen un constructor, independientemente de que lo declaremos o no. Si no declaramos explícitamente un constructor para una clase, el compilador construirá uno por defecto, veamos un ejemplo:

public class Carro 
{
 public Carro(){} // este es un constructor
 
 public void Carro(){} // este es un método con un nombre no muy adecuado, pero legal
}


Reglas y diferencias de los constructores (con los métodos):
  • La primera diferencia que debemos tener en cuenta entre los constructores y los métodos es que los constructores nunca, nunca retornan un valor.
  • Los constructores pueden ser declarados con cualquiera de los niveles de acceso.
  • Un constructor puede tener argumentos, incluido los var-args.
  • La gran regla de un constructor nos dice que este siempre debe tener el mismo nombre de la clase en la que es declarada.
  • Los constructores no pueden ser declarados como "static", "abstract" o "final".
  • Los constructores no se heredan.

Ejemplo de declaraciones legales e ilegales de constructores:

Public class Auto
{
 //Constructores legales
 Auto(){}

 private Auto(int numero){}

 Auto(int numero){}

 public Auto(int numero, String... tipos){}



 //Constructores ilegales
 void Auto(){}  // esto es un método 

 Auto2(){}   //No es un método ni un constructor

 static Auto(){} //No puede ser static 

 final Auto(int numero){} // No puede ser final

 abstract Auto(int numero){} // No puede ser abstract

 Auto(String... tipos, int numero){} //mala sintaxis var-args 
}



Declaración de variables:


Las variables son valores modificables, es decir, son nombres que representan un valor de cierto tipo y el valor asociado al nombre se puede variar dependiendo de cada caso.

En Java contamos con dos tipos de variables:

Variables primitivas: Una variable primitiva puede ser uno de los siguientes ocho tipos: "int", "char", "boolean", "byte", "short", "double", "float" y "long". Una vez que declaras una variable de tipo primitivo, podemos cambiar el valor, más no el tipo, pero podemos asignarle valores de tipos equivalentes haciendo un cast, por ejemplo a un "int" podemos asignarle un valor "long" haciendo el cast apropiado:

int i = (int)1000L;


Variables de referencia: Es usada para hacer referencia a un objeto, una variable de referencia es declarada para que sea de un determinado tipo y este nunca puede ser cambiado. Puede ser usado para hacer referencia a cualquier objeto del tipo declarado o de un sub tipo del tipo que se declaro con un tipo compatible o equivalente a él, por ejemplo, tenemos una clase "Perro" que extiende de la clase "Animal" y podemos hacer esto.

Animal animal = new Perro();


Declarando primitivos y rango de valores primitivos:


Los valores primitivos pueden ser declarados como variables de clase, parámetros de método, variables de instancia o variables locales.

Podemos declarar más de una variable del mismo tipo en una sola línea, por ejemplo:

int numero;

int numeroUno, numeroDos, numeroTres;


Es importante saber que para los tipos enteros la secuencia de menor a mayor es "byte", "short", "int", "long", y que los "double" son más grandes que los de tipo "float".

Los seis tipos de números en Java se componen de un número determinado de bytes de 8-bits cada uno, y son números reales, lo que significa que puede ser negativo o positivo. El bit más a la izquierda (la cifra más significativa) se utiliza para representar el signo, donde 1 significa negativo y 0 significa positivo, como se muestra en la siguiente figura:



La figura anterior nos muestra que de los 8 bits que se usan para representar un tipo de dato "byte" uno de estos bits se usan para representar el signo, y 7 para representar el valor de la variable. Lo que nos dice que un byte puede tener 27 posibles valores. Esto sumado al signo nos dice que el rango de valores de un byte es de -27 a 27-1 (este último -1 ocurre porque el 0 se considera dentro de los números positivos).

Digamoslo de otra forma para que quede más claro ya que el concepto en si es un poco extraño. El mínimo valor positivo que puede tener un byte es (representado en bits) 00000000, donde el primer 0 representa el signo (en este caso positivo). El máximo valor positivo del byte sería 01111111 donde, nuevamente el primer 0 representa el signo positivo. Por lo tanto tenemos que el rago positivo va desde 00000000 hasta 01111111, o sea (traduciendolo a decimal) desde 0 hasta 127. Si brincaramos al siguiente número binario (10000000) ya estariamos colocando el último bit en 1 lo que lo convertiria en un número negativo (en este caso -128)

De la misma forma. El mínimo valor negativo que puede tener un byte es 10000000, donde el primer 1 representa el signo (en este caso negativo). Y el máximo valor negativo que puede tener es 11111111. O sea, va desde -128 hasta -1.

O sea que un byte va desde -128 hasta 127.¿Cómo es esto posible? Pues esto es posible porque esto trabaja usando complemento a dos.

La misma explicación funciona para el resto de los tipos primitivos numéricos.

En la siguiente tabla mostramos los rangos de los números primitivos existentes en java y su respectivo equivalente en bits, bytes y su rango mínimo y máximo:

TipoBitsBytesRango InferiorRango Superior
byte81-27 (-128)27-1 (127)
short162-215 (-32768)215-1 (32767)
int324-231 (-2147483648)231-1 (2147483647)
long648-263 (-9223372036854775808)263-1 (9223372036854775807)
float324n/an/a
double648n/an/a


El rango de los números de punto flotante es dificil de determinar (debido a que la cantidad de cifras después del punto depende de la plataforma).

Para los tipos booleanos no hay un rango, solo pueden ser true o false. La cantidad de bits que se usan para representar estos valores depende de la implementación de la máquina virtual, pero para efectos del exemen esto no es importante.

El tipo char contiene un solo caracter unicode de 16 bits sin signo, o sea que sus valores van desde 0 hasta 65535 (a diferencia de los short ya que estos últimos usan un bit para el signo).


Variables de instancia:


Las variables de instancia son declaradas dentro de una clase pero fuera de algún método y son inicializadas solo cuando la clase es instanciada. Las variables de instancia son los campos que diferencian a un objeto de otro, por ejemplo veamos los campos (variables de instancia) de nuestra clase "Auto":

public class Auto
{
 // definiendo campos (variables de instancia) para Auto
 private String color;
 
 private String marca;  
}


El código anterior nos dice que cada vez que instanciamos la clase "Auto" este tendrá su propio "color" y su propia "marca", debemos tener en cuenta que "campo", "variable de instancia", "propiedad" o "atributo" significan lo mismo.

A continuación mostramos las reglas que debemos seguir para declarar correctamente estas variables de instancia:
  • Se puede usar cualquiera de los 4 niveles de acceso
  • Puede ser marcado con "final"
  • Puede ser marcado con "transient"
  • No puede ser marcado con "abstract"
  • No puede ser marcado con "synchronized"
  • No puede ser marcado con "strictfp"
  • No puede ser macado con "native"
  • No puede ser marcado con "static" porque de esa manera seria una variable de clase

Hasta el momento hemos aprendido que como declarar y que modificadores de acceso deben tener las clases, los métodos, variables locales y las variables no locales (de instancia) para que sea correcta su declaración, a continuación mostramos una tabla en la que resumimos todo lo aprendido hasta el momento:

CLASESVARIABLES LOCALESVARIABLES NO LOCALESMÉTODOS

public

final

abstract

final

final

static

transient

volatile

protected

private

public


final

static

abstract

native

syncrhronized

strictfp

public

private

protected


A continuación vamos a hablar más detalladamente sobre las variables locales:


Variables locales:


Como dijimos anteriormente las variables locales son las variables que se declaran dentro de un método. El ciclo de vida de estas variables empieza dentro del método y termina cuando el método ha sido completado.

Las variables locales siempre se encuentran en el "stack" (la sección de memoria que mantiene los valores de las variables para métodos particulares), no en el "heap" (la sección de memoria en donde se crean los objetos). Aunque el valor de una variable podría ser pasado a, digamos, otro método que almacene ese valor en una variable de instancia la variable en sí, vive solo dentro del ámbito del método. Solo no olviden que mientras la variable local se encuentra en el stack, si la variable es una referencia a un objeto, el objeto mismo seguirá siendo creado en el heap, no existe una cosa como un objeto en el stack, solo una variable en el stack. Algunas veces escucharemos a programadores usar el término "objeto local" pero lo que en realidad quieren decir es "una variable de referencia declarada localmente" Así que si escuchan a un programador usar esa expresión, sabrán que tan solo es flojo como para decir la frase en una forma técnicamente precisa.

En las declaraciones de las variables locales no se pueden utilizar la mayoría de los modificadores que se pueden aplicar a las variables de instancia, como "public" (o los modificadores de acceso de otro tipo), "transient", "volatile", "abstract", o "static", sino como hemos visto en la tabla mostrada anteriormente, las variables locales pueden ser marcadas con el modificador "final", por ejemplo veamos un ejemplo de variables locales:

public class Ejemplo
{
 public void unMetodo()
 {
  int numero = 5;
 }
}


Normalmente deberíamos inicializar una variable local en la misma línea en que la declaramos, aunque también podríamos inicializarla más adelante en el método. Lo que siempre debemos de recordar es que una variable local debe ser inicializada antes de ser usada. Si no hacemos esto, el compilador rechazara y marcara como error cuando intentemos usar una variable local sin antes haberle asignado un valor, ya que, a diferencia de una variable de instancia, la variable local no tiene un valor por defecto.

Al compilar un método que utiliza una variable con un valor sin inicializar obtendremos el siguiente error que nos dice que la variable en cuestión no ha sido inicializada:

variable nombreVariable  might not have been initialized


Debemos recordar que una variable local no puede ser usada o referenciada fuera del método en el que se declaró, por ejemplo:

public class Ejemplo
{
 public void unMetodo()
 {
  int numero = 5;
 }
 
 public void otroMetodo(int otroNumero)
 {
  numero = otroNumero;//Error, numero no puede ser usado fuera //del método unMetodo()
 }
}


Al compilar el código anterior obtendremos el siguiente error ya que el compilador no conoce a esa variable local en otro lugar que no fuese dentro del método donde fue declarado, el error es el siguiente:

cannot find symbol


Una variable local puede llevar el mismo nombre que una variable de instancia, a esto se le conoce como ensombrecimiento (shadowing), por ejemplo:

public class Ejemplo
{
 int numero = 8;

 public void unMetodo()
 {
  int numero = 5;
  System.out.println(numero);
 }

 public void otroMetodo()
 {
  System.out.println(numero);
 }

 public static void main(String[] args)
 {
  new Ejemplo().unMetodo() //variable local
  new Ejemplo().otroMetodo() //variable de instancia
 } 
}


Si ejecutamos el código anterior la salida correspondiente es:

5
8


Ahora veamos qué ocurriría si hacemos esto:

public class Ejemplo
{
 int numero = 8;
 
 public void unMetodo(int numero)
 {
  numero = numero; // que numero es igual a q numero?
 }
}


Para no tener este tipo de problema cuando declaramos el mismo nombre de la variable a la variable local (que ocurre por ejemplo típicamente en los métodos "setter") debemos usar la palabra reservada "this", también conocido como el apuntador "this" o la referencia "this". La palabra reservada "this" siempre hace referencia al objeto que se está ejecutando y en este caso hace referencia a la variable de instancia, por ejemplo:

public class Ejemplo
{
 int numero = 8;
 
 public void unMetodo(int numero)
 {
  this.numero = numero; // this.numero hace referencia a la //variable de instancia
 }
}



Declaración de arreglos:


En Java, los arreglos son objetos que guardan múltiples variables del mismo tipo, o variables que son todas las subclases del mismo tipo. Los arreglos pueden contener ya sea primitivos o referencias de objetos, un arreglo siempre será un objeto, incluso si el arreglo fuera contenedor de elementos primitivos. En otras palabras, no hay tal cosa como un arreglo primitivo, pero podemos hacer un arreglo de elementos primitivos.

Declarando un arreglo de elementos primitivos:

int[] arreglo;// los corchetes antes del nombre (recomendado)

int arreglo[];// los corchetes después del nombre (legal pero menos legible)


Declarando un arreglo de referencias de objetos:

String []arreglo;
 
String arreglo[];


También podemos declarar arreglos multi-dimensionales, que en realidad son arreglos de arreglos y se declaran de la siguiente manera:

String []arreglo[];

String [][][]arreglo;


Nuca debemos indicar el tamaño del arreglo en la declaración de este, se debe declarar cuando se realiza la instanciación del arreglo con el operador new, por ejemplo:

String []arreglo = new String[10];

//ó

String []arreglo2;
arreglo2 = new String[10];


El siguiente ejemplo nos arrojara un error ya que estamos asignando el tamaño del arreglo en la declaración de este:

String [10]arreglo;


Si intentamos cómpilar un código con un arreglo declarado de la forma anterior obtendremos el siguiente error:

Arreglos.java:5: not a statement
    String [10]arreglo;
           ^
Arreglos.java:5: ';' expected
    String [10]arreglo;
               ^
2 errors



Variables marcadas con el modificador "final"


Cuando declaramos una variable con el modificador final, garantizamos que esta variable no pueda ser reinicializada (cambiar su valor) una vez que su valor ha sido asignado explícitamente (explícitamente, no por defecto). Para los tipos primitivos esto significa que el valor nunca podrá ser cambiado por ejemplo, si asignamos una variable llamada "PI" como double y marcamos con "final" asignándole un valor de "3.1416", este valor nunca podrá ser cambiado. Cuando marcamos una variable de referencia a un objeto con el modificador final, quiere decir que esta variable no podrá ser asignada para referir a otro objeto diferente, los datos del objeto puede ser modificados pero el objeto nunca podrá ser cambiado, a continuación pondremos unos ejemplos que explican cómo se comporta el identificador final en diferentes situaciones:




Variables marcadas con el modificador "transient"


Cuando marcamos una variable con el modificador "transient" le estamos diciendo a la maquina virtual de java (JVM) que ignore esta variable cuando se intenta serializar el objeto que lo contiene. La serialización es una de las mejores características de Java que permite almacenar un objeto, escribiendo su estado (o sea el valor de sus variables de instancia) en un tipo especial de stream de I/O o sea en un flujo de lectura y escritura. Con la serialización, podemos guardar un objeto en un archivo, o enviarlo a través de un cable para deserializarlo al otro lado, en otra JVM.


Variables con el modificador "volatile"

Por el momento solo necesitamos saber que al igual que "transient" solo se aplican a variables de instancia.



Variables y métodos estáticos:

El modificador estático ("static") es usado para crear variables y métodos que existen independientemente de cualquier instancia creada para la clase. Todos los miembros estáticos existen antes de haya una nueva instancia de una clase, y habrá un solo ejemplar de un miembro estático, independientemente del número de instancias de esa clase. En otras palabras, todas las instancias de una clase comparten el mismo valor para cualquier variable dada estática .

Podemos marcar como estático:
  • Las variables de instancia
  • Los métodos
  • Bloques de inicialización
  • Una clase anidada dentro de otra clase, pero no dentro de un método

Declarando Enums:


Desde la versión 5.0 Java nos permite poder restringir una variable a unos cuantos valores predefinidos por nosotros, estos valores se obtienen a partir de una lista enumerada de valores declarados, se logra esto a través de los ENUMS.

Si por ejemplo queremos restringir el tamaño de una persona con solo 3 valores podemos realizarlo de la siguiente manera:

enum EstaturaPersona {ENANO, NORMAL, ALTO} 


Con el código anterior estamos restringiendo que las variables de tipo "EstaturaPersona" sólo pueden tener 3 valores porsibles: "ENANO", "NORMAL", "ALTO".

Si deseamos asignarle un valor a la variable lo hacemos de la siguiente manera:

EstaturaPersona ep = EstaturaPersona.ENANO;


No es necesario que declaremos los valores con mayúsculas, pero si recordamos las convenciones de Sun nos dicen q las constantes debe ser escritas con mayúsculas.

Las enum pueden ser declaradas separadas de la clase, o como un miembro de clase, sin embargo, no debe ser declarada dentro de un método, por ejemplo

enum EstaturaPersona {ENANO, NORMAL, ALTO} // No puede ser protected o private

class Persona
{
 EstaturaPersona estatura;
}

public class PersonaTest
{
 public static void main(String[] args)
 {
  Persona persona = new Persona();
  persona.estatura = EstaturaPersona.ENANO;// enum fuera de clase
 }
}


El punto clave a recordar es que una enum que no está encerrado en una clase se puede declarar sólo con el modificador público y por defecto, al igual que una clase no interna. Ejemplo de declarar una enumeración dentro de una clase:

class Persona
{
 enum EstaturaPersona {ENANO, NORMAL, ALTO} 
 EstaturaPersona estatura;
}

public class PersonaTest
{
 public static void main(String[] args)
 {
  Persona persona = new Persona();
  persona.estatura = Persona.EstaturaPersona.ENANO;// requiere //nombre de clase
 }
}


Debemos recordar que las enumeraciones pueden ser declaradas dentro de su propia clase, o encerrado en otra clase, y que la sintaxis para acceder a los miembros de una enumeración depende del lugar donde fue declarada la enumeración, el siguiente ejemplo es incorrecto:

public class PersonaTest
{
 public static void main(String[] args)
 {
  enum EstaturaPersona {ENANO, NORMAL, ALTO}//No declarar en un método
  
  Persona persona = new Persona();
  persona.estatura = Persona.EstaturaPersona.ENANO;
 }
}


Es importante saber que el punto y coma al final de la declaración de un enum (;) es opcional:

class Persona
{
 enum EstaturaPersona {ENANO, NORMAL, ALTO};//<-- Punto y coma opcional
}

public class PersonaTest
{
 public static void main(String[] args)
 {
  Persona persona = new Persona();
  
  persona.estatura = Persona.EstaturaPersona.ENANO;// requiere //nombre de clase
 }
}


También debemos recordar que un enum no es un String o un int, cada enumeración (valor) de "EstaturaPersona" es una instancia de "EstaturaPersona", es decir "ENANO" es del tipo "EstaturaPersona". Debemos pensar en los enums como un tipo de clase. El enum de ejemplo se puede ver conceptualmente de la siguiente manera:

public class EstaturaPersona
{
 public static final EstaturaPersona ENANO = new EstaturaPersona("ENANO", 0);
 public static final EstaturaPersona NORMAL = new EstaturaPersona("NORMAL", 1);
 public static final EstaturaPersona ALTO = new EstaturaPersona("ALTO", 2);

 public EstaturaPersona(String nombre, int posicion){}
}


Como dijimos anteriormente pensemos que cada valor del enum "EstaturaPersona" es del tipo "EstaturaPersona" y son representadas con "static" y "final" porque así nos dice Java que debemos declarar a las constantes. Además cada enum conoce su posición, es decir, el orden en que fueron declarados.


Declarando constructores, métodos y variables dentro de un enum


Como dijimos anteriormente: un enum es algo así como un tipo especial de clase, y podemos hacer más que sólo declarar una lista de variables. Veremos que podemos declarar constructores, métodos, variables y algo realmente extraño llamado cuerpo de un elemento específico (constant specific class body).

Para entender mejor esto veamos un ejemplo. Pensemos que tenemos una bebida que se vende en tres presentaciones: PERSONAL, MEDIANA y FAMILIAR, y que la bebida personal tiene como contenido 1 litro de bebida, la mediana 3 litro y la familiar 5 litros

Podemos tratar estos valores e implementarlo de muchas maneras, pero lo manera más simple es tratar estos valores (PERSONA, MEDIANA y FAMILIAR) como objetos y que cada uno de estos tengan sus propias variables de instancia. Esto lo logramos asignando estos valores en el momento que inicializamos el enum y pasando un valor al constructor del enum, como mostramos a continuación en el siguiente código:

enum BebidaPresentacion
{
 //1, 3 y 5 son pasados al constructor, de la siguiente manera
 PERSONAL(1), MEDIANA(3), FAMILIAR(5);

 BebidaPresentacion (int litros)// Constructor
 {  
  this.litros = litros;
 }

 private int litros;

 public getLitros()
 {
  return litros;
 }
}

class Bebida
{
 BebidaPresentacion presentacion;

 public static void main(String[] args)
 {
  Bebida bebida1 = new Bebida();
  bebida1.presentacion = BebidaPresentacion.PERSONAL;

  Bebida bebida2 = new Bebida();
  Bebida2.presentacion = BebidaPresentacion.FAMILIAR;
  
  System.out.println(bebida1.presentacion.getLitros()); //1
  System.out.println("********************");
  
  for(BebidaPresentacion present : BebidaPresentacion.values())
  {
   System.out.println(present + " " + present.getLitros()); 
  }
 }
}


El código anterior imprimirá como salida:

1
********************
PERSONAL 1
MEDIANA 3
FAMILIAR 5


Como todo lo que hemos visto hasta ahora, los enums también tienen unas reglas que debemos seguir para su correcto uso:
  • NUNCA debemos invocar al constructor del enum directamente. El constructor del enum se invoca automáticamente, con los argumentos que se definen después del valor constante. Por ejemplo, PERSONAL(1) invoca al constructor de "BebidaPresentacion" que toma un "int", pasando el "int" "1" al constructor.
  • Podemos definir más de un argumento al constructor, y podemos sobrecargar los constructores de un enum, tal como se puede sobrecargar un constructor de una clase normal. Para inicializar una "BebidaPresentacion" tanto con la cantidad de litros como el color del envase, se pasan dos argumentos al constructor como PERSONAL(1, "Azul"), lo que significa que tiene un constructor que recibe tanto un int y un String.

Aquí termina el primer post relacionado a conceptos básicos de Java, hemos aprendido sobre cómo declarar correctamente los identificadores, las convenciones JavaBeans, denominaciones estándar, cómo declarar correctamente clases, métodos y variables. Hemos visto como se declaran las interfaces, métodos y clases abstractos, los modificadores de acceso y los de no acceso, los var-args, arreglos y finalmente los enums, en el siguiente post hablaremos sobre conceptos de Orientación a Objetos en Java.

Si tienen alguna duda comentario o sugerencia no duden en colocarlo en la sección correspondiente y con todo gusto Alan o yo contestaremos lo más pronto posible.

Saludos y gracias a Alan.

Entradas Relacionadas: