26 de noviembre de 2020

Interface Comparable

En la entrada anterior hemos ordenado una lista con elementos que tenían un orden natural, pero si lo intentamos hacer con una lista de vehículos no vamos a poder ordenarlos. Para poder ordenar una lista sus elementos deben implementar la interface Comparable.

NOTA: Si lo intentamos sin implementar la interface nos saldrá la excepción java.lang.ClassCastException: class Vehiculo cannot be cast to class java.lang.Comparable que deja bien claro cuál es el problema.

Se declara como cualquier otra interface, pero en este caso necesitamos poner un tipo genérico para indicar qué tipo puede ordenar la implementación. El código queda así:

public class Vehiculo implements Comparable<Vehiculo> {

  ...
  //  Nos hacemos un getter porque lo usamos dos veces
  private String getModelo() {
    return modelo;
  }

  @Override
  public String toString() {
    return getModelo() + " (" + getColor() + ")";
  }

  @Override
  public int compareTo(Vehiculo vehiculo) {
    return getModelo().compareTo(vehiculo.getModelo());
  }

}

Con este código ya se van a poder comparar todos los vehículos por orden alfabético de su modelo. Lo probamos con el siguiente código:

List<Vehiculo> vehiculos = Arrays.asList(
    new Coche("Volvo", "Gris"),
    new Vehiculo("Triciclo", "Rosa"),
    new Moto("Aprilla", "Azul")
  );

vehiculos.forEach(System.out::println);
vehiculos.sort(null);

System.out.println("\nLista ordenada (por modelo):");
vehiculos.forEach(System.out::println);

¿Qué significa el null en el parámetro del tipo Comparator?

Como estamos ordenando por el orden natural no le estamos pasando ningún parámetro al método sort. En realidad es como pasarle el comparador de orden natural: Comparator.naturalOrder()

En la siguiente entrada vamos a utilizar este parámetro para ordenar por color (incluso aunque ya tenga implementada la interface Comparable) y a ver las diferencias entre las interfaces Comparable y Comparator.

23 de noviembre de 2020

Ordenar colecciones (List)

En esta entrada vamos a utilizar una colección de Strings para ver cómo ordenar una colección:

List<String> listaStrings = Arrays.asList("a", "c", "b", "ab");

Ordenar una colección como ésta es muy fácil debido a que hay una forma natural de ordenar cadenas de texto alfabéticamente. Para ello vamos a usar el método sort. Si nuestra variable es del tipo Collection no contaremos con ese método. Una Collection no contempla el orden, para eso debemos usar una List como en el ejemplo.

Una variable nos asegura un tipo y nos permite usar la API de ese tipo, pero el objeto puede tener implementaciones "más completas".

Para ello voy a crearme otra variable de tipo Collection y le asignaré el mismo objeto creado del tipo List:

Collection<String> strings = listaStrings;

Vamos a imprimirnos nuestra colección para ver el estado inicial desordenado:

System.out.println(strings);

Nos da como salida:

[a, c, b, ab]

Si intentamos usar el método sort queda claro que un objeto no es lo mismo que una variable.

listaStrings.sort(null);
//  strings.sort(); // No aparece siendo el mismo objeto

Después de ordenar volvemos a imprimir y ya nos sale ordenada:

System.out.println(strings);
[a, ab, b, c]

También podemos usar Collections, que es una clase de utilidad para las colecciones, pero igualmente nos pide un tipo List:

Collections.sort(listaStrings);

Sin embargo, no siempre vamos a querer ordenar con éste criterio o puede que necesitemos ordenar un tipo que no tiene un orden natural. En la siguiente entrada vemos cómo trabajar con comparadores y la interface Comparable.

19 de noviembre de 2020

Colecciones (Collection)

Ahora que ya somos capaces de hacer nuestras clases, interfaces y objetos, con una jerarquía que nos permite reutilizar el código eficientemente y que podemos representarlas en consola y hacer comprobaciones básicas como ver si dos objetos son equivalentes, vamos a empezar a manejar colecciones de objetos usando la interface Collection (y no un array que es la única forma que hemos visto de usar un grupo de valores). Mirando el enlace anterior podemos ver la jerarquía que tiene ésta interface.

Vamos a crearnos una colección de Vehiculos.

Primero me creo dos vehículos (código ya conocido):

String matricula = "1234ABC";
Coche coche = new Coche("Ford Fiesta", "Rojo"); // ¿por que no usar variable Vehiculo?
coche.setMatricula(matricula);
Vehiculo moto = new Moto("Suzuki", "Verde");

Después me creo una colección y uso estos vehículos para añadirlos a la colección:

Collection<Vehiculo> vehiculos = new ArrayList<>();

En este ejemplo creamos una colección vacía usando la implementación ArrayList<>(). Ahora mismo no entenderéis ésta línea, pero tendrá sentido unas entradas más adelante, por ahora simplemente aceptar que se hace así.

En las siguientes líneas vemos que se pueden añadir más objetos a la colección, lo cual es una gran ventaja en comparación con los arrays.

vehiculos.add(coche);
vehiculos.add(moto);
Las tres líneas de código anteriores son "parecidas" (sería una lista fija) a: vehiculos = Arrays.asList(coche, moto);

Vamos a imprimir todos nuestros vehículos:

System.out.println(vehiculos);

Vemos que nos imprime algo con más significado que cuando lo hacíamos con los arrays y debíamos usar Arrays.toString().

También podemos usar esta forma bastante compacta para ver los objetos en líneas distintas:

vehiculos.forEach(System.out::println);

Esto es lo que se llama método por referencia, pero tampoco lo hemos visto aún, simplemente que quede como ejemplo para ir usándolo y que quede el código más compacto.

