Category Archive: Java

Факторизация числа на Java

Факторизация - это разложение числа на произведение простых чисел.

Например: число 100 разлагается на 2 * 2 * 5 * 5.

Ниже приведена реализация на Java с алгоритмической сложностью sqrt(N).

Мы перебираем делители он 2 до квадратного корня из факторизируемого числа, и делим число на делитель, пока это возможно.

В конце остается либо 1, либо простое число, которое добавляется в результат.

List<Integer> factorization(int num) {
    List<Integer> factors = new ArrayList<>();
    for (int i = 2; i <= Math.sqrt(num); i++) {
        while(num % i == 0) {
            num /= i;
            factors.add(i);
        }
    }
    if(num != 1) {
        factors.add(num);
    }
    return factors;
}

Коллекция утилитарных классов Anar

Описание коллекции на GitHub: https://github.com/Nyquest/anar

Эффективность фильтра Блума в памяти

В предыдущей статье был рассмотрен расчет оптимальных параметров фильтра Блума и его реализация.

Со статьей можно ознакомиться здесь: Фильтр Блума

Для оценки эффективности будем рассматривать сколько оперативной памяти потребляют различные структуры данных в Java 11.

Для примера будем размещать в памяти информацию о 20 миллионах ИИН и делать наблюдения через VisualVM.

ИИН представляет собой строку из 12 символов.

Хранение в ArrayList<String>

Хранение 20 миллионов ИИН в ArrayList:

Создается 20 миллионов объектов String, каждый объект String ссылается на массив байт из 12 элементов.

ЭлементРазмерКол-воОбщий размер
String20 B20 000 000553 MB
byte[12]36 B20 000 000687 MB

Суммарно данный способ занимает в памяти примерно 1240 MБ.

Хранение в HashSet<String>

Хранение 20 миллионов ИИН в HashSet<String>:

Создается 20 миллионов объектов таких же как и для ArrayList, дополнительно хранятся узлы корзины HashMap.Node (под капотом HashSet находится HashMap).

ЭлементРазмерКол-воОбщий размер
String20 B20 000 000553 MB
byte[12]36 B20 000 000687 MB
HashMap.Node44 B20 000 000839 MB

Суммарно данный способ занимает в памяти примерно 2079 MБ.

Хранение в HashSet<Long>

Хранение 20 миллионов ИИН в HashSet<Long>:

Создается 20 миллионов объектов Long и HashMap.Node.

ЭлементРазмерКол-воОбщий размер
Long24 B20 000 000458 MB
HashMap.Node44 B20 000 000839 MB

Суммарно данный способ занимает в памяти примерно 1297 MБ.

Хранение в ArrayList<Long>

Создается 20 миллионов объектов Long.

ЭлементРазмерКол-воОбщий размер
Long24 B20 000 000458 MB

Суммарно данный способ занимает в памяти примерно 458 MБ.

Хранение в long[]

Создается массив из 20 миллионов элементов типа long.

ЭлементРазмерКол-воОбщий размер
long8 B20 000 000152 MB

Суммарно данный способ занимает в памяти примерно 152 MБ.

Хранение в MyBloomFilter

В реализации MyBloomFilter используется BitSet, BitSet в свою очередь хранит биты в виде массива long, поэтому основную память забирает этот массив.

В нашей реализации BitSet хранит информацию о 268 435 456 битах.

В один long помещается 64 бит.

268 435 456 / 64 = 4 194 304 (кол-во элементов в массиве)

Практически нет дополнительных потерь при хранении байтов (24 байта транится на сам массив).

268 435 456 бит / 8 = 33 554 432 байт

Суммарно данный способ занимает в памяти примерно 32 MБ.

Хранение в GoogleBloomFilter

Как было рассмотрено в предыдущей статье о фильтре Блума, необходимо рассчитать оптимальное кол-во бит для хранения.

Для 20 миллионов ИИН с вероятностью ошибок 1% оптимальное кол-во бит составляет 191 701 168.

BloomFilter из набора guava для хранения бит использует AtomicLongArray, который в свою очередь использует для хранения бит также long массив (long[]).

Рассчитаем кол-во элементов в массиве:

⌈191 701 168 / 64⌉  = ⌈2 995 330.75⌉ =  2 995 331

Теперь рассчитаем размер в байтах:

191 701 168 бит / 8 = 23 962 646 байт

Суммарно данный способ занимает в памяти примерно 23 MБ.

Итоговая сравнительная таблица

Структура данныхПотребляемая памятьПримечание
HashSet<String>2079 MБОптимальные операции добавления и поиска, но высокое потребление памяти
HashSet<Long>1297 MБДополнительная работа с Long, из-за наличия у ИИН ведущих нулей
ArrayList<String>1240 MБСписок должен быть отсортирован, что была возможность поиска
ArrayList<Long>458 MБСписок должен быть отсортирован. Дополнительная работа с Long, из-за наличия у ИИН ведущих нулей.
long[]152 MБФиксированный размер.
MyBloomFilter32 MБНеоптимальная реализация
Bloom Filter (Google Guava)23 MБОптимальная реализация

Заключение

В данной статье было рассмотрено несколько вариантов хранения информации о 20 миллионах ИИН, где вместо 1-2 гигабайт, можно обойтись 20-30 мегабайтами (экономия на порядок) при допущении заданной вероятности ошибок.

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

Фильтр Блума имеет достаточно специфические варианты использования, и есть небольшая вероятность, что он вам пригодится.

Фильтр Блума на Java

Давно хотелось попробовать реализовать фильтр Блума (хотя я его называют фильтром Блюма).

Фильтр Блума - это вероятностная структура данных, позволяющая проверить принадлежность элемента к множеству. Данная структура может точно ответить на запрос об отсутствии элемента в множестве, но может ошибаться с заданной нами вероятностью о наличии элемента во множестве.

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

Фильтр Блума может использоваться, например, для подсчета уникальных посетителей, для проверки орфографии, в различных СУБД для уменьшения обращения к ПЗУ (постоянное запоминающее устройство).

Будем пробовать фильтр Блума на примере хранения 20 миллионов ИИНов.

ИИН - индивидуальный идентификационный номер физического лица в Республике Казахстан, состоящий из 12 цифр.

Я заранее сгенерировал список из 20 миллионов ИИНов (102 MB).

Скачать список можно здесь: https://drive.google.com/file/d/1SIfp_CRl4cQetc3Kw2jqDcWpF3fYPc2s/view?usp=sharing

Исходные коды генератора можно посмотреть в github: https://github.com/Nyquest/iin_generator

Фильтр Блума должен преобразовать наше множество из 20 миллионов ИИН в набор битов с помощью нескольких хэш-функций.

ИИН считается принадлежащим множеству, если значения всех вычисленных хэш-функции от ИИНа указывают на номера отмеченных бит. Если хотя бы один бит не отмечен, ИИН однозначно не принадлежит множеству.

Формулы

Оптимальное количество бит и достаточное кол-во хэш-функций можно рассчитать по формулам.

Формула оптимального количества бит:

    \[    \displaystyle m=-{\frac {n\ln \varepsilon }{(\ln 2)^{2}}} \]

m - искомый размер битового массива (кол-во бит);

n - размер исходного множества;

ln - функция натурального логарифма;

ε - желаемая вероятность ошибок (например, 0.01 - ожидаем 1% ошибок)

Формула достаточного кол-ва кэш-функций зависит от выделенного кол-ва бит:

    \[    \displaystyle k={\frac {m}{n}}\ln 2 \]

k - кол-во кэш-функций;

m - кол-во битов в результирующем массиве;

n - кол-во элементов в исходном множестве;

ln - функция натурального логарифма.

