Java中字符串存储的剖析
上期图书馆管理代码的各功能实现🚑
- 借阅图书
public class BorrowOperation implements IOperation{
public void work(BookList bookList){
System.out.println("借阅图书!");
System.out.println("请输入您需要借阅的图书的书名:");
Scanner scanner=new Scanner(System.in);
String name=scanner.nextLine();
int size=bookList.getUsedSize();
for (int i = 0; i < size; i++) {
Book book=bookList.getPos(i);
if(book.getName().equals(name)){
System.out.println("该图书已找到,并被您借走!欢迎下次再来!");
book.setBorrowed(true);
System.out.println(book);
return;
}
}
System.out.println("查无此书!");
}
}
2.删除图书
public class DelOperation implements IOperation{
public void work(BookList bookList){
System.out.println("删除图书!");
Scanner scanner=new Scanner(System.in);
System.out.println("请输入您需要删除的图书的书名:");
String name=scanner.nextLine();
int size=bookList.getUsedSize();
int pos=-1;
for (int i = 0; i < size; i++) {
Book book=bookList.getPos(i);
if(book.getName().equals(name)){
pos=i;
break;
}
}
if(pos==-1){
System.out.println("查无此书!");
return;
}
for (int i = pos; i < size-1; i++) {
//bookList.getPos(i)= bookList.getPos(i+1);错误表达
Book book=bookList.getPos(i+1);
bookList.setPos(i,book);
}
bookList.setPos(bookList.getUsedSize(), null);//引用类型记得置空
bookList.setUsedSize(bookList.getUsedSize()-1);
System.out.println("删除成功!");
}
}
3.打印图书
public class DisplayOperation implements IOperation{
public void work(BookList bookList){
System.out.println("打印图书!");
int size=bookList.getUsedSize();
for (int i = 0; i <size ; i++) {
Book book=bookList.getPos(i);
System.out.println(book);
}
}
}
4.退出系统
public class ExitOperation implements IOperation{
public void work(BookList bookList){
System.out.println("退出系统!");
System.exit(0);
}
}
5.查找图书
public class FindOperation implements IOperation{
public void work(BookList bookList){
System.out.println("查找图书!");
System.out.println("请输入您需要查找的图书的书名:");
Scanner scanner=new Scanner(System.in);
String name=scanner.nextLine();
int size=bookList.getUsedSize();
for (int i = 0; i < size; i++) {
Book book=bookList.getPos(i);
if(book.getName().equals(name)){
System.out.println("图书已经找到!其信息如下:");
System.out.println(book);
break;
}
}
System.out.println("查无此书!");
}
}
6.归还图书
public class ReturnOperation implements IOperation{
public void work(BookList bookList){
System.out.println("归还图书!");
System.out.println("请输入您需要归还的图书的书名:");
Scanner scanner=new Scanner(System.in);
String name=scanner.nextLine();
int size=bookList.getUsedSize();
for (int i = 0; i < size; i++) {
Book book=bookList.getPos(i);
if(book.getName().equals(name)){
if(book.isBorrowed()==false){
System.out.println("该书并未借出,无需归还!");
return;
}else{
book.setBorrowed(false);
System.out.println("归还成功!欢迎下次再来!");
System.out.println(book);//归还成功,打印出来看一下
return;
}
}
}
System.out.println("图书馆查无此书的记录,无需归还,谢谢!");
}
}
字符串💛
- 字符串就是用双引号括起来的一串字符,当然这一串字符也可以是“什么都没有”,单引号不可以引一串字符!
- java中没有C中那样的字符串结束标志!
- String 的初始化方式
- String str1=“abcdef”;//abcdef是一个常量字符串
- String str2=new String(“abcdef”);//这个就是构造方法进行初始化了
- char [] chars={‘a’,‘b’,‘c’,‘d’,‘e’,‘f’};
String str=new String(chars);//这个也是构造方法(一旦new对象了,就肯定会调用某构造方法)
- 查看String类的源码:(ctrl+左键;ALT+7就可以看到String的所有构造方法)
这里有两个重要字段:char[] value和int hash;value被final修饰,这里就和C中的const修饰在指针符号*的前面一样,这里表达的意思就是value不可以换指向,或者说value存的地址不可更改
- 一个小问题:
public class Main {
public static void func(String s,char[] c){
s="pig";
c[0]='g';
}
public static void main(String[] args) {
String str="abcdef";
char[] chars={'a','b','c','d','e','f'};
func(str,chars);
System.out.println(str);
System.out.println(Arrays.toString(chars));
}
}
运行结果是什么?
答案:abcdef和[g,b,c,d,e,f]
我们要明白的是:栈上的s接受str时确实和str指向同一个String对象,但是s后来换了一个指向,这对实参的指向没有任何的影响,所以str还是指向原来的abcdef
而c[0]在C中我们知道等价于*(c+0)也就是说:在func中c[0]已经在对实参事项的数组作操作了!可想而知被func修改过后的数组会有变化。
- 文件池的概念🍢
- class文件池:编译好的字节码文件中的常量都会放进class文件池
- 运行时常量池:顾名思义,当上述class字节码文件跑起来后,上述文件池会变为运行时常量池
- 字符串常量池:上述运行过程中:由双引号引起来的字符串常量将被放进字符串常量池
运行时常量池在方法区!而字符串常量池从JDK1.8开始在堆内,其本质是一个哈希表!
文件池存在的意义:提高效率。工具箱给你备好了,比你一个个去找工具来的强。
- 顺着上面,上面是哈希表?
其实质就是一种数据结构,用来描述和组织数据。 - 那哈希表有什么特别的吗?
哈希表存储数据时根据哈希函数将元素落位,取某元素时又用一样的手段将其取出,换言之,查找某个元素可以将时间复杂度降低至O(1)!而我们常知道的一般的数组的顺序查找的时间复杂度是O(N) - 那什么是哈希函数?
哈希表在组织数据时,会根据一个映射关系将一个个元素落位到指定的位置,如哈希函数是:key%length,来一个12,它将放进10个格子的哈希表中,那么它根据哈希函数计算结果为2,那就将其放到下标为2的位置上。多个元素根据这个哈希函数,可能都会放到同一个位置上,这时每个位置就用一个单向链表把这些元素串起来,这就是哈希表。
字符串的内存布局
- 一个小问题:(问运行结果)📦
public class Main {
public static void main(String[] args) {
String str1="abcdef";
String str2=new String("abcdef");
System.out.println(str1==str2);
}
其结果为false
Why?
简言之:str1指向的字符串会由编译器生成一个String对象并将第一个字段Value指向一个字符数组,这个字符数组存储的就是hello
由于常量字符串”hello“需入字符串常量池,所以编译器将根据哈希函数将其落位到指定的位置,链表的节点的第二个域存储的是String对象的地址;而str1指向的就是哈希表的链表节点第二个域存的String对象的地址。
每个链表节点长这样:
而str2由于会new一个String对象,首先它也会去字符串常量池去找有没有hello,没有就会存一个进哈希表,并将新建的String对象的第一个域val指向该字符数组,有就直接指向其即可。
当然我们也可以通过调试去查看上述图是否合理:
发现确实两个String对象存的value都是同一个位置,而结果却是打印false。
- 将上述代码改成:(打印结果是什么?)🌈
public class Main {
public static void main(String[] args) {
String str1="hello";
String str2="hello";
System.out.println(str1==str2);
}
}
打印true
Why?
双引号引起来的字符串先放入字符串常量池,栈上的str1和str2都指向底层实现的String对象(该对象第第一个域val存的就是存储hello字符数组的首字符地址),所以str1==str2.
- 改代码问结果:🌡
public class Main {
public static void main(String[] args) {
String str1="hello";
String str2="hel"+"lo";
System.out.println(str1==str2);
}
}
true
Why?
因为hel和lo在编译时已经等同于hello了,所以这题和上一题没有区别。
- 再改代码问结果👊
public class Main {
public static void main(String[] args) {
String str1="hello";
String str2="he";
String str3=str2+"llo";
System.out.println(str1==str3);
}
}
false
Why?
可以看出,先是两个字符串:hello和he入池先放好,底层优化调用StringBuilder(new了一个),然后先后append(“he”)和append(“llo”)且llo也入池了,也就是说当前StringBuilder的val指向的字符数组是hello(不入池!)但是StringBuilder对象的地址不可以给str3,所以StringBuilder又调用toString(),新生成一个String对象,并使其第一个域存放刚才的字符数组的首字符地址,返回这个String对象的地址给str3,综述str3!=str1.
- 再改😋
public class Main {
public static void main(String[] args) {
String str1="11";
String str2=new String("1")+new String("1");
System.out.println(str1==str2);
}
}
false
Why?
解读:"11"入池,且栈上的str1指向底层生成的String对象(该对象第一个域存放的是“11”字符数组的首字符地址),new一个StringBuilder对象,并调用无参构造函数,“1”入池,先后两个new String对象存放“1”,设计字符串拼接时,先后将“1”append进StringBuilder对象,调用toString()生成String对象存放刚才的字符数组地址。并返回这个String对象的地址给str2.综述:显然str1!=str2;str1指向的字符串对象已入池,str2指向的对象没入池。
- 再改🐰
public class Main {
public static void main(String[] args) {
String str1=new String("1")+new String("1");
str1.intern();
String str2="11";
System.out.println(str1==str2);
}
}
true
Why?
“1”入池,底层生成的String对象的地址并被两个new String对象第的第一个域所存放,涉及字符串拼接,又被底层优化成StringBuilder拼接完成之后并调用toString()生成String对象去存储那个字符数组的地址后,进行了手动入池的操作,后续"11"编译时,会先在字符串常量池去找是不是已经有了“11”这个字符串了,显然这里是已经有了(因为有手动入池“11”)所以底层将不再重新生成String对象,而是str2直接指向刚才入池的String对象!。所以说str1==str2.
- 再改👶
public class Main {
public static void main(String[] args) {
String str2="11";
String str1=new String("1")+new String("1");
str1.intern();
System.out.println(str1==str2);
}
}
false
Why?
这里只不过是将上一题的先后顺序作了调换,目的是想说明,如果字符串常量池里已经有某个字符串了,那一个存储了相同字符串的String对象是没法入池的,即使你做了入池的操作,所以没入池的话,str2和str1指向两个不同的String对象。故为false。
- 一个易错点:
int[] arr={1,2,3,4,5};
arr={2,3,4};
上述代码有没有问题?
有!这样是错误的,为什么说这个,是因为刚才的String对象的引用都可以换指向,这里arr也是一个引用,为什么不能换对象呢!
因为java语法规定:类似于{2,3,4}这样的形式,只能用于数组的初始化,而数组的整体赋值又只有一次机会,所以上述代码错误!一句话:违反了初始化语法。但可以改成这样:
int[] arr={1,2,3,4,5};
arr=new int[]{2,3,4};//这才是换了个指向
一个问题留给读者:(以下代码生成了多少个String对象?包括底层生成的。)
public class Main {
public static void main(String[] args) {
String str="hello";
for (int i = 0; i < 10; i++) {
str+=i;
}
System.out.println(str);
}
}
括底层生成的。)
public class Main {
public static void main(String[] args) {
String str="hello";
for (int i = 0; i < 10; i++) {
str+=i;
}
System.out.println(str);
}
}
- 我们知道String对象的第一个域val是被private final修饰的,它不仅私有且不可被修改,但是有一个手段可以对其进行查看并修改,那就是反射,后期涉及到反射,再着重讲一下。