Abstract data types (ADT) and encapsulation
The concept of abstraction, the two conditions of an ADT, information hiding, Ada packages, C++ and Java classes, parameterized ADTs (generics) and encapsulation constructs for large programs (namespace, package).
The way to cope with the complexity of a program is abstraction: seeing only the properties of an entity that matter at the moment and hiding the rest. Subprograms are process abstraction (you call sort without knowing the sorting algorithm). This week the second kind: data abstraction, that is the abstract data type (ADT). On exams conceptual questions are asked (the two conditions of an ADT, the benefit of information hiding, language comparisons) and the output of code that uses an ADT.
Golden rules:
- ADT = hidden representation + a type and operations defined in a single unit.
- The user sees only the interface. Changing the inside does not break user code.
- A stack is LIFO, a queue is FIFO. In output questions write the contents at each step.
- A parameterized ADT catches type errors at compile time.
Abstract data type
A user-defined type is an ADT if it satisfies two conditions:
- Hidden representation: the internal structure of the type’s objects is hidden from the units that use the type. The user accesses the objects only through the operations the type defines.
- A single unit: the declaration of the type and the protocols of the operations on it are in a single syntactic unit. Other units can create variables of this type.
In fact float is also like an ADT: you use + and * without knowing how the bits are arranged. User-defined ADTs bring the same comfort to the programmer’s own types.
Benefits of information hiding:
- Reliability: the user cannot corrupt the internal state of the object (cannot set the stack’s
topfield to a random value). - Changeability: if the representation changes (a linked list instead of an array), the user code doesn’t change, because it depends only on the interface.
- Fewer name clashes and smaller, manageable units.
ADTs in Java: the class
static class IntStack {
private int[] items = new int[10];
private int top = -1;
public void push(int x) { items[++top] = x; }
public int pop() { return items[top--]; }
public int peek() { return items[top]; }
public boolean isEmpty() { return top == -1; }
}
IntStack s = new IntStack();
s.push(3); s.push(7); s.pop(); s.push(2); s.push(9);
System.out.println(s.pop() + s.peek());
11
operation stack (bottom → top)
push 3 [3]
push 7 [3, 7]
pop [3] (7 returned)
push 2 [3, 2]
push 9 [3, 2, 9]
pop [3, 2] (9 returned)
peek (2, not removed) → 9 + 2 = 11
Writing s.top = 5; or a.balance = 5000; from outside gives a compile error:
error: balance has private access in Account
Access levels in Java: private (only the class), package access (if not specified: the same package), protected (package + subclasses), public. Access is at the class level: a Money object can read the private field of another Money object.
Immutable ADT
static final class Money {
private final int cents;
Money(int c) { cents = c; }
Money add(Money o) { return new Money(cents + o.cents); }
int cents() { return cents; }
}
Money m1 = new Money(150);
Money m2 = m1.add(new Money(75));
System.out.println(m1.cents() + " " + m2.cents());
150 225
Objects that don’t change can be shared; aliasing problems (being changed from one place and surprised from another) disappear. Java’s String is like this too.
Interface separate from representation
List<Integer> nums = new ArrayList<>(List.of(4, 1, 3));
List<Integer> view = Collections.unmodifiableList(nums);
try { view.add(5); } catch (UnsupportedOperationException e) { System.out.println("UnsupportedOperationException"); }
nums.add(5);
System.out.println(view.size());
UnsupportedOperationException
4
ADTs in other languages
| Language | Construct | Hiding | Note |
|---|---|---|---|
| Ada | package: specification + body | the private part of the spec | private type: :=, = available; limited private: not |
| C++ | class (and struct) | private, protected, public | class default private, struct public; friend grants access; constructor/destructor |
| Java | class | access modifiers | All objects on the heap, no destructor (garbage collection) |
| C# | class, struct | internal in addition to Java’s | property: an accessor with get/set that looks like a field |
| Ruby | class | methods default public, fields always hidden | attr_reader generates an accessor |
In Ada the private part of the specification package is there for the compiler to see (it needs to know the size of the object), but the user cannot access the fields there. To hide the representation completely, the type is declared as a pointer and the structure it points to is defined in the body.
In C++ the destructor is called automatically when the object’s lifetime ends and gives back the memory allocated on the heap. They run in the reverse order of creation. In Java a similar order is seen in try-with-resources:
try (Res r1 = new Res("A"); Res r2 = new Res("B")) {
System.out.print("body ");
}
open A open B body close B close A
Parameterized ADTs
You want to write a stack that does the same job whatever the element type is. A parameterized ADT (generic class) takes a type parameter:
| Language | Syntax |
|---|---|
| Ada | generic type Element is private; package Stack is ... |
| C++ | template <class T> class Stack { ... }; |
| Java 5+ | class Stack<T> { ... } (reference types only; Integer, not int) |
| C# 2005+ | class Stack<T> |
List<Integer> xs = new ArrayList<>();
xs.add("five");
error: no suitable method found for add(String)
C++ templates generate separate code for each different type (instantiation at compile time). Java generics use a single piece of code and the type information is erased after compilation (type erasure); that is why you cannot write new T().
Encapsulation for large programs
An ADT encapsulates a single type. In a system with thousands of classes, you need to group related types and subprograms, compile them separately and prevent name clashes.
- C: a header file (
.h, interface) + an implementation file (.c). The compiler does not guarantee that the two files are consistent. - C++, C#:
namespace. Qualified names likestd::coutorusing. - Java:
package. A package is both a namespace and a unit of access: members without a modifier are visible inside the package. - Ada: packages can be nested and compiled separately.
- Ruby:
module.
Common mistakes
| Wrong | Correct |
|---|---|
| ADT = any struct | The representation must be hidden |
| Hiding is only for security | The real gain: user code doesn’t change when the representation changes |
| peek() removes the element | It only reads |
| Resources are closed in the order they were opened | In reverse order |
| C++ class is default public | private; struct is public |
| In Java private = specific to the object | Specific to the class; another object of the same class can access it |
| No assignment for an Ada private type | There is; not for limited private |
List<int> is valid in Java | List<Integer>; the generic parameter is a reference type |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
State the two conditions that a user-defined type must satisfy to be an abstract data type.
Show answer
(1) Information hiding: the representation (internal structure) of the type's objects is hidden from the units that use it, and the user accesses it only through the defined operations. (2) The declaration of the type and the protocols of its operations are gathered in a single syntactic unit (class, package); other units can create variables of this type.
What is printed in Java?
IntStack s = new IntStack(); // push, pop, peek on an int array
s.push(3); s.push(7); s.pop(); s.push(2); s.push(9);
System.out.println(s.pop() + s.peek());Show answer
The stack is LIFO: [3] → [3, 7] → pop 7 → [3] → [3, 2] → [3, 2, 9]. pop() returns 9, the stack is [3, 2]. peek() is 2 (does not remove). 9 + 2 = 11.
What is printed in Java?
IntStack t = new IntStack();
for (int i = 1; i <= 4; i++) t.push(i * i);
int sum = 0;
while (!t.isEmpty()) { int v = t.pop(); if (v % 2 == 0) sum += v; }
System.out.println(sum);Show answer
1, 4, 9, 16 go into the stack. They come out in reverse order: 16, 9, 4, 1. The even ones are 16 + 4 = 20. The user code never saw the array inside; it only used push, pop, isEmpty.
What is printed? Queue2<T> wraps a LinkedList with enqueue (add at end), dequeue (remove from front) and size.
Queue2<String> q = new Queue2<>();
q.enqueue("a"); q.enqueue("b"); q.dequeue(); q.enqueue("c");
System.out.println(q.dequeue() + q.size());Show answer
The queue is FIFO: [a, b] → dequeue a → [b] → [b, c] → dequeue b, remaining [c]: size 1. The result is "b" + 1 = "b1". Queue2 is a parameterized (generic) ADT: the same code works with any element type.
Does this Java code compile? Why?
class Account {
private int balance = 100;
public int getBalance() { return balance; }
}
// in another class:
Account a = new Account();
a.balance = 5000;Show answer
A private member can be accessed only from inside the class. Changing it directly from outside is prevented: information hiding. The account balance staying valid (for example not going negative) can be guaranteed by the class's own methods. An accessor (getBalance) is used for reading.
In what order are the messages printed?
// Res(String n) prints "open n", close() prints "close n"
try (Res r1 = new Res("A"); Res r2 = new Res("B")) {
System.out.print("body ");
}Show answer
Resources are opened in the order written, and when the block ends they are closed in reverse order. This is similar to C++ destructors being called in the reverse order of constructors: the one created later may depend on the one created earlier, so it is cleaned up first.
Does this compile? Which ADT feature does it illustrate?
List<Integer> xs = new ArrayList<>();
xs.add("five");Show answer
List<Integer> is a parameterized ADT instantiated with Integer. Adding a String is caught at compile time: "no suitable method found for add(String)". Raw collections before Java 5 held Object, and the error would appear only at run time, during a cast.
In C++, what is the default access of members of a class and of a struct?
Show answer
In C++ the only difference between class and struct is the default access. Three access levels: private (only the class and friends), protected (also subclasses), public (everyone). In Java, if nothing is specified, package access (classes in the same package) applies.
In Ada, what is the difference between a private type and a limited private type declared in a package specification?
Show answer
In Ada an ADT is defined with a package: the specification package holds the interface, the body package holds the implementation. With a private type the user can use :=, = and /= but cannot access the internal structure. With limited private these are not available either; only the operations the package provides can be used. For types containing pointers, the package must determine the meaning of copying.
What is printed in Java?
static final class Money {
private final int cents;
Money(int c) { cents = c; }
Money add(Money o) { return new Money(cents + o.cents); }
int cents() { return cents; }
}
// in main:
Money m1 = new Money(150);
Money m2 = m1.add(new Money(75));
System.out.println(m1.cents() + " " + m2.cents());Show answer
Money is an immutable ADT: the field is final and private, add returns a new object and does not change m1. Note: o.cents is the private field of another Money object, but it is valid because it is accessed from inside the same class. In Java access is at the class level, not the object level.
What is printed in Java?
List<Integer> nums = new ArrayList<>(List.of(4, 1, 3));
List<Integer> view = Collections.unmodifiableList(nums);
try { view.add(5); }
catch (UnsupportedOperationException e) { System.out.println("UnsupportedOperationException"); }
nums.add(5);
System.out.println(view.size());Show answer
view is an interface that offers only read operations on the same list; modification operations throw an exception. But the data was not copied: when nums changes, view changes too (4 elements). This is exactly the separation of interface from representation.
Name the encapsulation construct for large programs: (a) in C++ and C#; (b) in Java; (c) in Ada.
Show answer
In large programs, encapsulations larger than an ADT are needed to prevent name clashes and to compile related code together. C++ namespace and using, C# namespace, Java package and import (which also gives package access), Ada packages (which can be nested). In C this job is done with header files (.h) and separately compiled .c files.
Verification: The stack/queue ADT outputs, the try-with-resources closing order, and the immutable class and unmodifiable view examples were run with the JDK (java verify/SEN2001/W10.java). The private access and generic type error examples were attempted to compile with javac and gave the expected compile errors (verify/SEN2001/w10b/W10b.java, W10c.java). The C++ and Ada examples are conceptual; their compilers are not installed.