عرض عملي: مدير درجات الطلاب باستخدام المصفوفات
في هذا الدرس 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
letterGrademethod 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:
average(double[])highestIndexandlowestIndex, which return a position, not a valueletterGrade(double)andindexOfName(String[], String)- A
reportthat 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)
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):
Average: 78.75
Highest: 95.0 (Lina)
Lowest: 59.5 (Sami)
0.0
-1The 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)
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:
A B F
Lina: 95.0 A
Omar: 72.0 C
'Zed': not foundExactly 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)
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:
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.
averagecould be reused in a web page, a file export, or a test, because it does not print. Onlyreportprints. - 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.
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.
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.
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.
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.
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
-1for "none". - Guard the empty case before dividing or reading
a[0]. i <= lengthcrashes 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
- Why do the highest/lowest methods return an index?
- What should
averagedo for an empty array, and why? - Which loop condition causes the crash in
buggyTotal? - Why call
equalsIgnoreCaseandtrimin the search? - In which order should the
ifchain inletterGradetest the thresholds?
أظهر الإجابات
- The index gives both the grade (from
grades) and the name (fromnames). - Return a defined result (or print a message) because dividing by zero length is meaningless; guard first.
i <= grades.length; it should bei < grades.length.- People type different case and stray spaces; normalizing makes the search forgiving.
- 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
0tolength - 1, and a bad index throwsArrayIndexOutOfBoundsException. - Oracle,
Stringclass, Java SE 21 API. It supports:equalsIgnoreCaseignores case, andtrimremoves leading and trailing spaces. - Not verified from a primary page: that
0.0 / 0givesNaNwhile integer division by zero throwsArithmeticException(standard Java behaviour that I did not look up for this lesson). All code outputs were produced by compiling and running with JDK 21.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.