Del mismo modo que podemos añadir elementos se pueden quitar. Aquí es fundamental el método equals, ya que la forma de encontrar el objeto a eliminar es usando ese método:

System.out.println("\nQuito la moto");
vehiculos.remove(moto);
vehiculos.forEach(System.out::println);

¿Qué pasa si usáramos un objeto igual pero no el mismo?

Me creo un objeto que sea igual al coche que tengo en la lista:

coche = new Coche("Ford Fiesta", "Blanco");
coche.setMatricula(matricula);

Ahora voy a añadirlo:

System.out.println("\nAñado nuevo coche");
vehiculos.add(coche);
vehiculos.forEach(System.out::println);

Y por último lo quito:

System.out.println("\nQuito coche " + coche);
vehiculos.remove(coche);
vehiculos.forEach(System.out::println);

Mirando el resultado parece que hubiera algún problema, pero es todo correcto si examinamos la documentación (remove elimina una instancia, la primera). Por eso es importante éste método, y no es el único caso donde se va a usar equals ni hashCode como ya se dijo en su entrada.

NOTA: Para eliminar todos deberíamos usar algo como: vehiculos.removeIf(coche::equals);

Desde ahora ya podemos manejar un conjunto de objetos como una colección usando lo que hemos visto, pero hay muchas cosas que se pueden hacer con una colección como ordenarla. Vamos a verlo en la siguiente entrada de ordenación.

16 de noviembre de 2020

Interfaces - Lo básico

Las Interfaces (el tipo por referencia, no una interfaz de usuario) son posiblemente la herramientas más poderosa que tenemos para reutilizar código con la mayor flexibilidad.

Cuando declaremos una interfaz lo que vamos a definir es un contrato, un comportamiento externo con el resto de código, que deben asegurar todos los objetos que implementen esa interfaz (recordad que poníamos el ejemplo de una sierra, cualquier sierra debe tener el método cortar, sin embargo también podemos decir que una tijera puede cortar, e incluso el propio papel puede cortarte el dedo XD).

Estos objetos que implementan (cumplen el contrato) pueden ser de tipos con ninguna relación como hemos visto con el ejemplo de cortar (sierras, tijeras o incluso papel).

Con las interfaces podemos declarar el comportamiento de tipos completamente distintos


Como deben asegurar un comportamiento, todos los miembros declarados en una interfaz son públicos, con lo que no hace falta añadir modificaciones de acceso pues ya se toma por defecto. Si quisiéramos declarar uno con un acceso más restrictivo obtendremos un error.

Dos de las Interfaces más famosas, en mi opinión, son Collection y Comparable. Éstas interfaces utilizan genéricos y para sacarles todo el partido hay que conocer los Tipos Variables, así que iremos profundizando en ellas poco a poco.

Para entender bien qué es y cómo usar una interfaz voy a crear una interfaz llamada Arrancable, que sólo tenga un método: arrancar(). El código necesario para esto es:
public interface Arrancable {

  void arrancar();
 
}
Vemos que el método arrancar() no tiene cuerpo. Es normal y se escribe de esa forma para declarar que es un método abstracto, es decir, sólo defino qué métodos debe tener, pero no cómo se implementan, ese trabajo se deja para las clases que quieran adherirse a este contrato "Arrancable".

Ahora voy a usar nuestra clase Coche para implementar esta interfaz. Implementar una interfaz quiere decir que debe tener una implementación para cada método que falte por definir. Luego veremos porque digo "los que falten". El código para implementarla es:
public class Coche extends VehiculoConRuedas implements Arrancable {

  ...

  @Override
  public void arrancar() {
    System.out.println("Coche arrancado");
  }
}
Declaramos que se implementa una interfaz con implements y el nombre de la interfaz (o varias separando su nombre por comas). Eclipse va a detectar qué métodos faltan por implementar, y me va a ayudar a escribir el código. Aceptamos la opción "Add all unimplementd methods" que nos da el error o los creamos nosotros. Para terminar nos queda rellenar el cuerpo y habremos acabado.

Como se puede ver también aparece la anotación @Override como ya vimos en herencia.

Palabra reservada default

Existe una palabra reservada llamada default que nos va a servir para declarar una implementación por defecto para un método de interfaz.

Por otra parte, nuestra clase que implemente una interfaz podría heredar de una clase abstracta que también la implementase parcialmente (recordamos que las clases abstractas no necesitan tener implementados todos sus métodos)

Por estos dos motivos decía que implementar la interfaz es declarar todos los métodos que faltan, ya que podríamos tener parte del trabajo hecho o incluso todo (la interfaz Serializable no necesita que se implemente ningún método)

Te propongo que practiques lo siguiente:
  1. Comenta la implementación de arrancar() en Coche e implementa Arrancable en VehiculoConRuedas con un código distinto al comentado en Coche para arrancar(). Observa que ya no se pide implementar ningún método en Coche.
  2. Descomenta arrancar() en Coche, coméntala en VehiculoConRuedas y añade una implementación default distinta de las anteriores en Arrancable. Ahora créate un objeto Coche y un objeto VehiculoConRuedas e invoca en los dos el método arrancar(). Deben salirte resultados distintos entre ellos ¿Sabes por qué?

Estos ejemplos son muy básicos y podrías pensar que esto mismo lo lograrías definiendo un método arrancar() directamente en VehiculoConRuedas. Tienes razón. Por eso en la siguiente sesión vamos a ver LO IMPORTANTE de las interfaces haciendo que clases completamente distintas acepten el contrato que marca una interfaz.

Compárteme

Entradas populares