Результатом выполнения каждой кэш-функции является номер бита, который нужно выставить (отметить). Таким образом, несколько хэш-функции высталяют различные биты в массиве бит. Хотя номера могут случайно совпадать для различных хэш-функций. Независимость хэш-функции друг от друга позволяет уменьшить повторяемость их результатов.

Реализация формул на Java

Самой главной проблемой при реализации фильтра Блума является выбор хэш-функций.

Первое, что пришло в голову это брать SHA-256 (привет любителям блокчейна) от ИИНа, и части SHA-256 использовать как отдельные значения хэш-функций.

Но, на самом деле, SHA-256 недостаточно, поэтому был взят SHA-384.

Чтобы было удобнее брать части SHA-384 делается выравнивание размера битового массива.

Как и требуется, хэши SHA обладают лавинным эффектом, небольшое изменений в хэшируемых данных меняет хэш до неузнаваемости.

Общая схема работы

Функция расчета оптимального кол-во бит

/**
 * Вычислить оптимальное кол-во бит
 * @param totalCount общее кол-во элементов
 * @param errorProbability вероятность ошибок
 * @return оптимальное кол-во бит
 */
static long optimalBitCount(long totalCount, double errorProbability) {
    return (long) Math.ceil(
            -totalCount * Math.log(errorProbability) / Math.pow(Math.log(2), 2));
}

Подставим наши значения (20 миллионов - кол-во элементов, 0.01 - ожидаем 1% ошибок) и получим размер битового массива, равный 191 701 168:

System.out.println(optimalBitCount(20_000_000, 0.01)); // 191_701_168

Но, именно для нашей реализации, данный размер не подходит, так как не все биты используются.

Это можно увидеть, переведя размер 191 701 168 в двоичный вид 1011011011010010000010110000:

System.out.println(Long.toBinaryString(optimalBitCount(20_000_000, 0.01)));

Чтобы задействовать возможные биты, нужно сделать расчет размера с выравниванием:

/**
 * Вычислить оптимальное кол-во бит с выравниванием
 * @param totalCount общее кол-во элементов
 * @param errorProbability вероятность ошибок
 * @return оптимальное кол-во бит в порции с выравниванием
 */
static long optimalBitCountWithAlignment(long totalCount, double errorProbability) {
    long result = optimalBitCount(totalCount, errorProbability);
    long l = Long.highestOneBit(result);
    return l == totalCount ? totalCount : l << 1;
}

Еще раз подставим наши значения, получим размер, равный 268 435 456

System.out.println(optimalBitCountWithAlignment(20_000_000, 0.01)); // 268_435_456

Переведем 268 435 456 - 1 в двойчный вид 1111111111111111111111111111 (28 бит).

От размера массива нужно вычесть единицу, так как индексация массива идет с 0 и нужно посмотреть битовое представление последнего индекса.

System.out.println(Long.toBinaryString(
                optimalBitCountWithAlignment(20_000_000, 0.01) - 1));

Размер стал больше, но зато все биты задействованы.

Таким образом, считаем размер битового массива равным 268 435 456.

Функция расчета кол-ва хэш-функций:

/**
 * Расчет кол-ва хэш-функций
 * @param totalCount общее кол-во элементов
 * @param bitArraySize размер битового массив
 * @return минимальное кол-во хэш-функций
 */
static long hashFunctionCount(long totalCount, long bitArraySize) {
    return (long) Math.ceil(Math.log(2) * ((double)bitArraySize / totalCount));
}

Подставим наши значения, получим 10 хэш-функций:

System.out.println(hashFunctionCount(20_000_000, 268_435_456)); // 10

268 435 456 значений можно закодировать 28 битами, что составляет 3.5 байта.

    \[    \displaystyle 2^{28} = 268 435 456 \]

Для удобства мы можем брать по 4 байта хэш-функции SHA, при этом 3 байта целиком, а один только на половину, другую же половину просто игнорируем.

