30 de octubre de 2018

Objetos - método equals vs operador "=="

En esta entrada vamos a construir varios objetos y vamos a comprobar si son iguales o el mismo.

Lo primero es que hay que distinguir qué queremos decir con "iguales" y qué con "el mismo" (que son dos cosas distintas):
  • Ser igual (comparación): es verdadero si los aspectos que considero relevantes son el iguales. Digamos que tengo la clase Persona y esta clase tiene muchos datos personales, sin embargo, en este ejemplo, para decidir si dos personas son la misma sólo me interesa comparar el número de su tarjeta de identidad. Aunque nos parezca raro, si esa es nuestra definición (en nuestro método equals), dos objetos personas con distinto nombre, fecha de nacimiento o color de pelo serían tratadas como iguales si coinciden en ese número.
  • Ser el mismo (identidad): en la vida real es fácil: un objeto sólo es él mismo. Pensemos que en programación trabajamos el valor de un objeto con variables que apuntan a distintos o al mismo objeto. Así pues la comparación se hará entre dos variables y se dira que es verdadero si apuntan al mismo objeto utilizando el operador de igualdad "==".
En definitiva, no es lo mismo coger dos billetes iguales de la cartera que intentar coger el mismo billete dos veces.

No es lo mismo ser igual a otro (equals), que ser él mismo (==)


Como ya hemos adelantado estas preguntas (igual o el mismo) las resolvemos con dos herramientas: el operador de igualdad "==" y el método equals.

Operador de igualdad

El operador de igualdad "==" devuelve true cuando sus dos operandos apuntan al mismo objeto.

Método equals

El método equals(Object obj) debe tener una lógica implementada. Al invocarlo nos devolverá el resultado (true o false) de la comparación implementada entre nuestro objeto y el que recibe como argumento.

Si nuestro objeto fuera null la invocación fallaría. Para defendernos de este caso podemos usar una invocación estática Objects.equals(Object a, Object b).

Este método no sólo es importante para las llamadas que hagamos nosotros sino que otras clases que usemos en el futuro como colecciones o mapas entre otras, lo van a usar para su funcionamiento con lo que implementarlas correctamente es una parte fundamental para sacarlas rendimiento.



Demostración

Siguiendo con nuestro ejemplo de coches vamos a añadirle un atributo matrícula. El método equals queremos que nos devuelva true cuando la matrícula y el modelo sea el mismo, asumiendo que al coche le puede faltar una rueda o le pueden haber pintado, pero sería el mismo e igual coche. También sería un coche igual cualquier otro coche del mismo modelo al que le pusiéramos la misma matrícula (pero éste ya sería otro coche, no el mismo, sólo igual). El código completo hasta ahora quedaría así:
public class Coche {
    private String modelo;
    private String color;
    private int numeroDeRuedas;
    private String matricula;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    void setMatricula(String matricula) {
        this.matricula = matricula;
    }
    
    public Coche() {
        numeroDeRuedas = 4;
    }
    
    public Coche(String color) {
        this();
        setColor(color); // como ya tengo el setter lo utilizo
    }
    
    public Coche(String modelo, String color) {
        this(color);
        this.modelo = modelo;
    }

    @Override
    public String toString() {
        return "Placa " + matricula + " - " + modelo + " (" + getColor() + "), " + numeroDeRuedas + " ruedas";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((matricula == null) ? 0 : matricula.hashCode());
        result = prime * result + ((modelo == null) ? 0 : modelo.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Coche other = (Coche) obj;
        if (matricula == null) {
            if (other.matricula != null)
                return false;
        } else if (!matricula.equals(other.matricula))
            return false;
        if (modelo == null) {
            if (other.modelo != null)
                return false;
        } else if (!modelo.equals(other.modelo))
            return false;
        return true;
    }
    
}
Alguno puede sorprenderse al ver la implementación de los métodos equals y hashCode. Ahora no os pido que sepáis implementarlos, pero cuando se implementa uno formalmente implica que hay que implementar ambos (porque están muy unidos). Vamos a usar Eclipse para ayudarnos como de costumbre usando Source > Generate hashCode() and equals().... Simplemente elegimos en el asistente los campos que quiera tener en cuenta para comparar y le damos a Generar.

Vamos a hacer una prueba con dos coches y ver qué resultado nos dan. Este es el método main que usaremos:
Coche coche1 = new Coche("Seat Ibiza", "Rojo");
coche1.setMatricula("1234 BBB");
Coche coche2 = new Coche("Seat Ibiza", "Rojo");
coche2.setMatricula("1234 BBB");
Coche coche3 = coche1;

// Igualdad
System.out.println("coche1 y coche2 son iguales: " + coche1.equals(coche2));
System.out.println("Pinto coche2 de Negro");
coche2.setColor("Negro");
System.out.println("Los coches siguen siendo iguales: " + coche1.equals(coche2));
System.out.println("Cambio la matrícula a coche2");
coche2.setMatricula("5555 CCC");
System.out.println("Los coches siguen siendo iguales: " + coche1.equals(coche2));

//Identidad
System.out.println("coche1 y coche2 son el mismo: " + (coche1 == coche2));
System.out.println("coche1 y coche3 son el mismo: " + (coche3 == coche1));
La salida es:

coche1 y coche2 son iguales: true
Pinto coche2 de Negro
Los coches siguen siendo iguales: true
Cambio la matrícula a coche2
Los coches siguen siendo iguales: false
coche1 y coche2 son el mismo: false
coche1 y coche3 son el mismo: true

Con esto ya hemos visto que hay campos importantes para decir si dos objetos son iguales y otros que no importan. También hemos visto que dos objetos distintos pueden darnos el resultado de ser iguales si cumplen nuestra definición de igualdad.

Podría parecer que el mismo objeto siempre va a ser igual a sí mismo, pero estaríamos olvidando que con equals todo depende de nuestra implementación. Si hiciéramos un método equals (Object o) { return false; } ningún objeto que pasáramos podría evaluarse como igual. Con el operador "==" todo depende de dónde apunten los operandos.

26 de octubre de 2018

Objetos - Constructores

Para poder utilizar objetos lo primero que necesitamos es un constructor que nos construya un objeto de la clase donde tenemos el constructor. Esto se llama instanciar un objeto.

Los constructores se definen de manera similar a un método, pero su identificador coincide con el nombre de la clases y no tienen tipo de retorno. Se le pueden aplicar igualmente modificadores de acceso.

Para crear objetos necesitamos un constructor. El identificador del constructor coincide con el nombre de la clase


El constructor por defecto

Vamos a seguir con nuestra clase Coche para ver ejemplos. Éste es nuestro código actual:
  public class Coche {
    String modelo;
    String color;
    int numeroDeRuedas;

    @Override
    public String toString() {
        return modelo + " (" + color + "), " + numeroDeRuedas;
    }

  }
Como hemos dicho hace falta un constructor para instanciar un objeto pero no hemos declarado ninguno. A falta de la declaración de un constructor, Java nos proporcionará un constructor por defecto sin parámetros. Lo que hará será crear un objeto con los valores a los que hayamos inicializado sus variables de instancia. En este código no tenemos inicializada ninguna variable con lo que las variables modelo y color al ser del tipo String tendrán el valor null y numeroDeRuedas tendrá valor cero.

En Java instanciamos un objeto llamando al constructor mediante la palabra reservada new seguido del método constructor que queramos usar (en este caso el constructor por defecto Coche()):
  Coche miCoche = new Coche();
  System.out.println(miCoche);
La expresión con el new devuelve un nuevo objeto del tipo Coche. y al imprimirlo en consola nos sale:

Coche [modelo=null, color=null, numeroDeRuedas=0]

Asignación a un tipo de variable "compatible"

Al declarar de qué tipo es la variable se puede acceder a los miembros que forman parte de ese tipo de objetos a los que se tenga acceso. Cuando asignamos un valor a la variable vamos a apuntarla hacia el espacio de memoria que contiene al objeto creado, así podremos utilizar nuestro nuevo objeto del tipo Coche.

Recuerdo que es importante diferenciar el concepto de variable del concepto de objeto:
  1. Una variable puede usarse para referenciar a múltiples objetos compatibles, pero sólo a uno a la vez
  2. Las variables NO tienen una copia del objeto asignado ni ocupan espacio en la memoria como crear un nuevo objeto.
  3. Los objetos ocuparán el mismo espacio en memoria independientemente de las referencias que le apunten.
  4. Asignar de nuevo una variable no modifica el objeto que contenía antes, pero modificar un objeto hará que se observe ese cambio en todas las referencias al mismo objeto (ya lo vimos con el ejemplo de arrays).
  5. El tipo de la variable indica que el objeto deber "pertenecer" a ese tipo (en la línea jerárquica), aunque no quiere decir que sea una instancia de esa clase (podría ser de una clase que hereda o implementar esa interfaz entre otras). La línea jerárquica/herencia y la implementación de interfaces se verá en su sesión correspondiente.
  6. Si el objeto pertenece a una subclase/interfaz con más miembros, la variable nos dará acceso a los miembros del tipo declarado, independientemente del tipo real del objeto al que apunta.
  7. La variable puede apuntar a null lo que produciría un error en ejecución del tipo NullPointerException si intentamos acceder a métodos de instancia.
  8. Un objeto puede tener varias o ninguna referencia (en ese caso será destruido por el Recolector de Basura para liberar recursos porque ya no se puede volver a usar)
No se pretende entender todos los puntos anteriores porque se verán en sesiones más adelante. Lo importante es saber que "UNA VARIABLE NO ES UN OBJETO".

Una variable no es un objeto


Definición de constructores

Todos esperamos que un coche tenga cuatro ruedas. Vamos a declarar el constructor por defecto para que asigne este valor:
  public Coche () {
      numeroDeRuedas = 4;
  }
Este constructor ya es más correcto y ahora la salida del último código cambiará a:

Coche [modelo=null, color=null, numeroDeRuedas=4]

Siguiendo con el ejemplo, ahora que tenemos un objeto del tipo Coche, deberíamos tener métodos para manejar sus atributos. Por ejemplo vamos a crear métodos para leer el valor del color del coche y para establecerle uno nuevo. Éstos métodos los introdujimos en la sesión anterior sobre clases (getter y setter):
  public String getColor() {
      return color;
  }

