Classes, interfaces

Les classes et les interfaces sont les 2 types référence les plus importants en java.
Autres types référence : Arrays, enumerated types (“enums”), annotation types (“annotations”).

Un fichier .java peut contenir :
- un seul type référence déclaré public ainsi que 0 ou plus types références de visibilité package.
ou alors :
- un ou plusieurs types références de visibilité package.

Si un fichier .java contient un type référence public, alors le nom du fichier doit être identique au nom du type.
(ex : MaClasse.java doit contenir le type public MaClasse).

Cette règle n'est pas imposée pour les types de visibilité package.
Attention lorsqu'on ne respecte pas la règle : 1 fichier java ⇿ 1 classe du même nom.
Voir exemples/java/packages/danger.

Classes

Syntaxe générale :
public abstract class TheClass extends TheSuperClass implements Interface1, Interface2 { ... }
La déclaration d'une classe contient, dans l'ordre :
  1. Modifiers (abstract, public, final) - mais pas private ou protected
  2. Le mot-clé class
  3. Le nom de la classe
  4. Eventuellement extends le nom de la classe parent
  5. Eventuellement implements suivi du nom de(s) interface(s) implémentée(s)
  6. Le corps de la classe, encadré par { ... }
Exemple de classe :
public class Circle {

    public static final double PI= 3.14159;

    public static double radiansToDegrees(double radians) { return radians * 180 / PI; }
    
    private double r;

    public Circle() { this(1.0); } // L'overloading (polymorphisme ad'hoc) est aussi possible pour les constructeurs
    
    public Circle(double r) { this.r = r; }
    
    public double getRadius() { return r; }
    
    public void setRadius(double r) {
        if (r < 0.0){
            throw new IllegalArgumentException("radius may not be negative.");
        }
        this.r = r;
    }
    
    public double area() { return PI * r * r; }
    
    public double circumference() { return 2 * PI * r; }
    
}
(code dans Circle.java)

Visibilité des classes

Membres d'une classe

Une classe peut avoir comme membres des variables, des méthodes ou des classes (voir classes internes plus loin).

Les membres peuvent être de classe (static) ou d'instance.

Visibilité des membres

publicprotecteddefaultprivate
Classe où le membre est défini Oui Oui Oui Oui
Classe dans le même package Oui Oui Oui Non
Sous-classe dans un package différent Oui Oui Non Non
Non sous-classe dans un package différent Oui Non Non Non
Les visibilités public et protected font partie de l'API publique de la classe (puisque protected peut être utilisé dans un autre package).
Mais pas les visibilités default et private.

Donc bien noter que la visibilité default est plus restrictive que protected.

Variables de classe ou d'instance

La syntaxe pour déclarer ou initialiser une variable de classe ou d'instance est similaire aux variables locales (à l'intérieur d'une méthode).
class Exemples{
    public static final int DAYS_PER_WEEK = 7;
    
    protected String[] daynames = new String[DAYS_PER_WEEK];

    private String name;
    
    int x = 1;
    
    public int a = 17, b = 37, c = 53;
    
    private static int nbInstances = 0;
}

Valeurs par défaut

Si elles ne sont pas initialisées, les variables de classe prennent une valeur par défaut.
ATTENTION, ce comportement est différent des variables locales, qui doivent être initialisées avant d'être utilisées.
class TestDefaultValues1{
    static int a;
    public static void main(String[] args){
        System.out.println("a = " + a);
    }
}
java TestDefaultValues1
a = 0
Mais cette classe ne passe pas à la compilation :
class TestDefaultValues2{
    public static void main(String[] args){
        int b;
        System.out.println("b = " + b);
    }
}
(code dans exemples/java/classes)
TypeValeur par défaut
byte0
short0
int0
long0L
float0.0f
double0.0d
char'\u0000'
booleanfalse
Types référencesnull

Manipuler des variables de classe (static)

