Comprehensive Study Guide

Here is a comprehensive study guide for college board material. This can both help with Java fundamentals and act as a study guide to help me prepare for the AP Exam.

Unit 1 Primitives

Primitives are the most basic data types in Java and most other languages. Built into most programming languages, even low level ones, primitives are predefined data types holding only values (unlike objects). They also cannot call methods are have attributes of their own and thus fundamentally different from objects.

The main types of primitives used in java are int, double, boolean, and char. Some properties unique to primitives in Java are that they can be compared with using the == operator, can be casted, and can be converted into Wrapper Classes (Java Object).

Casting (Division & Rounding/Truncating)

Casting is a way to convert primitives into other primitive types (commonly from int to double or vice versa). When converting double to int, we can also truncate/round decimals to the nearest integer

int a = 10;
int b = 3;
// Examples of Casting
// Casting in Division w/ casting (rounds down a/b in comparison to a more precise answer)
System.out.println(a/b);
System.out.println((double) a / b);

// Casting to Round/Truncate
double c = 3.5;
System.out.println((int)(a + c));
3
3.3333333333333335
13

Wrapper Classes

Wrapper classes enable an object oriented approach to primitives giving them access to methods like the toString method, the ability to be used in purely w ArrayLists. Some wrapper classes include Integer, Double, Boolean, and Character. They hold the same value as their primitive counterparts but open them to object oriented programming.

import java.util.ArrayList;

// To initialize a Wrapper class as a variable, you instantiate just like any object
Integer wrapperInteger = new Integer(10);
System.out.println(wrapperInteger);

// Some object only methods are shown below
// toString() method
System.out.println("This is a Wrapper Class: " + wrapperInteger.toString());

// ArrayLists
ArrayList<Integer> integers = new ArrayList<Integer>();
integers.add(wrapperInteger);
System.out.println(integers);
10
This is a Wrapper Class: 10
[10, 5]

Unit 2 Using Objects

