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.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 решениях.

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

Средняя зарплата в IT

110 500 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 7 138 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 62

    +3
    Хотелось бы еще увидеть сравнение скорости работы этих программ
      +8
      Думаю в этой задаче разница будет не заметна, скорость будет определяться внешними факторами: открытием страниц и записью на диск. Но попробовать можно, правда у меня интернет не очень, могу только предложить jar для теста.

      Моё субъективное мнение: скорость — это не главное. В крупном приложении скорость определяется архитектурой. На много важнее, чтобы приложение работало правильно и легко изменялось.
        –6
        В плане «работало и легко изменялось» имхо выигрывает Go, как в принципе и какие-нибудь другие компилируемые ЯП, которые не требуют установки виртуальных машин/интерпретаторов.
        • НЛО прилетело и опубликовало эту надпись здесь
            –2
            Так какая разница, если на машине стоит старый рантайм? Всё зависит от продукта и клиентов, одно дело тырпрайз, где ваш продукт может нигде кроме как уютненьких серверов не крутиться, и совсем другое — десктоп, где может быть дичайший зоопарк. Вот не установит/снимет галочку/удалит/не обновит юзер нужный вам Java runtime, и всё.
            Ну или я не понял суть OSGi.
            • НЛО прилетело и опубликовало эту надпись здесь
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Я говорил к тому, что бывают случаи(подобные этому), где имеет значение отсутствие лишних прослоек и зависимостей.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • НЛО прилетело и опубликовало эту надпись здесь
                        +1
                        Сейчас за 1500 рублей продаются вполне работающие китайские андроиды — для них можно скомпилировать бинарник из Go-кода.
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • НЛО прилетело и опубликовало эту надпись здесь
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                        • НЛО прилетело и опубликовало эту надпись здесь
          0
          Ну вы же понимаете, что создание 100 потоков на Java — это не то же самое, что создание 100 горутин? В данном простом примере воркеров всего 4, так что можно и забить, а когда будет что-то помасштабнее, чем просто опрос одного HTTP-сервера, то в Java начнутся проблемы. В общем, пока в Java не реализуют настоящую вытесняющую многозадачность, Java так и будет сосать в сторонке.
            +3
            Правда интересно что в джава не так с этим?
              +4
              Каждый поток кушает ресурсы, даже если в данный момент они ничего не делает (например, висит на блокирующем вызове).

              Но эта проблема спокойно решается в библиотеке, без изменения языка.
                0
                Гляньте вот этот тред. Там всё довольно неплохо описано
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Я что-то слышал про то, что в Java есть зеленые потоки, которые легковесны как горутины. Было бы не плохо, если бы кто-то что-то про это разъяснил.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Сейчас пилят библиотеку quasar для реализации зеленых потоков, но там какой-то хардкор с байткод-инструментацией происходит, чтобы всё это могло работать поверх JVM. А вот пример уже рабочего кода.
                        0
                        пишите на clojure и используйте core.async ;)
                          0
                          в следующей жизни )
                        +1
                        Akka? Вместо горутин — акторы.
                          0
                          Akka реализуется поверх обычных OS потоков, в результате чего наследует все те же проблемы. Если не делать блокирующие вызовы, то да, канает. Но а если я хочу? Ну потому что синхронный код выглядит проще и читабельнее, чем асинхронный. Да, есть фьючеры, коллбэки, скедулеры, но с ними код выглядит сложнее — это факт. Спросите любого эрланг-программиста, пересаживающегося на Akka.
                        +3
                        Читая статью, создается впечатление, что все более-менее нормально, но вот когда открываешь код…
                            +3
                            Код написан плохо не потому, что в нем не хватает паттернов. Грабберы при первом IOException завершатся — программа никогда не закончится (фактически зависнет). Один и тот же файл одновременно открывается на запись и на чтение. Входные параметры не проверяются. Нет синхронизации. Если программа остановится в момент записи в файл — он может быть поврежден, далее на чтении валидность не проверяется и программа прочитает битый хэш. За Hex.encodeHexString еще можно было побороться и написать самому, а не тащить сразу либу. Хэши сохраняются в файл как массив байт и на чтении каждый раз конвертятся в строку — можно было сразу записать строку. Используются обычные стримы, но вдруг — BufferedWriter, который все время флашится. Число записей в секунду округляется.
                          +1
                          Сравнивайте go с питоном. там тоже кодестайл влияет на смысл кода.
                            +2
                            То же на go: time.Sleep(100 * time.Millisecond)
                            Что-то на Java я такое не увидел.

                            Ну и не совсем понятен посыл статьи. Производилось сравнение? Какое сравнение? Читабельности кода? Скорости? Использование тредов вместо гороутин — это совсем не равноценная замена: по семантике они чем-то похожи, а по смыслу — совсем не похожи. Делать выводы на основе одной несложной задачи тоже не совсем верно. Что автор хотел показать-то?
                              –1
                              Использование time.Sleep обсуждали в этой ветке комментариев. Если я правильно понял, это нужно для того, чтобы дать возможность выполниться основному потоку. Но в моём случае это не нужно, т.к. я явно указал приоритет worker.setPriority(2). Чтобы сохранить дословность пересказа, можно использовать Thread.sleep(100), но лучше подойдёт Thread.yield() — он переместит поток в конец очереди планировщика без задержки.

                              О смысле статью расскажу подробнее. Я работаю с Java относительно давно, и в целом уже нормально разобрался, а о Go только слышал, но довольно много. Мне стало интересно, стоит ли бросать Java и переходить на более современный Go? Поэтому я и решил их сравнить на этой задаче. Как вывод скажу, что Go моложе и что-то в нём сделано удобнее, и если вы не знаете ни того ни другого, то возможно лучше выбрать Go. Но Java пока держится, хоть и некоторые решения выглядят как костыли, например аннотации. И имея опыт в Java пока рано её бросать.
                                +3
                                Я работаю с Java относительно давно

                                Ха-ха. Как сказал Joel Spolsky, 5 years of experience may be 1 year repeated 5 times

                                Мало того, что у вас ужасный код, так ещё и непотокобезопасный. Почему вы quotesCount модифицируете из нескольких потоков без синхронизации?
                                  0
                                  Это похоже на вызов :) Я не могу не ответить.
                                  Вам не кажется глупым ради обнуления счётчика городить синхронизацию? Ведь самое страшное, что может произойти — мы недосчитаемся цитаты при подсчёте скорости.
                                  Тогда уж больше опасность представляет чтение этих переменных, они ведь не объявлены volatile, и результат может быть устаревшим.
                                  Вы уверены, что нужно уделять столько внимания вспомогательной функциональности? Вероятность ошибки ведь очень низкая.
                                    0
                                    volatile
                                      0
                                      Вот честное слово не это хотел написать :)
                                      На работе pastebin заблокирован злой прокси, кода не видел. Не хватило бы сделать счетчик volatile?
                                        +1
                                        Синхронизация и volatile несут несколько различный смысл.
                                        Думаю, что volatile значительно улучшило бы ситуацию в случае инкремента. Без volatile переменная может кэшироваться в потоке. Т.е. есть вероятность, что переменная обнулится из другого потока, а мы не заметим, и присвоим ей {старое значение + 1}.
                                        Но, теоретически, между чтением и записью переменная всё равно может быть изменена другим потоком. Наиболее вероятно это между подсчётом скорости и обнулением, т.к. там несколько операций выполняется. Поэтому для полной уверенности нужно синхронизировать блок от чтения до записи.
                              +4
                              Забавно смотреть, как автор, не разобравшись толком в Go, начинает сравнивать его с другими языками.

                              Для начала, флаги можно указать так:
                              var (
                              WORKERS = flag.IntVar(«w», 2, «количество потоков»)
                              REPORT_PERIOD = flag.Int(«r», 10, «частота отчетов (сек)»)
                              DUP_TO_STOP = flag.Int(«d», 500, «кол-во дубликатов для остановки»)
                              HASH_FILE = flag.String(«hf», «hash.bin», «файл хешей»)
                              QUOTES_FILE flag.String(«qf», «quotes.txt», «файл записей»)
                              )
                              это гораздо короче и удобнее того что автор понаписал как для java, так и для Go. Более того, аналога этой конструкции в java нет.

                              Далее, автор превратно понял логику обработки исключений в Go. Если по каким-то причинам удобнее при ошибке прерывать flow и обрабатывать в 1 месте, вы можете даже не проверять err != nil, а просто recover'ить рантайм панику при вызове метода у нулевого объекта, созданного при ошибке — не идеальное решение, но избавляет от проверки результатов работы функции.

                              Вообще же сама по себе логика, когда при почти любой ошибке прерывается flow и исключения «обрабатываются» где-то высоко, если явно не предусмотрено обратное — ущербна — непредусмотренная явно ошибка может привести к непредсказуемым последствиям. Особо приятно ловить в Java null pointer exception. В Go в этом смысле более явный и унифицированный подход, когда разворачивание стека вызовов наверх происходит только по делу — при паниках — а ошибки возвращаются из самой функции.

                              В данном случае у вас go-код, иллюстрирующий парсинг HTML, работает неидентично java-коду. Если в java-код добавить дополнительный try-catch вокруг запроса страницы, чтобы она не падала при сетевых ошибках — получится ещё уродливее, чем в go, в дополнение к неудобной проверке ошибки далеко от её места возникновения.

                              Прибавляя к этому абсурдность сравнения системных тред и лёгких тред, общий вывод печальный для автора: данная статья — треш, и её надо очень долго дорабатывать, чтобы она не обесценивала хабр своим присутствием.
                                +1
                                Да будет холивар :)

                                Вообще же сама по себе логика, когда при почти любой ошибке прерывается flow и исключения «обрабатываются» где-то высоко, если явно не предусмотрено обратное — ущербна

                                Вы, очевидно, не сталкивались с большими приложениями. Надо сказать спасибо, что это так. Основное применение java это бизнес приложения. У нас всегда несколько уровней и ошибка должна обрабатываться там, где известно что с ней делать, т.е. гораздно чаще нам и нужно бросить ошибку наверх. И спасибо, что у нас нету «err != nil».

                                непредусмотренная явно ошибка может привести к непредсказуемым последствиям

                                Вот это поворот!

                                Особо приятно ловить в Java null pointer exception.

                                Если вы ловите null pointer, значит проблема в коде, java тут не при чем
                                  +1
                                  И вообще не стоит называть что-либо ущербным, если заведомо знаете, что это субъективное мнение.
                                  +2
                                  Зато у нас есть другие плюшки, например можем ограничить длину очереди, если не будем успевать её разгрести.
                                  Ну так и в Гоу это есть: github.com/bolknote/NarchTools/blob/master/cgodownloader.go#L85 тут она как раз у меня ограничивается.

                                  С обработками ошибок я не понял. В Гоу вы тоже можете их не обрабатывать, программа и сама свалится, а если вы не используете код ошибки, то можно его не забирать. Т.е. вместо

                                  //открытие файла на чтение
                                  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)
                                  

                                  можно написать

                                  //открытие файла на чтение
                                  hash_file, _ := os.OpenFile(HASH_FILE, os.O_RDONLY, 0666)
                                  //открытие файла на запись
                                  hash_file, _ := os.OpenFile(HASH_FILE, os.O_APPEND|os.O_CREATE, 0666)
                                  
                                    –5
                                    Java с Go сравнивают не из-за синтаксиса, а из-за того, что Go компилируемый, а компилятор с хорошим оптимизатором. А Java собирается в байт-код и выполняется в VM.
                                      +2
                                      Ну а во-время выполнения байт-код компилируется, и по скорости выполнения проигрыш обычно совсем не значительный, в сравнении со скомпилированным кодом.
                                        0
                                        Ещё нужно учесть, что JIT-компиляторы знают про целевую машину больше, чем AOT, и могут применять различные оптимизации, доступные только этому железу.

                                        Плюс, хотспот умеет делать ещё много оптимизаций по ходу выполнения, зная какие участки программы будут работать чаще, и с какими данными.
                                          0
                                          Зато у AOT сколько угодно времени на компиляцию и оптимизацию, а у JIT — очень мало. В общем, и у AOT, и у JIT есть свои плюсы, не вижу смысла холивар устраивать по этому поводу.
                                            0
                                            Для JIT (хотя там со своими заморочками) у гугла имеется Dart. Логичнее было бы сравнивать Dart и Java.
                                        0
                                        Да, где байт-код транслируется в машинный, причем транслятор очень хороший.
                                        0
                                        Как-то мне try-resource показались значительно хуже defer. Особенно если их будет много да еще и вложенных.
                                        Заменять горутины нитями тоже не то: это как нити заменять процессами.
                                          0
                                          Обращу внимание на новые неблокирующие классы для работы с файлами из Java7. Найти из можно в java.nio


                                          Чагоу? В каком месте они, простите, неблокирующие?
                                            0
                                            java.nio.files.WatchService — хоть бы погуглили сначала
                                              +1
                                              Погуглить надо бы вам :) WatchService, что бы это ни было, обертка вокруг inotify, fsevents и что-там-в-винде или самопальная реализация в отдельном тредике, или и то и другое, не имеет никакого отношения к неблокирующей работе с файлами :)

                                              К неблокирующей работе с файлами можно отнести то, как с ними работает nodejs, erlang (при +A > 0), linux AIO, libeio. И то, все это можно назвать неблокирующей работой с файлами лишь условно :)
                                                0
                                                Соглашусь. Вы правы.
                                                  0
                                                  Мне тут рядом сообщили, что я не прав ;) Не про WatchService, но все же.
                                                  +1
                                                  Строго говоря, в java.nio есть классы для асинхронного (с Java7) и неблокирующего на основе селекторов (c Java 1.4) ввода-вывода. С файлами, правда, нельзя работать с помощью селекторов, а с сокетами — ещё как можно.
                                                    0
                                                    О, AsynchronousFileChannel это оно! Работает по тому же принципу что озвученные мной выше решения. Не знал что это есть в nio. Впрочем, автор, судя по всему, тоже не знал и имел ввиду что-то другое, так что профит от этого треда получили все :)

                                                    Спасибо :)
                                                      0
                                                      (повторился коммент)

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое