Lecture from: 22.11.2024 | Video: Videos ETHZ
We continued with a bit of polymorphism which I added to the previous lecture note instead…
The Object
Class
In Java, every class implicitly inherits from the java.lang.Object
class. This means that Object
sits at the top of the Java class hierarchy. It acts as the ultimate superclass for all other classes, providing a set of common methods that every object in Java inherits.
Key Properties of Object
:
- No Superclass: The
Object
class itself does not inherit from any other class. - Implicit Inheritance: You don’t need to explicitly write
extends Object
in your class definitions; it’s done automatically by the compiler. - Universal Reference: A reference variable of type
Object
can hold a reference to an object of any class.
Methods of the Object
Class
The Object
class defines several important methods:
Method | Description |
---|---|
toString() | Returns a string representation of the object. Useful for debugging and printing. |
equals(Object other) | Compares the current object with another object for equality. |
getClass() | Returns the runtime class of the object. |
hashCode() | Returns a hash code value for the object. |
notify() , notifyAll() , wait() | Methods for thread synchronization. |
Using Object
Variables
Because Object
is the superclass of all classes, you can use Object
variables to store references to objects of any type. However, this comes with limitations.
Example: Object
Variables and Method Access
Explanation: You can only call methods that are defined in the Object
class directly on an Object
variable. To access methods specific to the object’s actual type (like length()
for String
or nextDouble()
for Scanner
), you need to downcast the Object
reference to the correct type after checking with instanceof
.
Example: An isNull()
Method Using Object
This method can check for null for any object type because Object
is the universal supertype.
Comparing Objects: The Right Way and the Wrong Way
Incorrect Comparison with ==
:
The ==
operator compares references (memory addresses), not the content of objects. For objects, this is rarely the desired behavior.
Correct Comparison with equals()
:
The equals()
method, inherited from Object
, is designed to compare the content or state of objects. However, the default implementation in Object
simply behaves like ==
(reference comparison). Therefore, you need to override equals()
in your classes to provide meaningful content comparison.
Overriding equals()
and toString()
Overriding equals()
for Custom Classes
The equals()
method in Object
doesn’t know how to compare the internal state of your custom objects. You must override (!!!) it to provide a proper comparison.
https://stackoverflow.com/questions/8180430/how-to-override-equals-method-in-java
Important Considerations when Overriding equals()
:
-
Signature: The signature must be
public boolean equals(Object other)
. Changing the parameter type (e.g., toRational other
) is not an override and will cause unexpected behavior. -
Type Check and Cast: Inside your overridden
equals()
, you first need to check if theother
object is of the correct type usinginstanceof
, and then downcast it to your custom type. -
Null Check: Check if
other
isnull
before attempting any operations on it. -
Comparison Logic: Implement your logic to compare the relevant fields of your objects for equality.
Example: Correct equals()
Override for Rational
Class
Common Pitfalls:
-
Incorrect Parameter Type: Declaring
equals(Rational other)
instead ofequals(Object other)
creates an overload, not an override. TheObject
version will still be used by other methods expecting theObject
version (e.g., collections). -
Missing Type Check and Cast: Attempting to access members of your custom class directly on the
Object other
parameter will result in compiler errors. You must check the type and downcast. -
Forgetting Null Check: Dereferencing a null object will throw a
NullPointerException
.
Overriding toString()
The toString()
method in Object
returns a string representing the object’s memory address, which is often not useful. Override toString()
to provide a more informative string representation of your object’s state.
By overriding toString()
, you can control how your objects are displayed when printed or used in string concatenation. Using the @Override
annotation is good practice, as it helps catch errors if you accidentally misspell the method name or use an incorrect signature.
Shadowing
Shadowing occurs when a variable declared in a subclass has the same name as a variable declared in its superclass. The subclass variable shadows or hides the superclass variable. This is different from overriding methods!
Example: Shadowing Attributes
Accessing Shadowed Attributes:
To access a shadowed attribute of the superclass, you can use an explicit cast to the superclass type:
Why Shadowing is Discouraged
Shadowing can make your code harder to read and understand because the same variable name refers to different things depending on the context. It’s generally considered bad practice and should be avoided if possible. Use distinct names for variables in subclasses to improve clarity
Attributes vs. Methods in Inheritance
It’s essential to understand the difference between how attributes and methods behave in inheritance.
Feature | Methods | Attributes |
---|---|---|
Modification | Overriding (changes behavior) | Shadowing (hides, but both exist) |
Runtime Resolution | Dynamic Binding (dynamic type decides) | Static Binding (static type decides) |
Example: Attributes vs. Methods
Key Differences Explained:
-
Method Overriding (Dynamic Binding):
b.myS()
callsB
’s version ofmyS()
even when the static type isA
. Dynamic binding ensures the correct version based on the runtime type is executed. -
Attribute Shadowing (Static Binding):
b.s
accessesB
’ss
.((A) b).s
accessesA
’ss
because the cast changes how the compiler resolves the attribute (the static type matters). No overriding occurs for attributes.
final
Keyword
The final
keyword in Java can be applied to variables, methods, and classes, with different implications in each case.
final
Variables
- Primitive Types: A
final
primitive variable’s value cannot be changed after initialization. It becomes a constant.
- Reference Types: A
final
reference variable cannot be reassigned to point to a different object. However, the state of the object it refers to can be modified.
final
Methods
A final
method cannot be overridden by subclasses. This is useful when you want to ensure that a particular method’s behavior is preserved across the inheritance hierarchy.
final
Classes
A final
class cannot be extended (subclassed). This is often used for security or performance reasons, or when a class’s design is not intended for extension. Examples are the String
or Math
classes.
Abstract Classes and Methods
Abstract Classes: An abstract class cannot be instantiated directly. It serves as a blueprint for subclasses, defining common methods and attributes but leaving some implementations incomplete (abstract). Use the abstract
keyword before the class declaration.
Abstract Methods: An abstract method has no implementation (no method body). It’s declared with the abstract
keyword and a semicolon instead of a body. Subclasses must provide concrete implementations for all abstract methods inherited from the abstract class.
Example: Abstract Class and Method
When to Use Abstract Classes:
- Common Interface: To define a common interface or contract that subclasses must adhere to.
- Partial Implementation: To provide a base implementation with some common methods while requiring subclasses to implement specific behavior.
- Preventing Direct Instantiation: When it doesn’t make sense to create instances of the base class itself.
The AbstractList
class in Java’s Collections Framework is a prime example of an abstract class. ArrayList
and LinkedList
extend AbstractList
and provide concrete implementations for the abstract methods.
Designing Inheritance Hierarchies: is-a
vs. has-a
When designing inheritance hierarchies, it’s crucial to choose the right relationships between classes. The two most common relationships are “is-a” and “has-a.”
is-a
Relationship (Inheritance)
The is-a
relationship represents inheritance (subtyping). It means that a subclass is a specialized type of its superclass. A Dog
is a Animal
, a Circle
is a Shape
. This is the core principle behind using extends
.
Liskov Substitution Principle (LSP): A fundamental principle for sound inheritance hierarchies is the Liskov Substitution Principle. It states that objects of a subclass should be able to be used wherever objects of the superclass are expected, without altering the correctness of the program.
Violating the LSP: Examples
If you violate the LSP, your inheritance hierarchy is likely flawed. Here are some examples of violations:
- Square and Rectangle: A
Square
is not a true subtype of aRectangle
in the OOP sense (mathematically, a square IS a rectangle), even though every square is a rectangle. If you have a method that operates on aRectangle
and expects to be able to change the width and height independently, aSquare
object would break this expectation because changing one dimension must also change the other. - Immutable and Mutable Collections: An immutable collection (one that cannot be changed after creation) is not a perfect subtype of a mutable collection. Methods like
add()
orremove()
don’t make sense for an immutable collection and would violate the LSP if inherited.
has-a
Relationship (Composition)
The has-a
relationship represents composition or aggregation. It indicates that a class contains or is composed of another class. A Circle
has a center point (Point2D
), a car has an engine.
Composition/Aggregation vs. Inheritance:
When modeling a “has-a” relationship, composition is usually preferred over inheritance. Instead of extending the other class, create a reference attribute of the other class’s type within your class.
Example: Composition Instead of Inheritance
Benefits of Composition:
- Flexibility: Composition offers more flexibility than inheritance because you can change the contained objects at runtime.
- Encapsulation: Composition promotes better encapsulation by hiding the internal implementation details.
- Avoids LSP Violations: Composition helps avoid the LSP problems that can arise from inappropriate inheritance relationships.
When to Choose Inheritance or Composition
Relationship | Keyword | Description |
---|---|---|
is-a | extends | Subclass is a specialized type of superclass. Must adhere to the Liskov Substitution Principle. |
has-a | (no keyword) | Class contains or is composed of another class. Model this with a reference attribute. Often preferred to inheritance when representing “has-a” relationships to avoid LSP issues. |
Examples of Inheritance and Composition Choices:
Scenario | Inheritance or Composition? | Explanation |
---|---|---|
2D and 3D points | Inheritance (Point3D extends Point2D ) | A 3D point is a 2D point with an additional z-coordinate. |
2D points and circles | Composition (Circle has a Point2D ) | A circle is not a point; it has a center point. |
Squares and rectangles | Complex, depends on use case. Usually Composition | A square is a rectangle mathematically but might violate the LSP depending on the context |
Mutable and immutable sets | Composition preferred | An immutable set has a underlying data structure but significantly restricts functionality. Inheritance would likely violate the LSP. |
Inheritance Summary and Further Concepts
Overview of Inheritance Concepts
- Inheritance (extends): Creates the “is-a” relationship, establishing subtyping.
- Attributes: Inherited, but can be shadowed (discouraged).
- Methods: Inherited and can be overridden (
@Override
) to change behavior. - Dynamic Binding: At runtime, the actual object type (dynamic type) determines which overridden method is called.
- Static Binding: At compile time, the declared type (static type) determines which members are accessible.
- Sichtbarkeit(visibility):
public
,protected
,default
, andprivate
control access to members. Visibility can be increased but not decreased when overriding. - Konstruktoren (constructors): Not inherited, but the superclass constructor must be called using
super()
. - Object Class: All classes inherit from
Object
, which providestoString()
,equals()
, and other essential methods. - final Keyword: Prevents overriding (methods), extending (classes), and modification (variables).
- abstract Keyword: Declares abstract classes (cannot be instantiated) and abstract methods (no implementation, must be overridden by subclasses).
What if You Don’t Want Certain Behaviors?
Java provides modifiers to control inheritance behavior:
- Prevent overriding a method: Declare the method
final
. - Prevent inheriting from a class: Declare the class
final
. - Prevent instantiating a class: Declare the class
abstract
or provide only private constructors.
Working with Files
Files are the primary means of storing persistent data — data that exists beyond the lifetime of a program’s execution.
Persistent Data and Files
Data stored in variables within a program is lost when the program terminates. To make data persistent, we store it in files.
Example: Processing Temperatures from a File
Consider a file containing daily temperature readings. Our goal is to write a program that calculates the difference between consecutive temperature measurements.
The file contains a series of temperatures. The program reads these temperatures and outputs the change between each consecutive pair.
Working with Files in Java: The File
Class
The java.io.File
class provides an abstraction for interacting with files and directories in Java. It’s important to note that a File
object represents a file handle (a way to refer to a file), not the actual file’s contents.
Using the File
Class
- Import:
import java.io.File;
- Create a
File
Object:File file = new File("example.txt");
This creates aFile
object, not the file itself. The file might or might not already exist at this point.
Important Note: Creating a File
object doesn’t automatically create a file on disk. It creates a representation of a potential file path.
Methods of the File
Class
The File
class offers methods to work with files and directories. Here are some key methods:
Method | Description |
---|---|
exists() | Returns true if the file/directory represented by the File object exists. |
canRead() | Returns true if the file can be read. |
getName() | Returns the name of the file/directory. |
length() | Returns the file size in bytes. |
delete() | Deletes the file/directory. |
renameTo(File dest) | Renames the file/directory. |
Example: Using File
Methods
This code snippet demonstrates using the File
class to create a file handle and then potentially deleting the file if it exists and exceeds a certain size.
Reading from Files: The Scanner
Class
We can use the familiar java.util.Scanner
class to read data from files.
Using Scanner
with Files
-
Import:
import java.util.Scanner;
-
Create
Scanner
for File Input:Alternative (One-liner):
Example: Reading an Integer from a File
Handling FileNotFoundException
Creating a Scanner
to read from a file can throw a FileNotFoundException
if the specified file doesn’t exist. We need to handle this potential exception.
What “throws FileNotFoundException” Means
The throws FileNotFoundException
clause in the Scanner(File source)
constructor documentation indicates that this constructor might throw a FileNotFoundException
. Our code must handle this possibility.
Handling Exceptions: A Preview
Some operations in Java can lead to runtime errors called exceptions. When a method encounters a situation it can’t handle (like a missing file), it throws an exception.
Two Options for Handling Exceptions:
-
Catching the Exception: Using a
try-catch
block to handle the exception directly where it might occur. (Covered in detail later) -
Declaring that the Method Can Throw: Adding a
throws
clause to the method signature to indicate that the method might throw a particular exception. This delegates the responsibility of handling the exception to the caller of the method.
Using throws
to Declare Exceptions
A throws
declaration in a method signature signifies that the method might throw the specified exception. The caller of this method must either handle the exception or propagate it further up the call stack using another throws
declaration.
Example: Reading from a File with throws
In this example, the main
method declares that it can throw a FileNotFoundException
. If the file “input.txt” is not found, the Scanner
constructor throws the exception, and the program terminates (because the main
method didn’t catch the exception). Ultimately, the operating system will catch it and terminate the program.
Why Haven’t We Seen throws
Before?
Not all input sources for Scanner
can throw exceptions. For example, Scanner(System.in)
doesn’t require a throws
declaration. Also, some exception types (like NullPointerException
or ArrayIndexOutOfBoundsException
) do not need to be explicitly declared. These are called unchecked exceptions, in contrast to checked exceptions like FileNotFoundException
. The compiler forces you to handle checked exceptions, but not unchecked exceptions.
Input Cursor and Tokens
When reading from a file using Scanner
, the input is treated as a stream of characters. A newline character (\n
) marks the end of a line but is otherwise just another character in the stream.
Scanner
breaks the input stream into tokens—sequences of characters separated by whitespace (spaces, tabs, newlines). An input cursor keeps track of the current position within the input stream.
Consuming Tokens
When you call methods like nextInt()
, nextDouble()
, or next()
, Scanner
reads the next token from the input stream:
- Whitespace Skipping: Leading whitespace is ignored.
- Token Reading: The next token (up to the next whitespace) is read and returned.
- Cursor Advancement: The input cursor moves to the end of the consumed token.
- File Unchanged: The original file remains unmodified.
Handling Scanner Exceptions
Scanner
can throw various exceptions:
-
NoSuchElementException
: Thrown when trying to read past the end of the input. This happens when you callnext...()
methods but there are no more tokens available. -
InputMismatchException
: Thrown when the next token cannot be converted to the requested type (e.g., trying to read “1.23” withnextInt()
).
Preventing Scanner Exceptions
Scanner
provides methods to check for the presence and type of the next token without actually consuming the token. These methods are crucial for preventing exceptions:
Method | Description |
---|---|
hasNext() | Returns true if there is another token in the input. |
hasNextInt() | Returns true if there is another token and it can be read as an int . |
hasNextDouble() | Returns true if there is another token and it can be read as a double . |
… | … |
The hasNext()
Method
This loop safely reads all tokens from the file, regardless of the number of tokens, because hasNext()
checks for the presence of the next token before attempting to read it.
Back to the Temperature Example
Let’s return to the temperature processing example. Recall that we want to read temperatures from a file and calculate the differences between consecutive readings.
First Attempt: A Fixed Number of Readings
Problem: This code only works if there are exactly eight temperatures in the file. It doesn’t handle files with more or fewer readings. With fewer readings, it throws a NoSuchElementException
. With more readings, it simply ignores everything after the 8th value.
Second Attempt: Using hasNext()
Problem: Still throws InputMismatchException
if there are non-numeric tokens in the file. It also throws a NoSuchElementException
if the file starts with a non-numeric token because in.nextDouble()
is called before checking if a double exists at all.
Third Attempt: Using hasNextDouble()
Problem: This works only if the file is non-empty and starts with a valid double
and contains only double
values. It throws a NoSuchElementException
if the file is empty or does not begin with a valid double
.
Fourth Attempt: Handling the First Token
Problem: Now, the code handles empty files and files that don’t start with a number. However, if the file contains non-numeric tokens after the first number, the code will still throw an InputMismatchException. It will read the first valid double
if it exists, but won’t handle mixed data types correctly within the file.
But given that we usually expect a file with numbers this should suffice as a “good” enough solution.
Continue here: 19 Files, File Output, Exceptions, Checked vs Unchecked, Throwing, Exception Handling Best Practices