  public void setColor(String color) {
      this.color = color;
  }
Hasta que yo no le asigne un valor al color del coche el valor será null. Imaginemos que quisiéramos establecer un color determinado en el momento en que instanciamos nuestro coche. Con el constructor por defecto no podemos porque no se puede usar para asignar ningún color. Vamos a hacer una sobrecarga del constructor para tener uno que admita el color:
  public Coche (String color) {
      numeroDeRuedas = 4;
      setColor(color); // como ya tengo el setter lo utilizo
  }
Observamos que se repite la inicialización numeroDeRuedas = 4; si queremos que tome este valor al instanciarlo. Esto es un problema, si en un futuro los coches no necesitaran ruedas tendríamos que cambiar nuestros constructores (hay que huir de cambiar la misma línea en varios sitios).

Para ello tenemos dos soluciones:
  1. Inicializar el valor en la declaración de variable. Para un número no hay problemas, pero para asignar otro valor que pueda dar error en su instanciación (un objeto) tenemos que protegernos de la posible excepción que rompería nuestro código. Este último problema lo solucionaremos en la sesión de inicializadores estáticos.
  2. Llamar al código que ya tenía.
Hay partidarios de las dos formas. Personalmente soy más de la segunda si no se usan inicializadores estáticos. Como todavía no los hemos visto vamos a usar la segunda:
  public Coche (String color) {
      this();
      setColor(color);
  }
Como vemos estamos llamando al constructor por defecto con this() (aunque si tuviéramos más podríamos llamarlos igualmente simplemente proporcionando los parámetros que coincidieran con otro constructor) en nuestra primera línea, y así ya se asigna el valor 4 a las ruedas, y sólo queda añadir el código particular de este nuevo constructor. La única regla a esta forma es que la llamada al constructor debe ser la primera sentencia de todas.

Te propongo como ejercicio que añadas otro constructor que también admita el modelo y cuando lo tengas acabado lo compruebes con la solución.



  public Coche (String color, String modelo) {
      this(color);
      this.modelo = modelo;
  }

25 de octubre de 2018

Objetos - método toString()

Antes de empezar a trabajar con objetos voy a explicar su método toString(). Sus características son:
  1. Todos los objetos disponen de uno.
  2. Es un miembro de la clase Object (el tipo más básico del que heredan todas las clases, lo veremos en la sesión de herencia)
  3. Si no tienen definido uno propio, se utilizará el método toString() heredado del pariente más cercano, hasta llegar como máximo el método toString() de la clase Object (el pariente más lejano)
  4. Siempre devuelve una String y no toma parámetros.
  5. Es utilizado en general para tener una representación en texto de un objeto (es lo que utiliza nuestro amigo System.out.print, pero también lo utilizarán por ejemplo elementos de interfaz de usuario para mostrarlo en una tabla o un cuadro combinado por ejemplo). Esto al final será una representación que tendrá significado fácil de entender para una persona.

Por ello y porque ahora mismo nuestra forma de conocer "qué está pasando" es imprimir por consola, vamos a utilizar el método toString() desde este momento para facilitarnos las comprobaciones del resto de sesiones.

El método toString() se usará para tener un texto fácil de leer y representativo de un objeto


Para los ejemplos vamos a usar la clase Coche con el siguiente código:
  public class Coche {
      String modelo;
      String color;
      int numeroDeRuedas;
  }
En este fragmento de código no tenemos mas miembros que las variables de instancia sin inicializar.

Si imprimimos un nuevo objeto del tipo Coche con System.out.println(new Coche()); tenemos la siguiente salida:

Coche@52e922

Aunque no hemos llamado directamente al método toString(), Java usará la sobrecarga del método println(Object x) para tratarlo como un Object y llamarlo por nosotros.

Como vemos esto no es para nada un significado fácil de entender para una persona, con lo que se recomienda cambiar el método toString() para cada clase que hagamos.


Por ahora vamos a usar Eclipse para ayudarnos a crear nuestro método toString(). Pinchamos botón derecho en la línea donde queramos que nos añada el método (yo normalmente estos métodos tan básicos que sobrescribo los pongo al final de todo) y elegimos Source > Generate toString().... Nos sale un asistente donde podemos elegir qué queremos incluir. Por ahora lo dejamos como está con todos los campos marcados y pinchamos en Generate. Nos incluye el siguiente código:
  @Override
  public String toString() {
      return "Coche [modelo=" + modelo + ", color=" + color + ", numeroDeRuedas=" + numeroDeRuedas + "]";
  }
Vemos que es bastante genérico. El formato es poner el nombre de la clase y entre corchetes ir concatenando los atributos que hemos seleccionado en la pantalla del asistente junto con su valor.

Ahora si hacemos una impresión por consola con System.out.println(new Coche()); me produce la siguiente salida:

Coche [modelo=null, color=null, numeroDeRuedas=0]

Este resultado por muy simple que sea ya tiene un significado fácil de leer. Lo ideal sería ponerlo en un formato más práctico para nosotros y no tan esquemático. Os propongo que cambiéis el método para ponerlo en un formato más bonito para vosotros (es sólo crear un String). Cuando lo tengáis podéis ver mi solución propuesta:



  @Override
  public String toString() {
      return modelo + " (" + color + "), " + numeroDeRuedas + " ruedas";
  }
En la siguiente sesión vamos a construir objetos del tipo Coche con constructores personalizados.

24 de octubre de 2018

Clases

Ya hemos dicho que cada vez que nos hemos creado un archivo con File > New > Class estábamos creando una clase sin darnos cuenta. Sin embargo tenían muy poca funcionalidad, no hemos declarado nunca atributos y todos nuestros métodos los hemos marcado con el modificador static.

Las Clases van a servir para fabricar objetos de su tipo. Con ellas vamos poder encapsular el estado actual (atributos) y el comportamiento (métodos) de los objetos que vayamos creando (instanciando). Este encapsulamiento va a proteger a los miembros que lo necesiten de accesos no permitidos y va a ofrecernos la funcionalidad que corresponda con la responsabilidad de cada objeto. Así conseguimos tener valores agrupados que tienen relación entre ellos con una protección adecuada y un comportamiento que nos dará un valor añadido.

Con las Clases conseguimos tener valores agrupados que tienen relación entre ellos con un comportamiento y una protección adecuada que nos proporcionará un valor añadido


Si creamos una nueva clase con nombre MiClase y no marcamos la casilla para crear el método main, tendremos un archivo con este código:
  public class MiClase {

  }
Vamos a ver que significan los modificadores:
  • public: Significa que vamos a tener visibilidad (acceso) a esta clase desde otras partes del código (desde otros ámbitos/scope). Esto es importante pues al no haber creado un método main, no se ejecutará nada a menos que haya alguna llamada hasta esta clase desde un método main. Si no tuviera este modificador sólo sería accesible desde su propio package.
  • class: Define que es un tipo por referencia Clase. Recordad que hay más tipos por referencia, si pusiera interface en vez de class estaría declaran un tipo Interfaz.
Ahora vamos a definir algunos miembros (campos/atributos y métodos) de MiClase. Éste código aviso que va a ser un poco absurdo, pero es para ver conceptos. En la próxima sesión sobre Objetos ya usaremos un código de verdad funcional y con sentido. El código a usar es el siguiente:
  public class MiClase {

      int miNumero;
      private boolean miBoolean = true;
      protected String string1 = "Primer";
      public String string2; // mala practica
      protected static String string3;
    
      public int getMiNumero() {
          return miNumero;
      }
    
      public boolean isMiBoolean() {
          return miBoolean;
      }
    
      public String getString1() {
          return string1;
      }
    
      private String getString2() { // absurdo
          return string2;
      }

      void setString2(String string2) {
          this.string2 = string2;
      }

      public static String getString3() {
          return string3;
      }

      public static void setString3(String string3) {
          MiClase.string3 = string3;
      }
     
  }
Vamos a detallar qué estamos definiendo en el código:
  1. Atributos (Fields): Son declaraciones de variable al nivel raíz del cuerpo de la clase. Podemos hacer distinción entre variables de instancia y de clase:
    1. Variables de instancia: Atributos que tendrá cada objeto (instancia) que creemos de esta clase.
    2. Variable de clase: Son variables que no necesitan de ninguna instancia pues pertenecen a la clase en sí misma y es un valor único. Para declarar este tipo de variables se usa el modificador static (lo vemos en la declaración de string3)
  2. Hay nuevos modificadores que nos permiten proteger a los miembros estableciendo un control de acceso:
    1. private: Sólo se tiene acceso desde la propia clase.
    2. protected: Sólo se tiene acceso desde los subtipos y su paquete (package), además del acceso private.
    3. public: Igual que cuando la clase, vamos a tener acceso desde cualquier ámbito.
    4. (no modifier): Si nos fijamos en la declaración de miNumero o de setString2, vemos que no tienen ningún modificador. Si no se pone ninguno se entiende que es privado para el package (package-private) además de la visibilidad private. Todos estos modificadores pueden verse en la documentación de Java.



  3. Hay variables que están inicializadas porque tienen asignado un valor como miBoolean y string1. Estos valores son valores por defecto y cada objeto se hará una copia de ellos. Así, los otros Strings tendrán el valor null que es el valor por defecto para los tipos por referencia, pero string1 tendrá el valor "Primer".
  4. Hemos definido métodos al nivel raíz de la clase pero ahora sin el modificador static como siempre habíamos puesto hasta ahora (pues todos eran métodos de clase). Estos métodos son métodos de instancia (necesitan de un objeto) y se les conoce como miembros igual que a los atributos
  5. Los métodos para acceder a los atributos comienzan por get. En general se llaman "getters" y por ahora sólo devuelven el valor que corresponde.
  6. El getter de la variable miBoolean empieza por "is" en vez de "get". Para los métodos que devuelven booleanos ya dijimos que es habitual preguntar es/estaAlgo()en inglés como no hay diferencia se verá habitualmente "isSomething"
  7. Los métodos para asignar valores a las variables se llaman "setters", empiezan por set y reciben el valor para asignar.
  8. En el método setString2(String string2) usamos la palabra reservada this. Es necesario usarla debido a que en el ámbito/scope de éste método el parámetro string2 está ocultando el atributo string2. Con this nos referimos al objeto en sí mismo y usando el punto (.) accedemos a sus miembros, en este caso a su atributo string2. De esta forma podemos asignar al atributo string2 el valor que le pasemos como argumento aunque el parámetro y el atributo tengan el mismo identificador.
  9. Sin embargo a los miembros de clase (static) se accede desde el nombre de la clase en vez de usar el nombre de la variable como se puede ver en el método setString3.
Existen publicaciones que consideran que los getters y setters (denominados más recientemente como accesors) son el mal y tratan a los objetos como meros recipientes de datos alejándonos de la POO. Desde mi punto de vista es pura semántica, simplemente cómo llamar a las cosas no te hará cambiar tu forma de pensar, y lo que recomiendo es que no se creen si no son necesarios y seguros. Ya iremos viendo como decidir esto último. También recomiendo siempre proteger las variables al máximo (en general private) y una vez creados los getters y setters usarlos siempre, incluso dentro de la propia clase (si quiero incorporar lógica en esas operaciones sólo debo cambiar el método que corresponda)
En el vídeo puedes ver la demostración en vivo de todo esto y lo siguiente, cómo usarlo en un main en otro archivo:


¿Si no hay "main" cómo usamos MiClase?

Evidentemente tendrá que haber un método main que la llame desde fuera del archivo MiClase.java. Vamos a crearnos otra clase llamada Ejecucion con un main y el siguiente código:
  public class Ejecucion {

      public static void main(String[] args) {
        
          MiClase miObjeto = new MiClase(); // creo una instancia
        
          System.out.println("miNumero es " + miObjeto.miNumero);
//          System.out.println("miBoolean es " + miObjeto.miBoolean); // sin acceso
          System.out.println("miBoolean es " + miObjeto.isMiBoolean());
        
          System.out.println("string1 es " + miObjeto.string1);
          System.out.println("string1 tiene longitud " + miObjeto.string1.length());
        
//          System.out.println("string2 es " + miObjeto.getString2(); // absurdo
          System.out.println("string2 es " + miObjeto.string2);
        
          MiClase.setString3("string-3");
//          System.out.println("string3 es " + miObjeto.getString3()); // warning
          System.out.println("string3 es " + MiClase.getString3());
      }

  }
Aquí vemos que hay miembros que podemos ver y otros que no. Esto es controlado por el acceso que demos al código con los modificadores antes explicados. Para acceder a los miembros uso la notación por punto (.), así si quiero acceder al atributo miNumero del objeto referenciado por mi variable miObjeto usaré miObjeto.miNumero. Para los métodos es igual solo que añadiré a la invocación los paréntesis para cumplir con la firma que corresponda. En el vídeo hago unos cuantos cambios para verlo en profundidad.

Accedemos a los miembros de un objeto con el punto (.) limitados por el control de acceso que marquemos (public, protected, ...)


Aunque hemos construido un objeto del tipo MiClase todavía no hemos visto cómo personalizar su construcción. A falta de un constructor específico en MiClase para crear objetos, Java siempre tiene un constructor por defecto que creará una instancia y asignará los valores por defecto que encuentre.

En la siguiente sesión trabajamos con varios objetos y diferentes constructores.

16 de octubre de 2018

Calificación de Tipos (primitivos y por referencia)

Según la especificación de Java hay dos tipos: tipos primitivos y tipos por referencia.

Tipos primitivos

Están predefinidos por el lenguaje y nombrados con palabras reservadas. Son los que hemos usado mayoritariamente para las variables que hemos usado: int, float, double, boolean, etc...

Tipos por referencia

Hay cuatro tipos por referencia:
  1. Clases: por ahora hemos trabajado con alguna como String o System, aunque formalmente también hemos creado una nueva cada vez que creabamos un nuevo archivo con File > New > Class. Se nombran con formato UpperCamelCase y son las que vamos a ver en la siguiente sesión. Sus características a grandes rasgos son que pueden tener atributos, servir de "plantilla" para crear objetos y como máximo se puede heredar de una. Ya veremos que significa la herencia en su sesión.
  2. Interfaces: Por ahora no las hemos visto pero empezaremos a usarlas en la sesión de colecciones ya que Collection es una interfaz. También se nombran con formato UpperCamelCase. Sus características principales son que no tienen atributos, no sirven para crear objetos, pero se pueden implementar varias para asegurar un comportamiento (tienen que responder a los métodos que define la interfaz) y proporcionar mayor flexibilidad. Si esto último no se entiende no pasa nada, lo veremos en la sesión de Interfaces.
  3. Tipos variables: Tampoco se han visto y su uso implica operadores que tampoco hemos visto (<?>). Empezaremos a usarlos en la sesión de genéricos. Básicamente nos van a servir para poder escribir código haciendo referencia a un tipo sin definir hasta tiempo de ejecución.
  4. Arrays: Este tipo acabamos de verlo en su sesión correspondiente.


Teniendo claro esta clasificación vamos a empezar a definir qué es una clase.

Fundamentos de la POO

Hasta ahora básicamente nos hemos limitado a trabajar con datos de muy bajo nivel. Formalmente los únicos objetos con los que hemos tratado han sido los Arrays, las cadenas de texto String y el objeto System.out (que seguramente parece más complicado que los anteriores). En definitiva nos hemos limitado a saber cómo escribir lo básico que podemos encontrar en cualquier lenguaje de programación (variables, operadores, condiciones y bucles).

La orientación a objetos nos va a permitir pensar en soluciones a problemas utilizando objetos tal cual haríamos en el mundo real


Con los Arrays hemos visto la ventaja de contener varios valores en un único valor que los agrupe, pero no deja de ser algo parecido a una lista indexada sin ninguna semántica que nos ayude. Entonces ¿Cómo puedo unir valores relacionados con una misma entidad en un único valor que corresponda con la entidad al completo? La respuesta corta es con los objetos.

NOTA: En esta entrada quiero pintar una visión de conjunto de la nueva parte del curso que empezamos y que se centrará en los objetos. A lo largo de ella vamos a encontrar conceptos desconocidos que no nos deben abrumar, sólo están recogidos aquí para que según vayamos avanzando vayan siendo aclarados, pero que desde hoy podamos apreciar la mentalidad con la que se van a ir descubriendo.

Como desarrolladores contamos con numerosas formas de acercarnos a la solución de un problema. Para evitar el síndrome del papel en blanco, podemos tomar ejemplos de casos de éxito. Estos ejemplos se conocen como paradigmas de programación.

Actualmente el más popular de ellos es la "orientación a objetos" y es una forma muy natural de resolverlo, ya que en la vida real resolvemos problemas con objetos del mundo real y por muy complejo que sea un problema podemos ir uniendo pequeñas soluciones con sus propios objetos que nos ayudan.

Así pues, la Programación Orientada Objetos (POO) se basa en la creación de objetos, su manejo y el "intercambio de mensajes" entre ellos y asume que una solución puede ser resuelta por una serie de objetos que nos ayudarán con distintos problemas relacionados al igual que se podría hacer en la vida real: Imaginemos que necesito una mesa y usaré piezas de madera, una sierra, metro, lápiz, clavos, cola, martillo, etc... cada uno de esos objetos me ayudará en una parte de la construcción de la mesa:
  1. Con el metro y el lápiz marcaré la madera para delimitar las partes que formarán la mesa.
  2. Con la sierra podré recortarlas para tenerlas por separado
  3. Con los clavos y la cola uniré esas partes
  4. Necesitaré el martillo para cuando tenga que usar los clavos
Con esto ya tendría una mesa, pero seguro que en la versión final añadiré lija y puede que tiña la madera y la barnice para que tenga un mejor acabado (esto podríamos llamarlo como una segunda versión/iteración de nuestra primera solución porque incorpora mejoras)



De aquí tenemos ya presentes conceptos de la POO como por ejemplo:
  1. Madera, Sierra, etc... son Clases y podemos hacernos una idea sobre ellas sólo por su "NombreDeClase" (escrito así porque en Java los nombres de clases se escriben con la sintaxis UpperCamelCase)
  2. Sin embargo no todas las maderas o sierras son iguales:
    1. Hay maderas más duras que otras, su color también es distinto, pueden venir tratadas, hechas tableros y tendrán unas dimensiones. Esos valores que distinguen una pieza de madera de otra se llaman atributos y definen el estado de un objeto:
      1. Algunos de estos pueden ser valores heredados (por las propiedades del árbol del que salió la madera)
      2. Otros serán únicos de cada trozo de madera (sus dimensiones o esa peculiar veta que se ve en un trozo)
      3. Incluso tendrán ciertas limitaciones (una pieza recortada no puede ser más grande que el trozo del que proviene)
    2. Por supuesto que puedo tener dos tableros de madera iguales (equals), con idénticos atributos, pero son dos objetos distintos de la misma clase Madera y no son el mismo (!=).
    3. También hay distintas sierras (de mano, circular, etc...) que tendrán sus atributos, que si compro dos iguales tendrán esos atributos iguales pero serán dos objetos distintos de la clase misma clase Sierra igual que en el ejemplo de la clase Madera. Pero lo importante a ver aquí es que todas las sierran tendrán el método "cortar" que realizará una acción que nos convertirá el trozo de madera inicial (que será destruido) en dos distintos con los límites que marcamos con el lápiz, aunque cada sierra lo haga de una forma distinta (polimorfismo)
Por otra parte, hay algunos objetos que se consumen (madera, lápiz, clavos y cola), pero hay otros que me pueden servir para hacer otros muebles (sierra, metro y martillo). De aquí se puede sacar la idea de reutilizar objetos para otras soluciones y que incluso esas soluciones pueden tener relaciones con otra y ser también reutilizadas:
  1. Si en el futuro tenemos que hacer una silla, ya tenemos una serie de clases que sabemos que nos ayudarán a fabricarla
  2. También podremos reutilizar partes de la clase "Mesa" puesto que al ser un mueble comparte atributos (partes que la componen, dimensiones totales, color, etc...) que también estarán presentes en la clase "Silla" y que podemos abstraer estas partes comunes en la clase "Mueble" que nos puede servir en un futuro también para otras soluciones.
  3. Que si un tercero nos proporciona un módulo Tapicero, podremos tapizar nuestros muebles si implementan la interfaz necesaria para ser tapizados.
Nos va a resultar más eficiente contar con objetos que solucionen las partes más pequeñas en que dividimos el problema y que podremos reutilizar, o usar los de terceros


En definitiva, la POO nos va a permitir crear una arquitectura formada por componentes que nos ayudará a ser:
  • más productivos mediante la reutilización de código que será también más fácil de mantener por su encapsulamiento,
  • más fiables al tener esos componentes probados y
  • más flexibles para incorporar nuevas funcionalidades al aprovechar librerías de terceros cuya interfaz nos permita explotar las capacidades de software ajeno.

Con estas ideas en mente (no importan que todavía no estén claras) y las posibilidades que nos ofrecen vamos a empezar a ver todos estos conceptos en las siguientes entradas.

6 de octubre de 2018

Arrays

Hasta ahora hemos estado tratando valores de manera independiente, pero es normal utilizar valores agrupados que son todos del mismo tipo: una colección de números, de colores, de palabras, etc...

La forma más primitiva de tratar con varios valores agrupados en un sólo valor son los arrays. La forma de declarar una variable tipo array es:
  T[] identificador; // ejemplo int[] misNumeros;
La diferencia son los corchetes, el tipo puede ser cualquiera, igual que el número de elementos a aceptar, aunque un array tiene un número de posiciones fijas que se indican a la hora de crear el array (no la variable).

Los arrays nos permiten trabajar con varios valores juntos de un mismo tipo, aunque el número de éstos es fijo


Entonces ¿Cómo le digo cuántos elementos debe contener?

Para eso vamos a tener que crear un objeto de tipo array. Para crear objetos se usa la palabra reservada new, que va a apartar memoria suficiente para contener el tipo y número de elementos (entre corchetes) que definamos. Un array se crea y asigna de esta forma:
  int[] misNumeros;
  misNumeros = new int[2];
Así tendremos en la variable misNumeros una referencia al nuevo objeto que hemos creado que será capaz de apuntar a dos valores int. También se podría declarar un tamaño cero y se conoce como un array vacío.

Si más tarde necesitáramos ampliarlo a tres, tendremos que crear otro nuevo, pues ya el array anterior no nos serviría porque su tamaño fijo es dos.

El tamaño no tiene que ser un número fijo, puede ser cualquier expresión:
  String[] nombresAlumnos = new String[getNumeroAlumnos()];

¿Cómo accedo a las posiciones de un array?

Una vez que tengo espacio para un número de elementos, utilizaré una expresión de acceso de la forma miArray[indice], teniendo en cuenta que éstos índices deben ser del tipo int y se empiezan a contar desde cero. Estas expresiones pueden usarse como cualquier variable. Veamos un ejemplo:
  misNumeros[0] = 1;
  misNumeros[1] = misNumeros[0] + 1;
  misNumeros[2] = 3; // error en ejecución
Debemos asignar valores del tipo exacto al declarado y asegurarnos de mantener el índice dentro del tamaño, o nuestro código se romperá mientras se ejecuta.

Debo asignar valores del tipo declarado y no usar [índices] fuera del tamaño (entre 0 y n-1)


Por otra parte, si conozco los valores desde el inicio, también puedo asignarlos declarándolos en la misma asignación;
  misNumeros = new int[] { 1, 2, 3 };
Hay que tener en cuenta que cada vez que usemos new se creará un objeto nuevo aunque se asigne a la misma variable.

También se puede ver una forma más resumida (azúcar sintáctico - syntactic sugar) pero sólo si se declara la variable y se inicializa en la misma línea;
  int[] misNumeros = { 1, 2, 3 };

Arrays multidimensionales

Los de arriba son arrays de una sola dimensión, pero pueden ser multidimensionales si seguimos añadiendo corchetes:
  • int[][]: dos dimensiones (matriz)
  • int[][][]: tres dimensiones (matriz cúbica)
Básicamente son arrays de arrays de arrays... hasta que termines de poner corchetes.

NOTA: también puede verse declarado un array como int a[]; No se suele utilizar, sólo se usa si se declaran varios arrays de distintas dimensiones en la misma línea como int[] a, b[]; siendo a de una dimensión y b de dos. Esto tratarlo como advertencia por si lo veis, pero no es normal.

Para acceder a los elementos de sus distintos niveles, se concatenarán expresiones de acceso añadiendo corchetes con los índices que hagan falta:
  int[][] miMatriz = new int[3][2];
  miMatriz[2][0] = 1; // coloca 1 en la primera posición del tercer array de arrays[2]
  System.out.println(Arrays.toString(miMatriz[2])); // imprimo el tercer array[2]
NOTA: He usado la clase java.utils.Arrays que contiene herramientas para facilitar ciertas operaciones con tipos array. Puedes ver y practicar con más métodos siguiendo su documentación.

A la hora de llamar a una posición concreta en un array multidimensional hay que tener cuidado de no exceder los límites de posiciones de cada dimensión: miMatriz[3][0] producirá un error igual que miMatriz[0][2].

Recorrer un array

Una forma típica de recorrer un array es usar un bucle for de la siguiente forma:
  for (int i = 0; i < misNumeros.length; i++) {
      System.out.println("Posición " + i + ": " + misNumeros[i]);
  }
Aquí podemos ver un miembro de los arrays que es length, que es el encargado de decirme el tamaño del array.

Para recorrer un array multidimensional usaremos los bucles for anidados que necesitemos en función de los niveles del array. Para recorrer todos los elementos de un array de dos dimensiones haremos:
  public static void imprimirMatriz(int[][] matriz) {
      for (int i = 0; i < matriz.length; i++) {
          for (int j = 0; j < matriz[0].length; j++) {
              // hacer la operación que se necesite
              System.out.print(matriz[i][j] + "\t");
              // uso \t para alinear las columnas
          }
          System.out.println(); // siguiente fila
      }
  }
Por último y el más usando si se quieren recorrer todos los elementos de una "colección", se puede usar el bucle for mejorado (enhanced for):
  for (T elemento : miArrayDeTipoT) {
      System.out.print(elemento);
  }
Con los arrays podemos usar el bucle for mejorado


Pero ojo, los arrays sólo apuntan a valores, no los guardan

Podemos entender que los arrays son almacenes de datos, pero en realidad son un conjunto de variables pero que en vez de tener un identificador usan expresiones de acceso como se explicó antes.

Con los tipos por referencia no se harán copias aunque ocupen posiciones distintas del array.

Vamos a hacer un par de arrays que usaré para rellenar un array de arrays. Uno de ellos lo voy a poner dos veces y veremos que si se modifica cualquiera de los arrays, el cambio se produce en ellos, no en el array de arrays. Se ve más fácil en el vídeo:
  int[] a = {1 , 2};
  int[] b = {3 , 4};     
  int[][] aba = { a , b, a };
  imprimirMatriz(aba);
  System.out.println();
  // cambiamos el primer valor
  aba[0][0] = 5;
  imprimirMatriz(aba);
  // pero lo que cambia es "a"
  // no entender como una copia
  // es el mismo objeto
  // se ve en esta impresion
  System.out.println(Arrays.toString(aba));
  System.out.println("a es: " + Arrays.toString(a));
  System.out.println();

