Lecture from: 04.10.2024 | Video: Videos ETHZ

Functions

Think of functions as self-contained units of code designed to perform specific tasks. They’re like mini-programs within your larger program, encapsulating logic and making your code more organized, readable, and reusable.

Why Use Functions?

  • Modularity: Break down complex problems into smaller, manageable chunks, improving clarity and reducing cognitive load.
  • Reusability: Write code once and use it multiple times throughout your program, saving time and effort.
  • Readability: Well-named functions clearly convey their purpose, making your code easier to understand and collaborate on.
  • Abstraction: Hide implementation details behind a function interface, allowing you to focus on the “what” rather than the “how.”

Function Anatomy

public static int calculateArea(int length, int width) {
    return length * width;
}

Let’s dissect this example:

  • public: Makes the function accessible from any class in your project.
  • static: Belongs to the class itself, not a specific instance of the class. You call static functions using the class name.
  • int: Specifies the return type – the data type of the value the function will produce.
  • calculateArea: The function’s name, chosen to clearly describe its purpose.
  • (int length, int width): The parameter list – the input values the function expects. Each parameter has a name (length, width) and a data type (int).
  • return length * width;: The function body – the code that performs the calculation and returns the result.

Calling Functions

int rectangleArea = calculateArea(5, 10); // Call the function, passing values for length and width
System.out.println("The area of the rectangle is: " + rectangleArea); // Output: The area of the rectangle is: 50

Types and Parameters

Java uses value semantics for primitive types (int, double, boolean, etc.). When passing a primitive value to a function, a copy is made. Modifications inside the function only affect that copy; the original variable remains unchanged.

However, for non-primitive types (objects), Java uses reference semantics. A reference (memory address) to the object is passed, and any changes within the function directly modify the original object.

More on this later: Understanding How Java Passes Data Value Semantics vs. Reference Semantics

Important Note: Casting between primitive types can be done implicitly in some cases, but remember that casting from a wider type (e.g., double) to a narrower type (e.g., int) may result in errors or data loss due to truncation.

Advanced Concepts

FYI: Not part of this lecture yet…

  • Variable Arguments (Varargs): Allow a function to accept a variable number of arguments of a specific type. This is denoted by adding an ellipsis (…) after the parameter list in the function definition.
public static void printNumbers(int... numbers) { // Can accept any number of integers
    for (int num : numbers) {
        System.out.print(num + " ");
    }
    System.out.println();
}
 
printNumbers(1, 2, 3); // Output: 1 2 3
  • Keyword Arguments: Allow you to specify arguments by name, regardless of their order.
public static void greet(String name, String greeting = "Hello") {
    System.out.println(greeting + ", " + name + "!");
}
 
greet(name = "Alice", greeting = "Good morning"); // Output: Good morning, Alice!

Understanding How Java Passes Data: Value Semantics vs. Reference Semantics

FYI: Not part of this lecture yet… - at least not in this detail

In object-oriented programming, functions (or methods) are reusable blocks of code that work with data. A crucial concept is how this data travels between the part of your program calling a function and the function itself.

Java uses two distinct mechanisms for passing data: value semantics and reference semantics.

Value Semantics: Copying the Essentials

Primitive data types in Java, like integers (int), decimals (double), true/false values (boolean), and characters (char), are handled using value semantics. When you pass a primitive value to a function, Java doesn’t simply send a pointer; it creates a complete copy of that value.

Think of it like making photocopies: each function receives its own independent copy, unaffected by changes made in other copies.

Example:

public class ValueSemanticsDemo {
    public static void main(String[] args) {
        int number = 10;
        modifyNumber(number);
        System.out.println("Original value: " + number); // Output: 10 (unchanged!)
    }
 
    public static void modifyNumber(int num) {
        num = 20;  // Change the *copy* inside the function
        System.out.println("Modified value in function: " + num); // Output: 20
    }
}

Key takeaway: Any modifications made to num within the modifyNumber function only affect that specific copy. The original number variable remains untouched at 10.

Reference Semantics: Sharing the Address

Non-primitive data types, like objects (created using classes), behave differently. Java passes a reference to these objects—essentially a memory address pointing to their location. When you modify an object through this reference inside a function, the changes are reflected in the original object outside the function.

Example:

public class ReferenceSemanticsDemo {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        changeName(person);
        System.out.println("Updated name: " + person.getName()); // Output: Bob
    }
 
    public static void changeName(Person p) {
        p.setName("Bob"); // Modifying the *original* Person object
    }
}
 
class Person {
    private String name;
    private int age;
 
    // ... (Getters and setters for name and age)
}

Key takeaway: The changeName function directly manipulates the Person object referenced by person. This means that calling changeName permanently alters the name of the person instance in the main method.

Why Java Does It This Way

Java’s approach to data passing helps maintain program integrity and predictability:

  • Value Semantics (Primitives): Protects data from accidental modification within functions. Changes made are isolated to copies, preserving original values.
  • Reference Semantics (Objects): Allows for efficient sharing and modification of complex data structures. Functions can directly work on objects, simplifying code and promoting object-oriented principles.

Variable Scope: Where Your Data Resides

In programming, scope defines the accessibility of variables within different parts of your code. Think of it as layers of visibility – some variables are only visible within a specific function or block, while others have a wider reach.

Types of Scope

  • Local Scope: Variables declared inside a function or block (e.g., within an if statement or loop) are local to that function or block. They can only be accessed from within that specific region.
public static void myFunction() {
  int x = 5; // 'x' is local to 'myFunction'
  System.out.println(x); // Output: 5
}
 
// Attempting to access 'x' outside the function will result in an error
System.out.println(x); // Error: 'x' is not accessible here
  • Instance Scope: Variables declared inside a class but outside any function are instance variables. They belong to each individual object of that class and can be accessed by all functions within the class.
class MyClass {
  String name; // 'name' is an instance variable
 
  public void printName() {
    System.out.println(name); // Accessing the instance variable
  }
}
  • Static Scope: Static variables are declared using the static keyword and belong to the class itself, not individual instances. They can be accessed directly using the class name (e.g., MyClass.myStaticVariable) and are shared among all objects of that class.
class MyClass {
  static int counter = 0; // 'counter' is a static variable
 
  public void incrementCounter() {
    counter++;
  }
 
  public static void printCounter() {
    System.out.println("Counter value: " + counter); // Accessing the static variable
  }
}
  • Block Scope (Introduced in Java 14): Variables declared within a block of code (e.g., inside an if statement, for loop, or switch statement) have block scope and are only accessible within that specific block. A block is denoted by { //... }

Scope is crucial for:

  • Preventing Name Collisions: Different parts of your code can use variables with the same name without interfering with each other if they’re in different scopes.
  • Data Encapsulation: Local variables keep data localized, reducing unintended modifications and improving code organization.
  • Controlling Visibility: You can deliberately choose which variables are accessible from where to enforce security and maintain code clarity.

Continue here: 06 Java (Functions, Loops, Side Effects, Do-While)