Alle Überschreibungen einer Methode ermitteln

Aus Eclipse
Wechseln zu: Navigation, Suche

Problem

Eine Methode, die überschrieben wird oder selbst eine Überschreibung darstellt, gehört mit allen Überschreibungen derselben in eine Gruppe, deren Signatur oder Sichtbarkeit, wenn die Bedeutung des Programms nicht geändert werden soll, nur gemeinsam geändert werden darf. Außerdem ist (aufgrund des dynamischen Bindens) bei einem Aufruf mit der Signatur aus dem Programmtext heraus nicht klar, welche der Methoden aus der Gruppe aufgerufen wird.

In Java sind dabei auch die Überschreibungen in anonymen Klassen zu berücksichtigen.

Lösung

Die Gruppe von Methoden läßt sich anhand ihrer Signatur und eines Empfängertyps (Klasse oder Interface) wie folgt bestimmen:

Zur Erinnerung, der Begriff überschreiben ist in Java nicht auf konkrete Methoden beschränkt. Er schließt das Implementieren einer von einem Interface geerbten Deklaration ebenso ein wie das Deklarieren einer geerbten, abstrakten Methode (siehe JLS3, § 8.4.8.1). Zur Menge der gesuchten Methodendeklarationen gehören daher alle Deklarationen von Instanzmethoden mit gleicher Subsignatur in allen direkten und indirekten Superklassen, in allen implementierten Interfaces und deren Superinterfaces sowie in allen Subklassen und deren Superinterfaces -- vorausgesetzt, die Sichtbarkeitsregeln für das Überschreiben sind erfüllt (siehe Diskussion).

Die Lösung des Problems geben wir in zwei Schritten an: Wir suchen (1) alle von einer gegebenen Methodendeklaration überschriebenen Deklarationen und (2) alle die gegebene Methodendeklaration überschreibenden Deklarationen.

Alle überschriebenen Methodendeklarationen

Die folgende Lösung stammt weitgehend aus unserem intoJ Projekt Access Modifier Modifer (AMM). Hier benutzen wir allerdings der Kürze halber eine interne Methode der Java Development Tools (JDT), die sehr elegant die Menge der Supertypen bestimmt. Der Ansatz beruht auf IMethodBindings und setzt damit einen Abstract Syntax Tree (AST) voraus, der von einem ASTParser mit setReolveBindings(true) bezogen wurde.

Ausgehend von der gegebenen Methode method in Form eines IMethodBinding ermitteln wir die Klasse, die diese Methode deklariert (declaringClass), und suchen dann alle Superklassen und Superinterfaces auf (superTypes). Über die Menge aller Sypertypen iterieren wir und wählen aus allen Methoden, die diese Typen deklarieren (inheritedMethod), diejenigen aus, die die gegebene Methode (method) überschreibt: method.overrides(inheritedMethod)

    /**
     * Returns all methods overridden by method.
     * @see JLS3, §8.4.8.1
     */
    public static Collection<IMethodBinding> allOverriddenMethods(
            IMethodBinding method) {
        
        assert method != null : "Precond.: method != null";
        assert ! method.isConstructor() : "Precond.: ! method.isConstructor()";
        assert ! Modifier.isStatic(method.getModifiers()) : 
            "Precond.: ! method.isStatic()";
        
        Collection<IMethodBinding> result= new ArrayList<IMethodBinding>();
        
        ITypeBinding declaringClass= method.getDeclaringClass();
        ITypeBinding[] superTypes= Bindings.getAllSuperTypes(declaringClass); 
            
        for ( ITypeBinding type : superTypes ) {
            for ( IMethodBinding inheritedMethod : type.getDeclaredMethods() ) {
                if ( method.overrides(inheritedMethod) ) {     // overrides
                    result.add(inheritedMethod);
                }
            }
        }
        
        return result;
    }

Falls nicht garantiert werden kann, dass die Methode allOverriddenMethods nur mit nicht privaten Methoden als Argument aufgerufen wird, sollte eine Wächterbedingung vor der Ermittlung aller Supertypen die Effizienz erhöhen:

        if ( Modifier.isPrivate(method.getModifiers()) ) {
            return result;
        }