Итого там потребуется 40 байт, которые есть в SHA-384.

Хэш-функцияКол-во битКол-во байт
SHA-25625632
SHA-38438448

Из SHA-384 можно получить не 10, а 12 функций. Чем больше функций, тем меньше будет ошибок.

Реализация фильтра Блума на Java

Все необходимые расчеты сделаны, можно приступать к реализации.

Для начала реализуем получение хэша от строки (ИИНа) в виде массива байт.

MessageDigest digest = MessageDigest.getInstance("SHA-384");
byte[] digest(String value) {
    return this.digest.digest(value.getBytes(StandardCharsets.UTF_8));
}

Биты будем хранить в стандартной структуре данных BitSet. У данный структуры есть методы set и get для установки и чтения значения соответствующего бита.

BitSet bitSet = new BitSet(1 << 28); // размер BitSet, равный 268_435_456

Реализуем функцию перевода четырех байт в число, являющееся номером позиции бита в BitSet.

/**
 * переводим четыре байта в число
 * @param digest массив байт
 * @param position позиция, от которой берутся 4 байта
 * @return число
 */
private int bitsToInt(byte[] digest, int position) {
    return ((((1 << 4) - 1)) & digest[position]) << 24 // берем половину байта и смещаем его на 24 позиции влево
            | (0xff & digest[position + 1]) << 16 // берем следующий целый байт и смещаем его на 16 позиций влево
            | (0xff & digest[position + 2]) << 8 // берем следующий целый байт и смещаем его на 8 позиций влево 
            | (0xff & digest[position + 3]); // берем следующий целый байт без каких-либо смещений
}

Функция установки битов выглядит следующим образом:

private void saveBits(String value) {
    byte[] digest = digest(value);
    for (int i = 0; i + 3 < digest.length; i += 4) {
        bitSet.set(bitsToInt(digest, i));
    }
}

Функция проверки принадлежности значения множеству:

private boolean checkBits(String value) {
    byte[] digest = digest(value);
    for (int i = 0; i + 3 < digest.length; i += 4) {
        if(!bitSet.get(bitsToInt(digest, i))) {
            return false;
        }
    }
    return true;
}

Тестирование фильтра Блума

Пример применения фильтра Блума на 20 миллионах ИИН https://github.com/Nyquest/bloom_filter

Проверять фильтр Блума будем следующим образом:

  1. Загрузим ИИНы в HashSet;
  2. Загрузим ИИНы в фильтр Блума;
  3. Сгенерим 10 миллионов случайных ИИН и прогоним их через фильтр Блума;
  4. Если фильтр показывает, что ИИН не относится к множеству, но этот ИИН есть в HashSet, то выбрасываем исключение и останавливаем программу;
  5. Если фильтр показывает, что ИИН относится к множеству, но его нет в HashSet, учитываем данный факт как ложноположительное срабатывание;
  6. Подсчитываем ложноположительные срабатывания и их в процент от общего числа запросов.

В примере две реализации: собственная MyBloomFilter и GoogleBloomFilter(BloomFilter из guava).

При создании GoogleBloomFilter указывается вероятность ошибок, которая подтверждается эмпирическим путем.

Но так как наш фильтр модифицирован дважды, эмпирические данные показывают ошибки в 0.18% от всех тестовых запросов:

  1. Набор бит увеличен из-за выравнивания
  2. Взято больше хэш-функций (12 вместо 10)

Теперь рассчитаем ожидаемую вероятность ошибок по формуле:

    \[    \displaystyle (1-e^{-kn/m})^{k} \]

Функция расчета вероятности ошибок на Java

/**
 * Расчет вероятности ошибок
 * @param totalCount общее кол-во элементов
 * @param bitArraySize размер битового массив
 * @param hashFunctionCount кол-во хэш-функций
 * @return вероятность ошибок
 */