Objects are just an instance created out of a class created w/ a constructor (which takes in parameters describing the object Methods in objects can be void (returns nothing) or have a return type specified. Static methods and properties are tied to class rather than object (ie. same value for all objects). Methods can be overloaded (have different sets of parameters) as long as order of types differs between method definitions even with same name.

Concatenation

Concatenation refers to the combination of strings. To concatenate non String types, you have to convert them into a String. When converting, primitives must be converted to wrapper classes in order to use the toString method. All objects must use toString method in order to be concatenated. Use + to concatenate strings.

String name = "Don"; Integer age = new Integer(17);
System.out.println("My Name: " + name);
System.out.println("My Age: " + age.toString());
My Name: Don
My Age: 17

Math Class (Random)

Math class enables a lot of mathematical operations like floor, roof, exponents, rounding, logs, and other operations not in Java by default. It enables complex mathematical operations which can be used for programs like calculators.

// Floor Function  
System.out.println(Math.floor(10.999999999));

// Ceiling Function
System.out.println(Math.ceil(10.999999999));

// Exponents
System.out.println(Math.pow(10, 3));

// Logarithms in base 10
System.out.println(Math.log(2));

// Rounding
System.out.println(Math.round(15.546432));

// Random (generates random number btwn 0 & 1)
System.out.println(Math.random());
10.0
11.0
1000.0
0.6931471805599453
16
0.7980473864210647

Unit 3 Booleans, If/Else Statements, & Comparison

Booleans are a key concept within computer science as a whole where they only store either True or False. Built on 0s and 1s, computers are essentially boolean based machines. Likewise, one can build complex methods through booleans and comparison operators/statements as the help with control flow in Java of code. The most common operators dealing with this include <, >, <=, >=, ==, !=, &&, and ||. If statements take in a boolean or boolean expression and run if the expression evaluates to "true". Else & Else if statements can be used in conjunction with if statements to run code if the if statement evaluates to false.

Comparisons w/ Primitives (numbers, characters, booleans), Strings, & Objects

Comparing with Primitives, as stated above evaluate through == operator. Strings and Objects are evaluated through the .equals() method which is a default method inherited from the objects class. (more on that below)

int a = 0;
int b = 0;
int c = 1;

// Comparing two same numbers
System.out.println(a == b);

// Comparing two different numbers
System.out.println(a == c);

String as = new String("yay");
String bs = new String("cool");

// Comparing the same string to itself (SAME memory location)
System.out.println(as == as);

// Comparing strings with same content using wrong operator (DIFFERENT memory location)
System.out.println(as == bs);

// Comparing strings with same content using correct .equals()
System.out.println(as.equals(bs));
true
false
true
false
false

Compound Boolean Expression

Using combinations of boolean operators, you can make compound boolean expressions. Operators that can be used include and (&&), or (||), not (!), as well as parenthesis for grouping purposes.

boolean a = true;
boolean b = false;

// Creating a compound expression
boolean compound = !(a && b) && (b || a) && (!b && !a);

// Printing the result
System.out.println(compound);
false

Truth Tables

Can be used to see the values of boolean expressions. For example, below is truth tables for some common compound boolean expressions. (AND, OR, XOR, NOT)

De Morgan's Law

De Morgan's Law is useful for simplifying boolean expressions and logic. States that !(a && b) = !a || !b AND !(a || b) = !a && !b (distribute and switch the middle sign almost like commutative property)

boolean a = true;
boolean b = false;
boolean c = true;
boolean d = false;

// complicated boolean expression
boolean res1 = !((!(a && b)) || (!(a || b)));

// simplified using De Morgan's Law once
boolean res2 = !((!a || !b) || (!a && !b));

//simplified using De Morgan's Law twice
boolean res3 = !(!a || !b) && !(!a && !b);

// all results are the same
System.out.println(res1 + " " + res2 + " " + res3);
false false false

Unit 4 Loops

While loop runs while a boolean condition is true. For loops create a variable which is modified on every loop iteration and has an end condition (useful for iterating through arrays, especially in different ways based on the modification, ie. i += 2 for all even indexes). For & while loops can be nested inside each other to achieve more iteration (really useful with 2D arrays). For each/Enhanced for loops really useful for looping through an array (int val : array) but limited in that they go through all elements from first to last and that cannot be modified. Loops help control repetition in our code as a sequential code flow controller. It also prevents us from excruciating arthritis.

For Loop & Enhanced For Loop

For loops can be used to iterate through an index, and modify it in different ways in the for loop declaration. The enhanced for loop is exclusively used for iterating fully through an iterable (such as array).

// looping through even numbers
for (int i = 0; i<10; i+=2) {
    System.out.println(i);
  }
  
  int[] arr = {1, 2, 3, 7, 8};
  
  // looping through array with conventional for lopo
  for (int i = 0; i<arr.length; i++) {
    System.out.println(arr[i]);
  }
  
  // looping through array with enhanced for loop
  for (int i : arr) {
    System.out.println(i);
  }

While Loop & Do While Loop

While loops run while a condition is true, the condition is checked before each iteration of the code is run.

Do while loops also run while a condition is true, but the condition is checked AFTER each iteration of the code is run. This means that no matter what the do block runs at least once before the condition is checked

int i = 0;
boolean falseBool = false;

// printing even numbers with while loop
while (i < 10) {
  System.out.println(i);
  i += 2;
}

// if condition is false while loop does not run at all
while (falseBool) {
  System.out.println("inside while loop");
}

// if condition is false in do while, the loop runs once
do {
  System.out.println("inside do-while loop");
} while (falseBool);

Nested Loops

Loops can be used inside each other for better iteration and they are especially useful for 2D arrays which can be 2D and thus need to be iterated through twice to access each individual element.

int[][] arr = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
  };
  
  // using nested for loops for 2D array
  for (int i = 0; i<arr.length; i++) {
    for (int j = 0; j<arr[i].length; j++) {
      System.out.print(arr[i][j] + " ");
    }
    System.out.println();
  }
1 2 3 
4 5 6 
7 8 9 

Unit 5 Classes (Own Lesson)

Classes can be used for creating objects and have two main things: properties and methods. Properties are used to store information about each object of a class (can be made private/public which determines accessibility outside of class). Methods are used to modify the object & do things. Getter and Setter Methods can be used to modify properties of a class which are made private.

Breakdown of a Class

Breakdown of a Method

Access Modifiers

Access modifiers control whether properties and methods can be accessed outside the class. The public means the property/method is accessible outside while if it is private it is not accessible from outside the class. Protected usually refers to within the package (a package in this case is almost like a special type of directory). Default is when no access modifier is specified and by default only subclasses can access variables and package subclasses. Below is a table of them

Static Methods & Class Methods

Static properties and methods are part of the class rather than each object. Static methods do not require an object, and static properties only have one instance that is the same for all objects.

