Java: leer un archivo en una ArrayList

    Introducción

    Hay muchas formas de leer y escribir archivos en Java.

    Por lo general, tenemos algunos datos en la memoria, en los que realizamos operaciones, y luego persisten en un archivo. Sin embargo, si queremos cambiar esa información, debemos volver a poner el contenido del archivo en la memoria y realizar las operaciones.

    Si, por ejemplo, nuestro archivo contiene una lista larga que queremos ordenar, tendremos que leerlo en una estructura de datos adecuada, realizar operaciones y luego conservarlo una vez más, en este caso un ArrayList.

    Esto se puede lograr con varios enfoques diferentes:

    • Files.readAllLines()
    • FileReader
    • Scanner
    • BufferedReader
    • ObjectInputStream
    • API de Java Streams

    Files.readAllLines ()

    Desde Java 7, es posible cargar todas las líneas de un archivo en un ArrayList de una manera muy sencilla:

    try {
        ArrayList<String> lines = new ArrayList<>(Files.readAllLines(Paths.get(fileName)));
    }
    catch (IOException e) {
        // Handle a potential exception
    }
    

    También podemos especificar un charset para manejar diferentes formatos de texto, si es necesario:

    try {
        Charset charset = StandardCharsets.UTF_8;
        ArrayList<String> lines = new ArrayList<>(Files.readAllLines(Paths.get(fileName), charset));
    }
    catch (IOException e) {
        // Handle a potential exception
    }
    

    Files.readAllLines() abre y cierra los recursos necesarios automáticamente.

    Escáner

    Tan agradable y simple como era el método anterior, solo es útil para leer el archivo línea por línea. ¿Qué pasaría si todos los datos se almacenaran en una sola línea?

    Scanner es una herramienta fácil de usar para analizar tipos primitivos y cadenas. Utilizando Scanner puede ser tan simple o tan difícil como el desarrollador quiera hacerlo.

    Un ejemplo simple de cuando preferiríamos usar Scanner sería si nuestro archivo tuviera solo una línea, y los datos deben analizarse en algo utilizable.

    Un delimitador es una secuencia de caracteres que Scanner utiliza para separar valores. Por defecto, usa una serie de espacios / tabulaciones como delimitador (espacio en blanco entre valores), pero podemos declarar nuestro propio delimitador y usarlo para analizar los datos.

    Echemos un vistazo a un archivo de ejemplo:

    some-2123-different-values- in - this -text-with a common-delimiter
    

    En tal caso, es fácil notar que todos los valores tienen un delimitador común. Simplemente podemos declarar que “-” rodeado por cualquier número de espacios en blanco es nuestro delimitador.

    // We'll use "-" as our delimiter
    ArrayList<String> arrayList = new ArrayList<>();
    try (Scanner s = new Scanner(new File(fileName)).useDelimiter("\s*-\s*")) {
        // \s* in regular expressions means "any number or whitespaces".
        // We could've said simply useDelimiter("-") and Scanner would have
        // included the whitespaces as part of the data it extracted.
        while (s.hasNext()) {
            arrayList.add(s.next());
        }
    }
    catch (FileNotFoundException e) {
        // Handle the potential exception
    }
    

    Ejecutar este fragmento de código nos daría una ArrayList con estos elementos:

    [some, 2, different, values, in, this, text, with a common, delimiter]
    

    Por otro lado, si solo hubiéramos utilizado el delimitador predeterminado (espacio en blanco), el ArrayList se vería así:

    [some-2-different-values-, in, -, this, -text-with, a, common-delimiter]
    

    Scanner tiene algunas funciones útiles para analizar datos, como nextInt(), nextDouble()etc.

    Importante: Llamando .nextInt() será NO devuelve el siguiente int valor que se puede encontrar en el archivo! Devolverá un int valor solo si los siguientes elementos Scanner “escaneos” es válido int valor, de lo contrario se lanzará una excepción. Una forma sencilla de asegurarse de que no surja una excepción es realizar una comprobación “has” correspondiente, como .hasNextInt() antes de usar realmente .nextInt().

    Aunque no vemos eso cuando llamamos a funciones como scanner.nextInt() o scanner.hasNextDouble(), Scanner utiliza expresiones regulares en segundo plano.

    Muy importante: Un error extremadamente común al usar Scanner ocurre cuando se trabaja con archivos que tienen varias líneas y se utiliza .nextLine() en conjunto con .nextInt(),nextDouble()etc.

    Echemos un vistazo a otro archivo:

    12
    some data we want to read as a string in one line
    10
    

    A menudo, los desarrolladores más nuevos que usan Scanner escribiría código como:

    try (Scanner scanner = new Scanner(new File("example.txt"))) {
        int a = scanner.nextInt();
        String s = scanner.nextLine();
        int b = scanner.nextInt();
    
        System.out.println(a + ", " + s + ", " + b);
    }
    catch (FileNotFoundException e) {
        // Handle a potential exception
    }
    //catch (InputMismatchException e) {
    //    // This will occur in the code above
    //}
    

    Este código parece ser lógicamente sólido: leemos un número entero del archivo, luego la siguiente línea y luego el segundo número entero. Si intenta ejecutar este código, el InputMismatchException se lanzará sin una razón obvia.

    Si comienza a depurar e imprimir lo que ha escaneado, verá que int a bien cargado, pero eso String s esta vacio.

    ¿Porqué es eso? Lo primero que hay que tener en cuenta es que una vez Scanner lee algo del archivo, continúa escaneando el archivo desde el primer carácter después de los datos que escaneó previamente.

    Por ejemplo, si tuviéramos “12 13 14” en un archivo y llamamos .nextInt() una vez, el escáner luego fingiría como si solo hubiera “13 14” en el archivo. Observe que el espacio entre “12” y “13” todavía está presente.

    La segunda cosa importante a tener en cuenta: la primera línea de nuestra example.txt el archivo no solo contiene el número 12, contiene lo que se denomina un “carácter de nueva línea”, y en realidad es 12n en lugar de solo 12.

    Nuestro archivo, en realidad, se ve así:

    12n
    some data we want to read as a string in one linen
    10
    

    Cuando llamamos por primera vez .nextInt(), Scanner lee solo el número 12 y deja el primero n no leído.

    .nextLine() luego lee todos los caracteres que el escáner aún no ha leído hasta que llega al primer n carácter, que salta y luego devuelve los caracteres que leyó. Este es exactamente el problema en nuestro caso: tenemos un sobrante n personaje después de leer el 12.

    Entonces cuando llamamos .nextLine() obtenemos una cadena vacía como resultado ya que Scanner no agrega el n carácter a la cadena que devuelve.

    Ahora el Scanner está al principio de la segunda línea de nuestro archivo, y cuando intentamos llamar .nextInt(), Scanner encuentra algo que no se puede analizar en un int y lanza lo antes mencionado InputMismatchException.

    Soluciones

    • Como sabemos exactamente qué es lo que está mal en este código, podemos codificar una solución. Simplemente “consumiremos” el carácter de nueva línea entre .nextInt() y .nextLine():
    ...
    int a = scanner.nextInt();
    scanner.nextLine(); // Simply consumes the bothersome n
    String s = scanner.nextLine();
    ...
    
    • Dado que sabemos como example.txt está formateado podemos leer el archivo completo línea por línea y analizar las líneas necesarias usando Integer.parseInt():
    ...
    int a = Integer.parseInt(scanner.nextLine());
    String s = scanner.nextLine();
    int b = Integer.parseInt(scanner.nextLine());
    ...
    

    BufferedReader

    BufferedReader lee texto de un flujo de entrada de caracteres, pero lo hace almacenando caracteres en búfer para proporcionar .read() operaciones. Dado que acceder a un HDD es una operación que requiere mucho tiempo, BufferedReader recopila más datos de los que solicitamos y los almacena en un búfer.

    La idea es que cuando llamamos .read() (u operación similar) es probable que volvamos a leer pronto del mismo bloque de datos que acabamos de leer, por lo que los datos “circundantes” se almacenan en un búfer. En caso de que quisiéramos leerlo, lo leeríamos directamente desde el búfer en lugar de desde el disco, que es mucho más eficiente.

    Esto nos lleva a lo que BufferedReader es bueno para leer archivos grandes. BufferedReader tiene una memoria búfer significativamente mayor que Scanner (8192 caracteres por defecto frente a 1024 caracteres por defecto, respectivamente).

    BufferedReader se utiliza como contenedor para otros lectores, por lo que los constructores de BufferedReader tomar un objeto Reader como parámetro, como un FileReader.

    Estamos usando try-with-resources para no tener que cerrar el lector manualmente:

    ArrayList<String> arrayList = new ArrayList<>();
    
    try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
        while (reader.ready()) {
            arrayList.add(reader.readLine());
        }
    }
    catch (IOException e) {
        // Handle a potential exception
    }
    

    Se aconseja envolver un FileReader con un BufferedReader, exactamente debido a los beneficios de rendimiento.

    ObjectInputStream

    ObjectInputStream solo debe usarse junto con ObjectOutputStream. Lo que estas dos clases nos ayudan a lograr es almacenar un objeto (o una matriz de objetos) en un archivo y luego leerlo fácilmente desde ese archivo.

    Esto solo se puede hacer con clases que implementan el Serializable interfaz. los Serializable La interfaz no tiene métodos o campos y solo sirve para identificar la semántica de ser serializable:

    public static class MyClass implements Serializable {
        int someInt;
        String someString;
    
        public MyClass(int someInt, String someString) {
            this.someInt = someInt;
            this.someString = someString;
        }
    }
    
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // The file extension doesn't matter in this case, since they're only there to tell
        // the OS with what program to associate a particular file
        ObjectOutputStream objectOutputStream =
            new ObjectOutputStream(new FileOutputStream("data.olivera"));
    
        MyClass first = new MyClass(1, "abc");
        MyClass second = new MyClass(2, "abc");
    
        objectOutputStream.writeObject(first);
        objectOutputStream.writeObject(second);
        objectOutputStream.close();
    
        ObjectInputStream objectInputStream =
                    new ObjectInputStream(new FileInputStream("data.olivera"));
    
        ArrayList<MyClass> arrayList = new ArrayList<>();
    
        try (objectInputStream) {
            while (true) {
                Object read = objectInputStream.readObject();
                if (read == null)
                    break;
    
                // We should always cast explicitly
                MyClass myClassRead = (MyClass) read;
                arrayList.add(myClassRead);
            }
        }
        catch (EOFException e) {
            // This exception is expected
        }
    
        for (MyClass m : arrayList) {
            System.out.println(m.someInt + " " + m.someString);
        }
    }
    

    API de Java Streams

    Desde Java 8, otra forma rápida y sencilla de cargar el contenido de un archivo en un ArrayList estaría usando la API de Java Streams:

    // Using try-with-resources so the stream closes automatically
    try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
        ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
    }
    catch (IOException e) {
        // Handle a potential exception
    }
    

    Sin embargo, tenga en cuenta que este enfoque, al igual que Files.readAllLines() solo funcionaría si los datos se almacenan en líneas.

    El código anterior no hace nada especial y rara vez usamos transmisiones de esta manera. Sin embargo, dado que estamos cargando estos datos en un ArrayList para que podamos procesarlo en primer lugar, las transmisiones proporcionan una excelente manera de hacerlo.

    Podemos ordenar / filtrar / mapear fácilmente los datos antes de almacenarlos en un ArrayList:

    try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
        ArrayList<String> arrayList = stream.map(String::toLowerCase)
                                            .filter(line -> !line.startsWith("a"))
                                            .sorted(Comparator.comparing(String::length))
                                            .collect(Collectors.toCollection(ArrayList::new));
    }
    catch (IOException e) {
        // Handle a potential exception
    }
    

    Conclusión

    Hay varias formas diferentes en las que puede leer datos de un archivo en un ArrayList. Cuando solo necesita leer las líneas como elementos, use Files.readAllLines; cuando tenga datos que se puedan analizar fácilmente, utilice Scanner; cuando trabaje con archivos grandes utilice FileReader envuelto con BufferedReader; cuando se trata de una serie de objetos, utilice ObjectInputStream (pero asegúrese de que los datos se escribieron usando ObjectOutputStream).

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *