英文:
generic function to traverse a View hierarchy and find contained Views of a specific type
问题
My outer problem, in Java 1.8, is I want to traverse (or spider) a tree of View objects and call a callback on each one of a specific type. Here's an example caller:
findEachView(main.popupForm, (EditText v) -> {
CharSequence error = v.getError();
if (error != null)
assertNull((String) v.getTag(), error.toString());
});
That is supposed to traverse all the controls on the popupForm and, for each one that is an EditText, call my callback which asserts that it does not have an error.
The inner problem is my generic method to do it has a syntax error:
static <T extends View> void findEachView(@NonNull ViewGroup view, @NonNull Consumer<T> viewAdjustorVisitor) {
for (int i = 0; i < view.getChildCount(); i++) {
View v = view.getChildAt(i);
if (v instanceof T) // ERROR: reifiable types in instanceof are not supported in -source 8
viewAdjustorVisitor.accept((T) v);
if (v instanceof ViewGroup)
findEachView((ViewGroup) v, viewAdjustorVisitor);
}
}
So my outer question is: How to efficiently and typesafely rip every View of the given type?
And my inner question is: How to get the generics to not throw away type information and let me call instanceof
on a type that I know is reified?
We can't use isAssignableFrom()
because it takes an object of type T
and my template doesn't have one. Its point is to downcast a View
into a T
, not mess with a T
object that already exists. And I can't create a new
object because T
is "not reified":
if (v.getClass().isAssignableFrom(new T()))
So maybe if someone could answer the outer question instead of downvoting?
When I add the ?
we get this, with the next syntax error. I think this is an unanswered question in Java 1.8 template theory:
static <T extends View> void findEachView(@NonNull
ViewGroup viewGroup, @NonNull
Consumer<Class<? extends View>> visitor) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View v = viewGroup.getChildAt(i);
if (null != (Class <? extends android.view.View>) v) // ERROR: incompatible types: View cannot be converted to Class<? extends View>
visitor.accept((Class <? extends android.view.View>) v);
if (v instanceof ViewGroup)
findEachView((ViewGroup) v, visitor);
}
}
英文:
My outer problem, in Java 1.8, is I want to traverse (or spider) a tree of View objects and call a callback on each one of a specific type. Here's an example caller:
findEachView(main.popupForm, (EditText v) -> {
CharSequence error = v.getError();
if (error != null)
assertNull((String) v.getTag(), error.toString());
});
That is supposed to traverse all the controls on the popupForm and, for each one that is an EditText, call my callback which asserts that it does not have an error.
The inner problem is my generic method to do it has a syntax error:
static <T extends View> void findEachView(@NonNull ViewGroup view, @NonNull Consumer<T> viewAdjustorVisitor) {
for (int i = 0; i < view.getChildCount(); i++) {
View v = view.getChildAt(i);
if (v instanceof T) // ERROR: reifiable types in instanceof are not supported in -source 8
viewAdjustorVisitor.accept((T) v);
if (v instanceof ViewGroup)
findEachView((ViewGroup) v, viewAdjustorVisitor);
}
}
So my outer question is: How to efficiently and typesafely rip every View of the given type?
And my inner question is: How to get the generics to not throw away type information and let me call instanceof
on a type that I know is reified?
We can't use isAssignableFrom()
because it takes an object of type T
and my template doesn't have one. Its point is to downcast a View
into a T
, not mess with a T
object that already exists. And I can't create a new
object because T
is "not reified":
if (v.getClass().isAssignableFrom(new T()))
So maybe if someone could answer the outer question instead of downvoting?
when I add the ?
we get this, with the next syntax error. I think this is an unanswered question in Java 1.8 template theory
static <T extends View> void findEachView(@NonNull
ViewGroup viewGroup, @NonNull
Consumer<Class<? extends View>> visitor) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View v = viewGroup.getChildAt(i);
if (null != (Class <? extends android.view.View>) v) // ERROR: incompatible types: View cannot be converted to Class<? extends View>
visitor.accept((Class <? extends android.view.View>) v);
if (v instanceof ViewGroup)
findEachView((ViewGroup) v, visitor);
}
}
答案1
得分: 1
我认为你正在把这个问题弄得比必要复杂。最简单的解决方案就是传入你要查找的类。
一般来说,如果你发现需要类型信息但无法获得,那就将它传递进来,就像你为方法需要的任何其他信息一样。
由于类型擦除的原因,使用通用类型信息总是很棘手的,正如其他人所说的那样。在这些情况下,最好明确地传入类。如果你绝对需要知道某个东西的通用类型是什么,那么你可以像GSON一样使用子类包装器来通过 SubClass.class.getGenericSuperclass()
访问超类的通用信息。但这是一个不好的想法,会引入冗余并且不直观。
我还建议检查运行时类型信息是一种代码异味。你应该尽量避免这种情况,因为它会使优化变得更加困难(例如无法合并类),并且本质上是脆弱的。尽量看看是否可以通过接口默认方法、抽象基类或类似的方式完成相同的工作。如果有可能的话,使用标准的面向对象编程方法来完成工作,避免显式依赖于运行时类型信息。
// 目标是实现类似于 Android 开发中常见的方式。
findEachView(main.popupForm, EditText.class, (EditText v) -> { ... });
static <T extends View> void findEachView(
@NonNull ViewGroup viewGroup,
@NonNull Class<T> clz,
@NonNull Consumer<? super T> visitor) {
...
if (clz.isInstance(v)) {
// 做一些事情
}
...
}
谢谢!我将把我的工作代码添加到你的答案中,这样你就可以获得经验点数。
我的之前的代码使用字符串操作来匹配类名,所以我试图去掉那个参数:
findEachView(
viewGroup,
"EditText",
(view) -> ((EditText) view).setError(null));
你的修复允许像这样调用,的确更清晰,因为它少了一个类型转换:
findEachView(
viewGroup,
EditText.class,
(view) -> view.setError(null));
所以获胜的代码是:
static <T extends View> void findEachView(
@NonNull ViewGroup view,
Class<T> clz,
@NonNull Consumer<? super T> visitor) {
for (int i = 0; i < view.getChildCount(); i++) {
View v = view.getChildAt(i);
if (clz.isInstance(v))
visitor.accept((T) v); // 这个转换会产生一个委员会会忽略的警告
if (v instanceof ViewGroup)
findEachView((ViewGroup) v, clz, visitor);
}
} // 注意这不会访问顶层项目
将聪明的 Lambda 调用转换为只返回一个简单的 View 列表是留给读者的练习。
英文:
I think you're making this more complicated than it needs to be. The simplest solution is just to pass in the class that you're looking for.
As a general rule, if you find that you need type information that isn't available, then pass it in. Much like you would do for any other information that a method needs.
Working with generic type information is always tricky because of type erasure, as others have stated. It's generally best to pass in the class explicitly in these situations. If you absolutely need to know what the generic type of something is, then you can do what GSON does and use a subclass wrapper to access the superclass's generic information via SubClass.class.getGenericSuperclass()
. But this is a bad idea which introduces bloat and is non-intuitive.
I would also suggest that checking runtime type information is a code smell. You should do what you can to avoid this stuff since it makes it harder to optimize (cannot merge classes, for example) and is inherently fragile. Try to see if you can accomplish the same work with an interface default method, an abstract base class, or something similar. If at all possible, use standard OOP methods to do the work and avoid explicitly relying on runtime type information.
// Aim for something like this which is common in Android-land.
findEachView(main.popupForm, EditText.class, (EditText v) -> { ... });
static <T extends View> void findEachView(
@NonNull ViewGroup viewGroup,
@NonNull Class<T> clz,
@NonNull Consumer<? super T> visitor) {
...
if (clz.isInstance(v)) {
// do stuff
}
...
}
Thanks! I'm going to add my working code to your answer so you get the experience points.
My previous code used string surgery to match the class name, so I was trying to take that argument out:
findEachView(
viewGroup,
"EditText",
(view) -> ((EditText) view).setError(null));
Your fix allows a call like this, which is indeed cleaner because it has one fewer typecast:
findEachView(
viewGroup,
EditText.class,
(view) -> view.setError(null));
So the winning code is:
static <T extends View> void findEachView(
@NonNull ViewGroup view,
Class<T> clz,
@NonNull Consumer<? super T> visitor) {
for (int i = 0; i < view.getChildCount(); i++) {
View v = view.getChildAt(i);
if (clz.isInstance(v))
visitor.accept((T) v); // this cast gives a warning that the committee will ignore
if (v instanceof ViewGroup)
findEachView((ViewGroup) v, clz, visitor);
}
} // note this does not visit the top level item
Converting the clever lambda call to just returning a dumb list of Views is left as an exercise for the reader.
答案2
得分: 0
Java在泛型方面使用类型擦除,因此类的字节码表示中T
的类型为java.lang.Object
,因此instanceof
对任何对象都返回true,并且编译器会拒绝。
最简单的修复方法是添加一个Class<? extends T>
参数,并直接使用类对象进行运行时反射。
英文:
Java works using type erasure for generics, so the bytecode representation of the class has java.lang.Object
as the type for T
, so the instanceof will be true for any object and the compiler rejects.
Simplest fix is to add a Class<? extends T>
argument and use runtime reflection using the class object directly
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论