Java vs Go

от автора

Последнее время язык Go стал очень обсуждаемым, и довольно часто его сравнивают с Java. Неделя Go принесла нам очень интересную ознакомительную статью Dreadd, и мне стало интересно, как справится с описанной задачей Java.
По мере написания кода стало понятно, что и в Java тоже есть много интересного, но мало освещённого в прессе. Я постарался использовать самые интересные нововведения из Java7, надеюсь тут найдут полезную информацию как начинающие, так и опытные, но ленивые Java разработчики.

Задача

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

Оригинальная постановка задачи

… срочно, под покровом темноты, загрузить себе полный дамп всех цитат на модерации[http://vpustotu.ru/moderation/] для дальнейшего секретного исследования…

Таким образом нужна программа, которая:

  1. Должна последовательно обновлять и парсить (разбирать) страницу, записывая цитату.
  2. Должна уметь отбрасывать дубликаты.
  3. Должна останавливаться не только по команде, но и по достижению определенного числа “повторов”, например 500!
  4. Так как это, скорее всего, займет некоторое время: необходимо уметь продолжить “с места на котором остановились” после закрытия.
  5. Ну и раз уж все-таки это надолго – пусть делает свое грязное дело в несколько потоков. Хорошо-бы в целых 4 потока (или даже 5!).
  6. И отчитывается об успехах в консоль каждые, скажем, 10 секунд.
  7. А все эти параметры пускай принимает из аргументов командной строки!

Параметры командной строки

Начнём, как в оригинальной статье, с начала, т.е. с разбора параметров. Стандартной библиотеки для этих целей в 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.io.Closeable, и они будут закрыты по окончанию блока 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 решениях.

Спасибо за внимание.

ссылка на оригинал статьи http://habrahabr.ru/post/197926/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *