简体   繁体   中英

Call Subclasses' Method Depending on Object Type in Java

PROBLEM: Dynamically choose corresponding subclass and call its method, depending on passed parameter type.

RESEARCH: Struggled to apply the generics approach to the task:

public abstract class MetaContainer
  extends Node {

  public static abstract interface CommonContainer {
    ObservableList<Object> getChildren(Object container);
  }

  public abstract class AnchorPaneContainer
    extends AnchorPane
    implements CommonContainer {

    public ObservableList<Object> getChildren(Object container) {
      // Special approach for AnchorPanes.
    }
  }

  public abstract class TabPaneContainer
    extends TabPane
    implements CommonContainer {

    public ObservableList<Object> getChildren(Object container) {
      // Special approach for TabPanes.
    }
  }
}

I try to use the class like this (and get error, because CommonContainer is an interface and cannot have static methods):

  private ObservableList<Node> getElements(Node container)
    throws ClassNotFoundException {

    ObservableList<Node> nodes = FXCollections.observableArrayList();
    ObservableList<Object> objects = FXCollections.observableArrayList();

    objects.addAll(
      MetaContainer.CommonContainer.
      getChildren(
        (Object) container));

    for (Object object : objects) {
      nodes.add(
        (Node) object);
    }

    return nodes;
  }

QUESTION: How can I call getChildren() on the whole MetaContainer and pass it container of any type in parameter, waiting it to address the correct getChildren() in subclass of container type?

EXPLANATION: In few words, I need to browse down the node-container in search for simple controls. So you know not what type of node that is beforehand - only dynamically, when iterating. Some subnodes are containers that also have to be browsed, but each type requires specific approach. I could do something like switch-case on types, but feel that should do something more elegant like subclasses for each type and one common interface.

OK, let me take a stab at something, though I still don't know if I really understand the question. I think you want to get a subset of the scene graph by getting child nodes of different Parent subclasses in different ways (ie not necessarily just by calling Parent.getChildrenUnmodifiable() ). So if it's a simple Pane , you would just call getChildren() , but if it's a TabPane , you would get each Tab and get the content of each tab, forming a collection from those. (And similarly for other "container-type controls", such as SplitPane , etc.) If it's a "simple" control, you don't regard it as having any child nodes (even though, behind the scenes, a Button contains a Text , for example).

