Java 8 – Cómo ordenar la lista con Stream.sorted()

En esta guía tutorial, cubriremos todo lo que necesita saber sobre la API Stream.sorted(). Ordenaremos enteros, cadenas y objetos personalizados en orden ascendente y descendente, así como también definiremos comparadores personalizados en Java.

Introducción

Una secuencia representa una secuencia de elementos y admite diferentes tipos de operaciones que conducen al resultado deseado.

La fuente de estos elementos suele ser una Colección o una Array, desde la cual se proporcionan datos a la transmisión.

Los flujos se diferencian de las colecciones en varios aspectos; sobre todo porque los flujos no son una estructura de datos que almacena elementos. Son de naturaleza funcional, y vale la pena señalar que las operaciones en un flujo producen un resultado, pero no modifican su fuente.

Ordenar una lista de enteros con Stream.sorted()

Encontrado dentro de la interfaz Stream, el método sorted() tiene dos variaciones sobrecargadas que estaremos investigando.

Ambas variaciones son métodos de instancia, que requieren que se cree un objeto de su clase antes de poder usarlo:

1
public final Stream<T> sorted() {}

Este método devuelve una secuencia que consta de los elementos de la secuencia, ordenados de acuerdo con el orden natural, el orden proporcionado por la JVM. Si los elementos de la secuencia no son Comparables, se puede generar una java.lang.ClassCastException al ejecutarse.

Usar este método es bastante simple, así que echemos un vistazo a un par de ejemplos:

1
Arrays.asList(10, 23, -4, 0, 18).stream().sorted().forEach(System.out::println);

Aquí, hacemos una instancia de List a través del método asList(), proporcionando algunos enteros y stream(). Una vez transmitido, podemos ejecutar el método sorted(), que ordena estos números enteros de forma natural. Una vez ordenados, simplemente los imprimimos, cada uno en una línea:

1
2
3
4
5
-4
0
10
18
23

Si quisiéramos guardar los resultados de la clasificación después de ejecutar el programa, tendríamos que ‘recopilar()’ los datos en una ‘Colección’ (una ‘Lista’ en este ejemplo), ya que ‘clasificado()’ no lo hace. t modificar la fuente.

Guardemos este resultado en una lista ordenada:

1
2
3
4
5
List<Integer> list = Arrays.asList(10, 23, -4, 0, 18);
List<Integer> sortedList = list.stream().sorted().collect(Collectors.toList());

System.out.println(list);
System.out.println(sortedList);

Ejecutar este código producirá:

1
2
[10, 23, -4, 0, 18]
[-4, 0, 10, 18, 23]

Aquí vemos que la lista original no se modificó, pero guardamos los resultados de la clasificación en una nueva lista, lo que nos permite usar ambos si los necesitamos más adelante.

Ordenar una lista de enteros en orden descendente con Stream.sorted()

Stream.sorted() por defecto ordena en orden natural. En el caso de nuestros números enteros, esto significa que están ordenados en orden ascendente.

A veces, es posible que desee cambiar esto y ordenar en orden descendente. Hay dos formas sencillas de hacer esto: proporcione un Comparador y cambie el orden, que veremos en una sección posterior, o simplemente use Collections.reverseOrder() en la llamada sorted():

1
2
3
4
5
6
List<Integer> list = Arrays.asList(10, 23, -4, 0, 18);
List<Integer> sortedList = list.stream()
        .sorted(Collections.reverseOrder())
        .collect(Collectors.toList());

System.out.println(sortedList);

Esto resulta en:

1
[23, 18, 10, 0, -4]

Ordenar una lista de cadenas con Stream.sorted()

Sin embargo, no siempre ordenamos números enteros. Ordenar cadenas es un poco diferente, ya que es un poco menos intuitivo sobre cómo compararlas.

Aquí, el método sorted() también sigue el orden natural, tal como lo impone la JVM. En el caso de las cadenas, se ordenan lexicográficamente:

1
Arrays.asList("John", "Mark", "Robert", "Lucas", "Brandon").stream().sorted().forEach(System.out::println);

Ejecutar esto produce:

1
2
3
4
5
Brandon
John
Lucas
Mark
Robert

Si quisiéramos guardar la lista recién ordenada, aquí se aplica el mismo procedimiento que con los números enteros:

1
2
3
4
List<String> list = Arrays.asList("John", "Mark", "Robert", "Lucas", "Brandon");
List<String> sortedList = list.stream().sorted().collect(Collectors.toList());

System.out.println(sortedList);

Esto resulta en:

1
[Brandon, John, Lucas, Mark, Robert]

Ordenar cadenas en orden inverso es tan simple como ordenar enteros en orden inverso:

1
2
3
4
5
6
List<String> list = Arrays.asList("John", "Mark", "Robert", "Lucas", "Brandon");
List<String> sortedList = list.stream()
        .sorted(Collections.reverseOrder())
        .collect(Collectors.toList());
        
System.out.println(sortedList);

Esto resulta en:

1
[Robert, Mark, Lucas, John, Brandon]

Clasificación de objetos personalizados con Stream.sorted(Comparator<? super T> comparador)

