Последнее время язык Go стал очень обсуждаемым, и довольно часто его сравнивают с Java. Неделя Go принесла нам очень интересную ознакомительную статью Dreadd, и мне стало интересно, как справится с описанной задачей Java.
По мере написания кода стало понятно, что и в Java тоже есть много интересного, но мало освещённого в прессе. Я постарался использовать самые интересные нововведения из Java7, надеюсь тут найдут полезную информацию как начинающие, так и опытные, но ленивые Java разработчики.
Задача была взята без изменений, и решить её попробуем как можно более близким к оригиналу способом. У нас точно так же будет несколько потоков чтения данных, один поток сохранения, оповещение по таймеру и по закрытию программы. Параметры будем получать из командной строки при запуске.
Начнём, как в оригинальной статье, с начала, т.е. с разбора параметров. Стандартной библиотеки для этих целей в Java нет, но сторонние есть на любой вкус. Мне нравится jcommander. Решение, как говорится, “java way”.
Аннотации любой код делают лучше.
В Go для передачи цитат использовались каналы, в Java мы возьмём ближайший аналог — BlockingQueue:
Мы не сможем читать из нескольких очередей в одном потоке. Зато у нас есть другие плюшки, например можем ограничить длину очереди, если не будем успевать её разгрести.
Горутин у нас нет, а есть Runnable. Конечно неприятно создавать объект ради одного метода, но это дело принципа.
Да, довольно многословно, не поспоришь, но это не предел.
Вот теперь действительно многословно. Но это плата за дополнительные возможности, например указание приоритета потока.
Что касается вызываемого метода, тут уже лучше.
В принципе содержимое метода аналогично. Для парсинга HTML так же используется внешняя библиотека, довольно приятная jsoup. Всяко удобнее встроенного в swing.
Многие гнобят Java за громоздкую обработку исключений, но использование для этого if err == nil в Go просто ужасно. И в Java можно отказаться от обработки, чем мы и воспользуемся в следующем примере.
Работа с файлами так же довольно похожа. Обращу внимание на новые неблокирующие классы для работы с файлами из Java7. Найти из можно в java.nio, использование почти полностью совпадает с аналогом в Go:
В Java, как я и обещал, можно отказаться от явной обработки ошибок.
Мне очень понравился оператор defer в Go, кто пробовал закрыть поток в finally, должен оценить. Но к счастью мы спасены, и в Java7 добавлена конструкция try-resource.
Упомянутые в скобочках объекты должны реализовывать интерфейс java.lang.AutoCloseable, и они будут закрыты по окончанию блока try. Да, сильно смахивает на костыль, но не менее удобно, чем defer.
Отдельно можно обратить внимание на перевод массива байт в строку.
В стандартной библиотеке такого метода нет, для идентичности оригиналу я использовал библиотеку apache commons codec. Но один метод можно было и написать самому.
На самом деле он тут не нужен, ведь не важно в какой кодировке сохранять массив байт, это может быть и UTF-16 к примеру.
А можно и не кодировать совсем, нужен только Comparator для сравнения массивов. Например такой.
Основным потоком уже управляет очередь цитат, значит оповещения должны работать в своих потоках сами. Кроме этого момента различий в коде почти нет.
Закрытие обработаем с помощью shuldownHook’а.
Таймер возьмём у swing.
Чтобы иметь доступ к dupCount и quotesCount пришлось их вынести из метода в атрибуты класса, но на работу с ними это не повлияло.
Найти полный код можно тут:
http://pastebin.com/pLLVxTXZ
Что интересно, объём программ в строчках оказался примерно одинаковый. Читаемость, на мой взгляд тоже схожая, но это можно оценить только со стороны. В одном языке что-то сделано удобнее, в другом — другое, и однозначно выделить какой-то язык я не могу. Но это довольно небольшое приложение начального уровня, и было бы интересно сравнить языки и подходы в масштабных Enterprise решениях.
Спасибо за внимание.
По мере написания кода стало понятно, что и в Java тоже есть много интересного, но мало освещённого в прессе. Я постарался использовать самые интересные нововведения из Java7, надеюсь тут найдут полезную информацию как начинающие, так и опытные, но ленивые Java разработчики.
Задача
Задача была взята без изменений, и решить её попробуем как можно более близким к оригиналу способом. У нас точно так же будет несколько потоков чтения данных, один поток сохранения, оповещение по таймеру и по закрытию программы. Параметры будем получать из командной строки при запуске.
Оригинальная постановка задачи
… срочно, под покровом темноты, загрузить себе полный дамп всех цитат на модерации[http://vpustotu.ru/moderation/] для дальнейшего секретного исследования…
Таким образом нужна программа, которая:
- Должна последовательно обновлять и парсить (разбирать) страницу, записывая цитату.
- Должна уметь отбрасывать дубликаты.
- Должна останавливаться не только по команде, но и по достижению определенного числа “повторов”, например 500!
- Так как это, скорее всего, займет некоторое время: необходимо уметь продолжить “с места на котором остановились” после закрытия.
- Ну и раз уж все-таки это надолго – пусть делает свое грязное дело в несколько потоков. Хорошо-бы в целых 4 потока (или даже 5!).
- И отчитывается об успехах в консоль каждые, скажем, 10 секунд.
- А все эти параметры пускай принимает из аргументов командной строки!
Параметры командной строки
Начнём, как в оригинальной статье, с начала, т.е. с разбора параметров. Стандартной библиотеки для этих целей в Java нет, но сторонние есть на любой вкус. Мне нравится jcommander. Решение, как говорится, “java way”.
private static class CommandLine {
@Parameter(names = "-h", help = true)
boolean help;
@Parameter(names = "-w", description = "количество потоков")
int workers = 2;
@Parameter(names = "-r", description = "частота отчетов (сек)")
int reportPeriod = 10;
@Parameter(names = "-d", description = "кол-во дубликатов для остановки")
int dupToStop = 500;
@Parameter(names = "-hf", description = "файл хешей")
String hashFile = "hash.bin";
@Parameter(names = "-qf", description = "файл записей")
String quotesFile = "quotes.txt";
}
...
CommandLine commandLine = new CommandLine(); //тут будут переданные аргументы
JCommander commander = new JCommander(commandLine, args); //вот такой нетипичный вызов
if (commandLine.help) commander.usage(); //вызов справки надо обрабатывать вручную, недоработочка...
То же на Go
var (
WORKERS int = 2 //кол-во "потоков"
REPORT_PERIOD int = 10 //частота отчетов (сек)
DUP_TO_STOP int = 500 //максимум повторов до останова
HASH_FILE string = "hash.bin" //файл с хешами
QUOTES_FILE string = "quotes.txt" //файл с цитатами
used map[string]bool = make(map[string]bool) //map в котором в качестве ключей будем использовать строки, а для значений - булев тип.
)
func init() {
//Задаем правила разбора:
flag.IntVar(&WORKERS, "w", WORKERS, "количество потоков")
flag.IntVar(&REPORT_PERIOD, "r", REPORT_PERIOD, "частота отчетов (сек)")
flag.IntVar(&DUP_TO_STOP, "d", DUP_TO_STOP, "кол-во дубликатов для остановки")
flag.StringVar(&HASH_FILE, "hf", HASH_FILE, "файл хешей")
flag.StringVar("ES_FILE, "qf", QUOTES_FILE, "файл записей")
//И запускаем разбор аргументов
flag.Parse()
}
Аннотации любой код делают лучше.
Каналы
В Go для передачи цитат использовались каналы, в Java мы возьмём ближайший аналог — BlockingQueue:
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
Мы не сможем читать из нескольких очередей в одном потоке. Зато у нас есть другие плюшки, например можем ограничить длину очереди, если не будем успевать её разгрести.
Горутин у нас нет, а есть Runnable. Конечно неприятно создавать объект ради одного метода, но это дело принципа.
new Thread(new Grabber()).start();
Да, довольно многословно, не поспоришь, но это не предел.
Thread worker = new Thread(new Grabber());
worker.setPriority(2);
worker.setDaemon(true);
worker.start();
Вот теперь действительно многословно. Но это плата за дополнительные возможности, например указание приоритета потока.
Парсинг HTML
Что касается вызываемого метода, тут уже лучше.
public class Grabber implements Runnable{
...
public void run() {
try {
while (true) { //в вечном цикле собираем данные
Document doc = Jsoup.connect("http://vpustotu.ru/moderation/").get();
Element element = doc.getElementsByClass("fi_text").first();
if (element != null){
queue.put(element.text()); //и отправляем их в очередь
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
То же на Go
func() {
for { //в вечном цикле собираем данные
x, err := goquery.ParseUrl("http://vpustotu.ru/moderation/")
if err == nil {
if s := strings.TrimSpace(x.Find(".fi_text").Text()); s != "" {
c <- s //и отправляем их в канал
}
}
time.Sleep(100 * time.Millisecond)
}
}
В принципе содержимое метода аналогично. Для парсинга HTML так же используется внешняя библиотека, довольно приятная jsoup. Всяко удобнее встроенного в swing.
Многие гнобят Java за громоздкую обработку исключений, но использование для этого if err == nil в Go просто ужасно. И в Java можно отказаться от обработки, чем мы и воспользуемся в следующем примере.
Работа с файлами
Работа с файлами так же довольно похожа. Обращу внимание на новые неблокирующие классы для работы с файлами из Java7. Найти из можно в java.nio, использование почти полностью совпадает с аналогом в Go:
//открытие файла на чтение
InputStream hashStream = Files.newInputStream(Paths.get(commandLine.hashFile)
//открытие файла на запись
OutputStream hashFile = Files.newOutputStream(Paths.get(commandLine.hashFile), CREATE, APPEND, WRITE);
То же на Go
//открытие файла на чтение
hash_file, err := os.OpenFile(HASH_FILE, os.O_RDONLY, 0666)
//открытие файла на запись
hash_file, err := os.OpenFile(HASH_FILE, os.O_APPEND|os.O_CREATE, 0666)
В Java, как я и обещал, можно отказаться от явной обработки ошибок.
public static void main(String[] args) throws IOException
try-resource
Мне очень понравился оператор defer в Go, кто пробовал закрыть поток в finally, должен оценить. Но к счастью мы спасены, и в Java7 добавлена конструкция try-resource.
try (
OutputStream hashFile = Files.newOutputStream(Paths.get(commandLine.hashFile), CREATE, APPEND, WRITE);
InputStream hashStream = Files.newInputStream(Paths.get(commandLine.hashFile));
BufferedWriter quotesFile = Files.newBufferedWriter(Paths.get(commandLine.quotesFile),
Charset.forName("UTF8"), CREATE, APPEND, WRITE);) {
...
}
Упомянутые в скобочках объекты должны реализовывать интерфейс java.lang.AutoCloseable, и они будут закрыты по окончанию блока try. Да, сильно смахивает на костыль, но не менее удобно, чем defer.
Сравнение хешей
Отдельно можно обратить внимание на перевод массива байт в строку.
Hex.encodeHexString(hash);
В стандартной библиотеке такого метода нет, для идентичности оригиналу я использовал библиотеку apache commons codec. Но один метод можно было и написать самому.
static String encodeHexString(byte[] a) {
StringBuilder sb = new StringBuilder();
for (byte b : a)
sb.append(String.format("%02x", b & 0xff));
return sb.toString();
}
На самом деле он тут не нужен, ведь не важно в какой кодировке сохранять массив байт, это может быть и UTF-16 к примеру.
new String(hash, "UTF16");
А можно и не кодировать совсем, нужен только Comparator для сравнения массивов. Например такой.
static Set<byte[]> hashes = new TreeSet<>(new Comparator<byte[]>() {
public int compare(byte[] a1, byte[] a2) {
int result = a1.length - a2.length;
if (result == 0){
for (int i = 0; i < a1.length; i++){
result = a1[i] - a2[i];
if (result != 0) break;
}
}
return result;
};
});
Вспомогательные потоки
Основным потоком уже управляет очередь цитат, значит оповещения должны работать в своих потоках сами. Кроме этого момента различий в коде почти нет.
Закрытие обработаем с помощью shuldownHook’а.
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
System.out.printf("Завершаю работу. Всего записей: " + hashes.size());
}
});
Таймер возьмём у swing.
new Timer(commandLine.reportPeriod * 1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
System.out.printf("Всего %d / Повторов %d (%d записей/сек) \n", hashes.size(), dupCount, quotesCount/commandLine.reportPeriod);
quotesCount = 0;
}
}).start();
Чтобы иметь доступ к dupCount и quotesCount пришлось их вынести из метода в атрибуты класса, но на работу с ними это не повлияло.
Найти полный код можно тут:
http://pastebin.com/pLLVxTXZ
Вывод
Что интересно, объём программ в строчках оказался примерно одинаковый. Читаемость, на мой взгляд тоже схожая, но это можно оценить только со стороны. В одном языке что-то сделано удобнее, в другом — другое, и однозначно выделить какой-то язык я не могу. Но это довольно небольшое приложение начального уровня, и было бы интересно сравнить языки и подходы в масштабных Enterprise решениях.
Спасибо за внимание.