Nested types

( = types imbriqués)
Deux raisons liées à l'encapsulation peuvent mener à faire des types imbriqués :
- Si un type n'est utilisé que dans une petite partie du code, faire un type interne permet de renforcer l'encapsulation (par exemple Pile4 dans TP3).
- Si un type a besoin de manipuler l'état interne de son type englobant, le type interne accède aux données privées de son type englobant (de la même manière que les méthodes d'une classe).
Un type imbriqué n'a pas d'existence indépendante, existe au sein de son type englobant.

Souvent appelés "classes internes" (désignation imprécise, mais syntaxe java précise).

Important : bien distinguer inheritance hierarchy et containment hierarchy.

Membres static

Exemple dans TP3, question 2 : la classe Maillon est un membre static de Pile4.
class OuterClass {
    ...
    static class StaticNestedClass {
        ...
    }
}
On utilise le nom de la classe englobante pour y accéder, par ex :
OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
Les membres static d'accès public ou package peuvent être utilisés ailleurs :
import package1.Class1.ClasseInterne;
import package1.Class1.*;

Membre d'instance

(Uniquement pour les classes, pas les interfaces ou enums)
class OuterClass {
    ...
    class InnerClass {
        ...
    }
}
Pour instancier une inner class :
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

Shadowing

Si une variable porte le même nom qu'une autre variable dans un contexte englobant, elle cache le nom (shadowing), donc syntaxe spéciale.
public class ShadowTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x); // syntaxe spéciale
        }
    }

    public static void main(String... args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}
(code dans ShadowTest.java).

Le code suivant récapitule les ambiguités qu'on peut recontrer ; code dans exemples/java/nested/tests.
import static java.lang.System.out;

class Class1{
    
    // ***** static *****
    
    private static int a = 1;
    private static int b = 2;
    public static void printStatic(){
        out.println("Class1.printStatic(), a = " + a); // 1
    }
    
    public static class ClasseInterneStatic{
        private static int a = 3;
        public static void printStatic(){
            out.println("ClasseInterneStatic.printStatic(), a = " + a); // 3
            out.println("ClasseInterneStatic.printStatic(), Class1.a = " + Class1.a); // 1
            out.println("ClasseInterneStatic.printStatic(), b = " + b); // 2
            out.println("ClasseInterneStatic.printStatic(), Class1.b = " + Class1.b); // 2
        }
    }
    
    // ***** instance *****
    
    private int c = 4;
    private int d = 5;                                                                                                                                         
    public void printInstance(){
        out.println("Class1.printInstance(), c = " + c); // 4
        out.println("Class1.printInstance(), this.c = " + this.c); // 4
    }
    
    public class ClasseInterne{
        private int c = 6;
        public void printInstance(){
            out.println("ClasseInterne.printInstance(), c = " + c); // 6
            out.println("ClasseInterne.printInstance(), this.c = " + this.c); // 6
            out.println("ClasseInterne.printInstance(), Class1.this.c = " + Class1.this.c); // 6
            out.println("ClasseInterne.printInstance(), d = " + d); // 5
        }
        // Les deux lignes suivantes ne passent pas à la compilation :
        // on ne peut pas définir de membres static dans une classe qui est un membre d'instance 
        // public static int e = 7;                 // IMPOSSIBLE
        // public static void printStatic(){ }      // IMPOSSIBLE
    }
    
}
import static java.lang.System.out;

class Main{
    
    public static void main(String[] args){
        
        out.println("==== tests static ====");
        Class1.printStatic();
        Class1.ClasseInterneStatic.printStatic();
        
        out.println("\n==== tests instance ====");
        Class1 c1 = new Class1();
        Class1.ClasseInterne interne1 = c1.new ClasseInterne();
        c1.printInstance();
        interne1.printInstance();
        
    }
    
}
java Main 
==== tests static ====
Class1.printStatic(), a = 1
ClasseInterneStatic.printStatic(), a = 3
ClasseInterneStatic.printStatic(), Class1.a = 1
ClasseInterneStatic.printStatic(), b = 2
ClasseInterneStatic.printStatic(), Class1.b = 2

