The midterm will cover everything through the factory design pattern, but not include what KMP did most recently on the decorator pattern. In order to ensure you do well on the midterm you should do the following:
If you need a quick review of some java language basics/topics, TutorialsPoint is a great site.
Note that the review here is not replacing the lecture slides. This website is just emphasizing important topics that will be useful for the exam, or topics that students seem to be having a lot of trouble with.
Head over to the Midterm Practice page to see worked examples and explanations for previous test questions.
Remember that when dealing with an Object Orientated language, there are two types of variables we have to deal with - value type variables and reference variables. Click the title to see the lecture slides associated with data types.
The first of these are the primitive types. Such types serve only one purpose — containing pure, simple data of a certain type. Remember that primitive value types are stored by their value.
The main difference between a primitive and a reference type is that a primitive type always has a value - it can never be null. As a result, primitive values will have a default that will be assigned to it upon creation.
Type | Description | Default | Example |
---|---|---|---|
boolean | true / false | false | boolean b = false; |
byte | number [-128, 127] | 0 | byte b = 30; |
char | character | \u0000 | char c = 'c' |
short | number [-32,768, 32,767] | 0 | short s = 3234; |
int | number [-2,147,483,648, 2,147,483,647] | 0 | int i = 32443; |
long | number [google it] | 0 | long l = 434532L; |
float | real number | 0.0 | float f = 3.4324F; |
double | real number (twice the precision) | 0.0 | double d = 7.542D; |
Note that long
, float
, and double
have the letters after the number - this is
optional.
A reason that it might be used is that if you say float f = 12.3;
it will actually be assigned to a
double because the compiler sees 12.3
as a double, not a float.
System.out.println("0.1f == 0.1 is " + (0.1f == 0.1));
System.out.println("0.1f is actually " + new BigDecimal(0.1f));
System.out.println("0.1 is actually " + new BigDecimal(0.1));
Will result in the following output:
0.1f == 0.1 is false
0.1f is actually 0.100000001490116119384765625
0.1 is actually 0.1000000000000000055511151231257827021181583404541015625
Unlike value types, which are defined entirely by their value, reference types are structures in memory. A reference
type is a data type that’s based on a class rather than on one of the primitive types that are built in to the Java
language. The class can be something provided as part of the Java API class library (think of things like the Scanner
class which we used in Assignment 1) or a class that you write yourself (think of ColorPixel
).
When objects are initially made, they are set to the value null
.
public class Ball{
int x = 0;
int y = 2;
}
Ball ball;
System.out.println(ball.x); //cant do this because ball is null
ball = new Ball(); // Now we assign the variable ball to a new instance of Ball
System.out.println(ball.y);// prints 2
It is important to remember that when dealing with reference type variables (such as ball
of the type
Ball
in this case) they simply point to data structures, so it is entirely possible to have another
variable point to the same data structure. If this happens, when either variable modifies the data,
the change is reflected by both variables (because both variables are just looking at the same thing).
int a[] = new int[]{14, 2, 16}; // a now points to an array of ints
int b[] = a; // now b does the same
System.out.println(b[1]); // prints 2
b[1] = 401;
System.out.println(a[1]); // prints 401
Methods are a convenient way of organizing your code into little blocks. It is incredibly important to know and understand the lesson on methods found here.
When you define methods you need to be as specific as possible. This means that you have to clarify types for everything that is relevant, such as return type and the types of parameters, when defining a method:
public int addNumbers(int a, int b){ //This line is the "signature" of the method
return a + b;
}
You have to specify the types of the parameters that are going to be passed in the signature of the method. This is
the important difference between defining and calling a method, which we will elaborate more on in the next section.
Defining a method is basically making a template for the method call, and what types of inputs will be supported by
a given method. In this case, we are requiring that our first parameter is an int
and the second
parameter is also an int
. Since our return type is an int
, we must give
back an int
at the end of the method.
When calling a method we have to remember the ‘contract’ that we set up when we defined it. When we call addNumbers(3,
4);
we are passing the values of 3 and 4 to the method (and in this case that expression evaluates to 7). It
is important that we don’t include the types here, because the method will not accept the parameters if they are not
of the correct type - therefore, it is already understood that we are passing in two int
's. If anything
else is attempted to be passed, the method will throw an error.
Understand the basics of declaring and accessing arrays. Remember that you can have multiple variables point to the same array structure.
Another key concept to understand is the pass by reference versus pass by value. This is a great example of how confusing Java can be.
We have used 2-D arrays a lot in the assignments and it is easy to confuse what they really are. As the lecture slides say:
[A] multidimensional array is simply an array of arrays
Array[number of arrays][how many elements in each of those arrays]
lets visualize this.
int[][] a = new int[2][3];
a[0] -> [0] [1] [2]
a[1] -> [0] [1] [2]
So at the base level we have simply two indexes in our array. Index 0 points to an array of of integers of length 3.
Index 1 points to another array of integers of length 3.
If we expand those sub-arrays we see their contents
Lets look at the example of our PictureImpl
from past assignments. When we declare our array of pixels
there was some concern over whether new Pixel[width][height]
should have had our width or height coming
first. The answer is that it doesn’t matter. If you break this down and think about it as an array
of arrays, it would work either way - so long as you were consistent with which array was in charge of height, and
which array was in charge of width. For example, if we want our columns to be considered the outer array we would
have that number [this one][] be the width of our picture and [][this one] be the height of our picture. Then you
can think of the picture broken down like this.
[column you wish to access][row you wish to access]
or
a[column 0] -> [pix 0] [pix 1] [pix 2]
a[column 1] -> [pix 0] [pix 1] [pix 2]
OOP is a paradigm that allow your program to grow without becoming impossible to maintain/understand. It’s hard to get a real understanding of its importance right now, mainly because our assignments are relatively short.
There is a lot of material on encapsulation, polymorphism, and abstraction - review these and understand what each means and how it is used. This is something that sounds complicated, but is relatively simple. Usually questions about this come from the true/false.
Simple definition:
Encapsulation is one of the four fundamental OOP concepts. The other three are inheritance, polymorphism, and abstraction.
Encapsulation in Java is a mechanism of wrapping the data (variables) and code acting on the data (methods) together as a single unit. In encapsulation, the variables of a class will be hidden from other classes, and can be accessed only through the methods of their current class. Therefore, it is also known as data hiding.
To achieve encapsulation in Java:
Notice that all fields are private and accessed through the Javabean getters and setters.
public class EncapTest {
private String name;
private String idNum;
private int age;
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String getIdNum() {
return idNum;
}
public void setAge( int newAge) {
age = newAge;
}
public void setName(String newName) {
name = newName;
}
public void setIdNum( String newId) {
idNum = newId;
}
}
Definitions:
Polymorphism is the ability of an object to take on many forms. The most common use of polymorphism in OOP occurs when a parent class reference is used to refer to a child class object.
Any Java object that can pass more than one IS-A test is considered to be polymorphic. In Java, all Java objects are polymorphic since any object will pass the IS-A test for their own type and for the class Object.
Lets use the classic animal example
public interface Vegetarian{}
public class Animal{}
public class Deer extends Animal implements Vegetarian{}
Now, the Deer class is considered to be polymorphic since this has multiple inheritance. Following are true for the above examples −
When we apply the reference variable facts to a Deer object reference, the following declarations are legal:
Deer d = new Deer();
Animal a = d;
Vegetarian v = d;
Object o = d;
All the reference variables d, a, v, o refer to the same Deer object in the heap (memory/storage for our java programs).
Without talking about abstract classes I just want to define the idea of abstraction in general. The idea is that you take something complicated in pack it up in a box that is easy to understand or use at a higher level Think about pushing the gas pedal on a car. You dont care about all the complexities going on in the engine, you just want the car to move forward.
The purpose of an interface (as told in class), is to create a contract for classes to complete if they want to have certain properties. For example, in Assignment 3, we were introduced to the Picture interface, and the methods that came with it. In Assignment 4, we then added onto that idea of different subclasses of pictures, and thus were led to abstract classes.
In coding, it is often bad form to have repeated code - if you find yourself simply copy/pasting large sections of code, there’s probably a more efficient way to handle the problem. However, many subclasses under the Picture interface didn’t have much variation with how they implemented their methods - for example, PictureImpl and SubPictureImpl have the exact same code in their getHeight() and getWidth() methods. As such, we can abstract this code into an abstract class (hense the name abstract class), which will be the halfway point between the interface and the classes. Essentially, the abstract class will complete a portion of the contract, and leave the rest to its subclasses.
However, this comes with a catch - because the abstract class is supposed to only contain methods shared by all subclasses under the interface, it becomes harder and harder to have a well-defined abstract class the more subclasses you have under the interface. This is where the programmer’s discretion comes in - obviously, we want to support all possible methods under the abstract class, but if there’s only one or two methods under the abstract class, it’s up to the programmer (and the size of the project) to decide whether or not an abstract class is worth the effort.
Virtual methods are important to have a basic understanding of. Read the slides from KMP’s lecture before looking at the below examples.
public class Employee {
private String name;
private String address;
private int number;
public Employee(String name, String address, int number) {
System.out.println("Constructing an Employee");
this.name = name;
this.address = address;
this.number = number;
}
public void mailCheck() {
System.out.println("Mailing a check to " + this.name + " " + this.address);
}
public String toString() {
return name + " " + address + " " + number;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public void setAddress(String newAddress) {
address = newAddress;
}
public int getNumber() {
return number;
}
}
public class Salary extends Employee {
private double salary; // Annual salary
public Salary(String name, String address, int number, double salary) {
super(name, address, number);
setSalary(salary);
}
public void mailCheck() {
System.out.println("Within mailCheck of Salary class ");
System.out.println("Mailing check to " + getName()
+ " with salary " + salary);
}
public double getSalary() {
return salary;
}
public void setSalary(double newSalary) {
if(newSalary >= 0.0) {
salary = newSalary;
}
}
public double computePay() {
System.out.println("Computing salary pay for " + getName());
return salary/52;
}
}
Running this code:
public class VirtualDemo {
public static void main(String [] args) {
Salary s = new Salary("Mohd Mohtashim", "Ambehta, UP", 3, 3600.00);
Employee e = new Salary("John Adams", "Boston, MA", 2, 2400.00);
System.out.println("Call mailCheck using Salary reference --");
s.mailCheck();
System.out.println("\n Call mailCheck using Employee reference--");
e.mailCheck();
}
}
Output:
Constructing an Employee
Constructing an Employee
Call mailCheck using Salary reference --
Within mailCheck of Salary class
ailing check to Mohd Mohtashim with salary 3600.0
Call mailCheck using Employee reference--
Within mailCheck of Salary class
ailing check to John Adams with salary 2400.0
Here, we instantiate two Salary objects. One using a Salary reference s, and the other using an Employee reference e.
While invoking s.mailCheck(), the compiler sees mailCheck() in the Salary class at compile time, and the JVM invokes mailCheck() in the Salary class at run time.
mailCheck() on e is quite different because e is an Employee reference. When the compiler sees e.mailCheck(), the compiler sees the mailCheck() method in the Employee class.
Here, at compile time, the compiler used mailCheck() in Employee to validate this statement. At run time, however, the JVM invokes mailCheck() in the Salary class.
This behavior is referred to as virtual method invocation, and these methods are referred to as virtual methods. An overridden method is invoked at run time, no matter what data type the reference is that was used in the source code at compile time.
Here is just a quick example of using the super keyword. Subclasses take everything from their parents (that isn’t private) and can add more functionality.
public class Superclass {
int num = 1;
public static void main(String args[]) {
Sub sub = new Sub();
sub.methodInSuper();
sub.methodInSub();
}
void methodInSuper() {
System.out.println("In the super!");
}
}
class Sub extends Superclass {
int num = 5;
void methodInSub() {
System.out.println("In the sub!");
//and you could also do this
super.methodInSuper();
System.out.println(num + " " + super.num);
}
}
Output:
In the super!
In the sub!
In the super!
5 1
Aggregation implies a relationship where the child can exist independently of the parent. Example: Class (parent) and Student (child). Delete the Class and the Students still exist.
Composition implies a relationship where the child cannot exist independent of the parent. Example: House (parent) and Room (child). Rooms don’t exist separate to a House.
The above two are forms of containment (hence the parent-child relationships).
Lets start by imagining we build our inheritance tree from the ground up. At the very bottom we have
Object
that all java classes are children of. On top of this we build one class, Animal
.
Animal then has 3 child classes (still growing upwards), Cow
, Cat
, Dog
.
imagine our tree looks like this
Cow Cat Dog
\ | /
Animal
|
Object
The reason I like to think of it in terms of the tree is because we can think of falling down the tree as easier, so covariant, and climbing the tree as difficult (and risky) so contravariant.
Formal definition
-covariant if it preserves the ordering of types (≤), which orders types from more specific to more generic;
-contravariant if it reverses this ordering;
-bivariant if both of these apply (i.e., both I<A> ≤ I<B> and I<B> ≤ I<A> at the same time);
-invariant or nonvariant if neither of these applies.
Design patterns represent the best practices used by experienced object-oriented software developers. Design patterns are solutions to general problems that software developers faced during software development. These solutions were obtained by trial and error by numerous software developers over quite a substantial period of time.
Know the basics, in past tests the questions about design patterns have mostly been in the true/false.
These lecture slides do a really good job of explaining it.
The goal of the Factory design pattern is to control object creation. There can be many situations where this is desirable.
Factory pattern is one of the most used design patterns in Java. This type of design pattern comes under creational pattern as this pattern provides one of the best ways to create an object.
In factory pattern, we create object without exposing the creation logic to the client and refer to newly created object using a common interface.
The singleton is used when you only want there to be one possible instance of a class.
Singleton pattern is one of the simplest design patterns in Java. This type of design pattern comes under creational pattern as this pattern provides one of the best ways to create an object.
This pattern involves a single class which is responsible to create an object while making sure that only single object gets created. This class provides a way to access its only object which can be accessed directly without need to instantiate the object of the class.
Here is an example of a factory class that only has one possible instance:
public class SingleObject {
//create an object of SingleObject
private static SingleObject instance = new SingleObject();
//make the constructor private so that this class cannot be
//instantiated
private SingleObject(){}
//Get the only object available
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
Here is that class being used:
public class SingletonPatternDemo {
public static void main(String[] args) {
//we cannot use the new keyword because the constructor is private
//Compile Time Error: The constructor SingleObject() is not visible
//SingleObject object = new SingleObject();
//Get the only object available
SingleObject object = SingleObject.getInstance();
//show the message
object.showMessage();
}
}
Useful when choice needs to be made between several different subclasses
Shapes example:
Interface
public interface Shape {
void draw();
}
Then we make classes for each kind of shape we want.
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
Now instead of dealing with creating these objects, we have a factory class handle it for us.
public class ShapeFactory {
//use getShape method to get object of type shape
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
}
and if this code is run:
public class FactoryPatternDemo {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();
//get an object of Circle and call its draw method.
Shape shape1 = shapeFactory.getShape("CIRCLE");
//call draw method of Circle
shape1.draw();
//get an object of Rectangle and call its draw method.
Shape shape2 = shapeFactory.getShape("RECTANGLE");
//call draw method of Rectangle
shape2.draw();
//get an object of Square and call its draw method.
Shape shape3 = shapeFactory.getShape("SQUARE");
//call draw method of circle
shape3.draw();
}
}
Output:
Inside Circle::draw() method.
Inside Rectangle::draw() method.
Inside Square::draw() method.