Creating a Class

Classes can be created using the class keyword along with definitions in front of it. A class should always be defined using upper camelcase (camelcase but first letter capital).

class SomeClass{
    
}

Main Method & Tester Methods

The main method is used to test a class, is automatically called when class ran. It usually creates an object and can test methods hence it is mostly used as a tester method for classes. To execute the main portion of the code, a Main class with a main method is normally reserved which handles most of the executions of the code.

class SomeClass {
    // main method (definition for main method, more on individual parts later)
    public static void main(String[] args){
        System.out.println("cool main method");
    }
}

SomeClass.main(null);
cool main method

this Keyword

The "this" keyword allows you to access properties of the class. See constructor example to see use of this keyword.

Constructors

Constructors are called whenever the object is created and usually initializes fields (data/attributes defined in the class). It does not return anything because the object is automatically given to the user when constructor is called and thus its return type is implied.

// creating a class (camel casing w/ first letter capitalized)
class SomeClass {
    int someInt;
    String someString;

    // Constructor
    public SomeClass(int someInt, String someString){ // constructor passes in outside parameters if we want to initialize certain fields
        // this references objects own fields to differentiate (more on that later)
        this.someInt = someInt;
        this.someString = someString;
    }

    public static void main(String[] args){
        SomeClass obj = new SomeClass(123, "abc");
    
        System.out.println(obj.someInt);
        System.out.println(obj.someString);
    }
}

SomeClass.main(null);
123
abc

Mutator Methods & Setter Methods

These methods are used to get properties of an object from the outside the class definition. They are almost always necessary for private variables within classes.

Getters can be applied on only the properties which should be accessed outside the class. They always have a return type of whatever data field is being retrieved.

Setters are used to only set properties which are set outside the class. They are always void methods as their only purpose is to set variables.

class SomeClass {
    private int someInt;
    private String someString;

    public SomeClass(int someInt, String someString){ 
        this.someInt = someInt;
        this.someString = someString;
    }

    // Getter method
    public int getSomeInt(){
        return this.someInt;
    }

    public static void main(String[] args){
      SomeClass obj = new SomeClass(123, "abc");

      //Using Getter Method
      System.out.println(obj.getSomeInt());
    }
}

SomeClass.main(null);
123
class SomeClass {
  private int someInt;
  private String someString;

  public SomeClass(int someInt, String someString){ 
      this.someInt = someInt;
      this.someString = someString;
  }

  public int getSomeInt(){
      return this.someInt;
  }

  // Setter
  public void setSomeInt(int newInt){
       this.someInt = newInt;
  }

  public static void main(String[] args){
      SomeClass obj = new SomeClass(123, "abc");

      // Using setter method
      obj.setSomeInt(111);
      System.out.println(obj.getSomeInt());
  }
}

SomeClass.main(null);
111

Unit 6 Lists

Lists are a way to store a "collection" of data (particularly primitives). They have a set size and can only store data of the same type. The most important thing to consider are the bounds and size of the array which dictates the entries you can access and loop through.

int[] array = {1, 2, 3, 4, 5, 10, 15};

for (int num : array) {
  if (num % 5 == 0 && num % 3 != 0) {
    System.out.println(num);
  }
}

Unit 7 ArrayLists

An ArrayList is like an array but the length can be changed. For ArrayList you must use a wrapper class rather than a primitive Due to ArrayList using generics. ArrayList can also work with enhanced for loops and have many convenient functions.

// using "Integer" wrapper class
ArrayList<Integer> listOfIntegers = new ArrayList<>();

// Explicitly creating integer
listOfIntegers.add(new Integer(10));

// automatically converts to Integer
listOfIntegers.add(1);

// using toString method of ArrayList
System.out.println(listOfIntegers);

Unit 8 2D Arrays

Arrays can be placed inside arrays, creating 2D array. Useful for representing 2d space, or text (as 2d). Defined by using two pairs of square brackets after the type. Can be traversed using nested for loop. Concept of putting one for loop inside another. Really useful for traversing 2d arrays.

// defining 2d array
int[][] arr2d = {
    {1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10}
  };
  
  // using nested for loops
  for (int[] row : arr2d) {
    for (int val : row) {
      System.out.print(val + " ");
    }
    System.out.println();
  }

Unit 9 Inheritance

Inheritance can be used when two classes share similar functionality. Allows super class to have base functionality (ie. car). Sub class adds additional functionality to the base (ie. tesla car) "extends" and "abstract" keywords can be used to define inheritance in Java.

Extends key word

Defines a sub class that inherits all the methods from the super class. Useful because you don't need to redefine everything from super class.

public class Animal {
    String color;
    int age;
  
    public Animal () {}
  
    public Animal (String color, int age) {
      this.color = color;
      this.age = age;
    }
  
    public void sayHello () {
      System.out.println("hello, I am " + color + " and I am " + age + " years old.");
    }
  
    public void walk () {
      System.out.println("walking...");
    }
  
    public void eat () {
      System.out.println("eating");
    }
  }
  
  public class Cat extends Animal {
    String owner;
  
    public Cat (String color, int age, String owner) {
      this.color = color;
      this.age = age;
      this.owner = owner;
    }
  
    public void sayOwner () {
      System.out.println("my owner is " + owner);
    }
  }
  
  Cat c = new Cat("green", 2, "joe");
  
  // using method from parent class
  c.sayHello();
  // using method from child class
  c.sayOwner();

Subclass constructor, super Keyword

The constructor in a subclass can use "super" to access the parent class constructor "super" can also be used to access parent class methods

public class Animal {
    String color;
    int age;
  
    public Animal () {}
  
    public Animal (String color, int age) {
      this.color = color;
      this.age = age;
    }
  
    public void sayHello () {
      System.out.println("hello, I am " + color + " and I am " + age + " years old.");
    }
  
    public void walk () {
      System.out.println("walking...");
    }
  
    public void eat () {
      System.out.println("eating");
    }
  }
  
  public class Cat extends Animal {
    String owner;
  
    public Cat (String color, int age, String owner) {
      // reduce code duplication
      super(color, age);
      this.owner = owner;
    }
  
    public void sayOwner () {
      System.out.println("my owner is " + owner);
    }
  
    public void sayOwnerAndHello() {
      super.sayHello();
      sayOwner();
    }
  }
  
  Cat c = new Cat("green", 2, "joe");
  
  c.sayOwnerAndHello();

Polymorphism: Any of Overloading, Overriding, Late Binding

Polymorphism literally means many forms. In the context of OOP, it stands as one of its pillars. Through Polymorphism, methods can take on different implementations and instances thus making them capable of generating variety of outputs under different circumstances. Such nameSpacing helps organize our methods that do essentially the same outputs in different ways.

Overriding a Method (Same Signature of a Method)

Allows you to define a method in the parent class, but then change it in child class. Really useful to change functionality in child class.

public class Animal {
    String color;
    int age;
  
    public Animal () {}
  
    public Animal (String color, int age) {
      this.color = color;
      this.age = age;
    }
  
    public void sayHello () {
      System.out.println("hello, I am " + color + " and I am " + age + " years old.");
    }
  
    public void walk () {
      System.out.println("walking...");
    }
  
    public void eat () {
      System.out.println("eating");
    }
  }
  
  public class Cat extends Animal {
    String owner;
  
    public Cat (String color, int age, String owner) {
      // reduce code duplication
      super(color, age);
      this.owner = owner;
    }
  
    public void sayOwner () {
      System.out.println("my owner is " + owner);
    }
  
    // Adding more functionality in say hello for cat
    @Override
    public void sayHello() {
      super.sayHello();
      System.out.println("meow...");
    }
  }
  
  // Cat uses cat method
  Cat c = new Cat("green", 2, "joe");
  c.sayHello();
  
  // Animal uses animal method
  Animal a = new Animal("blue", 3);
  a.sayHello();

Abstract Class, Abstract Method

Abstract class means a class cannot be instantiated, it is simply a template. Abstract methods define the signature but the method must be implemented in child class. For example, lets say we don't want an animal to be created...

abstract class Animal {
    String color;
    int age;
  
    public Animal () {}
  
    public Animal (String color, int age) {
      this.color = color;
      this.age = age;
    }
  
    // must be defined in child class
    abstract void sayHello ();
  
    public void walk () {
      System.out.println("walking...");
    }
  
    public void eat () {
      System.out.println("eating");
    }
  }
  
  public class Cat extends Animal {
    String owner;
  
    public Cat (String color, int age, String owner) {
      // reduce code duplication
      super(color, age);
      this.owner = owner;
    }
  
    public void sayOwner () {
      System.out.println("my owner is " + owner);
    }
  
    // Defining method that was abstract
    @Override
    public void sayHello() {
      System.out.println("meow...");
    }
  }
  
  // Cat uses cat method
  Cat c = new Cat("green", 2, "joe");
  c.sayHello();