En todos los ejemplos anteriores, hemos trabajado con tipos Comparables. Sin embargo, si estamos trabajando con algunos objetos personalizados, que pueden no ser ‘Comparables’ por diseño, y todavía nos gustaría ordenarlos usando este método, necesitaremos proporcionar un ‘Comparador’ al ‘ordenado ( )` llamar.

Definamos una clase Usuario, que no sea Comparable y veamos cómo podemos ordenarlos en una Lista, usando Stream.sorted():

1
2
3
4
5
6
7
public class User {
    
    private String name;
    private int age;

    // Constructor, getters, setters and toString()
}

En la primera iteración de este ejemplo, digamos que queremos ordenar a nuestros usuarios por su edad. Si la edad de los usuarios es la misma, el primero que se agregó a la lista será el primero en el orden ordenado. Digamos que tenemos el siguiente código:

Vamos a ordenarlos por edad, primero. Si su edad es la misma, el orden de inserción en la lista es lo que define su posición en la lista ordenada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
List<User> userList = new ArrayList<>(Arrays.asList(
        new User("John", 33), 
        new User("Robert", 26), 
        new User("Mark", 26), 
        new User("Brandon", 42)));

List<User> sortedList = userList.stream()
        .sorted(Comparator.comparingInt(User::getAge))
        .collect(Collectors.toList());

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

Cuando ejecutamos esto, obtenemos el siguiente resultado:

1
2
3
4
User:[name: Robert, age: 26]
User:[name: Mark, age: 26]
User:[name: John, age: 33]
User:[name: Brandon, age: 42]

Aquí, hemos hecho una lista de objetos Usuario. Estamos transmitiendo esa lista y usando el método sorted() con un Comparator. Específicamente, usamos el método comparingInt() y proporcionamos la edad del usuario a través de la referencia del método User::getAge.

Hay algunos de estos comparadores integrados que funcionan con números (int, double y long): comparingInt(), comparingDouble() y comparingLong(). En última instancia, también puede simplemente usar el método comparing(), que acepta una función de clave de clasificación, al igual que las otras.

Todos ellos simplemente devuelven un comparador, con la función pasada como clave de clasificación. En nuestro caso, estamos usando el método getAge() como clave de clasificación.

También podemos revertir fácilmente este orden, simplemente encadenando el método reversed() después de la llamada comparingInt():

1
2
3
4
5
List<User> sortedList = userList.stream()
        .sorted(Comparator.comparingInt(User::getAge).reversed())
        .collect(Collectors.toList());

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

Esto resulta en:

1
2
3
4
User:[name: Brandon, age: 42]
User:[name: John, age: 33]
User:[name: Robert, age: 26]
User:[name: Mark, age: 26]

Definición de un comparador personalizado con Stream.sorted() {#definición de un comparador personalizado con streamsorted}

Mientras que Comparators producidos por métodos como comparing() y comparingInt(), son súper simples de trabajar y solo requieren una clave de clasificación - a veces, el comportamiento automatizado no es lo que estamos buscando .

Si ordenamos los ‘Usuarios’ y dos de ellos tienen la misma edad, ahora están ordenados por el orden de inserción, no por su orden natural, en función de sus nombres. Mark debería estar antes de Robert, en una lista ordenada por nombre, pero en la lista que hemos ordenado anteriormente, es al revés.

Para casos como estos, querremos escribir un ‘Comparador’ personalizado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
List<User> userList = new ArrayList<>(Arrays.asList(
        new User("John", 33),
        new User("Robert", 26),
        new User("Mark", 26),
        new User("Brandon", 42)));

List<User> sortedList = userList.stream()
        .sorted((o1, o2) -> {
            if(o1.getAge() == o2.getAge())
                return o1.getName().compareTo(o2.getName());
            else if(o1.getAge() > o2.getAge())
                return 1;
            else return -1;
        })
        .collect(Collectors.toList());

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

Y ahora, cuando ejecutamos este código, tenemos ordenado el orden natural de los nombres, así como las edades:

1
2
3
4
User:[name: Mark, age: 26]
User:[name: Robert, age: 26]
User:[name: John, age: 33]
User:[name: Brandon, age: 42]

Aquí, hemos usado una expresión Lambda para crear un nuevo ‘Comparador’ implícitamente y definimos la lógica para ordenar/comparar. Devolver un número positivo indica que un elemento es mayor que otro. Devolver un número negativo indica que un elemento es menor que otro.

Hemos utilizado los respectivos enfoques de comparación para los nombres y las edades: comparando nombres lexicográficamente usando compareTo(), si los valores de age son los mismos, y comparando edades regularmente a través del operador >.

Si no está acostumbrado a las expresiones Lambda, puede crear un ‘Comparador’ de antemano, aunque, en aras de la legibilidad del código, se recomienda acortarlo a Lambda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Comparator<User> customComparator = new Comparator<User>() {
    @Override
    public int compare(User o1, User o2) {
        if(o1.getAge() == o2.getAge())
            return o1.getName().compareTo(o2.getName());
        else if(o1.getAge() > o2.getAge())
            return 1;
        else return -1;
    }
};

List<User> sortedList = userList.stream()
        .sorted(customComparator)
        .collect(Collectors.toList());

También puede realizar técnicamente una instanciación anónima del comparador en la llamada sorted():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List<User> sortedList = userList.stream()
        .sorted(new Comparator<User>() {
            @Override
            public int compare(User o1, User o2) {
                if(o1.getAge() == o2.getAge())
                    return o1.getName().compareTo(o2.getName());
                else if(o1.getAge() > o2.getAge())
                    return 1;
                else return -1;
            }
        })
        .collect(Collectors.toList());

Y esta llamada anónima es exactamente lo que se acorta a la expresión Lambda desde el primer enfoque.

Conclusión

En este tutorial, cubrimos todo lo que necesita saber sobre el método Stream.sorted(). Hemos ordenado enteros y cadenas “comparables”, en orden ascendente y descendente, y también hemos utilizado un “comparador” incorporado para objetos personalizados.

Finalmente, hemos usado un ‘Comparador’ personalizado y una lógica de clasificación personalizada definida.