Lecture from: 08.11.2024 | Video: Videos ETHZ
Enums (Enumerated Types)
Enums are special classes used to represent a fixed set of named values. They are useful when you need a type that can only hold a specific set of constants.
Motivation
Using basic types (like int
or String
) to represent a fixed set of values has drawbacks:
- Large Value Range:
int
andString
allow values that might not be meaningful in your context (e.g., a negative order status). - Invalid Operations: You can perform operations on
int
andString
that don’t make sense for your enumerated type (e.g., adding two order statuses).
Defining Enums
Enum Benefits
- Type Safety: The compiler ensures that variables of the enum type can only hold one of the defined enum constants.
- Readability: Using named constants improves code readability.
- Built-in Functionality: Enums provide useful methods like
name()
,valueOf()
, andordinal()
. They also integrate well withswitch
statements.
Example: Order Status
Extending Enums (Beyond EProg Scope)
You can add attributes, methods, and constructors to enums to extend their functionality. However, this is beyond the scope of this introductory EProg course.
We’ve covered the essential elements for working with classes and objects in Java, providing a solid foundation for object-oriented programming. Remember that encapsulation and well-defined interfaces are critical for building robust and maintainable software systems.
Code Style, Conventions, and Refactoring
This lecture focuses on improving code quality through consistent style, adherence to conventions, and refactoring. These practices are essential for writing readable, maintainable, and extensible code, especially in collaborative software development projects.
High-Quality Software: Motivation and Goals
High-quality software possesses a range of desirable characteristics, including:
- Correctness: The software functions as intended and meets its specifications.
- Robustness: The software handles unexpected inputs and errors gracefully.
- Maintainability: The software is easy to understand, modify, and fix.
- Extensibility: New features can be added easily without breaking existing functionality.
- Portability: The software can run on different platforms or environments.
- Testability: The software can be easily tested to ensure its correctness.
- Scalability: The software can handle increasing workloads or data volumes.
- Cost-Effectiveness: The software is developed and maintained efficiently.
- Readability/Understandability: The source code is easy to read and comprehend.
- Performance: The software executes efficiently.
- User-Friendliness: The software is easy for users to interact with.
- Reusability: Parts of the software can be reused in other projects.
- Backward Compatibility: The software maintains compatibility with older versions.
- Marketability: The software is appealing to potential users or customers.
These properties often overlap (e.g., reusability and testability) and sometimes conflict (e.g., cost-effectiveness vs. robustness). Achieving high-quality software requires a combination of technical expertise and good software engineering practices, including careful attention to code style, conventions, and refactoring.
Code Style and Conventions: Goals and Enforcement
Code style and conventions are primarily concerned with the syntactic aspects of code, such as formatting, layout, and naming. While these aspects don’t affect the program’s execution (the compiler doesn’t care), they significantly impact readability and maintainability, which are crucial for human developers.
Goals of Consistent Code Style:
- Uniformity: Creates a consistent look and feel across the codebase.
- Improved Readability: Makes it easier to understand code written by different people.
- Facilitates Collaboration: Reduces friction and misunderstandings in team projects.
- Automated Checks: Enables automated style checking and enforcement.
Style Guides and Enforcement
Code style is typically defined and enforced through project- or company-specific style guides. These guides provide a set of rules and recommendations for formatting, naming, and other stylistic aspects of the code. Examples include style guides from Sun/Oracle, OpenJDK, and Google. While conventions offer flexibility and exceptions can be made if justified, consistency is paramount.
Tools for Automatic Formatting and Style Checking
IDEs like Eclipse and other tools (linters, style checkers) can automate formatting and style checking. These tools can:
- Apply Formatting Rules: Automatically reformat code to adhere to a specific style guide.
- Identify Style Violations: Detect and report deviations from the style guide.
- Integrate with Version Control: Enforce style checks as part of the code review or commit process (e.g., using Git/GitLab).
Although these tools are valuable, developers should not rely on them blindly. Sometimes, deviations from the strict rules of a style guide are justified for improved clarity or other reasons.
Naming Conventions
Consistent naming is fundamental for readable code. Java follows established naming conventions that enhance clarity and reduce ambiguity.
- General Principle: Names should be self-explanatory but concise. While extreme brevity (single-letter variables) is sometimes acceptable (e.g., loop counters), names should generally convey the purpose or meaning of the element they represent.
Specific Conventions
-
Variables:
- Start with a lowercase letter (e.g.,
currentLocation
). - Use camel case (e.g.,
numberOfStudents
). - Constants (declared
final
) are typically written in uppercase with underscores separating words (e.g.,MAX_VALUE
,PI
). - Avoid redundant type information in names (e.g., prefer
names
overnamesArray
).
- Start with a lowercase letter (e.g.,
-
Methods:
- Start with a lowercase letter (e.g.,
calculateArea()
). - Use camel case (e.g.,
getFirstName()
). - Typically use verbs or verb phrases to indicate the action performed by the method.
- Start with a lowercase letter (e.g.,
-
Classes:
- Start with an uppercase letter (e.g.,
Scanner
). - Use camel case (e.g.,
RandomNumberGenerator
). - Typically use nouns or noun phrases to describe the concept represented by the class.
- Start with an uppercase letter (e.g.,
-
Packages: (More relevant in larger projects.)
- Use all lowercase letters (e.g.,
java.util
). - Use dots to separate levels of nesting (e.g.,
com.example.project
).
- Use all lowercase letters (e.g.,
Formatting and Layout
Consistent formatting and layout are essential for readability. Here are key aspects of good code formatting in Java:
-
Line Length: Limit line length to a reasonable maximum (typically 80, 100, or 120 characters). Long lines should be broken up for clarity. This makes it easier to read code without horizontal scrolling.
-
Indentation: Consistently indent code blocks within curly braces
{}
. The standard practice is to use spaces (typically 2 or 4) for indentation. Consistent indentation clearly shows the structure and nesting of the code. -
Braces: There are different styles for placing curly braces, but consistency is key. A common practice is to always use braces even for single-statement blocks. This helps prevent errors when adding or removing code later.
-
One Statement per Line: Avoid placing multiple statements on the same line. Each statement should have its own line for readability.
-
Spacing:
- Use spaces around operators (e.g.,
x = 1 + 2;
). - Do not use spaces in method calls or array indexing (e.g.,
myMethod(arg1, arg2)
,data[i]
). - Use spaces before and after keywords and parentheses (e.g.,
for (int i = 0; i < n; i++)
,if (condition) { ... }
).
- Use spaces around operators (e.g.,
-
Blank Lines: Use blank lines to separate logical units of code, such as methods, classes, or blocks of code within a method. This improves visual organization and makes the code easier to scan.
What is Refactoring?
Refactoring is the process of restructuring existing code without changing its external behavior. It’s like reorganizing the internal structure of a house without altering its external appearance. The goal of refactoring is to improve the internal quality of the code, making it:
- More readable
- Easier to understand
- Easier to maintain
- Easier to extend
- Potentially more performant (although this is often a secondary goal)
Key Principles of Refactoring
- Behavior Preservation: The refactored code must have the same external behavior as the original code. This is crucial. Refactoring is about improving the structure, not changing the functionality.
- Small Steps: Refactoring is best done in a series of small, incremental changes. Each small change is easier to test and less likely to introduce errors.
- Testing: Thorough testing is essential throughout the refactoring process. After each small change, test the code to ensure that its behavior has not changed.
Common Refactoring Techniques
The following examples illustrate some typical refactoring techniques in Java. These are just a few examples, and many other techniques exist.
1. Extracting Methods
If a method is too long or performs multiple distinct tasks, break it down into smaller, more focused methods. This improves readability and makes the code easier to reuse.
Example: (Simplified – often involves more complex logic)
Before:
After: (Using a hypothetical sum
method – perhaps in a MathUtil
class)
2. Simplifying Conditional Logic
Complex nested if-else
statements can often be simplified.
Example:
Before:
After: (Using De Morgan’s law and combining conditions)
3. Replacing Conditional Logic with the Ternary Operator
For simple if-else
structures, the ternary operator can make the code more concise.
Example:
Before:
After:
4. Identifying and Removing Redundant Code
Duplicate code should be eliminated by extracting it into a common method or function. This reduces code size and makes maintenance easier. (See example 1 above).
Recognizing Refactoring Opportunities
Identifying opportunities for refactoring often requires experience and a critical eye. Some indicators that code might benefit from refactoring include:
- Long Methods: Methods that are too long or do too much.
- Duplicated Code: The same or very similar code appearing in multiple places.
- Complex Conditional Logic: Nested
if-else
structures that are hard to understand. - Large Classes: Classes that have too many responsibilities.
- Poorly Named Variables or Methods: Names that are unclear or misleading.
Refactoring and Tools
Some IDEs provide tools to assist with refactoring. These tools can automate common refactoring tasks, such as extracting methods or renaming variables, while ensuring that the code’s behavior is preserved. However, the most challenging part is not executing the mechanical steps of refactoring—it’s recognizing the opportunity and the need for refactoring.
Code Conventions: Key Takeaways
- Consistency is Key: The most important aspect of code conventions is consistency. Choose a style guide and stick to it throughout your project.
- Team Agreement: In team projects, agree on a common style guide early on and ensure everyone follows it.
- Tool Support: Use IDEs and automated tools to enforce style and formatting rules. But don’t rely on them blindly; sometimes, thoughtful deviations are justified.
- Subjectivity: Remember that some aspects of style are subjective. The goal is to write clear, readable, and maintainable code, not to achieve perfect adherence to every rule in a style guide.
Refactoring: Key Takeaways
- Small, Incremental Changes: Refactor in small steps, testing thoroughly after each change.
- Behavior Preservation: Ensure that the refactored code behaves identically to the original code.
- Practice and Experience: Recognizing refactoring opportunities and applying appropriate techniques effectively takes practice and experience. Regularly review your code critically and look for ways to improve its structure and clarity.
- Tool Support: IDEs offer refactoring tools that can automate many common tasks, making the process safer and more efficient.
Software Engineering Principles
The ideas of code style and refactoring are rooted in broader software engineering principles. Here are some key concepts (some mentioned earlier):
- Separation of Concerns: Divide your code into smaller, more manageable units, each responsible for a specific task or aspect of the functionality. This applies to both methods and classes.
- Don’t Repeat Yourself (DRY): Avoid code duplication. Extract common code into reusable functions or methods.
- Keep It Simple, Stupid (KISS): Strive for simplicity in your code. Avoid unnecessary complexity.
- YAGNI (You Aren’t Gonna Need It): Don’t implement features or functionality until they are actually needed. Avoid premature optimization or over-engineering.
- SOLID Principles: (More advanced – you’ll likely encounter these later in your studies.) A set of five design principles intended to make software designs more understandable, flexible, and maintainable.
Further Reading
For those interested in exploring these topics further, here are some highly recommended books (although they go beyond the scope of this introductory course):
- Clean Code by Robert C. Martin
- The Pragmatic Programmer by Andrew Hunt and David Thomas
- Refactoring: Improving the Design of Existing Code by Martin Fowler
- Tidy First? A Personal Exercise in Empirical Software Design by Kent Beck
- Code Complete by Steve McConnell
- Head First Design Patterns by Eric Freeman and Elisabeth Freeman
By applying the principles and techniques discussed in this lecture, you can write significantly better, more maintainable, and more professional code. While good style and refactoring might seem like extra effort initially, they pay off significantly in the long run by reducing development time, improving collaboration, and making your code more robust and adaptable to future changes.
Recursive Classes: Linked Lists
This lecture introduces recursive classes—classes that contain attributes of their own type.
Recursion Review
Before diving into recursive classes, let’s briefly review the concept of recursion in different contexts:
-
EBNF: In EBNF, recursion is used to define grammars. A non-terminal symbol can be defined in terms of itself.
<number> ::= (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9) [<number>]
-
Mathematics: Recursion is used to define functions or sequences. A function can be defined in terms of its value at a smaller input.
n! = 1, if n ≤ 1 n! = n * (n-1)!, otherwise
-
Java Methods: Recursive methods call themselves within their own definition.
Recursive Classes in Java
A recursive class is a class that contains one or more attributes (fields) whose type is the class itself. This self-referential structure allows us to create data structures like linked lists and trees.
Example:
Linked Lists: Motivation
Arrays have a limitation: their size is fixed at creation time. Inserting an element into an array often involves creating a new, larger array and copying all the elements, which can be inefficient. Linked lists provide a more flexible alternative, allowing us to add or remove elements at any position without resizing the entire structure.
Linked Lists: Structure
A linked list consists of a sequence of nodes. Each node stores:
- A value (the data element).
- A reference (
next
) to the next node in the sequence.
The last node’s next
reference points to null
, indicating the end of the list. The list itself is represented by a reference to the first node (often called the head). This next
reference is what makes the ListNode
class recursive.
Java Representation:
Example: (Conceptual Representation – we’ll see proper construction later)
list -> [42 | next] -> [-3 | next] -> [17 | null]
In this example:
list
is a reference to the first node (head) of the linked list.- The first node stores the value 42 and a reference to the next node.
- The second node stores -3 and a reference to the third node.
- The third node stores 17 and its
next
field isnull
, marking the end.
This linked list structure allows efficient insertion and deletion of elements because we only need to change a few references, without moving or copying large amounts of data.
Building Linked Lists: Node Construction and Linking
We’ll use the ListNode
class from above and add constructors to facilitate creating and connecting nodes.
Example: Creating a Linked List
This code creates the same linked list as shown in the conceptual representation as above.
Manipulating Linked Lists: Adding Elements
1. Adding at the End
To add an element at the end of the list, we need to traverse the list to find the last node and then update its next
reference.
Example: (Assume list
points to the first node of a list containing 10 and 20)
2. Adding at the Beginning
Adding an element at the beginning is simpler. We create a new node and set its next
reference to the current head of the list. The new node becomes the new head.
Example: (Again, assume list
points to a list containing 10 and 20)
3. Inserting in the Middle (or at a specific index)
Example: Suppose you have list1
: 10 → 20 and list2
: 30 → 40. You want to insert the node 30 (the head of list2
) after the 10 in list1
.
These examples demonstrate basic linked list manipulation.
Continue here: 15 Linked Lists, Inner Classes, Methods, Recursive Methods, Built-in Lists