Late Binding of Object, Referencing Superclass Object

Allows you to use the type of a superclass but have an object of the subclass. Useful if you know it will be part of the superclass, but don't know which subclass it is. I know it is an animal but I don't know which type. Works well with abstract methods.

abstract class Animal {
    String color;
    int age;
  
    public Animal () {}
  
    public Animal (String color, int age) {
      this.color = color;
      this.age = age;
    }
  
    // must be defined in child class
    abstract void sayHello ();
  
    public void walk () {
      System.out.println("walking...");
    }
  
    public void eat () {
      System.out.println("eating");
    }
  }
  
  public class Cat extends Animal {
    String owner;
  
    public Cat (String color, int age, String owner) {
      // reduce code duplication
      super(color, age);
      this.owner = owner;
    }
  
    public void sayOwner () {
      System.out.println("my owner is " + owner);
    }
  
    // Defining method that was abstract
    @Override
    public void sayHello() {
      System.out.println("meow...");
    }
  }
  
  // Defining cat as an animal!
  Animal c = new Cat("green", 2, "joe");
  
  // can use because abstract method guarentees implentation in child class
  c.sayHello();

Overloading a Method (Same Name Different Parameters)

Allows you to have a method, but different sets of arguments The method which is called is determined at compile time (early binding) based on the arguments passed in

abstract class Animal {
    String color;
    int age;
  
    public Animal () {}
  
    public Animal (String color, int age) {
      this.color = color;
      this.age = age;
    }
  
    // must be defined in child class
    abstract void sayHello ();
  
    public void walk () {
      System.out.println("walking...");
    }
  
    public void eat () {
      System.out.println("eating");
    }
  }
  
  public class Cat extends Animal {
    String owner;
  
    public Cat (String color, int age, String owner) {
      // reduce code duplication
      super(color, age);
      this.owner = owner;
    }
  
    public void sayOwner () {
      System.out.println("my owner is " + owner);
    }
  
    // Defining method that was abstract
    @Override
    public void sayHello() {
      System.out.println("meow...");
    }
  
    // same method with different arguments
    public void sayHello(String person) {
      System.out.println("meow... hello " + person);
    }
  }
  
  Cat c = new Cat("green", 2, "joe");
  
  // two different argument sets for same method
  c.sayHello();
  c.sayHello("mark")

Standard methods: toString(), equals(), hashCode()

All objects automatically inherit from Object class and have some methods. toString should list properties, equals should compare properties, hashCode should give unique identifier.

abstract class Animal {
    String color;
    int age;
  
    public Animal () {}
  
    public Animal (String color, int age) {
      this.color = color;
      this.age = age;
    }
  
    // must be defined in child class
    abstract void sayHello ();
  
    public void walk () {
      System.out.println("walking...");
    }
  
    public void eat () {
      System.out.println("eating");
    }
  }
  
  public class Cat extends Animal {
    String owner;
  
    public Cat (String color, int age, String owner) {
      // reduce code duplication
      super(color, age);
      this.owner = owner;
    }
  
    public void sayOwner () {
      System.out.println("my owner is " + owner);
    }
  
    // Defining method that was abstract
    @Override
    public void sayHello() {
      System.out.println("meow...");
    }
  
    // same method with different arguments
    public void sayHello(String person) {
      System.out.println("meow... hello " + person);
    }
  
    // Overriding the to string
    @Override
    public String toString () {
      return "[Cat]: " + "color=" + color + ", " + "age=" + age + ", " + "owner=" + owner;
    }
  }
  
  // Object superclass is automatically inherited
  Object c = new Cat("green", 2, "joe");
  
  // System.out.println uses toString method internally
  System.out.println(c);

Unit 10 Recursion

Recursion can be used in situations where you need to repeatedly do something, instead of loops. Recursion must call itself and have a base case. Base case allows the recursion to end at some point.

Big O notation for Hash map, Binary Search, Single loop, Nested Loop

Used to describe the most time it would take for a function to run (without constants). For example, the recursion below would take O(n) time as it has to go through n iterations to calculate the factorial.

public int factorial (int n) {
    if (n == 0 || n == 1) {
      return 1;
    }
    return n * factorial(n-1);
}
  
  System.out.println(factorial(5));