String类的深入理解(未完待续)
Posted qlqwjy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了String类的深入理解(未完待续)相关的知识,希望对你有一定的参考价值。
String不是基本数据类型,String和8种包装类型是不可变类。String和8种基本数据类型采用值传递。
0.不可变类的设计原则
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[];//数组是引用传递 /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0 .... public String(char value[]) { this.value = Arrays.copyOf(value, value.length); // deep copy操作 } ... public char[] toCharArray() { // Cannot use Arrays.copyOf because of class initialization order issues char result[] = new char[value.length]; System.arraycopy(value, 0, result, 0, value.length); return result; } ... }
如上代码所示,可以观察到以下设计细节:
- String类被final修饰,不可继承
- string内部所有成员都设置为私有变量
- 不存在value的setter
- 并将value和offset设置为final。
- 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
- 获取value时不是直接返回对象引用,而是返回对象的copy.
这都符合上面总结的不变类型的特性,也保证了String类型是不可变的类。
例如:
package cn.qlq.test; public class ArrayTest { public static void main(String[] args) { String str = "x1x1"; str.replace("1", "2"); System.out.println(str);// x1x1 str = str.replace("1", "2"); System.out.println(str);// x2x2 } }
1.创建过程与字符串拼接过程
1.创建过程研究
例如:
package cn.qlq.test; public class ArrayTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; String str3 = new String("abc"); String str4 = new String("abc"); } }
String s1 = new String("abc"); 是在堆中创建一个String对象,并检查常量池中是否有字面量为"abc"的常量,没有的话在常量区创建"abc"并将堆中的对象指向该常量,有的话堆中的对象直接指向"1";
String s2 = new String("abc"); 又在堆中创建一个String对象,并将s2指向该对象,其字面量"abc"在前面已经创建,所以不会再创建常量区中创建字符串;
String s3 = "abc"; 检查常量池中有没有字面量为"abc"的字符串,如果没有则创建并将s3指向该常量;有的话直接指向该该常量;
String s4 = "abc" 的时候常量池已经有abc,所以不会再创建对象,也就是s3与s4指向同一个对象。
所以我们可以用下面图解解释,String s = new String("xxx")在检查常量池的时候会涉及到堆中创建对象;String s = "x"直接检查常量池,不会涉及堆。
如下图解:
一道经典的面试题:new String("abc")创建几个对象?
简单的回答是一个或者两个,如果是常量区有值为"abc"的值,则只在堆中创建一个对象;如果常量区没有则会在常量区创建"abc",此处的常量区是方法区的运行时常量池(也称为动态常量区)。
我们需要明白只要是new都会在堆中创建对象。直接String s = "xxx"不会涉及堆,只在常量区检查是否有该常量。
反编译查看编译后的信息:
package cn.qlq.test; public class ArrayTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; String str3 = new String("abc"); String str4 = new String("abc"); } }
编译并且查看反编译信息:
C:UsersliqiangDesktop>javap -c -verbose ArrayTest.class Classfile /C:/Users/liqiang/Desktop/ArrayTest.class Last modified 2018-9-2; size 383 bytes MD5 checksum 0ab23a2d60142821a621d4d345b50622 Compiled from "ArrayTest.java" public class cn.qlq.test.ArrayTest SourceFile: "ArrayTest.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = String #16 // abc #3 = Class #17 // java/lang/String #4 = Methodref #3.#18 // java/lang/String."<init>":(Ljava/lang/String;)V #5 = Class #19 // cn/qlq/test/ArrayTest #6 = Class #20 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 ArrayTest.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Utf8 abc #17 = Utf8 java/lang/String #18 = NameAndType #7:#21 // "<init>":(Ljava/lang/String;)V #19 = Utf8 cn/qlq/test/ArrayTest #20 = Utf8 java/lang/Object #21 = Utf8 (Ljava/lang/String;)V { public cn.qlq.test.ArrayTest(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=5, args_size=1 0: ldc #2 // String abc 2: astore_1 3: ldc #2 // String abc 5: astore_2 6: new #3 // class java/lang/String 9: dup 10: ldc #2 // String abc 12: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 15: astore_3 16: new #3 // class java/lang/String 19: dup 20: ldc #2 // String abc 22: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 25: astore 4 27: return LineNumberTable: line 5: 0 line 6: 3 line 7: 6 line 8: 16 line 9: 27 }
上面的Constant pool:是所设计的常量信息,包括类名字、方法名字、字符串常量池信息信息。
下面就是编译之后的方法:
第一个构造方法研究:
public cn.qlq.test.ArrayTest(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0
编译器给我们生成的无参构造方法,访问类型是public,
aload_0 将第一个引用类型本地变量推送至栈顶(将this引用推送至栈顶,即压入栈。)
invokespecial #1 // Method java/lang/Object."<init>":()V 调用Object的<init>(构造方法)
return 函数结束(返回类型是void)
第二个main方法研究:
public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=5, args_size=1 0: ldc #2 // String abc 2: astore_1 3: ldc #2 // String abc 5: astore_2 6: new #3 // class java/lang/String 9: dup 10: ldc #2 // String abc 12: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 15: astore_3 16: new #3 // class java/lang/String 19: dup 20: ldc #2 // String abc 22: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 25: astore 4 27: return LineNumberTable: line 5: 0 line 6: 3 line 7: 6 line 8: 16 line 9: 27
访问标志符号是static、public类型
ldc: 该系列命令负责把数值常量或String常量值从常量池中推送至栈顶。该命令后面需要给一个表示常量在常量池中位置(编号)的参数(#2代表上面标记为#2的常量)
astore_1 将栈顶引用型数值存入指定第二个本地变量,超过3的格式变为 astore 4 此种格式
new 代表创建对象
invokespecial 代表调用方法
return 代表函数结束,返回类型是void
然后对着命令自己去查去吧。。。。。。。。
2.拼接过程研究
2.Intern()方法详解
3.equals()和hashCode方法详解
hashCode()源码查看:
public int hashCode() { int h = hash;//默认为0 if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
看出来String是遍历每个char,h乘以31加上对应char的ASCII码。
验证:
String s1 = "a"; String s2 = "b"; System.out.println(s1.hashCode());//97 System.out.println(s2.hashCode());//98
equals(obj)源码查看: 是将形参转变为String,然后遍历里面的char[],两个char[]进行依次对比。也就是比较字符串的值是否相等。
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
我们利用两个hashCode相等的字符串作为key存入map,查看:
package cn.qlq.test; import java.util.HashMap; public class ArrayTest { public static void main(String[] args) { String s1 = "Aa"; String s2 = "BB"; System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); HashMap map = new HashMap(); map.put(s1, "xxx"); map.put(s2, "xxxdddd"); System.out.println(map); } }
2112
2112
{BB=xxxdddd, Aa=xxx}
"Aa" 与"BB"的hashCode相等,那么是如何存入map的?--验证hashmap的实现原理基于数据+链表
先存入Aa,并放在第五个数组位置,当存BB的时候发现hashCode一样,会将BB存到第五个位置,并将第五个位置元素的next(也是一个Entry)存为Aa。也就是数组加链表实现原理
4..String引用传值图解(由于是不可变类,所以给形参赋值的时候相当于新建对象,不会影响实参)
更进一步的理解:"引用传值也是按值传递,只不过传的是对象的地址"。
比如下面一段代码:
package cn.qlq.test; import java.util.Arrays; public class ArrayTest { public static void main(String[] args) { String s = "hello"; test(s); System.out.println(s); } public static void test(String s1) { s1 = "world"; } }
结果:
hello
解释:调用test方法的时候采用引用传递(将s的地址传下去),执行s1="world"是新创一个"world"并赋值给s1,也就是s1此时已经指向其他对象,不再与s指向相同对象。
图解:
以上是关于String类的深入理解(未完待续)的主要内容,如果未能解决你的问题,请参考以下文章
2017-2-17,c#基础,输入输出,定义变量,变量赋值,int.Parse的基础理解,在本的初学者也能看懂(未完待续)