送大家以下 java 学习材料,文末有支付形式
String 不可变吗?
`public class App {`
`public static void main(String[] args) {`
`String a = "111";`
`a = "222";`
`System.out.println(a);`
`}`
`}`
有的人会认为下面这段代码应该输入:111
这样才和下面的不变性吻合。
哈哈哈,然而并不是这样滴。
222
这不对呀,不是不变吗?怎么变了呢?
其实在 JVM 的运行中,会独自给一块地分给 String。
下面的:
`Stirng a="111";`
咱们晓得字符串的调配和其余对象调配一样,是须要耗费昂扬的工夫和空间的,而且字符串咱们应用的十分多。JVM 为了进步性能和缩小内存的开销,在实例化字符串的时候进行了一些优化:
应用字符串常量池。每当咱们创立字符串常量时,JVM 会首先查看字符串常量池,如果该字符串曾经存在常量池中,那么就间接返回常量池中的实例援用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。因为 String 字符串的不可变性咱们能够非常必定常量池中肯定不存在两个雷同的字符串。
这里先去 JVM 给常量池里找,找到了就不必创建对象了,间接把对象的援用地址赋给 a。找不到会从新创立一个对象,而后把对象的援用地址赋给 a。同理 a =”222″; 也是先找,找不到就从新创立一个对象,而后把对象的援用地址赋给 a。
大家有没有发现我下面的形容中“援用地址”。比如说 Object obj = new Object(); 很多人喜爱成 obj 为对象,其实 obj 不是对象,他只是一个变量,而后这个变量里保留一个 Object 对象的援用地址罢了。
援用类型申明的变量是指该变量在内存中理论存储的是一个援用地址,实体在堆中。
所以网上很多文章老喜爱这么说
`User user = new User()`
创立了一个 user 对象,老喜爱把 user 称之为对象。这里不承受反驳。
所以下面 String a =“111”;表白的是变量 a 里保留了“111”这个对象的援用地址。变量是能够变的,不能变的是“111”。
String 为什么是不可变的?
简略的来说,String 类中应用 final 关键字字符数组保留字符串。代码如下:
`public final class String implements java.io.Serializable, Comparable<String>, CharSequence {`
`private final char value[];`
`public String() {`
`this.value = "".value;`
`}`
`public String(String original) {`
`this.value = original.value;`
`this.hash = original.hash;`
`}`
`public String(char value[]) {`
`this.value = Arrays.copyOf(value, value.length);`
`}`
`}`
从下面的这段源码中能够看出三点:
String 类是 final 润饰
String 存储内容应用的是 char 数组
char 数组是 final 润饰
这里就得温习一下,final 有啥用?
当用 final 润饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就能够用 final 进行润饰。final 类中的成员变量能够依据须要设为 final,然而要留神 final 类中的所有成员办法都会被隐式地指定为 final 办法。
当 final 润饰的办法示意此办法曾经是“最初的、最终的”含意,亦即此办法不能被重写(能够重载多个 final 润饰的办法)。此处须要留神的一点是:因为重写的前提是子类能够从父类中继承此办法,如果父类中 final 润饰的办法同时访问控制权限为 private,将会导致子类中不能间接继承到此办法,因而,此时能够在子类中定义雷同的办法名和参数,此时不再产生重写与 final 的矛盾,而是在子类中从新定义了新的办法。(注:类的 private 办法会隐式地被指定为 final 办法。)
当 final 润饰一个根本数据类型时,示意该根本数据类型的值一旦在初始化后便不能发生变化。如果 final 润饰一个援用类型时,则在对其初始化之后便不能再让其指向其余对象了,但该援用所指向的对象的内容是能够发生变化的。实质上是一回事,因为援用的值是一个地址,final 要求值,即地址的值不发生变化。另外 final 润饰一个成员变量(属性),必须要显示初始化。这里有两种初始化形式,(1. 在申明的时候给其赋值,否则必须在其类的所有构造方法中都要为其赋值)
比方:
`/**`
`* Description: final 润饰变量 `
`* @author : 田维常 `
`*/`
`public class FinalDemo {`
`private final String name;`
`public FinalDemo(String name) {`
`this.name = name;`
`}`
`public FinalDemo() {`
`}`
`}`
这是会会报错
img
对于 final 就简略说到这里
上面来看一个应用 String 的 案例
`/**`
`* Description:`
`*`
`* @author : 田维常 `
`* @date : 2020/11/3`
`*/`
`public class StringDemo {`
`public static void main(String[] args) {`
`String name = "老田";`
`name.concat("!");`
`System.out.println(name);`
`System.out.println(name.concat("!"));`
`}`
`}`
输入
img
顺道溜达溜达 String 中几个罕用办法源码
`public String concat(String str) {`
`int otherLen = str.length();`
`if (otherLen == 0) {`
`// 啥都没有,就间接把以后字符串给你 `
`return this;`
`}`
`int len = value.length;`
`char buf[] = Arrays.copyOf(value, len + otherLen);`
`str.getChars(buf, len);`
`// 看到了吗?返回的竟然是新的 String 对象 `
`return new String(buf, true);`
`}`
`void getChars(char dst[], int dstBegin) {`
`System.arraycopy(value, 0, dst, dstBegin, value.length);`
`}`
`public String replace(char oldChar, char newChar) {`
`// 如果两个是一样的,那就必要替换了,所以返回 this`
`if (oldChar != newChar) {`
`int len = value.length;`
`int i = -1;`
`// 把以后的 char 数组复制给 val,而后上面基于 val 来操作 `
`char[] val = value;`
`while (++i < len) {`
`if (val[i] == oldChar) {`
`break;`
`}`
`}`
`if (i < len) {`
`// 创立一个新的 char 数组 `
`char buf[] = new char[len];`
`for (int j = 0; j < i; j++) {`
`buf[j] = val[j];`
`}`
`while (i < len) {`
`char c = val[i];`
`buf[i] = (c == oldChar) ? newChar : c;`
`i++;`
`}`
`// 创立一个新的 String 对象 `
`return new String(buf, true);`
`}`
`}`
`return this;`
`}`
`public String substring(int beginIndex, int endIndex) {`
`if (beginIndex < 0) {`
`throw new StringIndexOutOfBoundsException(beginIndex);`
`}`
`if (endIndex > value.length) {`
`throw new StringIndexOutOfBoundsException(endIndex);`
`}`
`int subLen = endIndex - beginIndex;`
`if (subLen < 0) {`
`throw new StringIndexOutOfBoundsException(subLen);`
`}`
`// 失常返回的都是新 new 进去的 String 对象 `
`return ((beginIndex == 0) && (endIndex == value.length)) ? this`
`: new String(value, beginIndex, subLen);`
`}`
`public String trim() {`
`int len = value.length;`
`int st = 0;`
`char[] val = value; /* avoid getfield opcode */`
`while ((st < len) && (val[st] <= ' ')) {`
`st++;`
`}`
`while ((st < len) && (val[len - 1] <= ' ')) {`
`len--;`
`}`
`// 如果是该字符串中蕴含了空格,调用 substring 办法,否则就是啥都没干本来返回 `
`// 就是如果字符串里有空格,那么还是新生一个 String 对象返回 `
`return ((st > 0) || (len < value.length)) ? substring(st, len) : this;`
`}`
无论是 concat、replace、substring 还是 trim 办法的操作都不是在原有的字符串上进行的,而是从新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被扭转。
得出两个论断:
String 对象一旦被创立就是固定不变的了,对 String 对象的任何扭转都不影响到原对象,相干的任何变动性的操作都会生成新的对象。
String 对象每次有变动性操作的时候,都会从新 new 一个 String 对象(这里指的是有变动的状况)。
回到后面的例子
`//String a = "111"; 相当于 `
`char data [] ={'1','1','1'};`
`Stirng a = new String(data);`
`//a = "222";`
`char data [] ={'2','2','2'};`
`a = new String(data);`
img
这会变量 a 里保留的是 ”222″ 对应 String 对象的援用。
持续看上面的代码
`public class App {`
`public static void main(String[] args) {`
`String a = "111";`
`String a1 = "111";`
`String b = new String("111");`
`// 对象地址是同一个 `
`System.out.println(a==a1);`
`// 对象内容是一样的 `
`System.out.println(a.equals(a1));`
`// 对象地址不一样 `
`System.out.println(a==b);`
`// 对象内容是一样的 `
`System.out.println(a.equals(b));`
`}`
`}`
输入
`true`
`true`
`false`
`true`
第一个输入 true,阐明 a 和 a1 两个变量保留的援用地址是同一个。
第二个也输入 true,阐明 a 和 a1 援用地址中内容是一样的。
a 和 a1 放在栈上,寄存着对象的援用地址。
new 的对象是在堆中。
常量其实是要看 jdk 版本的。
img
img
所以 String a = “111”; 在 JVM 申请内存寄存 ”111″ 对应的对象,并将对象保存起来。当 String a1=”1111″; 的时候,会先去 JVM 的那块地里寻找是否存在 ”111″,刚好后面保留过,所以找到,而后间接把对象的援用地址给了 a1。所以此时的 a 和 a1 都保留着同一个援用地址。
接触 java 后都晓得能够 new 一个对象。所以 String b = new String(“111”); 就是创立一个对象而后把对象援用地址赋给变量 b。然而这里有个非凡点,那就是(“111”), 这里会先去 JVM 里的那块地里找找,找到了间接寄存援用地址。找不到创立一个对象而后把援用地址给 String 的有参构造方法里。
所以第三个中输入 false,因为 a 和 b 所保留的对象援用是不一样的。
最初一个输入 true。那是因为两个变量所保留的援用地址中的内容都是“111”.
答案:
如果常量池中存在,则只需创立一个对象,否则须要创立两个对象。