Java >> Tutoriel Java >  >> Java

Syntaxe et conception de boucle en Java

Content de te revoir! Dans ce didacticiel, nous allons plonger dans une nouvelle technique de flux de contrôle connue sous le nom de boucle.

Récursivité

Jusqu'à présent, nous avons pu jouer avec des variables, des classes, des méthodes et même des branchements. Après avoir introduit la création de branches, nous avons décidé de nous attaquer à une petite introduction à un outil de vérification appelé test unitaire. À ce stade, nous devrions être assez familiers avec les bases de la logique et des conditions. Mais, que fait-on si on veut exécuter du code qui se répète ?

Curieusement, nous n'avons pas besoin d'introduire de nouvelle syntaxe pour pouvoir boucler un extrait de code. Par exemple, que se passe-t-il lorsque nous exécutons la méthode suivante ?

public static void printForever(String printMe) {
    System.out.println(printMe);
    printForever(printMe);
}

Eh bien, allons-y.

Passer en revue un exemple

Disons que nous appelons Foo.printForever("Hello, World!") . Pour référence, Foo est un terme générique pour la classe dans laquelle cette méthode peut apparaître.

Tout d'abord, nous remarquerons que la chaîne d'entrée est transmise à notre instruction d'impression. Dans la console, nous devrions voir la chaîne "Hello, World!" . Ensuite, la méthode descend jusqu'à une ligne amusante :

printForever(printMe);

À partir de cette ligne, il apparaît que nous appelons la méthode depuis l'intérieur lui-même. En fin de compte, cela est tout à fait légal et entraîne ce que l'on appelle un appel récursif .

Malheureusement, cela entraîne l'impression infinie de notre chaîne car la méthode s'appelle elle-même pour toujours. Heureusement, nous finirons par voir le programme planter avec une exception de débordement de pile.

Rappel de la pile

Si nous repensons à notre didacticiel sur les méthodes, nous nous souviendrons que les appels de méthode se frayent un chemin dans la pile de mémoire. Au fur et à mesure que nous imbriquons des méthodes, la pile d'appels s'agrandit. En règle générale, nous atteignons une limite finie dans les appels de méthode avant de revenir en arrière le long de la pile des appels.

Cependant, dans notre exemple ci-dessus, nous n'avons jamais atteint cette limite. Au lieu de cela, nous continuons à ajouter des appels de méthode jusqu'à ce que nous manquions de mémoire. Ne vous inquiétez pas! C'est assez facile à corriger. Nous devons juste ajouter une sorte de cas de base qui définit ce dernier appel de méthode dans la pile.

Essayons d'utiliser un entier pour spécifier le nombre d'impressions que nous voulons faire.

public static void recursivePrint(String printMe, int numOfPrints) {
    // Base case
    if (numOfPrints <= 0) {
        System.out.println("Finished printing!");
    } else {
        System.out.println(printMe); 
        printForever(printMe, numOfPrints - 1);
    }
}

Dans cette implémentation, nous fournissons un nouveau paramètre que nous utilisons pour spécifier combien de fois nous voulons que notre chaîne soit imprimée. Dans la méthode, nous ajoutons un cas spécial pour quiconque décide d'imprimer zéro ou moins de copies de sa chaîne.

La vraie magie se produit dans notre autre cas. Pour inciter la récursivité à atteindre un cas de base, nous fournissons toujours le prochain appel récursif avec un nombre d'impressions de moins. De cette façon, le numOfPrints paramètre détermine le nombre d'appels global.

Ne vous inquiétez pas si cela semble déroutant ! La récursivité n'est pas quelque chose que nous utiliserons à ce stade, mais c'est certainement un concept agréable à avoir dans notre poche arrière. En fait, il sera beaucoup plus utilisé lorsque nous aborderons des structures de données telles que des arbres et des graphiques. Pour l'instant, plongeons dans quelque chose de plus intuitif !

Boucle itérative

La récursivité est un moyen de boucler un morceau de code, mais il est souvent plus rapide et plus intuitif d'utiliser une approche itérative.

