13 - Data Structures and Algorithms Stacks, Queues, and Generics
13 - Data Structures and Algorithms Stacks, Queues, and Generics
one implementation) for all types of data, even types that are
implemented in the future.
Generics use the notation <Item> after the class name to define the
name Item as a type parameter, a symbolic placeholder for some
concrete type to be used by the client.
Without generics, different APIs for each type of data would have to
be defined and implemented.
Java has special mechanisms called autoboxing and auto-unboxing to
allow for the use of generics with primitive types.
Autoboxing automatically casts a primitive type to a wrapper type
and auto-unboxing automatically casts a wrapper type to a primitive
type.
Iterable collections are a paradigm that allows for clear and compact
code that is free from dependence on the details of a collection's
implementation, it is supported by Java and many other modern
languages.
Generics allows for the creation of more robust and versatile code,
as it allows for the use of a single API for different types of data
without the need for multiple implementations.
The use of generics in the API definition allows for better type-
checking at compile-time, reducing the likelihood of runtime errors.
Autoboxing and auto-unboxing enable the use of generics with
primitive types, allowing for the same API to be used for both
reference and primitive types.
Iterable collections are a powerful tool for client code, as they
provide a simple and consistent way to iterate through the elements
of a collection, regardless of the underlying implementation.
Iterable collections also allow for the use of enhanced for-loop and
other language constructs that are designed to work with iterable
collections, making the client code more readable and maintainable.
FIFO queues are collections that operate on the first-in, first-out
policy.
This policy is commonly encountered in everyday situations such as
waiting in line.
The principle of fairness is upheld by the FIFO discipline, where the
person/task that has been waiting the longest is served first.
Queues are a natural model for many everyday phenomena and play
a central role in numerous applications.
When a client iterates through the items in a queue, they are
processed in the order they were added.
A typical reason to use a queue in an application is to save items in a
collection while preserving their relative order.
For example, the readDoubles() method from the In class uses a
queue to get numbers from a file into an array without knowing the
file size ahead of time.
The queue is appropriate because it puts the numbers into the array
in the order in which they appear in the file.
The code uses autoboxing and auto-unboxing to convert between
the client's double primitive type and the queue's Double wrapper
type.
Pushdown stacks are collections based on the last-in-first-out (LIFO)
policy, where the last item added is the first item to be removed.
An example of a pushdown stack in everyday life is keeping mail in a
pile on a desk, where new mail is added to the top and read mail is
taken from the top.
Other examples include organizing email and browsing the web,
where new pages are pushed onto a stack and previous pages can be
accessed by popping from the stack.
The LIFO policy of a stack is useful for processing items in a collection
in the reverse order in which they were added.
One important use of stacks in computing is in arithmetic expression
evaluation. A classic algorithm developed by E. W. Dijkstra in the
1960s uses two stacks (one for operands and one for operators) to
convert a string of characters into the value it represents.
This algorithm is particularly useful for expressions that have fully
parenthesized operators and operands, as it allows for precise
interpretation of the order in which operations should be performed.
The algorithm uses one stack for operands (numbers) and another
stack for operators (+, -, *, /, etc.).
It starts by reading the expression from left to right, pushing
operands onto the operand stack and operators onto the operator
stack.
When an operator is encountered, the algorithm checks the operator
stack to see if there is a higher precedence operator already on the
stack.
If there is, the algorithm performs the operation and pushes the
result onto the operand stack.
The process continues until the entire expression has been read and
evaluated.
The final result is the value on top of the operand stack.
This algorithm is not only simple but also extensible, as more
operators and functions can be easily added to support a wide range
of mathematical expressions.