انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-015-L07

عرض عملي: مدير درجات الطلاب باستخدام المصفوفات

  • الدرس 7 من 8
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

This is a build-along lesson. You have the pieces: arrays, loops, methods, if chains, printf, and String methods. Now we assemble the core of the module lab, a student-grade manager, one small method at a time, and meet the classic array bug in the wild so you can recognize it fast.

The lockers appear in a new way here: we keep two rows of lockers side by side. The row names has the student's name in locker 3, and the row grades has that student's grade in locker 3. The same number links the two rows. These are called parallel arrays. They are a simple stand-in for something better (objects, which come in later modules), and they work well as long as you keep them the same length.

Why this matters: "compute statistics over a list, look something up by name, print a tidy report, and survive empty input" is the shape of a large fraction of everyday backend code.

This lesson builds on everything in this module so far, plus methods (BEG-014) and printf (BEG-011).

Learning Objectives

By the end of this lesson you will be able to:

  • Represent a table of records as parallel arrays linked by index.
  • Write small, single-purpose methods for average, highest, and lowest.
  • Return an index (instead of a value) so the caller can look up related data in the other array.
  • Write a letterGrade method that tests boundary values correctly.
  • Search by name ignoring case and surrounding spaces, and report "not found" with -1.
  • Spot and fix i <= length, and guard against an empty class.

Written Explanation

The plan

We will build four small methods and a report, and test the edge cases at each step:

  1. average(double[])
  2. highestIndex and lowestIndex, which return a position, not a value
  3. letterGrade(double) and indexOfName(String[], String)
  4. A report that prints an aligned table

The rules for every method come from earlier lessons: loops use i < length, strings are compared with equals or equalsIgnoreCase, and each method does one job and returns its answer instead of printing it (BEG-014).

Why return an index

A highest grade is just a number, but the report should say whose grade it is. If highestIndex returns the position, the caller gets the value with grades[i] and the name with names[i]: one answer serves both parallel arrays. The empty class has no highest student, so the methods return -1 there, a value that cannot be a valid index.

The empty class

An average over zero students divides by zero. With double arithmetic Java does not throw: 0.0 / 0 gives NaN, which would print as a confusing NaN in the report. (For int division, / 0 throws an ArithmeticException.) The fix is a guard: check length == 0 first and return a sensible result or print "(no students)".

Boundaries in letterGrade

The scale used here is: 90 and above is A, 80 up to 90 is B, 70 up to 80 is C, 60 up to 70 is D, and anything lower is F. Test the exact boundaries (90, 80, 70, 60) and values just below them (89.99, 79.99, ...). Writing the checks from the highest threshold down means each else if only needs one comparison, as in BEG-012.

Examples

Example 1: statistics with guards (simple)

java
public class Ex1 {
    static double average(double[] grades) {
        if (grades.length == 0) {
            return 0;
        }
        double total = 0;
        for (double g : grades) {
            total += g;
        }
        return total / grades.length;
    }

    static int highestIndex(double[] grades) {
        if (grades.length == 0) {
            return -1;
        }
        int best = 0;
        for (int i = 1; i < grades.length; i++) {
            if (grades[i] > grades[best]) {
                best = i;
            }
        }
        return best;
    }

    static int lowestIndex(double[] grades) {
        if (grades.length == 0) {
            return -1;
        }
        int worst = 0;
        for (int i = 1; i < grades.length; i++) {
            if (grades[i] < grades[worst]) {
                worst = i;
            }
        }
        return worst;
    }

    public static void main(String[] args) {
        String[] names = {"Ana", "Omar", "Lina", "Sami"};
        double[] grades = {88.5, 72.0, 95.0, 59.5};
        System.out.printf("Average: %.2f%n", average(grades));
        int hi = highestIndex(grades);
        int lo = lowestIndex(grades);
        System.out.printf("Highest: %.1f (%s)%n", grades[hi], names[hi]);
        System.out.printf("Lowest: %.1f (%s)%n", grades[lo], names[lo]);
        double[] none = new double[0];
        System.out.println(average(none));
        System.out.println(highestIndex(none));
    }
}

Output (compiled and run with JDK 21):

text
Average: 78.75
Highest: 95.0 (Lina)
Lowest: 59.5 (Sami)
0.0
-1

The enhanced for is enough for the average (no index needed). The two search methods use the indexed loop, starting at 1 because position 0 is the starting candidate. The empty array returns 0 and -1 instead of crashing. A caller must check for -1 before using the result as an index: in main we only did so for arrays known to be non-empty.

Example 2: letter grade and case-insensitive search (practical)

java
public class Ex2 {
    static char letterGrade(double g) {
        if (g >= 90) {
            return 'A';
        } else if (g >= 80) {
            return 'B';
        } else if (g >= 70) {
            return 'C';
        } else if (g >= 60) {
            return 'D';
        }
        return 'F';
    }

