Инкапсулировать специфические для класса и методы общие типы в одном типе
Скажем, мне нужен класс, ограниченный общим типом Comparable
:
class A<T extends Comparable<T>> {
// this is just an example of usage of T type
List<T> comparables;
int compareSomething(T smth) {
return comparables.get(0).compareTo(smth);
}
}
Класс имеет метод с собственными генериками в сигнатуре:
<V> Future<V> submit(Callable<V> task) {
return someExecutorService.submit(task);
}
Теперь, есть ли возможность ограничить ввод метода submit
только тем, который принимает Callables
, который также реализует T
? Я сначала попробовал это:
<V, W extends T & Callable<V>> Future<V> submit(W task) {
if(compareSomething(task) != 0)
throw new RuntimeException("");
return someExecutorService.submit(task);
}
но выяснил, что это невозможно (по причинам, описанным здесь). Есть ли какая-то элегантная возможность обойти его?
РЕДАКТИРОВАТЬ: Одна уродливая возможность, о которой я могу думать, состоит в том, чтобы разбить инкапсуляцию на два разных типа и передать пару объектов в submit
:
class A<T extends Comparable<T>> {
// this is just an example of usage of T type
List<T> comparables;
int compareSomething(T smth) {
return comparables.get(0).compareTo(smth);
}
<V> Future<V> submit(Callable<V> task, T comparable) {
if(compareSomething(comparable) != 0)
throw new RuntimeException("");
return someExecutorService.submit(task);
}
}
Основной недостаток заключается в том, что сигнатура метода становится более сложной, также мне потребуется некоторое взаимно однозначное отображение T
to Callable
для некоторого последнего кода. Может быть, можно предложить образец, который решает его соответствующим образом?..
ИЗМЕНИТЬ, возьмите два: позвольте мне вкратце объяснить, чего я пытаюсь достичь. Я работаю над пользовательской реализацией пула потоков, которая может выполнять какое-то специальное планирование задач. Для этого служба принимает только один специальный вид задач Callable
. Те Callable
должны реализовать пользовательский интерфейс, похожий на Comparable
. Сравнивая пары задач с использованием методов в этом интерфейсе, служба будет:
- Заблокировать входящую задачу, если она заблокирована любой выполняющейся задачей.
- Вызов ожидающих задач при завершении выполненной задачи
Future
.
- Определите порядок выполнения ожидающих задач, сравнив их.
Логика блокировки/сравнения должна предоставляться самими задачами. Таким образом, класс пула потоков должен определять только тот тип Comparable
, который принимает объект пула, и его вообще не волнует, что это за тип Callable
и каков его тип возврата.
EDIT, возьмите три: на основе Эрик Робертсон ответ, теперь можно предотвратить подачу вонючего задачи:
public static void test(String[] args) {
A<Valid> scheduler = new A<>();
scheduler.betterSubmit(new Valid()); // applies to method signature
scheduler.betterSubmit(new Forbidden()); // rejected on compile time
scheduler.betterSubmit(new ConformWithValid()); // still appliable because all required interfaces implementations recognised
}
// just a bunch of test classes
private static class Valid implements Comparable<Valid>, Callable<Void> {
@Override
public int compareTo(Valid o) {
return 0;
}
@Override
public Void call() throws Exception {
return null;
}
}
private static class Forbidden implements Comparable<Forbidden>, Callable<Void> {
@Override
public int compareTo(Forbidden o) {
return -1;
}
@Override
public Void call() throws Exception {
return null;
}
}
private static class ConformWithValid implements Comparable<Valid>, Callable<Boolean> {
@Override
public int compareTo(Valid o) {
return 1;
}
@Override
public Boolean call() throws Exception {
return Boolean.FALSE;
}
}
Приятно и легко! Надеюсь, когда-нибудь это поможет кому-то в той же ситуации, что и моя.: -)
Ответы
Ответ 1
Используйте Comparable<T>
, чтобы ограничить параметр, а не только T
.
Если единственным критерием является то, что объект Callable
и Comparable
, то вы можете просто поместить эти два интерфейса в аргумент. Это даже резервирует возможность добавления именованного класса в параметр, если это требуется для заполнения другого требования. Вам нужно будет подавить одно предупреждение, но это безопасное подавление, потому что вы знаете, что T
extends Comparable<T>
.
public class A<T extends Comparable<T>> {
// this is just an example of usage of T type
List<T> comparables;
ExecutorService someExecutorService = null;
int compareSomething(T smth) {
return this.comparables.get(0).compareTo(smth);
}
<V> Future<V> submit(Callable<V> task) {
return this.someExecutorService.submit(task);
}
@SuppressWarnings("unchecked")
<V, W extends Callable<V> & Comparable<T>> Future<V> betterSubmit(W task) {
if(this.compareSomething((T) task) != 0)
throw new RuntimeException("");
return this.someExecutorService.submit(task);
}
}
Я столкнулся с этой же проблемой с другим классом, который у меня есть, который ссылается сам по себе на свой собственный общий параметр. Я пока не думаю, что это чистая реализация, но мне нравится ваш пример. Он обеспечивает хороший аккуратный вариант использования, который я мог бы обработать каким-то новым кодом после. Я вообще не люблю подавлять предупреждения, но на самом деле я не против, потому что это влияет только на этот метод. Надеюсь, это поможет!
Ответ 2
Я думаю, что Erick Robertson answer ближе всего подходит к прямому решению. Я хотел бы предложить менее прямой (который избегает любых непроверенных бросков). Вы писали:
эта служба принимает только один специальный вид задач Callable
. Те Callable
должны реализовать пользовательский интерфейс, похожий на Comparable
.
Если мы действительно объявляем то, что вы описали как тип, мы предлагаем этот интерфейс пересечения:
interface CustomCallable<V> extends Callable<V>, Comparable<CustomCallable<?>> { }
Обратите внимание, как он реализует Comparable<CustomCallable<?>>
, а не Comparable<CustomCallable<V>>
. Это связано с тем, что я предполагаю, что CustomCallable
разных типов вывода все равно будет сопоставим друг с другом пулом. В противном случае не было бы смысла иметь V
в качестве параметра типа метода.
Предполагая, что ваш пул может принять любой тип этого интерфейса пересечения, мы можем просто удалить T
полностью, а ограничение языка будет спорным:
class A {
List<CustomCallable<?>> customCallables;
int compareSomething(CustomCallable<?> smth) {
return customCallables.get(0).compareTo(smth);
}
<V> Future<V> submit(CustomCallable<V> task) {
if (compareSomething(task) != 0) {
throw new RuntimeException("");
}
return someExecutorService.submit(task);
}
}
Если вы настаиваете на параметризации A
на определенный тип пользовательских вызовов, это становится более сложным. Единственный способ заставить его работать - добавить параметр "self type" к интерфейсу пересечения:
interface CustomCallable<V, SELF> extends Callable<V>, Comparable<CustomCallable<?, ?>> { }
Здесь SELF
предназначен для представления самого типа самого конструктора. Обратите внимание, что нет никакого способа для этого, чтобы быть исполненным однако. Больше информации о типах типов и их предостережениях на мой ответ здесь: Есть ли способ ссылаться на текущий тип с переменной типа?
С добавленным типом self теперь можно A
объявить T
, что submit
затем настаивает на использовании для собственного типа пользовательских вызываемых вызовов:
class A<T extends CustomCallable<?, ?>> {
List<CustomCallable<?, T>> customCallables;
int compareSomething(CustomCallable<?, T> smth) {
return customCallables.get(0).compareTo(smth);
}
<V> Future<V> submit(CustomCallable<V, T> task) {
if (compareSomething(task) != 0) {
throw new RuntimeException("");
}
return someExecutorService.submit(task);
}
}
Вот пример реализации CustomCallable
, который разрешает тип self:
class MyCustomCallable<V> implements CustomCallable<V, MyCustomCallable<?>> {
...
}
Аналогично предыдущему, обратите внимание на то, что тип self "расслаблен" до MyCustomCallable<?>
, а не MyCustomCallable<V>
, поскольку различные типы вывода взаимозаменяемы по дизайну.
И вот пример использования:
A<MyCustomCallable<?>> pool = new A<>();
MyCustomCallable<String> strCallable = ...;
MyCustomCallable<Integer> intCallable = ...;
Future<String> strFuture = pool.submit(strCallable);
Future<Integer> intFuture = pool.submit(intCallable);
Ответ 3
Я не вижу, как вы можете объединить как свой общий параметр класса T
, так и V
в аргументе метода Callable<V>
, по причинам, указанным в ответах на связанный пост. Однако, возможно, это был бы вариант - я не могу оценить, насколько противоречивым было бы это - изменить действие отправки задачи на метод, вызываемый самой задачей? Что-то вроде линий...
class A<T extends Comparable<T> & Callable<?>> {
public static abstract class Submittable<T extends Submittable<T,V>,V>
implements Comparable<T>, Callable<V> {
// ugly, but unavoidable
@SuppressWarnings("unchecked")
public Future<V> submitTo(A<? super T> scheduler) {
return (Future<V>) scheduler.submit((T) this);
}
}
// this is just an example of usage of T type
List<T> comparables;
int compareSomething(T smth) {
return comparables.get(0).compareTo(smth);
}
// *can* be left public, but that way you enforce a single way
// of submitting tasks, namely via submitTo
private Future<?> submit(T task) {
if(compareSomething(task) != 0)
throw new RuntimeException("");
// the following cast is a save conversion - you could also write it cast-free
// as Callable<?> callable = task; ...submit(callable);
return someExecutorService.submit((Callable<?>) task);
}
}
Теперь ваши задачи должны наследовать A.Submittable
, а затем могут быть отправлены в экземпляр планировщика через submitTo
, возвращая Future<V>
для правильного типа V
.
Конечно, это на самом деле полагается на T
, фактически являясь самолечением подклассов Submittable
. И что происходит в submitTo
- ну, уродливо. Но это дает вам возможность вернуть Future<V>
правильного типа.
Ответ 4
Помогает ли это решить проблему?
class A<V, T extends MyT<T, V>> {
// this is just an example of usage of T type
List<T> comparables;
int compareSomething(T smth) {
return comparables.get(0).compareTo(smth);
}
public <V> Future<V> submit(MyT<T, V> task) {
return ...;
}
}
class MyT<T, V> implements Comparable<T>, Callable<V> {
@Override
public int compareTo(T o) {
return 0;
}
@Override
public V call()
throws Exception {
return ...;
}
}
Ответ 5
Что-то вроде этого может работать:
class A<T extends Comparable<T>> {
List<T> comparables;
int compareSomething(T smth) {
return comparables.get(0).compareTo(smth);
}
public Future<T> submit(B<T> task) {
return someExecutorService.submit(task);
}
public class B<T> implements Callable<T> {
...
}
}
Ответ 6
ОК, действительно последняя попытка.
Можете ли вы изменить свою функцию generics на универсальный или внутренний класс? Вот так:
class A<T extends Comparable<T> > {
// this is just an example of usage of T type
List<T> comparables;
int compareSomething(T smth) {
return comparables.get(0).compareTo(smth);
}
class B<V>
{
T t;
T getT() { return t; }
Callable <V> v;
Callable <V> getV() { return v; }
public Future<V> submit(B<V> task) {
if(compareSomething(task.getT()) != 0)
throw new RuntimeException("");
return SomeExecutorService.submit(task.getV());
}
}
Ответ 7
Почему не так просто, как эта работа?
public class CustomThreadPool {
public <CallableReturn> Future<CallableReturn> submit(SelfSchedulingCallable<CallableReturn> task) {
// Use comparePriority or any other interface methods needed to determine scheduling
}
}
public interface SelfSchedulingCallable<V> extends Callable<V> {
public boolean isBlockedBy(SelfSchedulingCallable<?> otherTask); // Doesn't care about the return type of otherTask
public boolean invokeAfter(SelfSchedulingCallable<?> otherTask);
public int comparePriority(SelfSchedulingCallable<?> otherTask);
}