A l'extérieur de la classe
NomDeLaClasse.nomDeLaVariable, par exemple Circle.PI
Mais la syntaxe peut être simplifiée avec import static
import package1.Circle;
...
double piSur2 = Circle.PI / 2.0;
...
équivalent à
import static package1.Circle.*;
...
double piSur2 = PI / 2.0;
...
A l'intérieur de la classe
Tout simplement PI, ou Circle.PI ; this.PI est aussi possible dans les méthodes d'instance.

Manipuler des variables d'instance

A l'extérieur de la classe
Les instances d'une classe doivent être instanciées avec le mot-clé new.
public class Circle {
    ...
    public Circle() { this(1.0); }
    public Circle(double r) { this.r = r; }
    ...
}
Le choix du constructeur invoqué dépendra des paramètres utilisés :
Circle c1 = new Circle(3.5);
Circle c2 = new Circle();
Une fois l'instance créée, on utilise . pour accéder aux membres de la classe :
Circle c = new Circle(3.5);
double radius = c.r;
double surface = c.area();
A l'intérieur de la classe (this)
this représente l'instance courante.
Mais this peut être omis à l'intérieur de la classe.
    public double area() { return PI * this.r * this.r; }
    // identique à :
    public double area() { return PI * r * r; }
Attention : this est obligatoire si une variable locale porte le même nom qu'un membre de la classe.
Très fréquent, dans les constructeurs ou les setters :
public class Circle {
    public Circle(double r) { this.r = r; }
    public void setRadius(double r) { this.r = r; }
}

Modifier final

La variable ne peut plus être modifiée une fois qu'elle a été initialisée.
Une variable déclarée public static final désigne donc une constante.

Autres modifiers

transient sert à indiquer que la variable ne fait pas partie de l'état permanent de l'objet, pas besoin de le stocker lors de la serialization.
volatile sert lorsqu'une classe ou un objet est utilisé par plusieurs threads, pour indiquer que la variable ne doit pas être stockée au niveau du thread mais dans la mémoire centrale (commune).

Constructeurs

Méthodes static ayant exactement le même nom de la classe, servent à initialiser les variables d'instances des nouveaux objets créés.
Chaque classe a au moins un constructeur ; s'il n'est pas explicitement présent, javac en crée un (appelé constructeur par défaut), sans paramètre et qui ne fait rien.
La signature d'un constructeur ne comporte aucun type de retour (pas même void) et pas le mot-clé static.
Un constructeur ne doit rien renvoyer (pas même this).

Utilisé avec le mot-clé new.
Circle c = new Circle(0.25);
Une classe peut avoir plusieurs constructeurs dont les paramètres doivent être différents (analogue à l'overload pour les méthodes).

Un constructeur peut en appeler un autre en utilisant this() :
public Circle(double r) { this.r = r; }
public Circle() { this(1.0); }
Restriction importante : l'appel utilisant this() ne peut être que la première instruction (voir héritage plus loin).

Initialisations

Dans un contexte d'instance

public class ClasseAvecTableau {

    public int len = 10;
    
    public int[] table = new int[len];
    
    public ClasseAvecTableau() {
        for(int i = 0; i < len; i++){
            table[i] = i;
        }
    }
}
Dans les constructeurs, les initialisations faites sur les variables d'instance sont toujours disponibles.
Le constructeur précédent est strictement équivalent à :
    public int len;
    
    public int[] table;
    
    public ClasseAvecTableau() {
        len = 10;               // ou this.len = 10;
        table = new int[len];   // ou this.table = new int[this.len]
        for(int i = 0; i < len; i++){
            table[i] = i;
        }
    }
On peut aussi écrire du code d'initialisation entre accolades au même niveau que les déclarations des champs et méthodes :
public class MyClass{

    // variables
    private static final int NUMPTS = 100;
    private int[] data = new int[NUMPTS];
    
    // initialisation
    {
        for(int i = 0; i < NUMPTS; i++){
            data[i] = i;
        }
    }
    
    // méthodes
    public MyClass(){ ... }
    
}
Peut parfois clarifier le code en plaçant l'initialisation à coté de la déclaration de la variable.
Là aussi, équivalent à écrire ce code dans chaque constructeur.

Dans un contexte static

javac génère automatiquement pour chaque classe une méthode statique d'initialisation (static initializer), invoquée une seule fois à la première utilisation de la classe.
On peut définir un initialisateur static avec un bloc static{ ... } :
public class Circle2 {
    
    private static final int NUM = 500;
    
    private static double sines[] = new double[NUM];
    
    private static double cosines[] = new double[NUM];
    
    static {
        double delta_x = (Circle.PI/2)/(NUM-1);
        double x = 0.0;
        for(int i = 0; i < NUM; i++, x += delta_x) {
            sines[i] = Math.sin(x);
            cosines[i] = Math.cos(x);
        }
    }
    
    public static void main(String[] args){
        System.out.println("NUM = " + NUM);
        System.out.println("delta_x = " + delta_x); // Ne passe pas à la compilation
    }
}
Exercice : Compte en banque 1

Sous-classes et héritage

/**
    Un Circle est caractérisé par son rayon
    Un PlaneCircle est en plus caractérisé par les coordonnées de son centre dans un plan
**/
public class PlaneCircle extends Circle {
    
    private final double cx, cy;    // coordonnées du centre
    
    public PlaneCircle(double r, double x, double y) {
        super(r);
        this.cx = x;
        this.cy = y;
    }
    
    public double getCentreX() { return cx; }
    
    public double getCentreY() { return cy; }
    
    public boolean isInside(double x, double y) {
        double dx = x - cx, dy = y - cy;
        double distance = Math.sqrt(dx*dx + dy*dy); // théorème de Pythagore
        return (distance < getRadius());      // this.getRadius() nécessaire car Circle.r est private
    }
}
(PlaneCircle.java) Note importante : les variables privées de la classe mère ne sont pas accessibles dans la classe fille (sauf de manière indirecte si des getters et des setters ont été définis dans la classe mère). Mais lorsqu'on crée un objet de la classe fille, les variables privées de la classe mère font partie de cet objet.
Un objet de la classe PlaneCircle possède aussi un rayon.

Narrowing and widening conversions (rappel)

Tout objet PlaneCircle est un Circle parfaitement légal.
PlaneCircle pc = new PlaneCircle(1.0, 0.0, 0.0);
Circle c = pc;
Widening conversion, possible sans type cast ; c a perdu les facultés supplémentaires de pc ; qui peut le plus peut le moins, pas de problème.

Mais une narrowing conversion doit être faite avec un type cast explicite :
PlaneCircle pc2 = (PlaneCircle) c;
ATTENTION : ici le type cast ne va pas générer d'erreur à l'exécution car c était le résultat d'une narrowing conversion.
Dans le cas général, passe à la compilation mais génère une erreur à l'exécution :
public class TestTypecast{

    public static void main(String[] args){
        PlaneCircle pc1 = new PlaneCircle(1.0, 0.0, 0.0);
        Circle c1 = pc1; // widening conversion
        PlaneCircle pc1bis = (PlaneCircle) c1; // narrowing conversion OK
        System.out.println("pc1bis : " + pc1bis);
        
        Circle c2 = new Circle();
        PlaneCircle pc2 = (PlaneCircle) c2; // narrowing conversion erreur exécution
        System.out.println("pc2 : " + pc2);
    }
    
}
(TestTypecast.java)
javac TestTypecast.java
java TestTypecast 
pc1bis : PlaneCircle@42f30e0a
Exception in thread "main" java.lang.ClassCastException: Circle cannot be cast to PlaneCircle
	at TestTypecast.main(TestTypecast.java:13)

Classe déclarée final

Sous-classage impossible. Par exemple java.lang.String est final => si du code tiers (hors de votre contrôle) vous passe une String, vous êtes sûr(e) qu'il s'agit bien de cette classe, et pas d'une extension douteuse.

Constructeurs

L'utilisation de super() a les mêmes limites que l'utilisation de this() : Java garantit que le constructeur d'une classe est appelé lorsqu'un objet de cette classe est instancié.
Java garantit aussi que ce constructeur est appelé lorsqu'un objet de la sous-classe est créé.
Donc si un constructeur n'a pas pour première instruction this() ou super(), javac insère une instruction super() au début de chaque constructeur.

Exemple : lorsque l'instruction
PlaneCircle pc1 = new PlaneCircle(1.0, 0.0, 0.0);
est exécutée :
  1. Le constructeur de PlaneCircle est appelé.
  2. Ce constructeur appelle explicitement super(), et appelle donc le constrcteur de Circle.
  3. Le constructeur de Circle appelle implicitement le constructeur de Object.
    On atteint le sommet de la hiérarchie, les constructeurs vont pouvoir s'exécuter.
  4. Le constructeur de Object est exécuté.
  5. Le constructeur de Circle est exécuté.
  6. Le constructeur de PlaneCircle est exécuté.
Si on déclare une classe sans définir explicitement de constructeur, java en crée un implicitement.
Donc revient au même qu'avoir ce constructeur :
class MyClass {
    public MyClass() {
        super();
    }
}
Noter au passage que les constructeurs ne sont jamais hérités.

Constructeurs privés

Sert à deux choses : Voir une application dans le pattern Singleton.

Overloading, overriding, hiding

Ce sont trois notions distinctes qu'il ne faut pas confondre.

Overloading

Method overloading (surcharge de méthode) : consiste à définir dans une même classe plusieurs méthodes de même nom mais prenant des paramètres de types différents.
C'est l'exemple de PrintStream.println() ou org.junit.jupiter.api.Assertions.assertEquals().
On appelle ça le polymorphisme ad'hoc, qui n'est pas lié à la notion d'héritage.

Overriding

Method overriding (redéfinition ou spécialisation de méthode) : consiste à redéfinir dans une sous-classe une méthode, avec la même signature (ou presque, voir exemple de clone()).
Un exemple classique est la redéfinition de toString().

On appelle ça le polymorphisme par inclusion ou polymorphisme par héritage.

Important : cela n'est possible que pour les méthodes d'instance.
IMPORTANT : bien comprendre que seules les méthodes d'instances sont polymorphiques, pas les méthodes statiques.

On peut utiliser l'annotation @Override pour signaler notre intention au compilateur (c'est conseillé).