public static double errorProbability(long totalCount, long bitArraySize, long hashFunctionCount) {
    return Math.pow(1 - Math.pow(Math.E, 
            -(double)hashFunctionCount * totalCount / bitArraySize), hashFunctionCount);
}
}

Подставим наши значения и получим 0.0018, т.е. теоретические и практические результаты совпали.

System.out.println(errorProbability(20_000_000, 268_435_456, 12));

Заключение

Получилась рабочая версия фильтра Блума, но с рядом недостатков.

Сравнивать будет с реализацией от Google, хотя почему-то в guava Bloom Filter еще в beta-тестировании.

Сравнение, конечно же, не в нашу пользу.

  1. Потребляет больше оперативной памяти из-за выравнивания;
  2. Непараметризирован. Размер битового массива фиксирован под определенную задачу.
  3. Сильная завязка на выравнивание.
  4. Реализация медленнее. С тестовым заданием фильтр в последовательном режиме справляется за 11 секунд, а GoogleBloomFilter за 5 секунд.
  5. Сама реализация как и BitSet без потоковой безопасности (thread-safety), в отличие от Guava Bloom Filter.

В реальных проектах лучше использовать готовые реализации Bloom Filter с потоковой безопасностью.

Основная цель своей реализации была лучше разобраться в этой интересной, вероятностной и магической структуре данных.

В реализации Google используется магия и трюки (см. фрагмент кода BloomFilterStrategies.java по добавлению элемента в множество):

@Override
public  boolean put(
    @ParametricNullness T object,
    Funnel funnel,
    int numHashFunctions,
    LockFreeBitArray bits) {
  long bitSize = bits.bitSize();
  long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
  int hash1 = (int) hash64;
  int hash2 = (int) (hash64 >>> 32);

  boolean bitsChanged = false;
  for (int i = 1; i <= numHashFunctions; i++) {
    int combinedHash = hash1 + (i * hash2);
    // Flip all the bits if it's negative (guaranteed positive number)
    if (combinedHash < 0) {
      combinedHash = ~combinedHash;
    }
    bitsChanged |= bits.set(combinedHash % bitSize);
  }
  return bitsChanged;
}

Здесь один хэш получается из предыдущего. Если в вычислениях случается переполнение, в результате которого образумется отрицательное число, то происходит инверсия бит, чтобы получилось положительное число за счет смены бита, отвечающего за знак в том числе. Чтобы не выходить за пределы битового массива делается закольцовывание с помощью взятия остатка от деления на размер массива.

В следующем блоге смотрите эффективность фильтра Блума с точки зрения расхода памяти: Эффективность фильтра Блума в памяти

Преобразование в сетевой порядок расположения байт (Java)

HtoN (Host to Net) - узловой порядок в сетевой.

Функция htons преобразует узловой порядок расположения байтов в сетевой порядок расположения байтов. В short помещаются все возможные порты от 0 до 65535 (фактически от -32768 до 32767, т.к. в Java знаковый short):

static short htons(short val) {
    return (short) ((val & 0xff) << 8 
            | (val & 0xff00) >>> 8); // >>> - беззнаковый сдвиг
}

Функция htonl() преобразует узловой порядок расположения байтов в сетевой порядок расположения байтов. В int помещаются все возможные адреса IPv4 от 0.0.0.0 до 255.255.255.255 (фактически от -2 147 483 648 до 2 147 483 647, т.к. в Java знаковый int):

static int htonl(int val) {
    return (val & 0xff) << 24
            | (val & 0xff00) << 8
            | (val & 0xff0000) >> 8
            | (val & 0xff000000) >>> 24; // >>> - беззнаковый сдвиг
} 
TAG: ,

Суффиксный массив на Java за N*log^2(N)

