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 *