La signature de la méthode doit être la même à deux exceptions près :
- Lorsqu'on override, on peut rendre une méthode moins privée, mais pas plus privée.
- La méthode de la sous-classe peut renvoyer un sous-type.
Exemple fréquemment rencontré : clone()
Object.clone() est définie ainsi :
protected Object clone() throws CloneNotSupportedException
Lorsqu'on override clone(), on fait souvent :
class MyClass[
    @Override public MyClass clone(){ /* ... */ }
}
(noter que @Override ne génère pas de warning).

La surcharge de méthode est très pratique car elle permet d'invoquer la méthode de la sous-classe, même lorsque le type déclaré est celui de la superclasse :
public class TestOverriding{
    
    public static void main(String[] args){
        Animal[] animaux = {
            new Chien(),
            new Chat()
        };
        for(Animal a : animaux){
            System.out.println(a.grogner());
        }
    }
}

class Animal{
    public String grogner(){
        return "grogner() dans Animal";
    }
}
class Chat extends Animal {
    @Override
    public String grogner(){
        return "Miaou";
    }
}
class Chien extends Animal {
    @Override
    public String grogner(){
        return "Ouaf";
    }
}                                                                      
(TestOverriding.java)
java TestOverriding 
Ouaf
Miaou

Method hiding

Pour les méthodes statiques, cette subtilité n'est pas possible car on invoque une métode statique en spécifiant le nom de la classe.
On parle alors de method hiding.
public class TestMethodHiding{
    
    public static void main(String[] args){
        System.out.println(Animal.grogner());
        System.out.println(Chat.grogner());
        System.out.println(Chien.grogner());
    }
}

class Animal{
    public static String grogner(){
        return "grogner() dans Animal";
    }
}
class Chat extends Animal {
    public static String grogner(){
        return "Miaou";
    }
}
class Chien extends Animal {
    public static String grogner(){
        return "Ouaf";
    }
}
(TestMethodHiding.java)
java TestMethodHiding 
grogner() dans Animal
Miaou
Ouaf

Danger de confusion

Cependant, java permet d'invoquer des méthodes statiques sur des instances, bien que ça soit très déconseillé.
On reprend l'exemple TestOverriding, que l'on renomme en TestMethodHiding2.
On passe en static les méthodes grogner() :
public class TestMethodHiding2{
    
    public static void main(String[] args){
        Animal[] animaux = {
            new Chien(),
            new Chat()
        };
        for(Animal a : animaux){
            System.out.println(a.grogner());
        }
    }
}

class Animal{
    public static String grogner(){
        return "grogner() dans Animal";
    }
}
class Chat extends Animal {
    // @Override
    public static String grogner(){
        return "Miaou";
    }
}
class Chien extends Animal {
    // @Override
    public static String grogner(){
        return "Ouaf";
    }
}
(TestMethodHiding2.java)

A la compilation, javac nous indique :
TestMethodHiding2.java:21: error: static methods cannot be annotated with @Override
TestMethodHiding2.java:27: error: static methods cannot be annotated with @Override
En supprimant les annotations, on peut compiler sans erreur.
java TestMethodHiding2 
grogner() dans Animal
grogner() dans Animal

Field hiding

Dans une sous-classe, si un champ a le même nom qu'un champ de la classe mère, Il le cache, même s'il est d'un type différent.
C'est une pratique déconseillée.
Dans ce cas, si on veut accéder au champ caché, il faut utiliser le mot-clé super ou faire du type cast
public class TestFieldHiding{
    public static void main(String[] args){
        C c = new C();
        c.print();
    }
}

class A{
    public String x = "A";
}
class B extends A{
    public String x = "B";
}
class C extends B{
    public String x = "C";
    
    public void print(){
        System.out.println("x = \t\t" + x);
        System.out.println("this.x = \t" + this.x);
        System.out.println("super.x = \t" + super.x);
        System.out.println("((B)this).x = \t" + ((B)this).x);
        System.out.println("((A)this).x = \t" + ((A)this).x);
    }
}
(TestFieldHiding.java)
java TestFieldHiding 
x = 		C
this.x = 	C
super.x = 	B
((B)this).x = 	B
((A)this).x = 	A
Notez qu'on est ici dans un contexte d'instance, et que variables et méthodes ne sont pas traitées de la même manière :
- Pour les méthodes, on est en situation d'overriding.
- Pour les variables, on est en situation de hiding.

Utilisations de super

On a vu que le mot-clé super a plusieurs utilisations dont la syntaxe et la sématique sont différentes :
- super() pour appeler un constructeur de la superclasse.
- super pour se référer à un champ de la superclasse.
Cette dernière utilisation est aussi possible avec les méthodes :
public class TestSuper {
    public static void main(String[] args) {
        var obj = new SousClasse();
        obj.instanceMethod();
    }
}


class SuperClasse {
    public void instanceMethod() {
        System.out.println("SuperClasse.instanceMethod()");
    }
}

class SousClasse extends SuperClasse {
    @Override
    public void instanceMethod(){
        System.out.println("SousClasse.instanceMethod()");
        super.instanceMethod();
    }
}
(code dans TestSuper.java)
Exercice : Compte en banque 2

Abstract classes

Imaginons qu'on développe une API qui manipule différentes formes : Cercle, Carre, Triangle, et que chacune de ces formes ait deux méthodes en commun : aire() et circonference().
Pour pouvoir facilement manipuler des tableaux de formes, on a besoin de créer une classe Forme, mais donner une implémentation générale aux méthodes aire() et circonference() n'a pas de sens.

Les classes abstraites sont bien adaptées à ce genre de situation :
abstract class Forme{
    public abstract double aire();
    public abstract double circonference();
}

class Carre extends Forme{
    public double aire(){ return 1.0; }
    public double circonference(){ return 1.0; }
}

class Triangle extends Forme{
    public double aire(){ return 1.0; }
    public double circonference(){ return 1.0; }
}
Les règles sont :

Interfaces

Définir un contrat

Un aspect important des interfaces est qu'elles permettent de définir des contrats que les implémentations doivent respecter.
Exemple : une société qui développe des véhicules autonomes va définir et publier des interfaces pour faire déplacer une voiture (tourner, accélérer, freiner...), des interfaces liées au guidage (positionnement, environnement géographique, conditions de traffic) et développer le code qui articule ces sous-systèmes pour faire fonctioner la voiture.
Des sociétés tierces vont pouvoir développer des implémentations de ces différentes interfaces (constructeurs auto pour le déplacement, sociétés spécialisées dans le guidage électronique pour le guidage). Chacune ignore les implémentations des autres, mais elles vont pouvoir collaborer grace aux interfaces qu'elles sont tenues de respecter. Il sera aussi possible de changer l'implémentation d'un sous-système sans affecter les autres parties.
interface DéplacerVoiture{
    int tourner(Direction direction,
                double rayonDeCourbure,
                double vitesseInitiale,
                double vitesseFinale);
    
    int accélérer(...);
}
Implémentations développées par différents acteurs :
public Class Déplacer2CV implements DéplacerVoiture{
    public int tourner( ... ){ ... }
    public int accélérer( ... ){ ... }
}
public Class DéplacerMégane implements DéplacerVoiture{
    public int tourner( ... ){ ... }
    public int accélérer( ... ){ ... }
}

Définir une interface

Similaire à la définition d'une classe
interface Interface1 extends Interface2, Interface3{
    
    // déclaration de constante
    double constante1 = 2.718282;       // = public static final
 
    // déclaration de méthode
    void méthode1 (int i, double x);    // = public abstract
}
Comme pour une classe, lorsqu'on définit une nouvelle interface, on définit un nouveau type.
Un objet d'une classe implémentant des interfaces est à la fois du type de sa classe et de toutes les interfaces implémentées par cet objet.

Implémenter une interface

Pour déclarer une classe qui implémente une interface, on utilise la clause implements dans la déclaration de la classe.
Si la déclaration de la classe contient aussi une clause extends, on la place avant (par convention).
public Class1 extends class2 implements interface1, interface2{ ... }
Les classes doivent fournir une implémentation de toutes les méthodes abstraites définies dans les interfaces qu'elles implémentent.

Note : si une classe implémente plusieurs interfaces, et qu'une même méthode est déclarée dans plusieurs de ces interfaces, cela ne pose pas de problème :
interface Interface1{
    public void methode();
}
interface Interface2{
    public void methode();
}
class RepeteMethodes implements Interface1, Interface2{
    public void methode(){
        System.out.println("RepeteMethodes.methode()");
    }
}
(code dans RepeteMethodes.java)
Exercice interfaces1, questions 1 et 2.

Default methods

Depuis java 8, il est possible de fournir des implémentations par défaut des méthodes définies dans une interface.
interface DoIt {
    // ...
   default void aNewMethod(int i){
    // ...  implémentation fournie
   }
}
Une classe implémentant une interface peut implémenter les méthodes par défaut (mais n'est pas obligée).
Si elle l'implémente, c'est cette version qui sera utilisée, sinon, c'est l'implémentation par défaut fournie par l'interface.
Cela répond à deux besoins :
Une limite : overrider les méthodes de Object n'est pas possible.
// Ne passe pas à la compilation
// error: default method toString in interface I overrides a member of java.lang.Object
interface I{
    @Override default public String toString(){ return "interface I"; }
}
Exercice interfaces1, questions 3.

Implémenter plusieurs interfaces avec les mêmes default methods

interface Interface1{
    default void method1(){
        System.out.println("Interface1.method1()");
    }
}
interface Interface2{
    default void method1(){
        System.out.println("Interface2.method1()");
    }
}

/* 
Ne passe pas à la compilation :
DefaultMethods.java:23: error: class C1 inherits unrelated defaults for method1() from types Interface1 and Interface2
*/
class C1 implements Interface1, Interface2{}

/* Compilation OK */
class C2 implements Interface1, Interface2{
    public void method1(){}
}
(DefaultMethodsAmbigues.java)

Dans ce cas, l'implémentation de la classe doit lever l'ambiguité en ré-implémentant la méthode.

Etendre une interface avec des default methods

Si une interface dérive d'une interface avec une default method, trois situations sont possibles :
  1. Ne pas mentionner la default method - dans ce cas, hérite de la default method.
  2. Redéclarer la default method sans l'implémenter - dans ce cas, l'implémentation de la default method est "perdue".
  3. Redéfinir la default method - dans ce cas, la nouvelle implémentation remplace la première.
interface Interface0{
    default void methode(){
        System.out.println("methode() dans Interface0");
    }
}

// cas 1
// OK ; C1 utilise l'implémentation de Interface0
interface Interface1 extends Interface0{ }
class C1 implements Interface1{}

// cas 2
interface Interface2 extends Interface0{
    void methode();
}
// erreur de compilation
// ExtendsDefault.java:19: error: C2 is not abstract and does not override abstract method methode() in Interface2
class C2 implements Interface2{}

// cas 3
// OK ; C3 utilise l'implémentation de Interface3
interface Interface3 extends Interface0{
     default void methode(){
        System.out.println("methode() dans Interface3");
    }
}
class C3 implements Interface3{}
(ExtendsDefault.java)

Interface static methods

Depuis java 8, il est aussi possible d'implémenter des méthodes statiques dans les interfaces.
interface Interface1{
    static String doSomething(){ ... }
}
Lorsqu'on fabrique une API, permet de rassembler du code utilitaire dans des interfaces (avant, on mettait les constantes dans les interfaces et les méthodes static dans des classes).
Le mot-clé public est là aussi implicite.

Méthodes privées dans les interfaces

Dans la mesure où java 8 a introduit la possibilité des implémentations dans les interfaces (default ou static methods), il est logique de pouvoir implémenter des méthodes privées dans une interface ; possible depuis java 9.
Ces méthodes privées ne sont utilisables que dans le code de l'interface qui les définit.
Les classes implémentant l'interface bénéficient indirectement des méthodes privées sans avoir à les implémenter.
Voir PrivateMethodsInterface.java.

Interfaces vides

Appelées aussi marker interfaces, elles permettent de signaler des caractéristiques de certaines classes.

Exemple d'utilisation : l'interface java.util.RandomAccess est vide.
Elle est utilisée par certaines implémentations de java.util.list pour signaler cette caractéristique (la possibilité d'accéder rapidement à tous ses éléments). Par exemple, ArrayList l'implémente, mais pas LinkedList.
On peut s'en servir comme ça :
// on a une liste, mais on ne sait pas quelle est sa classe concrète
List l = ...;
// on veut trier l
// on sait que la classe concrète de l contient une méthode sort()
// mais l'implémentation de sort() est beaucoup plus efficace pour des listes en random access
// donc avant de faire le sort(), on teste si on est dans un cas de random access
// si ce n'est pas le cas, on convertit l dans une implémentation en random access
if (l.size() > 2 && !(l instanceof RandomAccess)){
    l = new ArrayList(l);
}
l.sort();
Un autre exemple fréquemment utilisé est la marker interface Cloneable ; voir la page java.lang.Object, paragraphe Clone().

Classe abstraite ou interface ?

Avant java 8, les interfaces ne pouvaient que contenir des spécifications de méthodes, sans aucune implémentation, ce qui pouvait mener à dupliquer du code, dans les différentes implémentations d'une interface.
On trouvait souvent des classes abstraites couplées à des interfaces : les interfaces contenaient la spécification de l'API, et une classe abstraite fournissait l'implémentation de certaines méthodes.
Les implémentations de l'interface dérivaient de la classe abstraite.

Par exemple List et AbstractList Depuis les default et static methods, on a le choix.
Quelques considérations :

Sealed classes et interfaces

Depuis java 17 - Permet de spécifier quelles classes vont pouvoir descendre d'une classe ou implémenter une interface.

TODO - voir docs.oracle.com