Currently I’m working on this part of the tutorial and one other surprise page …
Classes and Objects
Classes are definitions that encapsulate data and behavior into a single bundle. The language runtime/virtual-machine uses those definitions to create Objects during the execution of the program.
- Class is the description and implementation
- Objects are the physical instantiation of the Class definition
- Method is a function aware of Object variables/attributes
- Attributes are variables belonging to a Class or an Object
- Class variables are accessible by all the Objects instantiated from the Class
- Object variables are accessible only to the object
Methods
Methods provide the behavior of a class. The format to create them is :
<access> <return type> <method-name>(<type> <arg>, ….) { ….. }
public String blah(String str, int arg2) { ….. }
First we specify the access/visibility descriptor, next is the type of the return value if the method returns one, followed by the name of the method and the arguments that it accepts.
Java have the capability to limit the visibility and access to attributes and methods. Here is list of them and a description of how they behave.
Visibility | none | public | private | protected |
in the same class | yes | yes | yes | yes |
in the same package | yes | yes | no | yes |
ancestor class | no | yes | no | yes |
non-ancestor in another package | no | yes | no | no |
Why complicate the language by adding those ?
The reason is to make coding harder and your life miserable …. I’m joking of course 😉 ….. or m’I ?
Many times there a legitimate case you would like to hide portions of your class, for example
- having private attributes allows you to “channel” access to them via public methods, thus allowing you to do processes like validation, pre or post processing, before committing the change to the attribute.
- hiding unfinished or changing methods
- providing access only to the actual class clean API omitting/hiding the detailed implementation
- and so on …
Lets now explore Classes by creating a Class that implement Binary Tree Structure.
Binary tree is a tree where every node has maximum of two children.
I selected this example because it is simple enough, will need only one class and also because we haven’t discussed data structures yet.
So as you’ve seen so far the definition starts with specifier public class, followed by the class name.
Then after the already familiar say(), we define three attributes.
- the first one “name” will be publicly accessible and will store the name of the node.
- then we declare as private references to the children of the node. They are both of a Node class. The way we build a Tree structure, is to create a Node that has a Nodes as a children. By making them private we make sure that only methods of the Node class can access them.
Because the nodes are not initialized and also can’t be accessed from outside we have to provide methods to do this with names .add_left() and .add_right(). Declaring them void means that the methods will not have a return value. If on the other hand the attributes were public we did not have to create those methods.
(will discuss the constructor in the next section)
We are pretty much done … what is left is to create a method to walk() the tree and print the node names.
We do that by using recursion. First thing always to remember about recursion is to figure what and where to put the limiting condition. In our case we flip the logic i.e. we “recurse”‘ only when the current node has a child. By default a node has no children so the walk will tend to end of the recursion.
Also check the formatting trick, on every call to walk() we append a space character thus the deeper the node is in the hierarchy the further to the right the node name will be printed. >>
public class Node {
//shortcut print
public static <ARG> void say(ARG arg) { System.out.println(arg); }
public String name;
private Node left;
private Node right;
// ** constructor **
public Node(String name) {
this.name = name;
}
public void add_left(Node node) { left = node; }
public void add_right(Node node) { right = node; }
public void walk(String prefix) {
say(prefix + name);
//calling left.walk, not the root.walk
if (left != null) left.walk(prefix + " ");
if (right != null) right.walk(prefix + " ");
}
public static void main(String[] args) {
//create the root node
Node root = new Node("root");
//this time create them on the fly
root.add_right(new Node("-node2"));
//this one we want to extend, so we need the variable
Node left = new Node("+node3");
root.add_left(left);
left.add_left(new Node("+node4"));
left.add_right(new Node("-node5"));
root.walk(" ");
}
}
-----
Output:
root
+node3
+node4
-node5
-node2
Finally we use the main() method to test the class.
A tree starts with a root-node, so that is what we will do. The format to create/instantiate an Object is :
ClassName variable = new ClassName(arguments)
Once we have the root node cerated we would add children nodes by building them on the fly.
After that lets call the .walk() method to print the tree.
1
----- Output:
Constructors
Constructors are special methods in a class definition used to initialize an Object upon its creation.
In the class above we used :
public Node(String name) { this.name = name; }
where this keyword is used to refer to the object itself. In general we do not need to use this, Java assumes it implicitly. But in this case where the constructor argument and object attribute have the same name we need a way to distinguish between them. In general it is good practice to use this as a visual cue, to make the code more readable.
One more thing to mention is that when you define a constructor you do not specify a return type, because the return type is always an object of the current class.
But we could have used instead :
public Node(String name, Node left, Node right) { this.name = name; this.left = left; this.right = right; }
Which would have required to pass the children nodes on creation, but we can’t do that because they have to be created in advance which requires to pass children nodes … it seems this will need infinite recursion, but there is an escape patch, you could just pass null value instead.
But wait, there is a third option. What if we define both constructors. Yes, we can do that, it is called overloading.
overloading
When two methods have the same name but different arguments at compile time Java decides which is the correct method to call. I really like this feature.
The example below declares methods to add two numbers. By overloading the add() method and writing implementation for every type we keep the method name the same even for numbers disguised as Strings. Otherwise if overloading is not supported (f.e. Python) we had to write method with different name for every type add_int(), add_float()… and so on.
public class Overloading { public static <ARG> void say(ARG arg) { System.out.println(arg); } public int add(int a, int b) { return a + b; } // float instead of int public float add(float a, float b) { return a + b; } //single arg, instead of two public int add(int b) {return 5 + b; } // string like numbers public int add(String a, String b) { return Integer.parseInt(a) + Integer.parseInt(b); } public static void main(String[] args) { overloading o = new overloading(); say("Add 5 + 7 : " + o.add(5,7)); say("Add 5.0f + 7.0f : " + o.add(5.0f,7.0f)); say("Add 5.add(7) : " + o.add(7)); say("Add '5' + '7' : " + o.add("5","7")); } } ----- Output: Add 5 + 7 : 12 Add 5.0f + 7.0f : 12.0 Add 5.add(7) : 12 Add '5' + '7' : 12
overriding
overriding is what it sounds. This is the case when in a subclass we write a method with the same name and arguments as it is in the parent.
What happens is the new method will be executed in place of the parent method, when a call is made.
Generics/Templates
Java allows you to treat the “type-signature” of methods and classes as “type-variables” and then use those in the definitions later. But it is easier to show, rather than explain. Below I wrote two Functions print() and say() which produce the same result but are implemented differently.
print() uses the overloading technique that we just discussed. The disadvantage of this approach is that we have to list every possible scenario. (Mind you that is probably how println() is implemented internally by Java).
Then we use Generics to implement say(). In brackets <ARG> we define the type-variable and then we can use it in all the places where we specify types. Later the compiler will fill the exact type based on the argument type.
As you see it is just one line and covers not only the three cases that print() covers, but also any other type supported by println(). The advantage is obvious.
Note: Using Generics with arithmetic operations does not work because in Java arithmetic’s is not implemented as operators.
public class Generics { // OVERRIDING public static void print(Integer arg) { System.out.println(arg); } public static void print(Float arg) { System.out.println(arg); } public static void print(String arg) { System.out.println(arg); } // GENERICS public static <ARG> void say(ARG arg) { System.out.println(arg); } public static void main(String[] args) { print("This is a string"); print(555); print(55.678f); say("This is a string"); say(555); say(55.678f); } } ----- This is a string 555 55.678 This is a string 555 55.678
Did you notice the static keyword in the declaration of the “Functions”. This is a non-access modifier. When it is used in definition of a method it allows us to call the method w/o creating the object first.
Data Structures
So far we worked with scalar values but complex problems requires richer data structures.
Arrays
Arrays hold elements of the same type and can have multiple dimensions. The main advantage is that you can access any element fast and directly by index. The disadvantage is that once declared you can not add new elements or resize the array.
Fist I demonstrate how to create an array. When defining an array add square brackets [] at the end of the type.
I also demonstrate how you can modify elements and loop over the array both with for and foreach loop.
public class Multi { public static <ARG> void say(ARG arg) { System.out.println(arg); } public static void main(String[] args) { //initialize array int[] ints = {1,2,3,4,5}; String[] strs = {"one", "two", "three", "four", "five"}; float[] floats = new float[5]; //loop over the array for (int i = 0; i < ints.length; i++) { say(ints[i] + " : " + strs[i]); } //change value by index ints[3] = 7; int sum = 0; //foreach loop to calculate the sum of all elements for (int el : ints) { sum += el; } say("Sum : " + sum); } } ----- 1 : one 2 : two 3 : three 4 : four 5 : five Sum : 18
An Array can have multiple dimensions. This allows you to represent matrices, tensors or data that have tabular structure.
As you may suspect you add as many [] as you need to tell the compiler how many dimensions do you want.
Initialization of Arrays is done in similar way, you just need more brackets.
If the arrays are the same type and size you can copy them as shown in the example below.
It’s a simple : we create 2D array, copy it in another one and then print it as 2D square.
public class MultiDim {
public static <ARG> void say(ARG arg) { System.out.print(arg); }
public static void nl() { System.out.println(""); }
public static void main(String[] args) {
//initialization
int[][] ints = { {1,2,3},{4,5,6},{7,8,9} };
int[][] multi = new int[3][3];
//copy arrays
multi = ints;
//print the 2D array as a square
for (int i = 0; i < multi.length; i++) {//loop rows first
for (int j = 0; j < multi[i].length; j++) {
say(multi[i][j] + " ");
}
nl();//new line after we print a full row
}
}
}
-----
1 2 3
4 5 6
7 8 9
Lists
Lists are dynamic sequential structure, where you can add or delete elements.
Java has 3 List types : ArrayList, Vector and LinkedList. We will only use ArrayList in the examples.
As the name implies this type has characteristics of both Array and List.
When you define ArrayList you must also specify the type of the elements it contains in angle brackets, like this new ArrayList<String>. Now you have the object but the List is still empty, so we will .add() some elements.
Then I show you how to .remove() one of them and some other common operations.
And very importantly at the end you can see how to convert between Array and ArrayList.
import java.util.*; public class Lists { public static <ARG> void say(ARG arg) { System.out.println(arg); } public static void main(String[] args) { //create List of String's ArrayList<String> list=new ArrayList<String>(); //populate ... list.add("one"); list.add("two"); list.add("abc"); list.add("three"); list.add("four"); list.add("five"); String abc = list.remove(2); say("Removed element : " + abc); say("Element No. 3 : " + list.get(3)); say("Found 'five' at position : " + list.indexOf("five")); //loop over elements for (String num : list) { say(num); } //convert between Array <===> List String[] ary = list.toArray(new String[0]); List<String> list2 = Arrays.asList(ary); } } ----- Removed element : abc Element No. 3 : four Found 'five' at position : 4 one two three four five
We discussed how to create a BinaryTree, but now that we know how to use Generics and ArrayList we can extend it to support many children and also hold data.
If there was no Generics we had to create separate Node class for every data type we need.
But now we can define it as Node<T> where T behaves like variable, so in runtime we can instantiate providing the type we need and only have single class definition f.e. new Node<String>, new Node<Integer> …
We keep the .name attribute and add .data attribute of type T
The children this time are stored in ArrayList<Node>, thus we can have multiple children. So now instead of .add_left() and .add_right() we have only one method .add_child()
Walking the tree is still recursive but this time the limit condition uses another ArrayList method to check if the children list .isEmpty().
Finally we create Tree with nodes of heterogeneous data and because we added data aware constructor we can create the node on the fly.
import java.util.*;
public class Node<T> {
//shortcut print
public static <ARG> void say(ARG arg) { System.out.println(arg); }
public String name;
public T data;// generics variable type
public ArrayList<Node> children = new ArrayList<Node>();
// base constructor
public Node(String name) {
this.name = name;
}
// data aware constuctor
public Node(String name, T data) {
this.name = name;
this.data = data;
}
public void add_child(Node node) { children.add(node); }
public void walk(String prefix) {
say(prefix + name + " : " + data);
if (!children.isEmpty()) {
for (Node child : children) {
child.walk(prefix + " ");
}
}
}
public static void main(String[] args) {
//create the root node
Node root = new Node<Integer>("root", 5);
root.add_child(new Node<String>("node2", "first-child"));
Node node = new Node<String>("node3", "snd-child");
root.children.add(node);
node.add_child(new Node<String[]>("node4", new String[]{"John","Doe"}));
node.add_child(new Node("node5"));
root.walk(" ");
}
}
-----
root : 5
node2 : first-child
node3 : snd-child
node4 : [Ljava.lang.String;@534df152
node5 : null
sets
You can use sets if :
- You want to store distinct elements without duplication i.e. unique elements.
- You don’t care about the order of elements.
There three types of sets :
- HashSet : Standard set only guarantees uniqueness
- LinkedHashSet : In addition remembers the element insertion order
- TreeSet : Order by the set values or other custom ordering via Comparator
1
import java.util.*; public class Sets { //shortcut print public static <ARG> void say(ARG arg) { System.out.println(arg); } public static void main(String[] args) { //create and add elements HashSet<Integer> set = new HashSet<Integer>() {{ add(5);add(7);add(15); add(5);add(7); //duplicates }}; //... add more elements set.add(42); set.add(43); for (Integer num : set) { say(num); } } } ----- 5 7 42 43 15
1
----- Output: