java中字符串存储情况的剖析

Java中字符串存储的剖析

上期图书馆管理代码的各功能实现🚑

  1. 借阅图书
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 的初始化方式

    1. String str1=“abcdef”;//abcdef是一个常量字符串

    2. String str2=new String(“abcdef”);//这个就是构造方法进行初始化了

    3. 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修改过后的数组会有变化。

  • 文件池的概念🍢
    1. class文件池:编译好的字节码文件中的常量都会放进class文件池
    2. 运行时常量池:顾名思义,当上述class字节码文件跑起来后,上述文件池会变为运行时常量池
    3. 字符串常量池:上述运行过程中:由双引号引起来的字符串常量将被放进字符串常量池

运行时常量池在方法区!而字符串常量池从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对象的地址。

每个链表节点长这样:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mXS0LXaz-1640334147778)(C:\Users\lebronHArden\AppData\Roaming\Typora\typora-user-images\image-20211224112726799.png)]

而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修饰的,它不仅私有且不可被修改,但是有一个手段可以对其进行查看并修改,那就是反射,后期涉及到反射,再着重讲一下。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值