Java Notes


Shortcut to this page: ntrllog.netlify.app/java_notes

Notes provided by Professor Paul Cao (UCSD)

Note: reasons that mention "primitive" refer to memory models

Short Circuiting

if (a || b) {
}

If a is true, then b won't be evaluated.

Member Variables

Instance variables are member variables that are not static. Class variables are member variables that are static. (I think)

public class Test {
    int x;
}

Member variables are automatically initialized to a "zero" value (e.g. 0 for ints, null for non-primitives).

public class Test {
    int[] a;

    public Test() {
        a = {4, 3, 4}; // Compiler error
        a[0] = 4 // Runtime error
    }
}

For the second error, a new array object was never created.

Ragged Arrays

int[][] a = new int[4][];

2d arrays where each array has a different length

For Each Loop

The iterator is assigned the value of each item in the array. If it is an array of primitives, then the iterator is assigned a value. If it is an array of objects, then the iterator is assigned an address.

for (int x : a) {
    x = 3;
}

Contents of a are unchanged because x is a primitive

for (Test t : a) {
    t.value = 0;
}

Objects in a are changed because t is pointing to the objects

Object Class

All objects (that don't extend from a superclass) automatically extend from Java's Object class.

This is why all objects have an equals method and a toString method — among others.

The default implementation for the equals method is shallow comparison (==).

Static

Static variables and methods don't belong to an object; they belong to the class.

If a member variable is static, then all objects that are created share that variable. If a member variable is not static, then each object will have its own copy of the member variable.

A general rule for deciding whether a method should be static or not: if it needs to access an object's instance variable(s), then it should not be static.

References in Functions & Constructors

public void test(int x) {
    x = 10;
}

public static void main(String[] args) {
    int x = 5;
    test(x);
    // x will still be 5
}

The x inside test is not the same as the x in main (because it is a primitive).

public class Test {
    
    int x;
    
    public Test(int x) {
        x = x;
    }
    
    public static void main(String[] args) {
        Test t = new Test(32);
        // t.x will be equal to 0, not 32
    }
}

Scope!

Override vs. Overload

Override: method with same signature (name & parameters) and same return type (where the method is in the subclass)

Overload: two or more methods have same name, different (number/type of) parameters

class Snake {
    public void meth(Snake s) {
        System.out.println("1");
    }
}

class Cobra extends Snake {
    public void meth(Snake s) {
        System.out.println("2");
    }

    public void meth(Cobra c) {
        System.out.println("3");
    }
}

public static void main(String[] args) {

    Snake snake = new Snake();
    Cobra cobra = new Cobra();
    Snake snake_ref_cobra = new Cobra();

    snake.meth(snake); // 1
    snake.meth(snake_ref_cobra); // 1
    snake.meth(cobra); // 1

    snake_ref_cobra.meth(snake); // 2
    snake_ref_cobra.meth(snake_ref_cobra); // 2
    snake_ref_cobra.meth(cobra); // 2

    cobra.meth(snake); // 2
    cobra.meth(snake_ref_cobra); // 2
    cobra.meth(cobra); // 3

}

For the second set (snake_ref_cobra.meth(...)), at compile time, the compiler goes to the Snake class and checks if there is a method called meth that takes in a Snake. Since there is, at runtime, the program will execute the meth that takes in a Snake. This is what "method resolution is done at compile time" means.

The same happens for cobra.meth(snake_ref_cobra). At compile time, the compiler goes to the Cobra class and checks if there is a method called meth that takes in a Snake.

Inheritance

public class Person {

    String name;

}

public class Student extends Person {

    int units;

    public int getUnits() {

        return this.units;

    }
}

Student inherits name since it is public.

Person p = new Student();

Will not cause an error because "Student (right side) is a Person (left side)"

Why do something like this? Imagine many subclasses extending from one superclass, and want to store subclass objects in an array. Could create multiple arrays for each sublcass type, but it's faster/easier to create one array of the superclass type and store all those objects into that one array.

Student s = new Person();

Will cause a compiler error. For example, if there wasn't a compiler error, s.getUnits() will cause a runtime error because a Person object doesn't have the getUnits method.

Protected

Accessible to subclasses

This and Super

public class Test {

    int z;

    public Test(int x, int y) {

       this(x,y,z); // Compiler error

    }

    public Test(int x, int y, int z) {
    }

}

Because constructors are called before variables are initialized?

Polymorphism

public class Snake {
    
    public boolean isHungry(int x) {
        return x >= 4;
    }
    
    public static void main(String[] args) {
        Snake s = new Cobra();
        System.out.println(s.isHungry(4)); // will print false
    }
    
}

class Cobra extends Snake {
    
    public boolean isHungry(int x) {
        return x >= 10;
    }
}

Behavior depends on object, not reference

Type Casting

Snake var1 = new Cobra();
((Python)var1).method2();

At compile time, the compiler only checks to see if there is some sort of relationship between the reference and the type cast. It doesn't matter which is the superclass and which is the subclass.

At runtime, there must be a "right is-a left" relationship when type casting, or else there will be a runtime error. Cobra is-a Python in this case.

Abstract Classes

Don't want to create objects of that class (too generic)

Forces the children (subclasses) to override a method

Interfaces

Java only allows extending from one superclass, so implementing an interface is kinda like extending from multiple superclasses.

Only have public abstract methods or public static final constants

The type of an object can be the type of the interface (e.g. Comparable). This means it can be an instanceof Comparable.

Finding Midpoint (e.g. for binary search)

int mid = (low+high)/2;

Works most of the time, but leads to overflow when low and high are really large

int mid = low + ((high-low)/2);

This is the better way to do it