Checker: 2008 FRQ 4
A step-by-step solution to the 2008 AP CSA FRQ 4 (Checker), covering implementing a shared interface with multiple classes and composing them together like building blocks in Java.
Building a small family of interchangeable string-checking classes is the challenge in this AP Computer Science A free-response question — each one implements the same interface, and the final part asks you to combine existing ones together like building blocks instead of writing any new class at all.
What This FRQ Tests
- AP CSA units: Unit 5 (Writing Classes), plus the interface-based design ideas belonging to what current course materials number as Unit 10 — implementing a shared interface rather than extending a shared parent class
- Core skill: writing a class that
implementsan interface, storing whatever state its own logic needs - Secondary skill: composing already-existing objects into a new one instead of reimplementing their logic
- Official category: officially FRQ 4 by this exam's own printed numbering — but 2008 predates the fixed FRQ-number-to-category order introduced with the 2019–2020 CED redesign, where slot 4 is reserved for "2D Array." This question has nothing to do with 2D arrays; content-wise it's a class-design question built around a shared interface, closer in spirit to a modern "Classes" FRQ.
The Setup
- A
Checkerinterface (not modified) declares exactly one method:boolean accept(String text)
- (a)
SubstringCheckerimplementsChecker:- Constructor takes one
String— the substring to match accept(text)returns whethertextcontains that substring anywhere
- Constructor takes one
- (b)
AndCheckerimplementsChecker:- Constructor takes two
Checkerobjects accept(text)returnstrueonly if both storedCheckers accepttext
- Constructor takes two
- (c)
NotCheckeris already described (not asked to be written):- Constructor takes one
Checkerobject accept(text)returnstrueonly if its storedCheckerdoes not accepttext- You're asked to build a single
yummyCheckerexpression out of existing checkers, not write a class
- Constructor takes one
Part (a): Writing the SubstringChecker Class
Step-by-Step Approach
- Declare the class as
public class SubstringChecker implements Checker. - Store one field: the substring to match.
- The constructor takes that substring and stores it.
accept(text)checks whethertextcontains the stored substring anywhere within it.
The Code
public class SubstringChecker implements Checker
{
private String substringToMatch;
public SubstringChecker(String substringToMatch)
{
this.substringToMatch = substringToMatch;
}
public boolean accept(String text)
{
return text.indexOf(substringToMatch) != -1;
}
}
Why Each Piece Matters
implements Checker, notextends—Checkeris an interface with no code of its own, only a method signature.SubstringCheckerhas to provide a full, working body foraccept, since there's no inherited implementation to fall back on.this.substringToMatch = substringToMatch— the constructor parameter deliberately shares a name with the field it initializes;this.is what tells Java which one is being assigned to.indexOf(...) != -1— the standard "does this string contain X anywhere" pattern using a method that's on the AP Quick Reference sheet. It's also case-sensitive by nature, which matters for this problem (see the trace below).
Tracing the Example
Using the question's own broccoliChecker = new SubstringChecker("broccoli"):
| Call | indexOf("broccoli") |
Result |
|---|---|---|
accept("broccoli") |
found at index 0 | true |
accept("I like broccoli") |
found | true |
accept("carrots are great") |
not found (-1) |
false |
accept("Broccoli Bonanza") |
not found (-1) |
false |
The last row is the interesting one: "Broccoli Bonanza" does not accept, because indexOf compares characters exactly and "Broccoli" (capital B) is not the same sequence of characters as "broccoli" (lowercase b). All four results match the question's table exactly.
Common Mistakes to Avoid
- Forgetting
implements Checkerentirely, which leavesSubstringCheckerunrelated to the interface it's supposed to satisfy. - Using
.equals()instead ofindexOf..equals()checks whether two strings are identical in their entirety — it would only accept atextthat is exactly"broccoli"and nothing else. - Assuming matching should be case-insensitive. The question's own table confirms it isn't —
"Broccoli Bonanza"is correctly rejected.
Part (b): Writing the AndChecker Class
Step-by-Step Approach
- Declare the class as
public class AndChecker implements Checker. - Store both constructor arguments as fields, declared using the interface type
Checker— not any specific class likeSubstringChecker. accept(text)returnstrueonly when both stored checkers accepttext.
The Code
public class AndChecker implements Checker
{
private Checker checker1;
private Checker checker2;
public AndChecker(Checker checker1, Checker checker2)
{
this.checker1 = checker1;
this.checker2 = checker2;
}
public boolean accept(String text)
{
return checker1.accept(text) && checker2.accept(text);
}
}
Why Each Piece Matters
- Fields declared as
Checker, not a specific class — this is exactly what letsAndCheckercombine any two objects that implementChecker, including anotherAndChecker, as the question's ownveggiesexample does by wrappingbothChecker(itself anAndChecker). &&, not||— the rule is "accepted by both," and&&naturally short-circuits: ifchecker1.accept(text)is alreadyfalse,checker2.accept(text)never even runs, which is fine since the overall answer is already decided.- Delegating entirely to
checker1.accept(...)andchecker2.accept(...)—AndCheckernever needs to know how either stored checker decides its answer, only that each one returns aboolean.
Tracing the Example
Using the question's own setup — bChecker matches "beets", cChecker matches "carrots", bothChecker = new AndChecker(bChecker, cChecker), aChecker matches "artichokes", veggies = new AndChecker(bothChecker, aChecker):
| Call | Reasoning | Result |
|---|---|---|
bothChecker.accept("I love beets and carrots") |
contains both "beets" and "carrots" |
true |
bothChecker.accept("beets are great") |
contains "beets" but not "carrots" |
false |
veggies.accept("artichokes, beets, and carrots") |
bothChecker accepts (has both) and aChecker accepts (has "artichokes") |
true |
All three results match the question's table exactly.
Common Mistakes to Avoid
- Declaring the fields as
SubstringCheckerinstead ofChecker. This would compile fine for thebothCheckerexample, but break the moment anAndChecker(likebothCheckerinsideveggies) gets passed in instead. - Using
||instead of&&. The rule is "accepted by both," not "accepted by either." - Re-checking substrings directly inside
AndCheckerinstead of delegating tochecker1.accept(...)/checker2.accept(...)—AndCheckerhas no idea what criteria its two stored checkers actually use, nor does it need to.
Part (c): Constructing yummyChecker
The Rule, Broken Down
yummyCheckershould accept a string only if it contains neither"artichokes"nor"kale".NotCheckerflips a singleChecker's answer.- "Neither A nor B" is logically the same as "not A, and not B."
Step-by-Step Approach
- Wrap
aCheckerin aNotChecker— this accepts anything that does not contain"artichokes". - Wrap
kCheckerin aNotChecker— this accepts anything that does not contain"kale". - Combine the two wrapped checkers with an
AndChecker— this accepts only strings that pass both "doesn't contain" checks at once.
The Code
yummyChecker = new AndChecker(new NotChecker(aChecker), new NotChecker(kChecker));
Why Each Piece Matters
- No new class is needed — the three already-given building blocks (
SubstringChecker, already used to buildaChecker/kChecker;NotChecker; andAndChecker) are enough to express the whole rule as a single expression. - "Neither ... nor ..." becomes "NOT ... AND NOT ..." — this is the same logical rule as De Morgan's law (
!(a || b)is equivalent to!a && !b), just expressed with composed objects instead of boolean variables.
Tracing the Example
| Call | NotChecker(aChecker) |
NotChecker(kChecker) |
Combined (AND) |
|---|---|---|---|
yummyChecker.accept("chocolate truffles") |
no "artichokes" → true |
no "kale" → true |
true |
yummyChecker.accept("kale is great") |
no "artichokes" → true |
has "kale" → false |
false |
yummyChecker.accept("Yuck: artichokes & kale") |
has "artichokes" → false |
has "kale" → false |
false |
All three results match the question's table exactly.
Common Mistakes to Avoid
- Wrapping the whole
AndCheckerin a singleNotChecker(new NotChecker(new AndChecker(aChecker, kChecker))). That expression means "NOT (has both artichokes AND kale)," which would incorrectly accept a string containing artichokes but not kale — the rule needs "neither," not "not both." - Swapping which checker gets wrapped by which
NotChecker. It happens not to matter here sinceAndCheckeris symmetric, but mixing them up mentally is exactly where careless errors creep in on a problem like this. - Forgetting
newwhen constructing theNotChecker/AndCheckerobjects, or trying to call.accept()on a class name instead of an instance.
Notes: A Method Not on the AP CSA Quick Reference Sheet
String.contains(...) would let SubstringChecker.accept read a little more directly:
public boolean accept(String text)
{
return text.contains(substringToMatch);
}
containsisn't listed on the real exam's Java Quick Reference sheet — onlyindexOfis, among the relevantStringsearch methods. That doesn't mean it's off-limits; AP CSA graders accept any correct Java, whether or not it appears on that sheet.- What the sheet actually guarantees is that
indexOfspecifically is printed for you to look up if you forget its exact behavior mid-exam.containsis completely safe to use if you already know it well — theindexOf(...) != -1version above is simply the one you can double-check against the reference sheet if you're unsure.
Key Takeaways
- Implementing a shared interface (
implements) is what lets completely unrelated classes (SubstringChecker,AndChecker,NotChecker) all be swapped in anywhere aCheckeris expected — no shared parent class or inherited implementation required. - Storing collaborator objects using the interface type, not a specific concrete class, is what makes composition (a
Checkerbuilt out of otherCheckers) actually work for any combination. - "Neither X nor Y" always translates to "NOT X AND NOT Y," whether you're combining raw booleans or combining objects that already encapsulate that logic.