  // cambiamos el tercer array
  aba[2] = new int[] { 6, 7 };
  imprimirMatriz(aba);
  // ahora son 3 objetos distintos
  System.out.println(Arrays.toString(aba));
Tenemos la siguiente salida, aunque los números que identifican los objetos serán distintos para cada uno.

1      2 
3      4 
1      2 

5      2 
3      4 
5      2 
[[I@52e922, [I@25154f, [I@52e922]
a es: [5, 2]

5      2 
3      4 
6      7 
[[I@52e922, [I@25154f, [I@10dea4e]
NOTA (nivel avanzado): Esas cadenas de caracteres trípticos que hemos obtenido vienen del método getName() de la clase Class y nos dicen de qué tipo es el objeto (en nuestro caso array de int). Puedes leer más en la API de java.

He marcado en rojo los objetos repetidos.

Con los arrays hacemos referencia al tipo declarado, no se hacen copias de valores al asignarlos

2 de octubre de 2018

Contenido básico extra

Aquí voy a recopilar un índice de contenido que mejora nuestra programación y ayuda a entender el código que leamos aunque, salvo casos muy excepcionales, no se necesita si usamos las herramientas que ya hemos visto. No haré vídeos de ellos porque algunos serán de uso habitual como combinar el operador de asignación con otro y en los menos usados habrá una aplicación directa de ello en un vídeo de otra sesión.
  1. Recursividad
    1. Practica con los ejercicios omitidos anteriormente (6 en adelante)
  2. Operador de asignación combinado
  3. Sentencia switch
  4. Un return vs varios

Operador de asignación combinado

El operador de asignación también se puede usar combinado con algunos de los asignadores que ya hemos visto cuando queremos asignarle a una variable un valor que es el resultado de una operación en la que esta implicada esa misma variable. Al conocer uno de los operandos (el valor actual de la variable a la que se asignará el resultado), se puede resumir la expresión quedando:
  miVariable [Op]= valor; // [Op] es un operador visto

  // equivale a

  miVariable = miVariable [Op] valor;
Los operadores con los que se puede combinar son: + - * / % & |.

En un ejemplo se puede ver mejor:
  int a = 3;
  a += 2; // ahora a es 5
Que equivale a:
  int a = 3;
  a = a + 2;
Con lo que queda más compacto y será lo más habitual de ver al usar una expresión compatible.

Practica con los otros tres para que te quede claro su uso.

Asignación condicional

También existe algo muy útil que es el operador ternario de asignación condicional. Con él vamos a poder decidir, en función de una condición, qué expresión hay que resolver para asignar un valor a una variable de un tipo compatible. Si sintaxis es:
  variable = (condicion) ? valorTrue : valorFalse;
En un ejemplo práctico sería:
  texto = esPar(n) ? "ES par" : "NO es par";
De esta forma el código queda más compacto al no tener que usar condiciones if-else.

26 de septiembre de 2018

Bucles (Loops)

Cuando ejecutamos nuestros pequeños ejemplo, se están ejecutando una única vez. La sentencia que se ejecuta no vuelve a llamarse más. En esta entrada aprendemos los distintos bucles que tiene Java que nos van a permitir iterar (repetir) un fragmento de código las veces que necesitemos.

Repetir algo es una de las partes que vimos más generales a la hora de programar. Ya sólo nos faltará ver la Entrada (Input) en la siguiente sesión.

En general vamos a tener tres partes a la hora de definir un bucle:
  1. Condición para ejecutar el código del cuerpo del bucle
  2. Actualización de valor/es que influyen en la condición
  3. Código a ejecutarse que no es propio del control del bucle
Con estas ideas vamos a ver las distintas formas de declarar un bucle

Los bucles nos permiten repetir fragmentos de código mediante el control de una expresión condicional



Bucle while

Su estructura es:
  while (condicion) {
    // cuerpo con actualizacion
  }
Su funcionamiento es evaluar la condición marcada y ejecutar el código de su cuerpo si se cumple. El código del cuerpo debe influir de alguna forma en la siguiente evaluación de la condición o se quedaría infinitamente iterando o nunca lo haría.

Vemos un ejemplo muy gráfico basado en la curiosa conjetura de Collatz:
  public static void collatz(int n) {
    while (n != 1) {
        System.out.println(n);
        if (n % 2 == 0) { // n es par
            n = n / 2;
        } else {          // n es impar
            n = n * 3 + 1;
        }
    }
  }

Bucle for

Su estructura es:
  for (inicio; condicion; actualizo) {
    // cuerpo
  }
Este bloque centraliza el control del bucle en su primera línea. Cada una de las expresiones se dedica a:
  • inicio: Normalmente declara un estado inicial. Se ejecuta sólo cuando se inicia el bucle.
  • condicion: Se evaluará cada iteración del bucle.
  • actualizo: Se ejecutará al final de cada iteración y servirá para influir en la condicion.
En este ejemplo hacemos una cuenta atrás desde un número:
  for (int i = 3; i > 0; i--) {
    System.out.println(i);
  }
  System.out.println("!BOOM!");

Bucle do-while

Su estructura es:
  do {
    // cuerpo
  } while (condicion);
Se puede ver como un caso especial de while. La diferencia es que la condición se evaluará al final de la iteración. Esto implica que al menos se ejecutará una vez el cuerpo hasta llegar a la condición.
  do {
    System.out.println("Itero");
  } while (false);
En el ejemplo vemos que el cuerpo se ejecutará aunque la condición sea false desde el principio. Es importante terminar toda la declaración con punto y coma ";" ya que podríamos pensar que no hace falta como en los otros dos casos, pero sería un error de compilación (Eclipse nos avisará).

Diferencias entre los distintos bucles

Ahora que tengo varias formas de iterar ¿Cuál uso?
Puedes seguir estos consejos para elegir el bucle más idóneo:
  • for: cuando sabes cuantas veces vas a tener que iterar porque tiene un numero finito de elementos.
  • while: cuando no sabes cuántas veces tienes que iterar, incluido ninguna.
  • do-while: cuando no sabes cuántas veces tienes que iterar pero al menos será una.
Los bucles en general están formados por una situación inicial, una condición que va evolucionando y un código a ejecutar en su cuerpo. Cuanto más sepamos del problema a solucionar mejor decidiremos el bucle a emplear


Controlar el bucle con break y continue

Hay dos palabras reservadas que se usan mucho con los bucles: break y continue.
  • break: termina la ejecución del bucle y sale directamente
  • continue: termina la ejecución de la iteración y vuelve a evaluar la condición
En el siguiente ejemplo vemos que continue hace que se salte los números pares y que break termina la ejecución en 7 en vez de llegar al 9:
  for (int i = 0; i < 10; i++) { // de 0 a 9
    
      if(i == 7) { // termina cuando es 7
          break;
      }
      if(i % 2 == 0) { // pasa al siguiente si es par
          continue;
      }
    
      System.out.println("i = " + i);
  }

Conversiones for y while

Todo for puede convertirse en while y viceversa pero no es aconsejable. Si miras los ejemplos verás que todo funciona bien, pero a simple vista no queda bien y hace el código menos legible. Por eso es importante elegir bien qué estructura usar.
  for (int i = 0; i < 10; i++) {
  }

  // equivale a

  int i = 0; // inicio
  while (i < 10) { // condicion
      i++; // actualizo
  }
A la inversa sería tan simple como borrar el inicio y actualizo del for y en condición usar la del while:
  while (condicion) {
    // ...
  }

  // equivale a

  for (;condicion;) {
  }

24 de septiembre de 2018

Condiciones (if-then-else)

Todo el código que hemos visto va siguiendo unos pasos fijos cada vez que lo ejecutamos. Las condiciones nos van a permitir decidir qué código se ejecutará en función de los valores que tengamos durante la ejecución.

Os recuerdo que tomar una decisión es una de las partes más generales que vimos cuando aprendimos qué es programar.

De manera general una cláusula condicional se expresa de la siguiente forma:
  if (condicion) {
    // rama se cumple
  } else {
    // rama NO se cumple
  }
  • Ejecuta la "rama se cumple" si se cumple la condición o la rama "NO se cumple" si no se cumple la misma condición.
  • El fragmento else no es obligatorio si no se necesita su rama.
La cláusula if-then-else nos permite decidir el código a ejecutar en función de una condición


Este es un ejemplo básico:
  public static void main(String[] args) {
      int saldo = 100;
      String resultado;
    
      if (esRico(saldo)) {
          resultado = "Soy rico";
      } else {
          resultado = "NO soy rico";
      }
    
      System.out.println("¡" + resultado + "! Mi saldo es: " + saldo);
  }

  public static boolean esRico(int saldo) {
      return saldo >= 1000;
  }
Los bloques que marcan las ramas están encerrados entre llaves como de costumbre, pero si la rama sólo consta de una única sentencia se puede optar por omitir las llaves. Sin embargo hay que prestar especial atención a la indentación (las distintas sangrías que tienen las sentencias en función de la profundidad de su bloque) para no confundir líneas sucesivas como formando parte del bloque. En ese caso nos quedaría así:
  if (esRico(saldo))
      resultado = "Soy rico";
  else
      resultado = "NO soy rico";
O incluso podéis verlo así:
  if (esRico(saldo)) resultado = "Soy rico";
  else resultado = "NO soy rico";
No obstante, si repasáis la guía de estilo que marcamos para el curso, recomienda siempre poner las llaves.

Encadenar y Anidar condiciones

Si queremos tener condiciones más complejas se pueden encadenar. En este caso se puede continuar el comienzo de la rama else con otro if. Vamos a ver más claro este punto con un ejemplo:
  public static void main(String[] args) {
      int saldo = 100;
      String resultado;
    
      if (esRico(saldo)) {
          resultado = "Soy rico";
      } else if (esPobre(saldo)) {
          resultado = "Soy pobre";
      } else {
          resultado = "Voy tirando";
      }
    
      System.out.println("¡" + resultado + "! Mi saldo es: " + saldo);
  }

  public static boolean esRico(int saldo) {
      return saldo >= 1000;
  }

  public static boolean esPobre(int saldo) {
      return saldo <= 0;
  }
Si queremos ir afinando desde condiciones más generales a otras más precisas, se pueden anidar condiciones dentro de las ramas. Personalmente suelo usar el anidamiento y comentar las ramas si es necesario (ver en el código). La parte que cambia es la siguiente:
  if (esRico(saldo)) { // Soy rico
      resultado = "Soy rico";
  } else { // NO soy rico
      if (esPobre(saldo)){ // Soy pobre
          resultado = "Soy pobre";
      } else {
          resultado = "Voy tirando";
      }
  }
La composición de condiciones es algo habitual. Se puede ver como un modo de filtrar valores para no tener que evaluar una sentencia de nuevo como si formáramos una única condición concatenándolas usando los operadores lógicos (!esRico && !esPobre). Al final vemos una demostración.

El encadenamiento y anidamiento de condiciones nos sirven para filtrar y separar mejor el código


En el libro se puede ver otro uso de la palabra clave return. Como dijimos anteriormente se usa en los métodos con retorno para terminar la ejecución y devolver el valor. En este caso simplemente finaliza la ejecución. Personalmente creo que no es necesario y que hace más difícil seguir el código. Recomiendo tener un único punto de salida de los métodos.

Demostrando los operadores lógicos cortocircuitados

Como os adelanté, veríamos exactamente una demostración de ese cortocircuito. En este código podemos ver que se cortocircuita al no haber salidas de consola que se producirían si se evaluara !esPobre. En el vídeo se observa claramente:
  public static void main(String[] args) {
      int saldo = 1000;
      System.out.println("¿Voy Tirando? " + vaTirando(saldo));
  }

  public static boolean esRico(int saldo) {
      System.out.println("Comprobando RICO");
      return saldo >= 1000;
  }

  public static boolean esPobre(int saldo) {
      System.out.println("Comprobando POBRE");
      return saldo <= 0;
  }

  public static boolean vaTirando(int saldo) {
      return !esRico(saldo) && !esPobre(saldo);
  }

21 de septiembre de 2018

Métodos con retorno

Hasta ahora hemos declarado sólo métodos sin retorno (void methods) porque en la parte del resultado al definirlos siempre hemos usado la palabra reservada void.

Desde esta entrada vamos a empezar a hacer cosas más chulas ya que veremos los métodos con retorno (value methods), los cuales sólo difieren de los void en dos aspectos de su declaración:
  1. Declaran el tipo del valor que devuelven
  2. Utilizan la palabra reservada return para indicar el fin de la ejecución del método y devolver el valor. Cualquier código posterior en su ámbito no se ejecutará.
Los métodos con retorno declaran el tipo del valor que devuelven con la sentencia return que finaliza su ejecución


No se puede declarar un método con retorno sin estas dos modificaciones. De hecho nuestro IDE nos alertará de que falta la sentencia return si le indicamos un tipo de retorno pero no la hemos añadido para salir del cuerpo.


Esta diferencia implica unos cambios importantes:
  • Ahora los métodos value nos pueden devolver un resultado con el que podemos trabajar tras su ejecución.
  • Los cambios que generaban los métodos void se hacían sobre algún elemento externo (como hicimos con la consola añadiendo líneas). Esto se conoce como efectos colaterales (side effects) y va en contra de la programación funcional (functional programming).
  • La programación funcional es coherente ya que una invocación con los mismos argumentos siempre nos producirá el mismo resultado.
  • Es una buena práctica programar lo que podamos de esta forma para reutilizar más nuestro código y mejorar el mantenimiento (pues es mucho más fácil de depurar).

Los métodos con retorno producen valores con los que trabajar después y facilitan la programación funcional


Empecemos con un ejemplo sencillo:
    public static double suma(double a, double b) {
        double suma = a + b;
    
        return suma;
    }
Este código admite dos argumentos de tipo double y devuelve su suma. También es válido simplemente con:
    public static double suma(double a, double b) {
        return a + b;
    }
Ya que la variable suma no la necesitamos localmente y sólo necesitamos devolver el valor de la expresión a + b. En vuestros primeros códigos no tengáis pegas en añadir variables innecesarias si os va a servir para ir comprobando paso a paso que vuestro código funciona correctamente. Lo vamos a ver en la práctica de esta sesión, pero antes ¿Os atrevéis a hacer un método que reste los dos valores?


    public static double resta(double minuendo, double sustraendo) {
        double resta = minuendo - sustraendo;
    
        return resta;
    }
Si habéis dejado los nombres de a y b, sería conveniente dejar claro cuál es el minuendo y cuál el sustraendo pues no es una operación conmutativa. Cuanto más claros sean los nombres de los parámetros más fácil será usarlos.

Vamos a ver otro ejemplo típico: preguntar por un boolean. Cuando trabajemos con objetos será normal preguntar si es/estaAlgo (imaginad un objeto con color que puede ocultarse, podríamos preguntar si estaOculto o si esVerde).

Veamos este código:
    public static boolean esPositivo(double numero) {
        return numero > 0;
    }
El método esPositivo nos devolverá true si es mayor que cero, si no devolverá false. El nombre de este tipo de métodos siempre lo expresaremos en positivo (mejor esPositivo que noEsPositivo, eso lo expresariamos como !esPositivo)

Intentad hacer ahora uno que nos diga si un entero es par.


    public static boolean esPar(int numero) {
        int resto = numero % 2;
    
        return resto == 0; // Resumiendo sería "return (numero % 2) == 0;"
    }
Hay que fijarse que el tipo del parámetro es más adecuado que sea int y la forma más sencilla de calcularlo es usando el operador % para ver si el resto es cero.

Vamos a expresar los nombres de los métodos en positivo y con nombres de argumentos lo más claros posibles


Para terminar vamos a ver una práctica más larga que nos aporta el libro en la que tendréis que ir comprobando paso a paso que todo está saliendo bien, reutilizar todo el código que podáis y que sea funcional.

Práctica:

En el vídeo he ido haciendo paso a paso este método.


public static double area(double xc, double yc, double xp, double yp) {
    double area;
    double radio;
    
    radio = distancia(xc, yc, xp, yp);
    area = Math.PI * cuadrado(radio);
    
    return area;
}

private static double cuadrado(double radio) {
    return radio * radio;
}

private static double distancia(double xc, double yc, double xp, double yp) {
    double distancia;
    double sumaDeLosCuadrados;
    double cuadradoX;
    double cuadradoY;
    double restaX;
    double restaY;
    
    restaX = resta(xc, xp);
    restaY = resta(yc, yp);
    cuadradoX = cuadrado(restaX);
    cuadradoY = cuadrado(restaY);
    sumaDeLosCuadrados = suma(cuadradoX, cuadradoY);
    distancia = Math.sqrt(sumaDeLosCuadrados);

    return distancia;
}

18 de septiembre de 2018

Parámetros / Argumentos - Visibilidad y ámbito (scope)

Vamos a utilizar la declaración de parámetros y el paso de argumentos para personalizar el método que hicimos en la entrada anterior y poder decir el número de líneas nuevas que queremos añadir. También veremos los conceptos de visibilidad y ámbito (scope).

Con los parámetros podemos personalizar la ejecución de un método


En el ejemplo del libro se crea un nuevo método "tresLineas" que repite la invocación del método "nuevaLinea" tres veces. Ahora podemos cambiar el código para hacer una sola llamada a este nuevo método, pero ¿Y si queremos añadir dos, cuatro o diez líneas en vez de tres? ¿Hacemos otro método para cada caso? No, vamos a ver cómo personalizar las invocaciones usando argumentos.

Diferencia entre parámetros y argumentos

Antes que nada y para no seguir pensando que cada vez uso una palabra distinta hay que tener en cuenta estas definiciones:
  • Parámetros: Son cada una de las variables locales al método que se declaran en su firma entre paréntesis.
  • Argumentos: Son cada uno de los valores que corresponden a cada parámetro en la invocación de un método.
Después del siguiente ejemplo se ve claramente cuáles son los parámetros y cuáles los argumentos.

Modificamos nuestro código para que acepte un entero que indique el número de líneas que queremos añadir.

NOTA: He añadido un bucle (bloque for) para poder hacer el código. Este concepto y su uso se verá en otra sesión más adelante. Por ahora simplemente copiamos el código y ya aprenderemos a leerlo y escribirlo.
    public static void main(String[] args) {
        System.out.println("Hola Mundo");
        nuevaLinea(2);
        System.out.println("Adios Mundo");
    }
    
    public static void nuevaLinea(int numeroLineas) {
        for (int i = 0; i < numeroLineas; i++) {
            System.out.println("|--------------------|");
        }
    }
Pero alguno podrá pensar que la cadena que he dejado para rellenar la nueva línea es un "magic number". Vamos a añadir otro parámetro para que podamos decirle el texto que debe aparecer en cada nueva línea. El código queda así:
    public static void main(String[] args) {
        System.out.println("Hola Mundo");
        nuevaLinea(2, "|--------------------|");
        System.out.println("Adios Mundo");
    }
    
    public static void nuevaLinea(int numeroLineas) {
        nuevaLinea(numeroLineas, "");
    }
    
    public static void nuevaLinea(int numeroLineas, String texto) {
        for (int i = 0; i < numeroLineas; i++) {
            System.out.println(texto);
        }
    }
En el vídeo se puede ver cómo modifico el código sobre la marcha y se entiende mejor que encontrar el código final completo.


Después de la demostración podemos ver claramente la diferencia entre parámetro y argumento (los números de línea se refieren al último fragmento de código en esta página):
  1. Parámetros:
    1. línea 7: (int numeroLineas)
    2. línea 11: (int numeroLineas, String texto)
  2. Argumentos:
    1. línea 3: los valores 2 y "|--------------------|"
    2. línea 8: los valores numeroLineas y ""
    3. tambien líneas 2 y 4: "Hola Mundo" y "Adios Mundo"
Resumiendo: los parámetros se declaran parecido a las variables mientras que los argumentos son los valores que se asignan a esos parámetros al invocar el método.

Los parámetros se declaran pero los argumentos se asignan a esos parámetros al invocar el método


Visibilidad y ámbito (scope)

Ahora que ya tenemos distintos bloques de código podemos ver qué es una variable local y dónde está permitido su uso. En el vídeo hago la demostración. Simplemente debe quedarnos claro que una variable se puede usar sólo dentro del bloque de código, toda vez que se ha declarado, y se usará normalmente hasta que otra con un ámbito (scope) más cercano la "ensombrezca" (shadowing). Este último concepto de shadowing lo veremos cuando hablemos de constructores en las Clases.

Una variable se puede usar sólo dentro del bloque de código y si ya se ha declarado

17 de septiembre de 2018

Métodos

Por ahora conocemos cómo usar las variables y sus operadores para trabajar con ellas. Lo malo es que todo nuestro código está dentro del método "main" y lo único que hemos hecho es llamar a la impresión por consola. Vamos a ver cómo declarar más métodos, sus partes y qué se entiende por la firma de un método (signature).

Según la especificación Java 8, un método declara un código ejecutable que puede ser invocado con un número fijo de valores como argumentos. Nos va a permitir reutilizar fragmentos de código que podremos personalizar con los argumentos y que pueden proporcionarnos un resultado.

Los métodos nos permiten reutilizar código y obtener resultados personalizados por los argumentos que usemos


Un método tiene la siguiente estructura:
  1. Modificadores
  2. Cabecera del método con:
    1. Tipo de resultado que nos devolverá (el tipo)
    2. Declarador de método con:
      1. Identificador (nombre, en formato lowerCamelCase y, por convención, comenzando con un verbo)
      2. Parámetros (incluido su tipo)
  3. Cuerpo del método
En total hay algunas partes más, pero por ahora nos quedamos con estas que son las que nos hacen falta por ahora.

Vamos a verlo con el ejemplo del método "main" usado hasta ahora:



Firma del método (signature)

La firma del método es el conjunto nombre y parámetros (incluyendo el tipo) y no puede haber dos métodos con la misma firma en el mismo código.

Para usar un método nos interesa su firma ya que lo invocaremos llamándolo por su nombre y poniendo entre paréntesis los argumentos que encajen con sus parámetros.

En la próxima entrada hablaremos de ámbito y visibilidad y de los parámetros. Aquí simplemente mencionar que dos métodos pueden diferenciarse en los parámetros aunque tengan el mismo nombre y sería válido. Es lo que se llama sobrecarga.

Usaremos un método invocándolo con su nombre y poniendo entre paréntesis los argumentos que encajen con sus parámetros


Vamos a crear un método muy simple que imprima una nueva línea en nuestra consola. Nos creamos una nueva clase "EjemploMetodos" y usamos el siguiente código:
    public static void main(String[] args) {
        System.out.println("Hola Mundo");
        nuevaLinea();
        System.out.println("Adios Mundo");
    }

    public static void nuevaLinea() {
        System.out.println();
    }
Podéis ver el vídeo para ver mejor en vivo su definición e invocación:


14 de septiembre de 2018

Operadores lógicos

Para terminar la explicación básica de los operadores en Java, vamos a ver los operadores lógicos. Estos operadores nos van a permitir realizar operaciones con valores boolean.

Los operadores lógicos en Java son:
  1. Y (puerta lógica AND): &
  2. O (puerta lógica OR): |
  3. Y condicional: &&
  4. O condicional: ||
  5. Negación (puesta lógica NOT): !
Los 4 primeros son operadores binarios y el último es unario (niega un valor).

Los operadores lógicos realizan operaciones sobre valores boolean actuando como puertas lógicas


 Para los que no lo conozcan, el funcionamiento de las puertas lógicas es:
  • AND: El resultado será verdadero (true) sólo si ambos operandos son true. En el resto de casos se evalúa a falso (false)
  • OR: El resultado será false sólo si ambos operandos son false. En el resto de casos se evalúa a true.
  • NOT: Devuelve lo contrario.

¿Qué diferencia hay en los operadores condicionales?

A los operadores Y/O condicionales se les llama también "cortocircuito". La diferencia entre éstos y su versión no cortocircuitada se ve a la hora de evaluar la expresión. Veamos un ejemplo con Y condicional:
resultado = condicion1 && condicion2 && condicion3
En este ejemplo si condicion1 es true se seguirá evaluando si condicion2 también lo es, pues para saber el resultado hay que llegar hasta el final o hasta que una condición sea false. Si se encuentra una condición false entonces no hace falta seguir evaluando pues el resultado final va a ser false.

Si lo vemos para O condicional:
resultado = condicion1 || condicion2 || condicion3
Aquí pasa algo parecido pero a la inversa. Se parará de evaluar en cuanto se encuentre que una condición es true pues el resultado final será true independientemente del resto de condiciones.

Los operadores lógicos cortocircuitados evitan la evaluación restante de la expresión cuando conocen el valor resultante global


A primera vista podría simplemente parecer una diferencia de rendimiento (si evito operaciones irá todo más rápido), pero lo fundamental es si queremos que se evalúe una condición o no. Ahora mismo es pronto para ver la diferencia. Lo veremos cuando lleguemos a Clases y se explique el concepto de null.

Este código servirá para ver un ejemplo de las puertas lógicas:
boolean v = true;
boolean f = false;

System.out.println(v + " & " + v + " = " + (v & v));
System.out.println(v + " & " + f + " = " + (v & f));
System.out.println(v + " | " + f + " = " + (v | f));
System.out.println(f + " | " + f + " = " + (f | f));
System.out.println("!" + v + " = " + !v);
System.out.println("!" + f + " = " + !f);
Su salida es:

true & true = true
true & false = false
true | false = true
false | false = false
!true = false
!false = true


Echaréis en falta probar los operadores cortocircuitados. Precisamente son los que se utilizan habitualmente, pero en un ejemplo tan sencillo sin haber visto métodos es difícil demostrar el corte que se produce, por eso se ve cuando hemos visto métodos y aprendemos las condiciones.

Operadores de comparación

En esta entrada vamos a ver los operadores de comparación que usaremos para comparar valores. Vamos a emplearlos con decimales para empezar a usarlos y de paso veremos los errores de redondeos que no vimos en los operadores numéricos.

NOTA: En este punto nos vamos a desviar del índice del libro que sirve de guía. En Think Java el orden es ver la entrada de teclado, métodos sin retorno, condicionales y lógica, métodos con retorno y bucles. Yo prefiero el orden que he marcado en la programación del curso para tener todos los operadores juntos y terminar este parte con la entrada de teclado y poder hacer un programita chulo al final para verlo todo junto. Simplemente advertir que hasta que lleguemos a Arrays no vamos a seguir el orden del libro.

Ya hemos visto y usado el operador de asignación y los operadores numéricos habituales. Los operadores de comparación en Java son:
  1. Menor que: <
  2. Mayor que: >
  3. Menor o igual que: <=
  4. Mayor o igual que: >=
  5. Igual que: ==
  6. Distinto que: !=

Todos ellos operadores binarios y con sus operandos formarán una expresión que devolverá un valor de tipo boolean. Este tipo boolean sólo tiene dos valores posibles: verdadero o falso (true o false). Es la base para trabajar con condiciones que veremos en su sesión correspondiente.

Los operadores de comparación son binarios y devuelven un valor de tipo boolean (verdadero o falso)


Con este código vamos a tener un ejemplo de cada uno y podéis seguirlo en el vídeo:
float a = 2.1f;
double b = 3.9;

System.out.println(a + " < " + b + " : " + (a < b));
System.out.println(a + " > " + b + " : " + (a > b));
System.out.println(a + " <= " + b + " : " + (a <= b));
System.out.println(a + " >= " + b + " : " + (a >= b));
System.out.println(a + " == " + b + " : " + (a == b));
System.out.println(a + " != " + b + " : " + (a != b));
Su salida es:

2.1 < 3.9 : true
2.1 > 3.9 : false
2.1 <= 3.9 : true
2.1 >= 3.9 : false
2.1 == 3.9 : false
2.1 != 3.9 : true

Incluso si prefiris leer el blog a ver un vídeo, os aconsejo ver la demostración de los errores de redondeos y la comparación entre tipos decimales distintos. Esta demostración se aprecia mejor en vivo y no la he escrito en esta entrada.

Debemos tener especial cuidado cuando comparemos valores numéricos de distinto tipo debido al redondeo


Hay que tener en cuenta que estos operadores funcionan con valores numéricos pero no con todos los tipos que hemos visto hasta ahora (por ejemplo no podemos ver si un String es mayor que otro, pero sí podemos ver si es el mismo)

NOTA: Si quieres leer más sobre la compatibilidad entre tipos como en el ejemplo del vídeo entre int y double, puedes ver todas las conversiones que contempla la especificación de Java.

13 de septiembre de 2018

Operadores - Numéricos

Ya hemos visto el operador de asignación. Es uno de los 38 que contiene la especificación de Java 8. En esta entrada veremos los operadores numéricos. Estos operadores junto con otras expresiones, nos van a permitir realizar cálculos, que os recuerdo son una de las partes más generales en las que dividiamos cualquier programa cuando aprendimos qué era programar. En el libro que estamos siguiendo da muchos ejemplos de redondeos y diferencias entre distintos tipos numéricos como int, float o double.

Ahora mismo no voy a entrar en esa profundidad pues en general no influyen demasiado. Al final de esta entrada sí que voy a llamaros la atención en aspectos a tener en cuenta por experiencia personal. Por ahora vamos a ver cuales son los operadores numéricos y unos ejemplos.

Los operadores numéricos permiten hacer cálculos aritméticos. Existen diferencias usándolos con operandos enteros o decimales


Según la especificación de Java los operadores numéricos son (para enteros y decimales):
  1. Suma: +
  2. Resta/Cambio de signo: -
  3. Multiplicación: * (asterisco)
  4. División: /
  5. Módulo (resto): %
  6. Incremento: ++
  7. Decremento: --
Para enteros hay otros a nivel bit que es más raro su uso y no los veremos en esta entrada.

Los 5 primeros se usan con dos operandos. Cuando un operador utiliza dos operandos se conoce como operador binario.

Los operadores 6 y 7 se usan un sólo operando, son unarios (sólo necesitan un operando para incrementar o decrementar) y cambia su comportamiento si lo usamos antes o después de la variable como se ve en el vídeo.

El operador "-" también puede usarse como unario (para cambiar el signo)

El operador "+" podemos usarlo con una cadena de texto para que nos concatene todo en una sola cadena.


Este es un ejemplo para cada uno:
int operando1 = 2;
int operando2 = 3;
int resultado;
System.out.print("Sumo los operandos = ");
resultado = operando1 + operando2;
System.out.println(resultado);
System.out.print("Resto los operandos = ");
resultado = operando1 - operando2;
System.out.println(resultado);
System.out.print("Niego el operando1 = ");
resultado = -operando1;
System.out.println(resultado);
System.out.print("Multiplico los operandos = ");
resultado = operando1 * operando2;
System.out.println(resultado);
System.out.print("Divido los operandos = ");
resultado = operando1 / operando2; // Cociente
System.out.println(resultado);
System.out.print("Resto de dividir los operandos = ");
resultado = operando1 % operando2; // Resto
System.out.println(resultado);
// Comprobamos que los operandos no han cambiado hasta ahora
System.out.println("Imprimo los operandos: op1 = "
        + operando1 + " y op2 = " + operando2);
System.out.print("Incremento el operando1 = ");
System.out.println(++operando1); // Cambio antes de devolver el valor
System.out.print("Decremento el operando1 = ");
System.out.println(operando1--); // Cambio despues de devolver el valor
System.out.print("Imprimo el operando1 = ");
System.out.println(operando1); // Ahora vemos el cambio
Su salida por consola es:

Sumo los operandos = 5
Resto los operandos = -1
Niego el operando1 = -2
Multiplico los operandos = 6
Divido los operandos = 0
Resto de dividir los operandos = 2
Imprimo los operandos: op1 = 2 y op2 = 3
Incremento el operando1 = 3
Decremento el operando1 = 3
Imprimo el operando1 = 2 

Si bien podría haber puesto los ejemplos con números directamente, quiero que desde el principio os acostumbréis a usar variables.

Este uso de los operadores se llama expresión. Una expresión es un fragmento de código que devuelve un valor. El concepto de sentencia que explicamos anteriormente suele usar varias expresiones. El mejor ejemplo son las líneas 23 y 24. Las dos líneas contienen una única sentencia que va obteniendo los sucesivos resultados de concatenar el texto dos a dos hasta que tiene una única cadena de texto que imprime por pantalla.

Es interesante ver cómo las variables no cambian sus valores usando los primeros operadores. Esto es porque no les asignamos nuevo valor. El valor sólo se asigna a la variable "resultado". Sin embargo con el incrementador y decrementador sí que cambia el propio valor de su único operador, pero lo hace antes de devolver el nuevo valor si los usamos a la izquierda de la variable y después si usamos el operador a la derecha.

Las expresiones devuelven un valor, pero no implican un cambio del valor en sus variables.


Conclusiones a tener en cuenta

Como prometí al principio, aquí os dejo mis conclusiones al respecto:
  1. Redondeo de enteros: Esto si es normal encontrarlo. Si hago una división de enteros me redondeará a un entero y puedo tener grandes errores inesperados (sobre todo si es por debajo de 1). Por esto cuando tengamos que realizar operaciones con decimales usaremos float (que ocupa menos memoria) o double (si queremos tener una gran precisión).
  2. Respetar los datos que tenemos: Si leemos un dato externo que tiene el tamaño double y lo convertimos a float vamos a perder precisión. Si ese dato tiene esa precisión debemos valorar el por qué, si lo obviamos puede que confrontando datos con otros procesos haya discrepancias (imaginad una aplicación que sume millones de cantidades decimales distintas). Solo reduciremos el tamaño del dato cuando el error es asumible y no influye en el resultado final.
  3. Comparar decimales del mismo tipo: Lo mejor es comparar los datos del mismo tipo, en caso contrario uno de los dos se "convertirá" al otro tipo produciendo un pequeño error de ajuste. Esto producirá probablemente que una simple comparación de igualdad falle aunque asignemos "el mismo valor". Puedes ver la demostración en la siguiente entrada de operadores de comparación.
  4. Indicar los valores decimales float poniendo una "f" al final (ej: 5.3f), si no lo tomará como un double y nos saldrá un error.
  5. Existen unas clases que encapsulan los valores numéricos primitivos. Si no nos hace falta un valor como objeto (por ejemplo Collection nos obliga a tipos por referencia) se sacará más rendimiento usando tipos primitivos. Este punto puede no entenderse, pero quedará claro cuando veamos Clases, Objetos y Colecciones.

Si tenéis vosotros algún consejo más, os pido que lo dejéis en los comentarios.

Compárteme

Entradas populares