1. Общ преглед
От ранните дни на Java, многопоточността е основен аспект на езика. Runnable е основният интерфейс, предоставен за представяне на многонишкови задачи, а Callable е подобрена версия на Runnable , добавена в Java 1.5.
В тази статия ще изследваме разликите и приложенията на двата интерфейса.
2. Механизъм за изпълнение
И двата интерфейса са проектирани да представят задача, която може да бъде изпълнена от множество нишки. Изпълнимите задачи могат да се изпълняват с помощта на класа Thread или ExecutorService, докато Callables могат да се изпълняват само с помощта на последния.
3. Възвръщаеми стойности
Нека да разгледаме по-задълбочено начина, по който тези интерфейси обработват връщаните стойности.
3.1. С Runnable
Интерфейсът Runnable е функционален интерфейс и има един метод за изпълнение () , който не приема никакви параметри и не връща никакви стойности.
Това е подходящо за ситуации, в които не търсим резултат от изпълнението на нишката, например регистриране на входящи събития:
public interface Runnable { public void run(); }
Нека разберем това с пример:
public class EventLoggingTask implements Runnable{ private Logger logger = LoggerFactory.getLogger(EventLoggingTask.class); @Override public void run() { logger.info("Message"); } }
В този пример нишката просто ще прочете съобщение от опашката и ще го регистрира в лог файл. Няма стойност, върната от задачата; задачата може да бъде стартирана с помощта на ExecutorService:
public void executeTask() { executorService = Executors.newSingleThreadExecutor(); Future future = executorService.submit(new EventLoggingTask()); executorService.shutdown(); }
В този случай обектът Future няма да съдържа никаква стойност.
3.2. С Callable
Интерфейсът Callable е общ интерфейс, съдържащ метод с едно извикване () - който връща обща стойност V :
public interface Callable { V call() throws Exception; }
Нека да разгледаме изчисляването на факториал на число:
public class FactorialTask implements Callable { int number; // standard constructors public Integer call() throws InvalidParamaterException { int fact = 1; // ... for(int count = number; count > 1; count--) { fact = fact * count; } return fact; } }
Резултатът от метода call () се връща в бъдещ обект:
@Test public void whenTaskSubmitted_ThenFutureResultObtained(){ FactorialTask task = new FactorialTask(5); Future future = executorService.submit(task); assertEquals(120, future.get().intValue()); }
4. Обработка на изключения
Нека да видим колко подходящи са за обработка на изключения.
4.1. С Runnable
Тъй като подписът на метода няма посочената клауза „хвърля“,няма начин да се разпространяват допълнително проверени изключения.
4.2. С Callable
Методът call () на Callable съдържа клауза „хвърля изключение“ , за да можем лесно да разпространяваме допълнително проверени изключения:
public class FactorialTask implements Callable { // ... public Integer call() throws InvalidParamaterException { if(number < 0) { throw new InvalidParamaterException("Number should be positive"); } // ... } }
В случай на изпълнение на Callable с помощта на ExecutorService, изключенията се събират в обекта Future , който може да бъде проверен чрез извикване на метода Future.get () . Това ще хвърли ExecutionException - което обгръща първоначалното изключение:
@Test(expected = ExecutionException.class) public void whenException_ThenCallableThrowsIt() { FactorialCallableTask task = new FactorialCallableTask(-5); Future future = executorService.submit(task); Integer result = future.get().intValue(); }
В горния тест, ExecutionException се хвърля, тъй като предаваме невалиден номер. Можем да извикаме метода getCause () на този обект на изключение, за да получим първоначално провереното изключение.
Ако не извършим извикването на метода get () от клас Future - тогава изключението, хвърлено чрез метод call () , няма да бъде отчетено обратно и задачата пак ще бъде маркирана като завършена:
@Test public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){ FactorialCallableTask task = new FactorialCallableTask(-5); Future future = executorService.submit(task); assertEquals(false, future.isDone()); }
Горният тест ще премине успешно, въпреки че сме хвърлили изключение за отрицателните стойности на параметъра на FactorialCallableTask.
5. Заключение
В тази статия проучихме разликите между интерфейсите Runnable и Callable .
Както винаги, пълният код за тази статия е достъпен в GitHub.