Avec la récursivité, nous n'attaquons pas réellement notre problème initial jusqu'à ce que nous atteignions le dernier appel récursif. Le résultat de ce calcul est filtré jusqu'à ce que nous résolvions finalement le problème initial que nous avions.

Avec les boucles, nous exécutons un calcul à plusieurs reprises jusqu'à ce que nous atteignions notre résultat. En conséquence, les boucles sont généralement plus intuitives car elles reflètent notre façon de penser. C'est pourquoi des langages comme Java incluent une syntaxe en boucle. En fait, Java inclut la syntaxe d'au moins 4 boucles différentes, mais nous n'en couvrirons que deux :for et while .

La boucle While

La boucle while a la syntaxe suivante :

while (condition) {
    // loop body
}

Tant que la condition reste vraie, le corps de la boucle s'exécute en continu. Cette structure en boucle est assez simple et ouverte à la modification. Cependant, le principal avantage de cette structure en boucle est la clarté de la condition pour des raisons de lisibilité.

Si nous voulions implémenter notre exemple de récursivité en utilisant cette syntaxe, nous pourrions procéder comme suit :

public static void whilePrint(String printMe, int numOfPrints) {
    int count = 0;
    while (count < numOfPrints) {
        System.out.println(printMe);
        count++;
    }
}

Comme précédemment, nous fournissons une fonction à deux entrées :un String et un int . Cependant, cette fois, nous créons un compteur pour savoir combien de fois nous avons bouclé. La condition de boucle s'appuie ensuite sur le nombre pour augmenter jusqu'à ce qu'elle atteigne le nombre d'impressions demandées par l'utilisateur.

À ce stade, nous devons noter que count commence à zéro. Cela peut sembler inhabituel si vous n'avez pas d'expérience en programmation. Zéro n'est pas strictement obligatoire, mais c'est généralement la valeur utilisée lors du comptage dans une boucle. Nous verrons pourquoi un peu plus loin dans le tutoriel alors habituez-vous à le voir.

De plus, nous devons noter que nous incrémentons count en bas du corps de la boucle en utilisant le ++ opérateur. Cet opérateur unaire ajoute un à count qui est beaucoup plus propre que count = count + 1 .

La boucle For

En plus de la boucle while, Java nous donne la syntaxe de la boucle for :

for (initialization; condition; increment) {
    // loop body
}

Au début, cette syntaxe peut sembler complexe et écrasante. Que se passe-t-il lors de l'initialisation ? Pourquoi y a-t-il une instruction d'incrémentation ? En conséquence, de nombreux débutants se rabattent sur la boucle while. Cependant, la boucle for introduit un peu de sucre syntaxique qui peut rendre nos boucles un peu plus propres.

Essayons d'implémenter notre méthode d'impression une dernière fois avec cette syntaxe.

public static void forPrint(String printMe, int numOfPrints) {
    for (int count = 0; count < numOfPrints; count++) {
        System.out.println(printMe);
    }
}

Essentiellement, nous économisons deux lignes, mais la structure globale est plus facile à lire. Il est très clair que la boucle va de zéro à numOfPrints lors de l'impression de la chaîne d'entrée à chaque fois.

Cela dit, les boucles for peuvent devenir un peu plus moches si nous avons des conditions composées. Dans ces cas, nous devrions probablement opter pour la structure de la boucle while. Quoi qu'il en soit, nous avons maintenant 3 mécanismes de bouclage différents à notre actif.

Indices de boucle

Les boucles ne servent pas seulement à exécuter un morceau de code en boucle. Ils peuvent également être utilisés pour itérer sur une collection de données. Maintenant, nous n'avons encore abordé aucune sorte de structures de données, mais nous sommes familiers avec les chaînes.

Mais qu'est-ce qu'une chaîne ? Eh bien, c'est une collection de personnages. En d'autres termes, nous pouvons réellement utiliser une boucle pour parcourir ces caractères afin de faire quelque chose d'utile. Par exemple, nous pourrions essayer d'imprimer chaque caractère individuellement :

public static void printChars(String characters) {
    for (int i = 0; i < characters.length(); i++) {
        System.out.println(characters.charAt(i));
    }
}

Tout comme notre fonction précédente, nous avons utilisé une boucle for. Encore une fois, notre variable de comptage initial commence à zéro. C'est parce que les chaînes sont indexées à zéro. En d'autres termes, le premier caractère de chaque chaîne est à l'emplacement zéro. Lorsque nous appelons le characters.charAt(i) à la première itération, nous devrions obtenir le premier caractère de notre chaîne d'entrée.

Une autre partie critique de la boucle est la condition. Notez que la condition exécute l'index jusqu'à un avant la longueur de la chaîne. C'est parce que le dernier caractère de la chaîne a en fait un index de length() - 1 .

Si nous essayons d'accéder au caractère un au-delà de length() - 1 , nous obtiendrions une exception d'index hors limites. Cela peut sembler ennuyeux au début, mais c'est vraiment une fonctionnalité de sécurité importante. Des langages comme C et C++ n'ont pas ce type de protection, ce qui signifie que nous pouvons réellement fouiller dans la mémoire si nous ne faisons pas attention.

Refactorisation

Pendant que nous sommes ici, cela semble être une belle occasion d'aborder cette notion de refactoring .

Ci-dessus, nous avons vu trois méthodes qui implémentaient toutes la même fonctionnalité. Cela montre simplement que même sur un exemple simple, il existe plusieurs façons de mettre en œuvre une solution.

Lors de la rédaction de solutions, nous devons toujours nous efforcer d'abord d'être corrects. Nous devons nous assurer que notre solution fournit le comportement souhaité :les tests JUnit sont un bon début.

Ensuite, nous passons généralement par une phase de refactoring, ce qui signifie que nous essayons de trouver des moyens de nettoyer et d'optimiser notre code. Cependant, nous n'aimons pas toujours changer les noms de méthodes et les signatures de paramètres. Ces types de modifications peuvent entraîner l'échec de la compilation du code externe.

Au lieu de cela, nous modifions généralement ce qui se passe à l'intérieur d'une méthode. C'est l'occasion pour nous de répondre aux problèmes de performance et de fiabilité. Par exemple, nous pourrions changer notre implémentation de la récursivité en boucles juste pour des raisons de lisibilité. Dans d'autres cas, nous pourrions essayer de trouver des moyens de faire un compromis entre la vitesse et la mémoire dans une méthode.

Quoi qu'il en soit, cela devrait servir à rappeler que le code est une substance vivante. Il doit être revu et modifié au besoin pour renforcer son rôle dans un système.

Suivant

Maintenant que nous avons couvert le bouclage, nous allons pouvoir lancer des projets plus importants. En fait, nous terminerons probablement la série sur les bases de Java avec seulement deux autres didacticiels.

Ensuite, nous aborderons enfin la lisibilité qui inclut des sujets intéressants comme JavaDoc. De plus, nous approfondirons quelques sujets controversés concernant le style de programmation.

Ensuite, nous terminerons la série avec un examen global du matériel de cette série. Dans ce didacticiel, nous essaierons d'aborder la structure de classe un peu plus en profondeur. Nous voudrons nous familiariser avec les modificateurs d'accès ainsi qu'avec les getters et les setters. Lorsque nous aurons terminé, nous devrions pouvoir créer quelques classes et utiliser leurs objets pour faire des choses amusantes !

À l'avenir, nous commencerons à aborder des concepts orientés objet plus profonds, tels que les hiérarchies, les structures de données et les modèles de logiciels. Soyez pompé !

Comme toujours, si vous avez apprécié ce tutoriel, partagez-le avec vos amis. Si vous avez des questions ou des commentaires, n'hésitez pas à les laisser ci-dessous ou à me contacter directement. Et si vous voulez vous tenir au courant des derniers articles, n'oubliez pas de vous abonner à The Renegade Coder. Jusqu'à la prochaine fois !


Balise Java