==== tests instance ====
Class1.printInstance(), c = 4
ClasseInterne.printInstance(), c = 6
ClasseInterne.printInstance(), this.c = 6
ClasseInterne.printInstance(), Class1.this.c = 4
ClasseInterne.printInstance(), d = 5
Exercice
Allez voir l'API de javax.swing.Box.
- Quelle classe imbriquée statique (static nested class) est définie dans Box ?
- Quelle classe interne (inner class) est définie dans Box ?
- Quelle est la supeclasse de cette classe interne ?
- Quelle classe interne de Box peut on utiliser depuis n'importe quelle classe ?
- Quel code permet de créer une instance de la classe interne Filler ?

Classes locales

On peut définir une classe interne dans n'importe quel bloc, par ex dans une méthode, un initialisateur statique ou d'instance, une boucle for, dans un if.
Une classe locale a accès aux champs de sa classe environnante et aux variables locales de son bloc.
Ne peut pas être définie public, protected, private ou static.

Classes anonymes

Classes utilisées à un seul endroit ; n'ont même pas de nom.

Exemple : on cherche à afficher les fichiers ".java" d'un répertoire.
(code des exemples dans exemples/java/nested/filter).
De manière classique :
import java.io.File;
public class TestFilter1{
    public static void main(String[] args){
        File dir = new File(args[0]);
        String[] files = dir.list();
        for(String file : files){
            if(file.endsWith(".java")){
                System.out.println(file);
            }
        }
    }
}
On a utilisé java.io.File.list() :
package java.io;
public class File extends Object implements Serializable, Comparable<File>{
    ...
    
    /**
        Returns an array of strings naming the files and directories
        in the directory denoted by this abstract pathname.
    **/
    public String[] list(){ ... }
    
    /**
        Returns an array of strings naming the files and directories
        in the directory denoted by this abstract pathname that satisfy the specified filter.
    **/
    public String[] list(FilenameFilter filter){ ... }
    
    ...
}
Mais on peut aussi utiliser File.list(FilenameFilter filter), qui prend en paramètre un objet d'une classe implémentant java.io.FilenameFilter.
@FunctionalInterface
public interface FilenameFilter{
    boolean accept(File dir, String name);
}

Utilisation classique

import java.io.File;
import java.io.FilenameFilter;

public class TestFilter2{
    public static void main(String[] args){
        File dir = new File(args[0]);
        String[] files = dir.list(new JavaFilenameFilter());
        for(String file : files){
            System.out.println(file);
        }
    }
}

class JavaFilenameFilter implements FilenameFilter{
    public boolean accept(File f, String s) {
        return s.endsWith(".java");
    }
}

Avec classe anonyme

import java.io.File;
import java.io.FilenameFilter;

public class TestFilter3{
    public static void main(String[] args){
        File dir = new File(args[0]);
        String[] files = dir.list(new FilenameFilter() {
            public boolean accept(File f, String s) {
                return s.endsWith(".java");
            }
        });
        for(String file : files){
            System.out.println(file);
        }
    }
}
Mais comment le compilateur sait ce que représentent f et s ?
La réponse est dans l'implémentation de java.io.File.list(FilenameFilter filter) (voir le code source) :
public String[] list(FilenameFilter filter) {
    String names[] = list();
    if ((names == null) || (filter == null)) {
        return names;
    }
    List<String> v = new ArrayList<>();
    for (int i = 0 ; i < names.length ; i++) {
        if (filter.accept(this, names[i])) {
            v.add(names[i]);
        }
    }
    return v.toArray(new String[v.size()]);
}
Donc dans le code
File dir = new File(args[0]);
String[] files = dir.list(new FilenameFilter() {
    public boolean accept(File f, String s) {
        return s.endsWith(".java");
    }
});
La variable f fait donc référence à dir (puisque list() est une méthode de l'instance dir).

L'utilisation d'une classe anonyme peut être ici rendue plus compacte en utilisant une lambda expression.