    static int indexOfName(String[] names, String wanted) {
        for (int i = 0; i < names.length; i++) {
            if (names[i].equalsIgnoreCase(wanted.trim())) {
                return i;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        String[] names = {"Ana", "Omar", "Lina", "Sami"};
        double[] grades = {88.5, 72.0, 95.0, 59.5};
        System.out.println(letterGrade(90) + " " + letterGrade(89.99) + " " + letterGrade(59.5));
        String[] queries = {"lina", "  OMAR ", "Zed"};
        for (String q : queries) {
            int i = indexOfName(names, q);
            if (i == -1) {
                System.out.println("'" + q.trim() + "': not found");
            } else {
                System.out.println(names[i] + ": " + grades[i] + " " + letterGrade(grades[i]));
            }
        }
    }
}

Output:

text
A B F
Lina: 95.0 A
Omar: 72.0 C
'Zed': not found

Exactly 90 is an A, while 89.99 is a B. The search trims the query and ignores case, so " OMAR " finds Omar. It returns inside the loop at the first match and returns -1 after the loop when nothing matched. It prints the stored name names[i] (not what the user typed), which keeps the report consistent.

Example 3: report, empty class, and the planted bug (realistic)

java
public class Ex3 {
    static char letterGrade(double g) {
        if (g >= 90) {
            return 'A';
        } else if (g >= 80) {
            return 'B';
        } else if (g >= 70) {
            return 'C';
        } else if (g >= 60) {
            return 'D';
        }
        return 'F';
    }

    static void report(String[] names, double[] grades) {
        System.out.printf("%-8s %6s %6s%n", "Name", "Grade", "Letter");
        if (names.length == 0) {
            System.out.println("(no students)");
            return;
        }
        for (int i = 0; i < names.length; i++) {
            System.out.printf("%-8s %6.1f %6c%n", names[i], grades[i], letterGrade(grades[i]));
        }
    }

    static void buggyTotal(double[] grades) {
        double total = 0;
        for (int i = 0; i <= grades.length; i++) {
            total += grades[i];
        }
        System.out.println(total);
    }

    public static void main(String[] args) {
        report(new String[]{"Ana", "Omar", "Lina", "Sami"}, new double[]{88.5, 72.0, 95.0, 59.5});
        report(new String[0], new double[0]);
        buggyTotal(new double[]{80, 90});
    }
}

Output:

text
Name      Grade Letter
Ana        88.5      B
Omar       72.0      C
Lina       95.0      A
Sami       59.5      F
Name      Grade Letter
(no students)
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
	at Ex3.buggyTotal(Ex3.java:29)
	at Ex3.main(Ex3.java:37)

%-8s left-aligns in 8 characters; %6.1f right-aligns a number with one decimal in 6 characters. The empty class prints a friendly line and returns early. The last method is the planted bug: with i <= grades.length, the loop reaches i == 2 on a 2-element array. To fix it, read the trace from the top: the exception message says index 2, length 2, and the first line that names our code is buggyTotal(Ex3.java:29), which is the line total += grades[i];. The cause is one line earlier, in the loop condition. Change <= to <.

Backend Insight

This demo has the shape of a typical service function: take a list of records, compute summaries, find one by key, and render a result. Three habits carry over to real code:

  • One job per method, answers returned, not printed. average could be reused in a web page, a file export, or a test, because it does not print. Only report prints.
  • Edge cases are part of the specification. An empty list and a missing key are normal inputs, and the code decided in advance what they mean (0, -1, "(no students)").
  • Parallel arrays are a warning sign. If one array grows and the other does not, the data silently goes out of step. Real systems keep related fields together in one object per student. You will do that in the object-oriented modules; for now the arrays teach the skills underneath.

Common Mistakes

Mistake 1: Dividing by length without checking for zero

An empty class makes total / grades.length meaningless. Guard first.

Mistake 2: Starting highest at 0

With an all-negative data set, 0 would "win". Start from the first real element (and guard the empty case).

Mistake 3: Letting the parallel arrays differ in length

If names has 4 entries and grades has 3, a loop over names crashes on grades[3]. Check, or build them together.

Mistake 4: Using the user's query instead of the stored value, or forgetting to trim

Case and spaces differ between what people type and what is stored. Normalize on the way in (trim, equalsIgnoreCase), and print the stored value.

Mistake 5: Wrong boundary order in the if chain

Testing g >= 60 before g >= 90 makes every passing grade a D. Test from the highest threshold down.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Why are names and grades called parallel arrays, and what must always be true about their lengths?

أظهر الحل A1

A1. The same index refers to the same student in both arrays: names[3] and grades[3] describe one student. The lengths must always be equal.

A2. What does letterGrade(79.99) return with the scale in this lesson? And letterGrade(60)?

أظهر الحل A2

A2. letterGrade(79.99) is 'C' (it is at least 70 but below 80), and letterGrade(60) is 'D'.

A3. A search method returns -1. What does that mean, and why is -1 safe to use for this meaning?

أظهر الحل A3

A3. It means "not found". Valid indices are 0 or more, so -1 can never be mistaken for a real position.

Level B: Application

B1. Write static int countPassing(double[] grades) that counts the grades of 60 or more.

أظهر الحل B1

B1.

java
static int countPassing(double[] grades) {
    int count = 0;
    for (double g : grades) {
        if (g >= 60) {
            count++;
        }
    }
    return count;
}

B2. Write static double gradeOf(String[] names, double[] grades, String wanted) that returns the student's grade, or -1 when the name is not found. Reuse indexOfName. Name one weakness of using -1 as a grade here.

أظهر الحل B2

B2.

java
static double gradeOf(String[] names, double[] grades, String wanted) {
    int i = indexOfName(names, wanted);
    if (i == -1) {
        return -1;
    }
    return grades[i];
}

The weakness is that -1 looks like a grade; if a grading scheme allowed negative marks, the caller could not tell the cases apart. Returning the index and letting the caller decide avoids that.

B3. Write static String[] addName(String[] names, String newName) that returns a new array with one more name at the end, without changing the old array.

أظهر الحل B3

B3.

java
static String[] addName(String[] names, String newName) {
    String[] bigger = java.util.Arrays.copyOf(names, names.length + 1);
    bigger[bigger.length - 1] = newName;
    return bigger;
}

(With import java.util.Arrays; at the top you can write Arrays.copyOf.)

Level C: Challenge

C1. Print the students from the highest grade to the lowest without changing either array and without sorting. (Hint: keep a boolean[] printed, and each round pick the highest grade whose printed flag is still false.)

أظهر الحل C1

C1.

java
boolean[] printed = new boolean[grades.length];
for (int round = 0; round < grades.length; round++) {
    int best = -1;
    for (int i = 0; i < grades.length; i++) {
        if (!printed[i] && (best == -1 || grades[i] > grades[best])) {
            best = i;
        }
    }
    printed[best] = true;
    System.out.println(names[best] + " " + grades[best]);
}

Each round selects the best unprinted student, so the originals are never reordered and the names stay aligned with their grades. For an empty class the outer loop does not run.

C2. Write static double averageWithoutLowest(double[] grades) that ignores one lowest grade. What should it return for 0 or 1 grades, and why?

أظهر الحل C2

C2.

java
static double averageWithoutLowest(double[] grades) {
    if (grades.length < 2) {
        return 0;
    }
    double total = 0;
    for (double g : grades) {
        total += g;
    }
    total -= grades[lowestIndex(grades)];
    return total / (grades.length - 1);
}

With 0 or 1 grades there is nothing left after removing the lowest, so dividing by length - 1 would divide by zero (or by a negative count); returning 0 is the guard, and it should be documented. Another valid choice is to fall back to the plain average for one grade.

Mini Challenge

Extend the demo into a small menu program (use Scanner from BEG-011) that starts with sample data and offers: (1) print the report, (2) search a name, (3) print statistics, (4) quit. Search should accept any case and extra spaces and print "not found" for unknown names. Test it with the sample data and with an empty class (start with empty arrays) and make sure no option crashes.

Check your work: every loop uses <, no -1 index is ever used, text input is trimmed, and no division happens without a guard.

Summary & Next Lesson

Key takeaways

  • Parallel arrays link data by index; keep their lengths equal.
  • Small single-purpose methods return answers; only the report prints.
  • Return an index when callers need several related values, and use -1 for "none".
  • Guard the empty case before dividing or reading a[0].
  • i <= length crashes on the last pass; read the trace to find the line and fix the condition.
  • Test boundary values: exactly on and just below each threshold.

Next lesson

The next lesson, "Demo: string bugs: ==, off-by-one, and slow concatenation", shows three classic String mistakes happening live and fixes each one.

Self-check

  1. Why do the highest/lowest methods return an index?
  2. What should average do for an empty array, and why?
  3. Which loop condition causes the crash in buggyTotal?
  4. Why call equalsIgnoreCase and trim in the search?
  5. In which order should the if chain in letterGrade test the thresholds?
أظهر الإجابات
  1. The index gives both the grade (from grades) and the name (from names).
  2. Return a defined result (or print a message) because dividing by zero length is meaningless; guard first.
  3. i <= grades.length; it should be i < grades.length.
  4. People type different case and stray spaces; normalizing makes the search forgiving.
  5. From the highest threshold (90) down to the lowest (60).

References

  • Oracle, Java Language Specification SE 21, chapter 10: Arrays. It supports: array indices run from 0 to length - 1, and a bad index throws ArrayIndexOutOfBoundsException.
  • Oracle, String class, Java SE 21 API. It supports: equalsIgnoreCase ignores case, and trim removes leading and trailing spaces.
  • Not verified from a primary page: that 0.0 / 0 gives NaN while integer division by zero throws ArithmeticException (standard Java behaviour that I did not look up for this lesson). All code outputs were produced by compiling and running with JDK 21.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.