Accessors, Mutators, and the Immutable String Class

Accessors and Mutators

  • Accessors:

    • Any method that does not alter the object's member variables or, by extension, the state of the object. The state of the object remains exactly the same before and after the method call.

    • Simplest form: A getter method. Getters simply return the value of a member variable without changing it.

    • Example from Student Class: The canGraduate method is an accessor. It reads values like GPA and graduation year but does not modify id, gpa, graduationYear, or startDate.

    • Used for accessing information an object holds without changing it.

  • Mutators:

    • Any method that does alter the object's member variables or, by extension, the state of the object. The state of the object after the method call is different from what it was before.

    • Simplest form: A setter method (e.g., setID, setGPA). These methods change the value of one or more member variables.

    • Used for changing the data or state of an object.

Immutable Classes

  • A class is referred to as immutable if it contains absolutely no mutator methods.

  • This means that once an object of an immutable class is created, its internal data (state) cannot be altered or changed. It is "untouchable" after creation.

  • You can access the data, but you cannot change it.

  • The concept of immutability is common in Java, and understanding it provides significant context when working with various classes and documentation.

The String Class: A Deeper Look

  • Non-Primitive Type: String is not one of Java's eight primitive data types (e.g., int, boolean, char). Therefore, String is a non-primitive type, implying it has an associated Java class.

  • Storing Addresses: A String variable (e.g., String s;) stores an address (pointer) to a String object, not the characters themselves. This is similar to how array variables work.

  • String Object Structure: A String object, residing in the heap, conceptually contains an array of char values representing the string's text.

    • Each char typically consumes 22 bytes of memory. For example, "hello world" (11 characters: h, e, l, l, o, (space), w, o, r, l, d\text{h, e, l, l, o, (space), w, o, r, l, d}) consumes 11×2=2211 \times 2 = 22 bytes of memory just for its characters.

    • Strings can be memory-intensive, leading to Java's optimization strategies.

Creating String Objects

There are two primary ways to create String objects, with differing implications for how they are stored in memory:

  1. Using the new operator:

    • Example: String s = new String("hello world");

    • The new operator instructs the Java Virtual Machine (JVM) to allocate a new, unused chunk of memory in the heap (outside the string pool) for a String object.

    • A String constructor is called to initialize this new object with the provided characters.

    • The address of this newly created object is then stored in the String variable s.

  2. Assigning a String literal:

    • Example: String s = "hello world";

    • A string literal is a hard-coded, provided text value enclosed in double quotes.

    • Purpose: Java employs a special memory region within the heap called the string pool to optimize storage of string literals.

    • Algorithm for Literals:

      1. Check the pool: When a string literal is assigned, Java first checks if an object with that exact same sequence of characters already exists in the string pool.

      2. If found: Java returns the address of the existing matching string object from the pool. No new object is created.

      3. If not found: Java creates a new string object in the string pool, stores the literal's characters there, and returns the address of this new object.

The == Operator vs. .equals() Method for Strings

  • == Operator Behavior:

    • The == operator always compares the values stored in the variables.

    • For primitive types (e.g., int a = 5; int b = 5;), a == b compares the actual numerical values and returns true if they are the same.

    • For non-primitive (reference) types like String, the variables store addresses. Therefore, == compares whether the addresses stored in the variables are identical (i.e., whether both variables point to the exact same object in memory).

    • It does not compare the content (characters) of the strings.

  • Demonstration Scenarios:

    String s1 = "hello world";
    String s2 = "hello world";
    System.out.println(s1 == s2);
    
    • Execution:

      1. s1 = "hello world"; (literal): Java checks the string pool. "hello world" is not present, so a new object is created in the pool at address 1111111111111111. s1 stores 1111111111111111.

      2. s2 = "hello world"; (literal): Java checks the string pool. "hello world" is present at address 1111111111111111. Java returns this existing address.

      3. s2 stores 1111111111111111.

    • Result: true (because s1 and s2 both store the address 1111111111111111, pointing to the same object).

    String s1 = "hello world";
    String s2 = new String("hello world");
    System.out.println(s1 == s2);
    
    • Execution:

      1. s1 = "hello world"; (literal): A new object is created in the string pool at address 1111111111111111. s1 stores 1111111111111111.

      2. s2 = new String("hello world"); (using new): Java bypasses the string pool. A new object is created in the general heap (outside the pool) at a different address, say 2222222222222222. s2 stores 2222222222222222.

    • Result: false (because s1 stores 1111111111111111 and s2 stores 2222222222222222, which are different addresses).

    • java String s1 = new String("hello world"); String s2 = new String("hello world"); System.out.println(s1 == s2);

      • Execution:

        1. s1 = new String("hello world"); (using new): A new object is created in the general heap at address 2222222222222222. s1 stores 2222222222222222.

        2. s2 = new String("hello world"); (using new): Another new object is created in the general heap at a different address, say 3333333333333333. s2 stores 3333333333333333.

      • Result: false (because s1 stores 2222222222222222 and s2 stores 3333333333333333, which are different addresses, even though their contents are identical).

  • .equals() Method:

    • To compare the actual sequence of characters (content) of two String objects, use the .equals() method: s1.equals(s2).

    • This method performs a character-by-character comparison of the string's content.

String Immutability and its Importance

  • The String class is an excellent example of an immutable class in Java.

  • This design choice means that once a String object is created, its sequence of characters cannot be changed.

  • Why is this important? If multiple String variables point to the same String object, and that object were mutable, changing the string's content via one variable would inadvertently affect all other variables pointing to it. Immutability prevents this issue.

  • Immutable Methods (Accessors): All methods in the String class are accessors; there are no mutators.

    • For example, concat() and replace() methods might seem to modify strings.

    • However, they do not change the original String object they are called on. Instead, they return a new String object containing the concatenated or replaced characters. The original string remains unaltered.

    • This ensures data integrity and predictable behavior when working with strings. There are no methods in the String API that allow you to change the text content of an existing String object.

  • String is one of many immutable classes provided by Java, emphasizing its reliability and safety against unintended modifications.