Test t1 = new Test();
t1.i = 1
can be represented as
+-------+
| Test |
t1 -------> +-------+
| i (1) |
+-------+
now when you do
Test t0 = t1;
You are assigning to t0 same value as value from t1, which means that now they hold same value (address to same Test instance), so your situation is
+-------+
t1 ----+ | Test |
+--> +-------+
t0 ----+ | i (1) |
+-------+
Now after this code
Test t2 = new Test();
t2.i = 2;
you will have
+-------+
t1 ----+ | Test |
+--> +-------+
t0 ----+ | i (1) |
+-------+
+-------+
| Test |
t2 -------> +-------+
| i (2) |
+-------+
and when you do
t0 = t2;
you change your situation to
+-------+
t1 ----+ | Test |
+--> +-------+
| i (1) |
t0 ----+ +-------+
|
| +-------+
| | Test |
t2 ----+--> +-------+
| i (2) |
+-------+
so as you see now t0 holds same object as object held by t2 reference, but t1 reference wasn't changed, and that is why
System.out.println(t1.i)
prints 1.
=operator likereference = otherInstance, so if you want tot1to contain instance fromt0your only choice ist1 = t0.