Java. Считать из файла массивы разной длины в ArrayList

Есть csv файл. В файле список химических элементов, который выглядит примерно так:

Element,Number,Symbol,Weight,Boil,Melt,Density Vapour,Fusion, Aluminum,13,Al,26.98,2723.16,933.16,2700,284.34,10.68, Silicon,14,Si,28.09,2953.16,1683.16,2330,170.02,46.48, Phosphorus,15,P,30.98, Sulfur,16,S,32.06,717.76,392.16,2070,12.60,1.42, Chlorine,17,Cl,35.45,238.46,172.16,1560,10.22,3.22,

То есть, массивы, которые я буду отсюда считывать - разной длины. Из за этого возникает ошибка:

Exception in thread "main" java.lang.ExceptionInInitializerError at chemistry.Chemistry.allElements(Chemistry.java:21) at chemistry.Chemistry.main(Chemistry.java:30) Caused by: java.lang.ArrayIndexOutOfBoundsException: 5 at chemistry.ChemicalElement.(ChemicalElement.java:35) at chemistry.ChemicalElementDAO.(ChemicalElementDAO.java:34) ... 2 more

Ошибка возникает из за того, что считываемые в ArrayList массивы - разной длины. Если считываемые массивы одинаковые, ошибки не возникает, все работает.

Мой код: Конструктор

 public ChemicalElement(String element, int number, String symbol, double[]vals){

        this.element = element;
        this.number = number;
        this.symbol = symbol;
        this.weight = vals[0];
        this.boil = vals[1];
        this.melt = vals[2];
        this.density = vals[3];
        this.vapour = vals[4];
        this.fusion = vals[5];  
    }

Код, который считывает данные:

 public static List<ChemicalElement> chemicalElements = new ArrayList();
    
        static{
        try {   
            Scanner scanner = new Scanner(new FileReader("elements.csv"));
            String title = scanner.nextLine();
            while(scanner.hasNext()){
             
            String[] line = scanner.nextLine().split(",");
            String element = line[0];
            int numb = Integer.parseInt(line[1]);
            String symbol = line[2];
            // this creates a proper sized array.
            double[] values = new double[line.length-3];
            for (int i = 0; i < values.length; i++) {
                values[i] = Double.parseDouble(line[i+3]);
            }
            
            ChemicalElement chemicalElement = new ChemicalElement(element,numb,symbol,values);
            chemicalElements.add(chemicalElement);
            }
            
            //(String element, int number, String symbol, double weight, double boil, double melt, double density, double vapour, double fusion)
        } catch (FileNotFoundException ex) {
            System.out.println(ex.getMessage());
        } 
    }

Я думала, что если вместо последних double элементов добавить в конструктор массив, и варьировать количество элементов в массиве, то это поможет. Но нет. Может быть вы сможете подсказать какие-то варианты?


Ответы (2 шт):

Автор решения: Aziz Umarov

вы создаёте массив в зависимости от количества считанных

   double[] values = new double[line.length-3];

а используете фиксированную

    this.weight = vals[0];
    this.boil = vals[1];
    this.melt = vals[2];
    this.density = vals[3];
    this.vapour = vals[4];
    this.fusion = vals[5]; 

оттуда и ошибка

опирайтесь от длины массива и инициализируйте поля в зависимости что имеете

  if (val.length > 0) this.weight = vals[0];
  if (val.length > 1) this.boil = vals[1];
  if (val.length > 2) this.melt = vals[2];
  if (val.length > 3) this.density = vals[3];
  if (val.length > 4) this.vapour = vals[4];
  if (val.length > 5) this.fusion = vals[5]; 
→ Ссылка
Автор решения: Дмитрий

Мне не нравится такое решение причем по очень многим причинам. Самое главное - я предпочел бы универсальный парсер, т.е. он должен уметь читать любой csv-файл, преобразовывая его в список из любых экземпляров класса и при этом модель не должна иметь никаких спец.методов, в т.ч. конструкторов. Огромным минусом является то, что вы преобразуете все в массив, при этом игнорируете первую строку csv-файла с заголовками. В реальной разработке я бы предпочитал использовать для чтения csv библиотеку, а не делать это руками, но для учебного примера это не обязательно.
Модель выглядит так:

@lombok.Data
@lombok.NoArgsConstructor
 public class ChemicalElement {
    
    private String element;
    private Integer number;
    private String symbol;
    private Double weight;
    private Double boil;
    private Double melt;
    private Double density;
    private Double vapour;
    private Double fusion;
    
}

Если вы не знаете, что такое lombok, то поясню, зачем нужны эти аннотации: Data добавляет все геттеры, сеттеры, а также equls, hashcode, toString, а NoArgsConstructor - конструктор без аргументов.

И теперь сам парсер:

import com.google.gson.Gson;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Parser {

    private Gson GSON = new Gson();

    private final String delimiter;
    
    public Parser(String delimiter){
        this.delimiter = delimiter;
    }
    
    public Parser(){
        this.delimiter = ";";
    }

    public static void main(String[] args) throws Exception {
        Parser parser = new Parser(",");
        List<ChemicalElement> result = parser.readFile("elements.csv", ChemicalElement.class);
        System.out.println(result);
    }

    private List<ChemicalElement> readFile(String fileName) throws IOException {
        try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
            return stream
                    .skip(1)
                    .map(line -> new ChemicalElement(line.split(",")))
                    .collect(Collectors.toList());
        }
    }
    
    public <T> List<T> readFile(final String fileName, final Class<T> clazz) throws Exception {
        final List<T> valuesList = new ArrayList<>();
        Scanner scanner = new Scanner(new File(fileName));
        String[] title = scanner.nextLine().split(delimiter);
        while (scanner.hasNext()) {
            final String[] value = scanner.nextLine().split(delimiter);
            final Map<String, Object> values = new LinkedHashMap(value.length);
            for (int k = 0; k < value.length; k++) values.put(title[k], value[k]);
            final T obj = (T) GSON.fromJson(GSON.toJson(values), clazz.newInstance().getClass());
            valuesList.add(obj);
        }
        return valuesList;
    }
    
}

Ваш csv-файл содержал ошибку в заголовке, кроме того, в данном случае необходимо, чтобы имена полей совпадали с заголовками, поэтому все заголовки я исправил и написал с маленькой буквы.

element,number,symbol,weight,boil,melt,density,vapour,fusion, 
Aluminum,13,Al,26.98,2723.16,933.16,2700,284.34,10.68,
Silicon,14,Si,28.09,2953.16,1683.16,2330,170.02,46.48, 
Phosphorus,15,P,30.98,
Sulfur,16,S,32.06,717.76,392.16,2070,12.60,1.42, 
Chlorine,17,Cl,35.45,238.46,172.16,1560,10.22,3.22,

Я использую сериализатор gson для создания объектов, ссылка на репозиторий: https://mvnrepository.com/artifact/com.google.code.gson/gson/2.8.6

Вот и все. В парсере есть метод main, в нем видно, что помимо имени файла передается ChemicalElement.class. Именно список экземпляров этого класса вы получите. Главное не забывать простое правило - имена полей вашего класса должны совпадать с заголовками файла. Теперь вы можете из любого csv - файла получить коллекцию обеъктов, используя один и тот же парсер, добавив лишь модель (класс с полями) и ничего больше.

В случае с вашим примером решение на много проще. Метод парсера выглядит так:

private static List<ChemicalElement> readFile(String fileName) throws IOException {
    try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
        return stream
                .skip(1)
                .map(line -> new ChemicalElement(line.split(",")))
                .collect(Collectors.toList());
    }
}

а модель так:

@lombok.Data
@lombok.NoArgsConstructor
public class ChemicalElement {
    
    private String element;
    private Integer number;
    private String symbol;
    private Double weight;
    private Double boil;
    private Double melt;
    private Double density;
    private Double vapour;
    private Double fusion;
    
    public ChemicalElement(String[] vals) {
        try {
            this.element = vals[0];
            this.number = parseInteger(vals[1]);
            this.symbol = vals[2];
            this.weight = parseDouble(vals[3]);
            this.boil = parseDouble(vals[4]);
            this.melt = parseDouble(vals[5]);
            this.density = parseDouble(vals[6]);
            this.vapour = parseDouble(vals[7]);
            this.fusion = parseDouble(vals[8]);
        } catch (ArrayIndexOutOfBoundsException e) {}
    }
    
    private static Double parseDouble(String value) {
        try {
            return Double.valueOf(value);
        } catch (NumberFormatException | NullPointerException e) {
            return null;
        }
    }

    private static Integer parseInteger(String value) {
        try {
            return Integer.valueOf(value);
        } catch (NumberFormatException | NullPointerException e) {
            return null;
        }
    }
    
}

Мне показалось проще отловить исключение, чем написать целый набор условных операторов. Плюс методы для безопасного распарсивания String в Double и Integer явно не повредят.

→ Ссылка