Alle überschreibenden Methodendeklarationen

Anders als die Menge der überschriebenen Methoden, ist die Menge der überschreibenden Methoden offen und das Problem so allgemein nicht lösbar (vgl. Martin Fowler, Public versus published interfaces). Wir beschränken pragmatisch die Suche nach überschreibenden Methoden auf den gegebenen Eclipse-Workspace.

Die hier vorgestellte Vorgehensweise ist aus den intoJ Projekten Infer Type (IT) und Replace Inheritance With Delegation (RIWD) extrahiert.

Aus dem gegebenen IMethodBinding ermittelt allOverrindingMethods(..) die die Methode deklarierende Klasse bzw. das deklarierende Interface. Die Suche nach überschreibenden Methoden beschränken wir auf die Subtyphierarchie der deklarierenden Klasse. Diese Subtyphierarchie erhalten wir vom Java-Model. Deshalb konvertieren wir die Bindings in Java-Model-Elemente, ermitteln die möglicherweise überschreibenden Methoden aus der Subtyphierarchie (candidateOverridingMethods) und selektieren auf Grundlage der Bindings dann die tatsächlich überschreibenden Methoden (selectOverridingMethods).

    /**
     * Returns all methods in workspace overriding method.
     * @see JLS3, §8.4.8.1
     */
    public static Collection<IMethodBinding> allOverridingMethods(
            IMethodBinding methodBinding) 
            throws JavaModelException, InternalCalculationException  {
        
        ITypeBinding declTypeBinding = methodBinding.getDeclaringClass();
        IType declTypeElement = (IType)declTypeBinding.getJavaElement();
        IMethod methodElement = (IMethod)methodBinding.getJavaElement();
        
        Collection<IMethod> candidateElements = candidateOverridingMethods(
				declTypeElement, methodElement);
        
        Collection<IMethodBinding> result = selectOverridingMethods(
				methodBinding, candidateElements);

        return result;
    }

    /**
     * Returns all methods possibly overriding method declared in declType.
     * declType is used to compute the subtype hierarchy, which is scanned
     * for similiar methods (@see org.eclipse.jdt.core.IMethod.isSimilar(..)).
     */
    private static Collection<IMethod> candidateOverridingMethods(
            IType declType, IMethod method) throws JavaModelException {

        ITypeHierarchy hierarchy = declType.newTypeHierarchy(
                new NullProgressMonitor());
        IType[] subtypeElements = hierarchy.getAllSubtypes(declType);
        
        Collection<IMethod> candidates = new ArrayList<IMethod>();
        for ( IType eachSubtype : subtypeElements ) {
            for ( IMethod eachMethod : eachSubtype.getMethods() ) {
                if ( eachMethod.isSimilar(method) ) {
                        candidates.add(method);
                }
            }
        }
        return candidates;
    }

    /**
     * Selects methods overriding method from candidates.
     * Candidates are converted from Java Model Elements to
     * Bindings for that purpose and returned as bindings
     * if overriding method. 
     */
    private static Collection<IMethodBinding> selectOverridingMethods(
            IMethodBinding method, Collection<IMethod> candidates)
            throws InternalCalculationException {

        Collection<IMethodBinding> result = new ArrayList<IMethodBinding>();

        for ( IMethod eachCandidate : candidates ) {
            IMethodBinding candidateBinding = 
                    (IMethodBinding) Util_Conversion.getBinding(eachCandidate);
            if ( candidateBinding.overrides(method) ) {
                result.add(candidateBinding);
            }
        }
        
        return result;
    }

Für die Konvertierung von IMethod aus dem Java-Model in das IMethodBinding des AST verwenden wir hier die interne Hilfsmethode Util_Conversion.getBinding(IMember) unseres intoJ Projekts InferType, deren Implementation wir an anderer Stelle dieses Wikis zeigen werden.

Die Vereinigung der Rückgaben der Methoden allOverriddenMethods(..) und allOverridingMethods(..) löst die eingangs gestellte Aufgabe, alle Überschreibungen zu ermitteln. Die Berechnung beider Mengen ist selbstverständlich in einem Schritt möglich (vgl. Disskussion).


siehe auch