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.

Compárteme

Entradas populares