Capítulo 3 Cadenas, comparadores y filtros
Algunos métodos introducidos por el JDK son muy útiles para escribir código de estilo funcional. Ya estamos muy familiarizados con algunas clases e interfaces de la biblioteca JDK, como String. Para deshacernos del estilo antiguo al que estamos acostumbrados, tenemos que buscar activamente oportunidades para utilizar estos nuevos métodos. De manera similar, cuando necesitamos usar una clase interna anónima con un solo método, ahora podemos reemplazarla con una expresión lambda, sin tener que escribirla tan engorrosa como antes.
En este capítulo, usaremos expresiones lambda y referencias de métodos para recorrer cadenas, implementar la interfaz Comparator, ver archivos en el directorio y monitorear cambios en archivos y directorios. Algunos de los métodos introducidos en el capítulo anterior seguirán apareciendo aquí para ayudarnos a completar mejor estas tareas. Las nuevas técnicas que aprenda le ayudarán a convertir código largo y tedioso en algo conciso, rápido de implementar y fácil de mantener.
Iterar sobre cadena
El método chars() es un método nuevo en la clase String, que forma parte de la interfaz CharSequence. Es una herramienta muy útil si desea recorrer rápidamente la secuencia de caracteres de String. Con este iterador interno, podemos operar cómodamente con cada carácter de la cadena. Intente usarlo para procesar una cadena primero. A continuación se muestran algunas formas de utilizar referencias de métodos.
Copie el código de código de la siguiente manera:
cadena final str = "w00t";
str.caracteres()
.forEach(ch -> System.out.println(ch));
El método chars() devuelve un objeto Stream, que podemos usar su iterador interno forEach() para recorrer. En el iterador, podemos acceder directamente a los caracteres de la cadena. A continuación se muestra el resultado de recorrer la cadena e imprimir cada carácter.
Copie el código de código de la siguiente manera:
119
48
48
116
Este no es el resultado que queremos. Esperamos ver letras, pero el resultado son números. Esto se debe a que el método chars() devuelve un Stream entero en lugar de un tipo de carácter. Primero comprendamos esta API y luego optimicemos los resultados de salida.
En el código anterior, creamos una expresión lambda como parámetro de entrada del método forEach. Simplemente pasa los parámetros a un método println(). Dado que esta operación es muy común, podemos utilizar el compilador de Java para simplificar este código. Al igual que al usar referencias de métodos en la página 25, reemplácela con una referencia de método y deje que el compilador haga el enrutamiento de parámetros por nosotros.
Hemos visto cómo crear una referencia de método a un método de instancia. Por ejemplo, el método name.toUpperCase(), la referencia del método es String::toUpperCase. En el siguiente ejemplo, llamamos a un método de instancia que hace referencia estática a System.out. El lado izquierdo de los dos dos puntos a los que hace referencia el método puede ser un nombre de clase o una expresión. Con esta flexibilidad, podemos crear fácilmente una referencia al método println(), como se muestra a continuación.
Copie el código de código de la siguiente manera:
str.caracteres()
.forEach(System.out::println);
Como puede ver, el compilador de Java puede completar el enrutamiento de parámetros de manera muy inteligente. Recuerde que las expresiones lambda y las referencias a métodos solo pueden aparecer cuando se recibe una interfaz funcional, y el compilador de Java generará un método correspondiente allí (Anotación: el compilador generará una implementación de la interfaz funcional, que tiene solo un método). El método que usamos antes se refiere a String::toUpperCase, y los parámetros pasados al método generado eventualmente se convertirán en el objeto de destino de la llamada al método, como este: parámetro.toUpperCase(). Esto se debe a que la referencia del método se basa en el nombre de la clase (Cadena). La referencia del método en el ejemplo anterior se basa en una expresión, que es una instancia de PrintStream y se hace referencia a ella a través de System.out. Dado que el objeto de la llamada al método ya existe, el compilador de Java decide utilizar los parámetros del método generado como parámetros de este método println: System.out.println(nombre).
(Anotación: De hecho, existen principalmente dos escenarios. También se pasa una referencia a un método. Uno es el objeto atravesado, por supuesto el objeto de destino de la llamada al método, como name.toUpperCase, y el otro se usa como parámetro de la llamada al método, como System.println(nombre).)
El código es mucho más simple después de usar referencias de métodos, pero necesitamos tener una comprensión más profunda de cómo funciona. Una vez que nos familiaricemos con las referencias de métodos, podremos descubrir el enrutamiento de parámetros por nuestra cuenta.
Aunque el código de este ejemplo es bastante conciso, el resultado sigue siendo insatisfactorio. Esperábamos ver letras pero en su lugar aparecieron números. Para resolver este problema, escribamos un método para generar int como letras.
Copie el código de código de la siguiente manera:
printChar vacío estático privado (int aChar) {
System.out.println((char)(aChar));
}
El uso de referencias de métodos puede optimizar fácilmente los resultados de salida.
Copie el código de código de la siguiente manera:
str.caracteres()
.forEach(IterateString::printChar);
Ahora, aunque el resultado devuelto por chars() es int, no importa. Cuando necesitemos imprimir, lo convertiremos en caracteres. Esta vez el resultado final son letras.
Copie el código de código de la siguiente manera:
w
0
0
t
Si queremos procesar caracteres en lugar de ints desde el principio, podemos convertir ints directamente en caracteres después de llamar a chars:
Copie el código de código de la siguiente manera:
str.caracteres()
.mapToObj(ch -> Carácter.valueOf((char)ch))
.forEach(System.out::println);
Aquí usamos un iterador interno del Stream devuelto por chars(). Por supuesto, se pueden usar más métodos que este. Después de obtener el objeto Stream, podemos usar sus métodos, como map(), filter(), reduce(), etc. Podemos usar el método filter() para filtrar caracteres que son números:
Copie el código de código de la siguiente manera:
str.caracteres()
.filter(ch -> Carácter.isDigit(ch))
.forEach(ch -> printChar(ch));
Al generar de esta manera, solo podemos ver números:
Copie el código de código de la siguiente manera:
0
0
De manera similar, además de pasar expresiones lambda a los métodos filter() y forEach(), también podemos usar referencias de métodos.
Copie el código de código de la siguiente manera:
str.caracteres()
.filtro(Carácter::isDigit)
.forEach(IterateString::printChar);
La referencia del método aquí elimina el enrutamiento de parámetros redundantes. En este ejemplo, también vemos un uso diferente al de los dos métodos anteriores. La primera vez que hicimos referencia a un método de instancia, la segunda vez fue un método de referencia estática (System.out). Esta vez es una referencia a un método estático: las referencias a métodos se han estado pagando en silencio.
Las referencias a métodos de instancia y métodos estáticos tienen el mismo aspecto: por ejemplo, String::toUpperCase y Character::isDigit. El compilador determina si el método es un método de instancia o un método estático para determinar cómo enrutar los parámetros. Si es un método de instancia, utilizará los parámetros de entrada del método generado como el objeto de destino de la llamada al método, como el parámetro toUpperCase() (Por supuesto, hay excepciones, como el objeto de destino de la llamada al método; se ha especificado, como System::out.println ()). Además, si es un método estático, los parámetros de entrada del método generado se utilizarán como parámetros del método referenciado, como Character.isDigit (parámetro). El Apéndice 2 en la página 152 tiene instrucciones detalladas sobre cómo utilizar las referencias de métodos y su sintaxis.
Aunque las referencias a métodos son convenientes de usar, todavía existe un problema: la ambigüedad causada por conflictos de nombres de métodos. Si el método coincidente es tanto un método de instancia como un método estático, el compilador informará un error debido a la ambigüedad del método. Por ejemplo, si escribimos Double::toString así, en realidad queremos convertir un tipo doble en una cadena, pero el compilador no sabe si llamar al método de instancia de public String toString() o llamar a public static String. toString.(doble), porque ambos métodos son de clase Doble. Si se encuentra con una situación así, no se desanime, solo use expresiones lambda para completarla.
Una vez que nos sintamos cómodos con la programación funcional, podemos alternar entre expresiones lambda y referencias de métodos a voluntad.
En esta sección utilizamos un nuevo método en Java 8 para iterar sobre cadenas. Echemos un vistazo a las mejoras en la interfaz Comparator.