1

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);
    }
}
Phlip
  • 5,253
  • 5
  • 32
  • 48

2 Answers2

1

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.

Phlip
  • 5,253
  • 5
  • 32
  • 48
justhecuke
  • 765
  • 4
  • 8
0

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

John Spong
  • 1,361
  • 7
  • 8
  • `Class extends T>` on the what? Trying it now – Phlip Jun 17 '23 at 21:51
  • You would pass the class as an argument: `static void findEachView(@NonNull ViewGroup viewGroup, @NonNull Consumer viewAdjusterVisitor, @NonNull Class clazz)`. – Phlip Jun 17 '23 at 23:47
  • I'm Java 1.8. unexpected bounds on `Class` it can't handle the extends View part – Phlip Jun 17 '23 at 23:47