public static int[] getSuffixArray(String s) {

    System.out.println(s);

    int N = s.length();

    int steps = Integer.bitCount(Integer.highestOneBit(N) - 1);

    int rank[][] = new int[steps + 1][N];

    for (int i = 0; i < N; i++) {
        rank[0][i] = s.charAt(i) - 'a';
    }

    Tuple tuples[] = new Tuple[N];

    for (int step = 1, cnt = 1; step <= steps; step++, cnt <<= 1) {
        for (int i = 0; i < N; i++) {
            Tuple tuple = new Tuple();
            tuple.firstHalf = rank[step - 1][i];
            tuple.secondHalf = i + cnt < N ? rank[step - 1][i + cnt] : -1;
            tuple.originalIndex = i;

            tuples[i] = tuple;
        }

        Arrays.sort(tuples);

        rank[step][tuples[0].originalIndex] = 0;

        for (int i = 1, currRank = 0; i < N; i++) {
            if(!tuples[i - 1].firstHalf.equals(tuples[i].firstHalf)
                    || tuples[i - 1].secondHalf.equals(tuples[i].secondHalf)) {
                ++currRank;
            }
            rank[step][tuples[i].originalIndex] = currRank;
        }


    }

    int suffixArray[] = new int[N];

    for (int i = 0; i < N; i++) {
        suffixArray[i] = tuples[i].originalIndex;
    }

    return suffixArray;
}

static class Tuple implements Comparable {
    Integer originalIndex;  // хранит оригинальный индекс суффикса
    Integer firstHalf;      // хранит ранг первой половины суффикса
    Integer secondHalf;     // хранит ранг второй половины суффикса


    @Override
    public int compareTo(Tuple tuple) {
        return this.firstHalf.equals(tuple.firstHalf)
                ? this.secondHalf.compareTo(tuple.secondHalf)
                : this.firstHalf.compareTo(tuple.firstHalf);
    }

    @Override
    public String toString() {
        return "Tuple{" +
                "originalIndex=" + originalIndex +
                ", firstHalf=" + firstHalf +
                ", secondHalf=" + secondHalf +
                '}';
    }
}

Удаление числа из строки

Иногда мы пользуемся трюком, сохраняя числа (ID из базы данных) в строке через запятую. Например: "1024,15,8,0,55". И теперь, допустим, какое-то число из этого списка не нужно, и его необходимо убрать из исходной строки. Как это реализовать? Первый вариант: 1) разбить строку по запятой 2) отфильтровать удаляемое число 3) собрать строку заново Второй вариант (допустим, удаляем число 55): 1) вырезать из строки подстроки вида: ,55, 2) удаляем подстроки вида 55, и ,55 Реализация 1-го варианта (Java):
String val = "55";
String collect = Arrays.stream(
        "55,1024,55,15,8,0,55".split(","))
        .filter(v -> !v.equals(val))
        .collect(Collectors.joining(","));
System.out.println(collect);//1024,15,8,0
Реализация 2-го варианта (Java):
int del = 55;
String result = "55,1024,55,15,8,0,55"
        .replaceAll("," + del + ",", ",")
        .replaceAll(",?" + del + ",?", "");
System.out.println(result);//1024,15,8,0

Как из Java обратиться к сервису по протоколу HTTPS

Это юбилейная 100-я статья!!!

Уже не знаю в какой раз приходится обращаться к сервису по протоколу HTTPS, и каждый раз уходит время, чтобы воспроизвести шаги. Сегодня решил все-таки написать шпаргалку и больше не тратить время на такую проблему. Для начала открываем сайт в браузере: В адресной строке браузера рядом с текстом https:// есть иконка, нажав на которую появится возможность просмотреть / скачать / экспортировать сертификат. Нужно экспортировать сертификат в файл test.crt Файл будет иметь примерно такое содержание:
-----BEGIN CERTIFICATE-----
MIIEdjCCA16gAwIBAgIUGyZeQnd4LMFso5FQwrzjHmrNWVswDQYJKoZIhvcNAQEF
utCwINC/0L7QtNC70LjQvdC90L7RgdGC0Lgg0YHQtdGA0LLQtdGA0LAwMgYDVR0f
KqOTBEhH50jwo6WaQIUrD54ElD5gVO3VIT+eAMZm0HzXF+NKpkNaiR1b
-----END CERTIFICATE-----
Далее необходимо выполнить следующую команду:
sudo keytool -import -alias yourname -file test.crt
 -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit
-alias - произвольное уникальное название -file - путь к файлу -keystore - путь к хранилищу сертификатов Java $JAVA_HOME - путь к домашней директории Java, которую вы используете для запуска вашего приложения -storepass - пароль к хранилищу, по умолчанию changeit

Получение содержимого текстового файла (Spring Boot)

Вот уже несколько месяцев последние два проекта делаю с использованием Spring Boot. Бывает так, что нужно интегрироваться с внешней системой, но эта система не готова по каким-то причинам, а нужно показать свой функционал, в этом случае пишем заглушку. Т.к. в моем случае внешний сервис давал данные в формате JSON, я решил положить пример ответа сервиса в ресурсы (resource) по пути files/cities.json. Далее встал вопрос как его вытянуть. Погуглил, в итоге собрал такую функцию (сперва получаем контекст приложения, из которого получаем ресурс):
private static String getFileContent(String filePath) throws IOException {
    ApplicationContext appContext =
        new ClassPathXmlApplicationContext(new String[] {});

    Resource resource = appContext.getResource(filePath);

    StringBuilder sb = new StringBuilder();
    BufferedReader br = null;
    try{
        br = new BufferedReader(
            new InputStreamReader(resource.getInputStream(), "UTF-8"));
        String line;
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
    }finally {
        if(br != null) try {
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return sb.toString();
}
Пример вызова функции:
try {
    String text = getFileContent("files/cities.json");
} catch (IOException e) {
    e.printStackTrace();
}
Я использовал чтение файла для временной заглушки, но можно использовать и для других целей. p.s. Обратите внимание, что в функции указана кодировка UTF-8. Можно её не указывать, тогда возьмется кодировка системы. Кодировку можно указать и при запуске приложения:
java -Dfile.encoding=UTF8
Совет: лучше в функции явно прописать UTF-8, если, конечно, она вам нужна.

Замер времени (Java)

Иногда возникает потребность измерить время работы определенного фрагмента кода в вашем проекте. Можно, конечно, писать так:
long start = System.currentTimeMillis();
//тут замеряемый код
System.out.println(System.currentTimeMillis() - start);
Но такой подход быстро надоедает. Я выделил замер времени в отдельный класс TimeMeter, теперь можно удобно останавливать несколько раз таймер и измерять время, если нужно измерить промежуточные значения. Можно также использовать несколько экземпляров таймера. Реализация:
public class TimeMeter {
    private long start;
	private long stop;

    public TimeMeter(){
        start = now();
    }

    public void stop(){
        stop = now();
    }

    @Override
    public String toString() {
        return getDuration() + " msec";
    }

    public long getDuration(){
        return (stop == 0 ? now() : stop) - start;
    }

    private long now(){
        return System.currentTimeMillis();
    }
}
Самый простой вариант использования:
TimeMeter timeMeter = new TimeMeter();
//тут замеряемый код
System.out.println(timeMeter);
Таймер с несколькими остановками и замерами времени:
TimeMeter timeMeter = new TimeMeter();
//тут замеряемый код 1
timeMeter.stop();
System.out.println(timeMeter);
//тут замеряемый код 2
timeMeter.stop();
System.out.println(timeMeter);
Глобальный и внутренние таймеры:
TimeMeter totalTimeMeter = new TimeMeter();
for (int i = 0; i < 100; i++) {
    TimeMeter timeMeter = new TimeMeter();
    //тут замеряемый код
    System.out.println("time " + i + ": " + timeMeter);
}
System.out.println("total time: " + totalTimeMeter);
p.s. Можете придумать и сделать свою версию, здесь простор для творчества.