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
if (a || b) { }
If a
is true, then b
won't be evaluated.
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.
int[][] a = new int[4][];
2d arrays where each array has a different length
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
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 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.
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: method with same signature (name & parameters) and same return type (where the method is in the subclass)
used to perform actions specific to the subclass
method resolution is done at runtime
Overload: two or more methods have same name, different (number/type of) parameters
used to perform same operation on different types of input
method resolution is done at compile time
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
.
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.
Accessible to subclasses
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?
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
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.
Don't want to create objects of that class (too generic)
Forces the children (subclasses) to override a method
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
.
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