Академический Документы
Профессиональный Документы
Культура Документы
Introduction
Consider a car manufacturing. A motor giant BMW does not make all parts for its
new model of a car. Various pieces (wheels, doors, seats, breaks, sparks...) come from
different manufacturers. This model keeps the cost down and let BMW response to
the market needs in the most efficient way. The main BMW's responsibility is to
design a car, and then to assemble it from already existent parts (a small amount of
completely new details might be necessary). Object-oriented programming (OOP)
springs from the same idea of using preassembled modules. The OOP provides the
programmer with a natural way to divide an application into small reusable pieces,
called objects. At the same time, the OOP provides an elegant way to construct
applications in more efficient way by building up from a collection of reusable
components.
An object should never allow an external manipulation (the client access) over the
internal data or expose data to other objects. Why not give the client direct access to
the fileds? Because this will create problems in the long run. For example, if you
change the name of theat field, the client will no longer work. Also, the object should
never expose details of an algorithm implementation. Clients need only know what
your methods do, not how you implement them. In short,
Data is private
Implementation is hidden
Encapsulation is the idea of hiding data and methods implementations from the
client. By encapsulation you reduce data dependency and maximize reusability.
Inheritance allows you to define a new class in terms of an old class. The new class
automatically inherits all public members of the original class. Inheritance is useful
for creating specialized objects that share comon behavior. We will see more on
inheritance later in the course.
Interfaces act like inheritance in the way that they define a set of properties,
methods,and events, but unlike classes they do not provide implementation. A class
that implements the interface muct implement every method of that interface exactly
as it is defined. We will see more on interfaces later in the course.
Class Declaration
Freely speaking, a Java program is a set of packages, where each package is a set of
classes. A class can contain any number of fields and methods - they are
called members of a class. The implementation design might suggest using private and
public members. The private methods mostly used as helper methods to assist in the
implementation of public methods. The data in a class is typically private, therefore
you must ensure that a class implements public methods to access (and maybe
modify) the data. We will call public methods getters or accessors if they are
designed to only access the data. We will call public methods setters or mutators if
they allow to modification of the internal data.
Constructor
All Java classes have constructors (one ore more) that are used to allocate memory for
the object and to initialize the data fields. Constructors are not methods, though they
look like a method. There are three differences between constructors and methods:
When writing your own class, you don't have to provide constructors for it.
The default constructor is automatically provided. The default constructor doesn't do
anything. However, if you want to perform some initialization, you will have to write
one ore more constructors for your class.
Method Overloading
Java enables several methods of the same name to be defined as long as these methods
have a different set of parameters: the number of parameters, the types of the
parameters and the order of the parameters. This is called method overloading. When
an overloaded method is called, the Java compiler selects the proper method by
examining the number, types and order of the arguments in the call. Here is an
example of legal declarations:
public void methodA()
public void methodA(int z)
public void methodA(int z, String str)
public void methodA(String str, int z)
Method overloading is commonly used to create several methods with the same name
that perform similar tasks, but on different data types. A method's name along with
the number, type and order of its parameters is called the method signatures. If you
attempt to specify two methods with identical signature, the compiler issues an error
message.
Scope
The scope of an identifier for a variable, reference or method is the portion of the
program in which the identifier can be referenced. A local variable or reference
declared in a block can be used only in that block or in blocks nested within that
block. The scopes for an identifier are class scope and block scope. Methods and
instance variables of a class have class scope. Class scope begins at the opening left
brace, {, of the class definition and terminates at the closing right brace, }, of the class
definition. Class scope enables methods of a class to blockquoteectly invoke all
methods defined in that same class and to blockquoteectly access all instance
variables defined in the class.
Identifiers declared inside a block have block scope. Block scope begins at the
identifier's declaration and ends at the terminating right brace } of the block. Local
variables of a method have block scope as do method parameters, which are also local
variables of the method.
while(i < 3) {
sum += i;
i++;
}
will result in the error "cannot resolve symbol sum". Since the variable sum is defined
inside the for-loop it can be referenced only within that scope.
Pass by value
In Java objects are passed by value. Pass by value means that when an argument is
passed to a method, the method receives a copy of the original value. In the example
below, the string "change me" has two references s and x assigned to it.
public class Demo
{
public static void main(String[] args)
{
String s = "change me";
action(s);
System.out.println(s);
}
If the above data type was an array you would have an ability to internally modify the
object. Consider the following code fragment:
public class Demo
{
public static void main(String[] args)
{
int[] a = {1,2,3};
action(a);
System.out.println( Arrays.toString(a) );
}
It demonstrates that the first element of the array has been changed.
Static members do not formally belong to an object; they exist before the object was
created, Static members are created when you compile your program. In runtime,
when you instantiate a class, only instance members are created. Moreover, every new
object instantiated from the same class, has a new set of instance variables. In
contrary, there is only one copy of static members, regardless how many object you
created. Consider the following code fragment
public class StaticDemo
{
public static void main(String[] args)
{
Demo obj1 = new Demo();
Demo.number++;
Demo obj2 = new Demo();
System.out.println(obj2.getX());
}
}
class Demo
{
private int x;
static int number = 0;
In this code example we created two objects ob1 and obj2 that share a static variable
number. This variable was incremented three times: during instantiation of ob1,
during instantiation of obj2 and by a direct call to it. The staic variable number leaves
in a global context and can be invoked either using references obj1 and obj2 or using
the class name Demo.
OO design principles
There are many heuristics associated with object oriented design. For example,
To extend the behavior of the system, we do not modify old code that already works. -
we add new code, How can we do this? Let us consider a code fragment. Given the
Part class
public class Part
{
private double price;
public Part(double p)
{
price = p;
}
return total
}
that totals the price of each part in the specified array of parts. The code looks quite
innocent and you would not suspect a problem with it in the future. Now assume that
the accounting department comes out with a new pricing policy for some parts, for
example DVD_Drives are 20% off sale. To reflect this change we have to rewrite the
totalPrice method
public double totalPrice(Part[] parts)
{
double total = 0.0
for(int k = 0; k < parts.lebgth; k++)
{
if(parts[k] instanceof DVD_Drive)
total += 0.8 * parts[k].getPrice();
else
total += parts[k].getPrice();
}
return total
}
This is an example of bad design - every time the accounting department comes out
with a new pricing policy, we have to modify the totalPrice method. The code is not
closed for modification. What can we do? A better idea is not to fix the price but to
introduce various pricing policies via a PricingPolicy class.
public class Part
{
private double price;
private PricingPolicy policy;
public Part(double p)
{
price = p;
}
With this solution we can dynamically set pricing policies for each modified part. The
general idea is that we try to design and implement classes so that they use abstract
classes and interfaces. That way, we can extend the functionality by simply adding
new subclasses which add new behavior.
1. data hiding
2. implementation hiding
3. all of the above
4. none of the above
b. What is a method's signature?
1. the name of the method along with the number of its parameters.
2. the name of the method along with the number and type of its
parameters.
3. the name of the method along with the number, type and order of its
parameters.
4. the name of the method, its return type and the number, type, order of its
parameters.
b. Examine the following code segment
c. public class Demo
d. {
e. public static void main(String[] ags)
f. {
g. Test o1 = new Test();
h. Test o2 = new Test();
i. Test o3 = o2;
j. System.out.println(o1.number);
k. }
l. }
m. class Test
n. {
o. static int number=0;
p. public Test()
q. {
r. number++;
s. }
t. }
1. 0
2. 1
3. 2
4. none of the above
b. Analyze the following code and choose the best answer:
c. public class Analyze
d. {
e. private int x;
f.
g. public static void main(String[] args)
h. {
i. Analyze tmp = new Analyze();
j. System.out.println(tmp.x);
k. }
l. }
1. 0
2. 1
3. none of the above
Concept of Hashing
Introduction
The example of a hash function is a book call number. Each book in the library has
a unique call number. A call number is like an address: it tells us where the book is
located in the library. Many academic libraries in the United States, uses Library of
Congress Classification for call numbers. This system uses a combination of letters
and numbers to arrange materials by subjects.
A hash function that returns a unique hash number is called a universal hash
function. In practice it is extremely hard to assign unique numbers to objects. The
later is always possible only if you know (or approximate) the number of objects to be
proccessed.
Thus, we say that our hash function has the following properties
How to choose a hash function? One approach of creating a hash function is to use
Java's hashCode() method. The hashCode() method is implemented in the Object
class and therefore each class in Java inherits it. The hash code provides a numeric
representation of an object (this is somewhat similar to the toString method that gives
a text representation of an object). Conside the following code example
Integer obj1 = new Integer(2009);
String obj2 = new String("2009");
System.out.println("hashCode for an integer is " + obj1.hashCode());
System.out.println("hashCode for a string is " + obj2.hashCode());
It will print
hashCode for an integer is 2009
hashCode for a string is 1537223
The method hasCode has different implementation in different classes. In the String
class, hashCode is computed by the following formula
s.charAt(0) * 31n-1 + s.charAt(1) * 31n-2 + ... + s.charAt(n-1)
where s is a string and n is its length. An example
"ABC" = 'A' * 312 + 'B' * 31 + 'C' = 65 * 312 + 66 * 31 + 67 = 64578
Collisions
When we put objects into a hashtable, it is possible that different objects (by
the equals() method) might have the same hashcode. This is called a collision. Here is
the example of collision. Two different strings ""Aa" and "BB" have the same key: .
"Aa" = 'A' * 31 + 'a' = 2112
"BB" = 'B' * 31 + 'B' = 2112
The big attraction of using a hash table is a constant-time performance for the basic
operations add, remove, contains, size. Though, because of collisions, we cannot
guarantee the constant runtime in the worst-case. Why? Imagine that all our objects
collide into the same index. Then searching for one of them will be equivalent to
searching in a list, that takes a liner runtime. However, we can guarantee an expected
constant runtime, if we make sure that our lists won't become too long. This is usually
implemnted by maintaining a load factor that keeps a track of the average length of
lists. If a load factor approaches a set in advanced threshold, we create a bigger array
and rehash all elements from the old table into the new one.
HashSet
In this course we mostly concern with using hashtables in applications. Java provides
the following classes HashMap, HashSet and some others (more specialized ones).
HashSet is a regular set - all objects in a set are distinct. Consider this code segment
String[] words = new String("Nothing is as easy as it
looks").split(" ");
It prints "6 distinct words detected.". The word "as" is stored only once.
HashSet stores and retrieves elements by their content, which is internally converted
into an integer by applying a hash function. Elements from a HashSet are retrieved
using an Iterator. The order in which elements are returned depends on their hash
codes.
Spell-checker
You are implement a simple spell checker using a hash table. Your spell-checker will
be reading from two input files. The first file is a dictionary located at the
URLhttp://www.andrew.cmu.edu/course/15-121/dictionary.txt . The program should
read the dictionary and insert the words into a hash table. After reading the dictionary,
it will read a list of words from a second file. The goal of the spell-checker is to
determine the misspelled words in the second file by looking each word up in the
dictionary. The program should output each misspelled word.
HashMap
HashSet and HashMap will be printed in no particular order. If the order of insertion
is important in your application, you should
use LinkeHashSet and/or LinkedHashMap classes. If you want to print dtata in sorted
order, you should use TreeSet and or TreeMap classes
map.get(key) -- returns the value associated with that key. If the map does not
associate any value with that key then it returns null. Referring to
"map.get(key)" is similar to referring to "A[key]" for an array A.
map.put(key,value) -- adds the key-value pair to the map. This is similar to
"A[key] = value" for an array A.
map.containsKey(key) -- returns true if the map has that key.
map.containsValue(value) -- returns true if the map has that value.
map.keySet() -- returns a set of all keys
map.values() -- returns a collection of all value
Anagram solver
Priority Queue
We are often faced with a situation in which certain events/elements in life have
higher or lower priorities than others. For example, university course prerequisites,
emergency vehicles have priority over regular vehicles. A Priority Queue is like a
queue, except that each element is inserted according a given priority. The simplest
example is provided by real numbers and ≤ or ≥ relations over them. We can say that
the smallest (or the largest) numerical value has the highest priority. In practice,
priority queues are more complex than that. A priority queue is a data structure
containing records with numerical keys (priorities) that supports some of the
following operations:
Observe that a priority queue is a proper generalization of the stack (remove the
newest) and the queue (remove the oldest).
Elementary Implementations
There are numerous options for implementing priority queues. We start with simple
implementations based on use of unordered or ordered sequences, such as linked lists
and arrays. The worst-case costs of the various operations on a priority queue are
summarized in this table
Later on in the course we will see another implementation of a priority queueu based
on a binary heap.
The Comparable interface contains only one method with the following signature:
public int compareTo(Object obj);
The returned value is negative, zero or positive depending on whether this object is
less, equals or greater than parameter object. Note a difference between the equals()
and compareTo() methods. In the following code example we design a class of
playing cards that can be compared based on their values:
class Card implements Comparable<Card>
{
private String suit;
private int value;
Suppose we would like to be more flexible and have a different way to compare cards,
for example, by suit. The above implementation doesn’t allow us to do this, since
there is only one compareTo method in Card. Java provides another interface which
we can be uses to solve this problem:
public interface Comparator<AnyType>
{
compare(AnyType first, AnyType second);
}
Notice that the compare() method takes two arguments, rather than one. Next we
demonstrate the way to compare two cards by their suits, This method is defined in its
own class that implements Comparator:
class SuitSort implements Comparator<Card>
{
public int compare(Card x, Card y)
{
return x.getSuit().compareTo( y.getSuit() );
}
}
Objects that implement the Comparable interface can be sorted using the sort()
method of the Arrays and Collections classes. In the following code example, we
randomly generate a hand of five cards and sort them by value and then by suit:
String[] suits = {"Diamonds", "Hearts", "Spades", "Clubs"};
Card[] hand = new Card[5];
Random rand = new Random();
System.out.println("sort by value");
Arrays.sort(hand);
System.out.println(Arrays.toString(hand));
System.out.println("sort by suit");
Arrays.sort(hand, new SuitSort() );
System.out.println(Arrays.toString(hand));
Objects can have several different ways of being compared. Here is another way of
comparing cards: first by value and if values are the same then by suit:
class ValueSuitSort implements Comparator<Card>
{
public int compare(Card x, Card y)
{
int v = x.getValue() - y.getValue();
return ( v == 0) ? x.getSuit().compareTo(y.getSuit()) : v;
}
}
5. Consider the following sequence of integer keys to be hashed into a hash table
using the hash function H(key) = key modulo tablesize:
11, 4, 5, 12, 25, 18
Insert each key above, from left to right, into the hash table below using linear
probing to resolve collisions.
Create a hash set and traverse the array. Check if each element is already in the
hash set, and if it is, then you've found the duplicate. Otherwise, add the
element to the hash set.
7. Given an array of integers. Design an efficient algorithm that finds two
numbers x and y such that x + y = 0. Consider two cases a)sorted array;
b)unsorted array.
a)Do the same thing as in the previous question, but check if the element's
complement is in the hashset.
b)Do the same thing as in the previous question, but check if the element's
complement is in the hashset.
Build a Map whose key is a sorted word (meaning that its characters are sorted
in alphabetical order) and whose values are the word's anagrams.
return str1.compareTo(str2);
}
public void equals(Object obj){
String str1 = ((String)o1).toLowerCase();
String str2 = ((String)02).toLowerCase();
return str1.equals(str2);
}
}
return str1[1].compareTo(str2[1]);
}
public void equals(Object obj){
String str1 = ((String)o1).split("@.");
String str2 = ((String)02).split("@.");
return str1[1].equals(str2[1]);
}
}
The difference between a stream of characters from a stream of bytes is that the
former uses Unicode characters, which are represented by 16 bits - that is it, in java, a
character is made up of two bytes. In contrary, the C language uses 1 byte characters
(ASCII table). The natural question to ask is how Java deals with these encoding
problems: converting bytes to characters and back. The Java has different tables of
encoding and you can choose any of them. To see the default table you print
System.getProperty("file.encoding");
One of the encoding tables is called UTF-8, which can represent ASCII characters (one
byte), and other characters as two or three bytes. This is very important issue if you
are concerned with writing applications that operate in an international context.
Standard Output
Command-line Input
Any Java program takes input values from the command line and prints output back
to the terminal window. When you use the java command to invoke a Java
program Demofrom the command line, you type something like this
java Demo parameter1 parameter2
Standard Input
The string of characters that you type in the terminal window after the command
line is the standard input stream. The following code example (let us call this class
Average.java)takes a parameter from the command line, then reads so many
numbers from standard input and computes the average:
int n = Integer.parseInt(args[0]);
double sum = 0.0;
Scanner scanner = new Scanner(System.in); //it's defined in
java.util
Exceptions
Exceptions in the object-oriented language is a quite complex topic that requires time
to understand and apply properly. By now you perhaps met a few exception classes
such as the following
NullPointerException
ArrayIndexOutOfBoundsException
ArithmeticException
NullPointerException
String s = null;
StringBuffer sb = new StringBuffer(s);
All exceptions are objects. Once an exception is thrown you have three options:
Catching Exceptions
Throwing Exceptions
It's common to test parameters of methods or constructors for valid input data. If the
value is illegal, you throw an exception by using the throw keyword along with the
object you want to throw. Examine this code example
public Card(int suit, int value)
{
if (isValidValue(value) && isValidSuit(suit))
{
this.value = value;
this.suit = suit;
}
else
throw new RuntimeException("Illegal suit or value");
}
Throw behaves similar to return, it performs structured transfer from the place in your
program where an abnormal condition was detected to a place where it can be
handled. It also transmits information. The exception object may contain a detail
message that can be retrieved by invoking the getMessage() method
Any method that may throw an exception, must declare the exception in it's method
declaration. This is implemented by the use of the throws clause. Throws clause
follows the method name and lists the exception types that may be thrown:
public static main(String[ ] args) throws IOException
{
}
The throws clause is used
to propagate an exception.
in a method declaration to notify that an exception might be thrown by the
method.
Developer Client
The FileReader class represents an input file that contains character data. The class
throws FileNotFoundException (checked exception), so you have to wrap try and
catch block around each time you open a file. For the efficient reading, we use
the BufferedReader class. It provides the method readLine() that allows to read the
entire text line (a line is a sequence of characters terminated either by '\n' or '\r' or their
combination). The method returns a String, or null if the end of the stream has been
reached. The readLine() method throws IOException, so you have to handle it.
A text file can also be read using the Scanner class. Using the Scanner offers the
advantage for text processing of various data formats.. Scanner has a lot of other
features, with support for regular expressions, delimiter definitions, skipping input,
searching in a line, reading from different inputs and others..
classes: FileWriter
BufferdWriter
PrintWriter
exceptions: IOException
methods: println
close
New Line
The line separator string depends on your operating system. On UNIX, the value of
the line separator is "\n", on Windows - "\r\n", and on Mac - "\r" . In Java the line
separator is defined by the system property line.separator , which should be
called as it follows
String out = "any characters";
out += System.getProperty("line.separator");
Appending Data
Serialization
Object is serialized by transforming it into a sequence of bytes. Once serialized, the
object (actually its data fields) can be stored in a file to be read later, or sent across a
network to another computer. Serialization allows you to create persistent objects,
objects that can be stored and then reconstituted for later use. The basic mechanism
of serialization is simple. To serialize an object, you create an output stream
FileOutputStream out = new FileOutputStream("output.ser");
ObjectOutputStream outStream = new ObjectOutputStream(out);
and then use a method writeObject() to write an object into a file in a binary format.
It throws IOException and NotSerializableException.
outStream.writeObject("Today");
outStream.writeObject(new Date());
outStream.flush();
You can write primitive data types as well. They are written to the stream with the
methods, such as writeInt, writeFloat, and a few others.
The class of each serializable object is encoded including the class name
and signature of the class, the values of the object's fields and arrays, and the closure
of any other objects referenced from the initial objects. Only objects with
the Serializable interface can be serialized. The interface does not have any
methods, and serves only as a flag to the compiler. For most classes the default
serialization is sufficient. Many classes of API implement Serializable interface, for
example String, Vector.
Algorithmic Complexity
Introduction
Algorithmic complexity is concerned about how fast or slow particular algorithm
performs. We define complexity as a numerical function T(n) - time versus the input
size n. We want to define time taken by an algorithm without depending on the
implementation details. But you agree that T(n) does depend on the implementation!
A given algorithm will take different amounts of time on the same inputs depending
on such factors as: processor speed; instruction set, disk speed, brand of compiler and
etc. The way around is to estimate efficiency of each algorithm asymptotically. We
will measure time T(n) as the number of elementary "steps" (defined in any way),
provided each such step takes constant time.
Let us consider two classical examples: addition of two integers. We will add two
integers digit by digit (or bit by bit), and this will define a "step" in our computational
model. Therefore, we say that addition of two n-bit integers takes n steps.
Consequently, the total computational time is T(n) = c * n, where c is time taken by
addition of two bits. On different computers, additon of two bits might take different
time, say c1 and c2, thus the additon of two n-bit integers takes T(n) = c1 * n and T(n)
= c2* n respectively. This shows that different machines result in different slopes, but
time T(n) grows linearly as input size increases.
The process of abstracting away details and determining the rate of resource usage in
terms of the input size is one of the fundamental ideas in computer science.
Asymptotic Notations
The goal of computational complexity is to classify algorithms according to their
performances. We will represent the time function T(n) using the "big-O" notation to
express an algorithm runtime complexity. For example, the following statement
T(n) = O(n2)
says that an algorithm has a quadratic time complexity.
For any monotonic functions f(n) and g(n) from the positive integers to the positive
integers, we say that f(n) = O(g(n)) when there exist constants c > 0 and n 0 > 0 such
that
f(n) ≤ c * g(n), for all n ≥ n0
Intuitively, this means that function f(n) does not grow faster than g(n), or that
function g(n) is an upper bound for f(n), for all sufficiently large n→∞
1 = O(n)
n = O(n2)
log(n) = O(n)
2 n + 1 = O(n)
Exercise. Let us prove n2 + 2 n + 1 = O(n2). We must find such c and n0 that n 2 + 2 n
+ 1 ≤ c*n2. Let n0=1, then for n ≥ 1
An algorithm is said to run in constant time if it requires the same amount of time
regardless of the input size. Examples:
An algorithm is said to run in linear time if its time execution is directly proportional
to the input size, i.e. time grows linearly as input size increases. Examples:
array: linear search, traversing, find minimum
ArrayList: contains method
queue: contains method
binary search
Recall the "twenty questions" game - the task is to guess the value of a hidden number
in an interval. Each time you make a guess, you are told whether your guess iss too
high or too low. Twenty questions game imploies a strategy that uses your guess
number to halve the interval size. This is an example of the general problem-solving
method known as binary search:
locate the element a in a sorted (in ascending order) array by first comparing a
with the middle element and then (if they are not equal) dividing the array into
two subarrays; if a is less than the middle element you repeat the whole
procedure in the left subarray, otherwise - in the right subarray. The procedure
repeats until a is found or subarray is a zero dimension.
Note, log(n) < n, when n→∞. Algorithms that run in O(log n) does not use the whole
input.
We need the notation for the lower bound. A capital omega Ω notation is used in this
case. We say that f(n) = Ω(g(n)) when there exist constant c that f(n) ≥ c*g(n) for for
all sufficiently large n. Examples
n = Ω(1)
n2 = Ω(n)
n2 = Ω(n log(n))
2 n + 1 = O(n)
To measure the complexity of a particular algorithm, means to find the upper and
lower bounds. A new notation is used in this case. We say that f(n) = Θ(g(n)) if and
only f(n) = O(g(n)) and f(n) = Ω(g(n)). Examples
2 n = Θ(n)
n2 + 2 n + 1 = Θ( n2)
Analysis of Algorithms
The term analysis of algorithms is used to describe approaches to the study of the
performance of algorithms. In this course we will perform the following types of
analysis:
Consider a dynamic array stack. In this model push() will double up the array size if
there is no enough space. Since copying arrays cannot be performed in constant time,
we say that push is also cannot be done in constant time. In this section, we will show
that push() takes amortized constant time.
Asymptotically speaking, the number of copies is about the same as the number of
pushes.
2n+1 - 1
limit --------- = 2 = O(1)
n→∞ 2n + 1
We say that the algorithm runs at amortized constant time.