So I think you could perhaps do this by building a typesafe heterogeneous container (see Josh Bloch's Effective Java ) that mapped specific node types N to a Function<N, List<Node>> . The function would define how to retrieve child nodes for that type.

This might look like

public class ChildRetrievalMapping {

    public static final ChildRetrievalMapping DEFAULT_INSTANCE = new ChildRetrievalMapping() ;

    static {
        // note the order of insertion is important: start with the more specific type

        DEFAULT_INSTANCE.put(TabPane.class, tabPane -> 
                tabPane.getTabs().stream().map(Tab::getContent).collect(Collectors.toList()));
        DEFAULT_INSTANCE.put(SplitPane.class, SplitPane::getItems);
        // others...

        // default behavior for "simple" controls, just return empty list:
        DEFAULT_INSTANCE.put(Control.class, c -> Collections.emptyList());

        // default behavior for non-control parents, return getChildrenUnmodifiable:
        DEFAULT_INSTANCE.put(Parent.class, Parent::getChildrenUnmodifiable);


        // and for plain old node, just return empty list:
        DEFAULT_INSTANCE.put(Node.class, n -> Collections.emptyList());
    }

    private final Map<Class<?>, Function<? ,List<Node>>> map = new LinkedHashMap<>();

    public <N extends Node> void put(Class<N> nodeType, Function<N, List<Node>> childRetrieval) {
        map.put(nodeType, childRetrieval);
    }

    @SuppressWarnings("unchecked")
    public <N extends Node> Function<N, List<Node>> getChildRetrieval(Class<N> nodeType) {
        return (Function<N, List<Node>>) map.get(nodeType);
    }

    @SuppressWarnings("unchecked")
    public List<Node> firstMatchingList(Node n) {
        for (Class<?> type : map.keySet()) {
            if (type.isInstance(n)) {
                return getChildRetrieval((Class<Node>) type).apply(n);
            }
        }
        return Collections.emptyList();
    }
}

Now you can just call childRetrievalMapping.findFirstMatchingList(node); and it gets the list of children in the sense defined by the first type in the map which matches the node. So, using the DEFAULT_INSTANCE , if you passed it a TabPane , it would get all the content nodes; if you passed it a SplitPane , it would get the items; if you passed it another type of control, it would return an empty list, etc.

Here's an example of using this. This just builds a scene graph, and then when you press the button, it traverses it, getting just the "simple" nodes defined by the strategies in the above class. (And then it selects all instances of Labeled and passes the result of getText() to the system console.) Note how it (deliberately) avoids the labels that are part of the implementation of the tabs themselves, which a naïve root.lookupAll(".labeled") would not do.

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Labeled;
import javafx.scene.control.SplitPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class PerformActionOnNodeTypes extends Application {

    @Override
    public void start(Stage primaryStage) {
        VBox root = new VBox(5, 
                new Label("Label 1"),
                new HBox(5, new Label("Label 2"), new Button("Button 1")),
                new HBox(5, new TextField("Some text"), new ComboBox<String>()),
                new TabPane(new Tab("Tab 1", new VBox(new Label("Label in tab 1"))),
                        new Tab("Tab 2", new StackPane(new Button("Button in tab 2")))));

        Button button = new Button("Show labeled's texts");
        button.setOnAction(e -> {
            List<Node> allSimpleNodes = new ArrayList<>();
            findAllSimpleNodes(allSimpleNodes, root);
            doAction(allSimpleNodes, Labeled.class, (Labeled l) -> System.out.println(l.getText()));
        });

        root.setAlignment(Pos.CENTER);
        BorderPane.setAlignment(button, Pos.CENTER);
        Scene scene = new Scene(new BorderPane(root, null, null, button, null), 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void findAllSimpleNodes(List<Node> allSimpleNodes, Node n) {
        List<Node> children = ChildRetrievalMapping.DEFAULT_INSTANCE.firstMatchingList(n);
        allSimpleNodes.addAll(children);
        for (Node child : children) {
            findAllSimpleNodes(allSimpleNodes, child);
        }
    }

    private <T> void doAction(Collection<Node> nodes, Class<T> type, Consumer<T> action) {
        nodes.stream()
            .filter(type::isInstance)
            .map(type::cast)
            .forEach(action);
    }

    public static class ChildRetrievalMapping {

        public static final ChildRetrievalMapping DEFAULT_INSTANCE = new ChildRetrievalMapping() ;

        static {
            // note the order of insertion is important: start with the more specific type

            DEFAULT_INSTANCE.put(TabPane.class, tabPane -> 
                    tabPane.getTabs().stream().map(Tab::getContent).collect(Collectors.toList()));
            DEFAULT_INSTANCE.put(SplitPane.class, SplitPane::getItems);
            // others...

            // default behavior for "simple" controls, just return empty list:
            DEFAULT_INSTANCE.put(Control.class, c -> Collections.emptyList());

            // default behavior for non-control parents, return getChildrenUnmodifiable:
            DEFAULT_INSTANCE.put(Parent.class, Parent::getChildrenUnmodifiable);


            // and for plain old node, just return empty list:
            DEFAULT_INSTANCE.put(Node.class, n -> Collections.emptyList());
        }

        private final Map<Class<?>, Function<? ,List<Node>>> map = new LinkedHashMap<>();

        public <N extends Node> void put(Class<N> nodeType, Function<N, List<Node>> childRetrieval) {
            map.put(nodeType, childRetrieval);
        }

        @SuppressWarnings("unchecked")
        public <N extends Node> Function<N, List<Node>> getChildRetrieval(Class<N> nodeType) {
            return (Function<N, List<Node>>) map.get(nodeType);
        }

        @SuppressWarnings("unchecked")
        public List<Node> firstMatchingList(Node n) {
            for (Class<?> type : map.keySet()) {
                if (type.isInstance(n)) {
                    return getChildRetrieval((Class<Node>) type).apply(n);
                }
            }
            return Collections.emptyList();
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

I'm not sure if that's what you were wanting to do, and if so there may be more elegant ways to approach it. But I think declaring a strategy like this for particular types is quite a bit nicer than a big switch on types, and it leaves the option of configuring it to have the specific rules you want.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM