Чтение большого количества данных из файла в Java
У меня есть текстовый файл, который содержит 1 000 002
номера в следующем порядке:
123 456
1 2 3 4 5 6 .... 999999 100000
Теперь мне нужно прочитать эти данные и выделить их int
переменные (самые первые два числа) и все остальные (1 000 000 чисел) в массив int[]
,
Это не сложная задача, но - это ужасно медленно.
Моя первая попытка была java.util.Scanner
:
Scanner stdin = new Scanner(new File("./path"));
int n = stdin.nextInt();
int t = stdin.nextInt();
int array[] = new array[n];
for (int i = 0; i < n; i++) {
array[i] = stdin.nextInt();
}
Он работает как исключение, но для его выполнения требуется около 7500 мс. Мне нужно получить эти данные за несколько сотен миллисекунд.
Потом я попробовал java.io.BufferedReader
:
С помощью BufferedReader.readLine()
а также String.split()
Я получил те же результаты примерно за 1700 мс, но это все еще слишком много.
Как я могу прочитать этот объем данных менее чем за 1 секунду? Окончательный результат должен быть равен:
int n = 123;
int t = 456;
int array[] = { 1, 2, 3, 4, ..., 999999, 100000 };
Согласно ответу trashgod:
StreamTokenizer
Решение быстрое (занимает около 1400 мс), но все равно слишком медленное:
StreamTokenizer st = new StreamTokenizer(new FileReader("./test_grz"));
st.nextToken();
int n = (int) st.nval;
st.nextToken();
int t = (int) st.nval;
int array[] = new int[n];
for (int i = 0; st.nextToken() != StreamTokenizer.TT_EOF; i++) {
array[i] = (int) st.nval;
}
PS. Там нет необходимости для проверки. Я на 100% уверен, что данные в ./test_grz
файл правильный.
7 ответов
Спасибо за каждый ответ, но я уже нашел метод, который соответствует моим критериям:
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("./path"));
int n = readInt(bis);
int t = readInt(bis);
int array[] = new int[n];
for (int i = 0; i < n; i++) {
array[i] = readInt(bis);
}
private static int readInt(InputStream in) throws IOException {
int ret = 0;
boolean dig = false;
for (int c = 0; (c = in.read()) != -1; ) {
if (c >= '0' && c <= '9') {
dig = true;
ret = ret * 10 + c - '0';
} else if (dig) break;
}
return ret;
}
Для считывания 1 млн целых чисел требуется всего около 300 мс!
Вы можете сократить время на StreamTokenizer
результат с помощью BufferedReader
:
Reader r = null;
try {
r = new BufferedReader(new FileReader(file));
final StreamTokenizer st = new StreamTokenizer(r);
...
} finally {
if (r != null)
r.close();
}
Кроме того, не забудьте закрыть свои файлы, как я показал здесь.
Вы также можете сэкономить время, используя специальный токенизатор только для своих целей:
public class CustomTokenizer {
private final Reader r;
public CustomTokenizer(final Reader r) {
this.r = r;
}
public int nextInt() throws IOException {
int i = r.read();
if (i == -1)
throw new EOFException();
char c = (char) i;
// Skip any whitespace
while (c == ' ' || c == '\n' || c == '\r') {
i = r.read();
if (i == -1)
throw new EOFException();
c = (char) i;
}
int result = (c - '0');
while ((i = r.read()) >= 0) {
c = (char) i;
if (c == ' ' || c == '\n' || c == '\r')
break;
result = result * 10 + (c - '0');
}
return result;
}
}
Не забудьте использовать BufferedReader
за это. Этот пользовательский токенайзер предполагает, что входные данные всегда полностью действительны и содержат только пробелы, новые строки и цифры.
Если вы много читаете эти результаты и эти результаты не сильно меняются, вам, вероятно, следует сохранить массив и отследить время последнего изменения файла. Затем, если файл не изменился, просто используйте кэшированную копию массива, и это значительно ускорит результаты. Например:
public class ArrayRetriever {
private File inputFile;
private long lastModified;
private int[] lastResult;
public ArrayRetriever(File file) {
this.inputFile = file;
}
public int[] getResult() {
if (lastResult != null && inputFile.lastModified() == lastModified)
return lastResult;
lastModified = inputFile.lastModified();
// do logic to actually read the file here
lastResult = array; // the array variable from your examples
return lastResult;
}
}
Если возможно переформатировать ввод, чтобы каждое целое число находилось на отдельной строке (вместо одной длинной строки с одним миллионом целых), вы должны увидеть гораздо более высокую производительность, используя Integer.parseInt(BufferedReader.readLine())
из-за более умной буферизации строкой и отсутствия необходимости разбивать длинную строку на отдельный массив строк.
Изменить: я проверил это и удалось прочитать вывод, произведенный seq 1 1000000
в массив int
ну под полсекунды, но конечно это зависит от машины.
Сколько памяти у вас в компьютере? Вы можете столкнуться с проблемами GC.
Лучше всего, если это возможно, обрабатывать данные по одной строке за раз. Не загружайте его в массив. Загрузите то, что вам нужно, обработайте, запишите и продолжайте.
Это уменьшит ваш объем памяти и все равно будет использовать тот же объем файлового ввода-вывода
Я бы расширил FilterReader и проанализировал строку так, как она читается в методе read(). Пусть метод getNextNumber вернет числа. Код оставлен в качестве упражнения для читателя.
Использование StreamTokenizer в BufferedReader даст вам довольно хорошую производительность. Вам не нужно писать свою собственную функцию readInt().
Вот код, который я использовал для локального тестирования производительности:
/**
* Created by zhenhua.xu on 11/27/16.
*/
public class MyReader {
private static final String FILE_NAME = "./1m_numbers.txt";
private static final int n = 1000000;
public static void main(String[] args) {
try {
readByScanner();
readByStreamTokenizer();
readByStreamTokenizerOnBufferedReader();
readByBufferedInputStream();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void readByScanner() throws Exception {
long startTime = System.currentTimeMillis();
Scanner stdin = new Scanner(new File(FILE_NAME));
int array[] = new int[n];
for (int i = 0; i < n; i++) {
array[i] = stdin.nextInt();
}
long endTime = System.currentTimeMillis();
System.out.println(String.format("Total time by Scanner: %d ms", endTime - startTime));
}
public static void readByStreamTokenizer() throws Exception {
long startTime = System.currentTimeMillis();
StreamTokenizer st = new StreamTokenizer(new FileReader(FILE_NAME));
int array[] = new int[n];
for (int i = 0; st.nextToken() != StreamTokenizer.TT_EOF; i++) {
array[i] = (int) st.nval;
}
long endTime = System.currentTimeMillis();
System.out.println(String.format("Total time by StreamTokenizer: %d ms", endTime - startTime));
}
public static void readByStreamTokenizerOnBufferedReader() throws Exception {
long startTime = System.currentTimeMillis();
StreamTokenizer st = new StreamTokenizer(new BufferedReader(new FileReader(FILE_NAME)));
int array[] = new int[n];
for (int i = 0; st.nextToken() != StreamTokenizer.TT_EOF; i++) {
array[i] = (int) st.nval;
}
long endTime = System.currentTimeMillis();
System.out.println(String.format("Total time by StreamTokenizer with BufferedReader: %d ms", endTime - startTime));
}
public static void readByBufferedInputStream() throws Exception {
long startTime = System.currentTimeMillis();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(FILE_NAME));
int array[] = new int[n];
for (int i = 0; i < n; i++) {
array[i] = readInt(bis);
}
long endTime = System.currentTimeMillis();
System.out.println(String.format("Total time with BufferedInputStream: %d ms", endTime - startTime));
}
private static int readInt(InputStream in) throws IOException {
int ret = 0;
boolean dig = false;
for (int c = 0; (c = in.read()) != -1; ) {
if (c >= '0' && c <= '9') {
dig = true;
ret = ret * 10 + c - '0';
} else if (dig) break;
}
return ret;
}
Результаты, которые я получил:
- Общее время по сканеру: 789 мс
- Общее время по StreamTokenizer: 226 мс
- Общее время по StreamTokenizer с BufferedReader: 80 мс
- Общее время по BufferedInputStream: 95 мс