1

I was playing a bit with changing String value (i know that it's extremely unsafe and dangerous) with function:

public static void reverse(String s) {
    try {
        Field val = String.class.getDeclaredField("value");
        val.setAccessible(true);
        char[] value = (char[]) val.get(s);
        char[] inverse = s.toCharArray();
        for (int i = 0; i < s.length(); i++)
            value[i] = inverse[s.length()-i-1];
    }
    catch (Exception e) {
        e.printStackTrace();
    }
}

and after while I've discovered that depending on string creation it is acting extremely unpredictable. I've created a small mind-game with that (so many prints was necessary to get wanted effect):

public static void main(String[] args) {
    final String a = "abc";
    final String b = new String("abc");
    final String c = "abcd".substring(0, 3);

    System.out.println("Let's start!");
    System.out.print("a - ");
    System.out.println(a);
    System.out.print("b - ");
    System.out.println(b);
    System.out.print("c - ");
    System.out.println(c);

    System.out.print("Are they all equals? - ");
    System.out.println(a.equals(b) && a.equals(c) && b.equals(c));

    System.out.print("But they are different objects, right? - ");
    System.out.println(!(a == b || b == c || a == c));

    System.out.println("Let's reverse only 'a'. But all are final and String is not mutable, so what can go wrong?");

    reverse(a);

    System.out.println("Done. What we've got here?");

    // trick 1
    System.out.print("a = ");
    System.out.print(a);
    System.out.println(" - ok, 'a' is reversed. A bit strange, but it works. Super method");
    System.out.print("b = ");
    System.out.print(b);
    System.out.println(" - wait... We haven't touched this");
    System.out.print("c = ");
    System.out.print(c);
    System.out.println(" - this is untouched, wierd, huh? We've just reversed 'a' so 'b' and 'c' should act the same.");

    // trick 2
    System.out.println("\nOk, so 'c' should equals \"abc\", right?\n");
    System.out.println("\"abc\".equals(c)? = "+"abc".equals(c));
    System.out.println("...\n");

    System.out.print("Do you remeber, that");
    System.out.print(" a = ");
    System.out.print(a);
    System.out.print(" oraz b = ");
    System.out.print(b);
    System.out.println(" ?\n");

    // trick 3
    System.out.println("So let's check that");
    System.out.print("a.equals(b) = ");
    System.out.println(a.equals(b)+"\n");
    System.out.println("Ok, we had expected that.\n");
    System.out.println("But what do you think the result of (\" \"+a).equals(\" \"+b) will be?\n");
    System.out.print("(\" \"+a).equals(\" \"+b) = ");
    System.out.println((" "+a).equals(" "+b)+"\n");

    System.out.print("And do you remeber, that");
    System.out.print(" a = ");
    System.out.print(a);
    System.out.print(" ,a c = ");
    System.out.print(c);
    System.out.println(" ?\n");

    // trick 4
    System.out.println("So let's check if they are different:");
    System.out.print("a.equals(c) = ");
    System.out.println(a.equals(c));
    System.out.println("So they are different... but are they really different?\n");
    System.out.print("(\" \"+a).equals(\" \"+c) = ");
    System.out.println((" "+a).equals(" "+c));
    System.out.println("Booo!!! You could choose the blue pill!\n");

    System.out.println("Our actors were: ");
    System.out.print("a = ");
    System.out.print(a);
    System.out.print(", b = ");
    System.out.print(b);
    System.out.print(", c = ");
    System.out.print(c);
    System.out.print(" oraz abc = ");
    System.out.println("abc");
    System.out.print("\n");

    // trick 5
    System.out.println("Or in other words");
    System.out.println("a = "+a+", b = "+b+", c = "+c+" oraz abc ="+(" "+"abc")+"\n");

    System.out.println("But do you remember what we were revering? Was is rally b?");
    System.out.println("Have a nice day. Z-DNA");
}

But I don't get that play. All string are different object but with the same value.

So why in trick 1 string 'c' acted differently that 'b'?

Ok, I get trick 2. The "abc" is not anymore "abc" but "cba" (but why? I have changed value of String 'a', not value of string pool) so it can't be equal "abc", but how 'c' can be "abc" when I can't even get "abc" calling "abc"??

Why in trick 3 after adding space 'a' and 'b' was not equal anymore and why on earth in 4 'a' and 'c' with spaces was equal?!?!

Trick 5 show us that the value of 'a', 'b', 'c' and "abc" is changing depend on how are we calling it. (oh wait. 'c' is special. The most irrational method of creating string is actually most immune to that black magic).

Please help me understanding what I've actually done and what sort of darkness is function reverse.

7
  • Take a look at this answer stackoverflow.com/questions/35899879/… Commented Mar 17, 2016 at 20:49
  • 1
    There are too many questions here, and I for one can't be bothered to answer them all. The answer to the first one is easy. new String("abc") creates a new instance, but it shares the same backing array as "abc". Commented Mar 17, 2016 at 20:49
  • 2
    Of course it's unpredictable. This is why. Commented Mar 17, 2016 at 21:04
  • @LouisWasserman - Ok, but it is unpredictable only if you can't understand what is going on. It is repeatable so it can be explained. Commented Mar 17, 2016 at 21:17
  • As currently presented, I don't think that this question is a good fit for StackOverflow. You've torn out the heart of a String and made half a dozen questions. RealSkeptic has done you the favour of writing half a dozen answers. Some interesting stuff, but the format and presentation makes it unlikely for this to be useful to anyone in the future. Commented Mar 17, 2016 at 22:09

1 Answer 1

2

You already know about the fact that strings are interned in the string pool. So here are a few more facts for you.

  1. This is the source of the constructor for new String("abc").

    String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    

    So this means that your a and b have the same character array behind them. When you change the backing value of a, you change b.

  2. When you change the value of an interned string, of course, the interned string gets changed.

    This really means that every occurrence of "abc" in your code is not really "abc" anymore but "cba". The code says it's "abc", but memory says differently.

  3. However, the compiler calculates constants in advance and interns them separately. This means that if you have a constant such as " " + "abc", it's compiled as " abc" - a different string in the interned pool.

  4. Long string concatenations with + are translated using StringBuilder to avoid creation of several intermediate objects that will get discarded. Each operand of the + becomes a call to append.

So, c behaved differently than b because a shares store wit b, but b doesn't share store with c - because c was derived from a different constant (and nowadays a substring creates a new backing array anyway).

Now, trick 2 returns false for c equaling "abc" because, as we said, the constant itself is not what it was - you changed it.

Trick 3 - why, when a equals b, does adding a space before them make them unequal? Well, because " " + a is a constant, and gets interned in advance as " abc", whereas " " + b is not a constant known at compile time, therefore it gets calculated at runtime. Easy to check if you add

System.out.println( " " + a == " abc" );

This prints true - they are the same string, and this can happen only if " " + a was interened in advance based on the compiler's belief in the immutability of strings and the finality of finals.

So, " " + a is now certain to be " abc". So no wonder it is equal to " " + c. Although c is not a pre-interned constant, it is still "abc" and the concatenation still produces the same result.

Finally, the expression which you printed with different prints still took "abc" alone, so it prints it as "cba" which is its new value. But when you printed it in one big print, some of it is a compiler-time constant expression - specifically, the part in the parentheses:

System.out.println("a = "+a+", b = "+b+", c = "+c+" oraz abc ="+(" "+"abc")+"\n");

gets interned at compile time as " abc" - and you already know that's a separate constant.

Java translates a concatenation of strings into a StringBuilder with several appends. That expression is equivalent to:

StringBuilder sb = new StringBuilder();
sb.append( "a = abc b=" )
  .append( b )
  .append( ", c = " )
  .append( c )
  .append( " oraz abc =" )
  .append( " abc" )
  .append( "\n" );
System.out.println( sb.toString() );

Now, there are two groups of constants that were pre-joined, and one of them is the " " + "abc" that you put in parentheses.

If you remove the parentheses, the space and the "abc" get appended separately, and then "abc" displays as "cba".

You can see this if you use

javap -p -v <class file>
Sign up to request clarification or add additional context in comments.

2 Comments

That is GENIUS!!! Wow. For me you're java master. One, last question - why on last line oraz abc ="+(" "+"abc") when we delete parentheses it prints again abc = cba? I was needed to put that into () to print it as abc. Thanks for your help.
@Z-DNA I changed the end of my answer a little bit as it was not entirely accurate - only the beginning of the concatenation gets interned together. The rest are appended using a StringBuilder.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.