关注「Java视界」公众号,获取更多技术干货

String类你了解多少?

String类型不属于基本数据类型之一,它实际上是对数组的特殊包装,是一个匿名对象。

/** The value is used for character storage. */
private final char value[];

1、String类的两种实例化方式及区别

  • 第一种:   直接赋值
String str1 = "haha";
String str2 = "haha";
System.out.println(str1 == str2); //true

以上结果是true,也就是虽然定义了两个字符串对象,但实际堆内存只有一块,两个引用均能指向。之所以会这样,是因为JVM里有专门的字符串池,新增字符串时,会先检查池有无相同的字符串,若有就不再重新创建,而直接引用指向字符串池中已经存在的字符串。直接赋值可实现字符串池的自动保存,操作性能较高。

  • 第二种:  new 构造方法
String str1 = new String("haha");

此时实际是开辟两个堆内存空间,如下:

虽然开辟了两个内存空间,但使用时只会使用其中一个,匿名产生的那块空间会成为垃圾空间(因此推荐使用直接赋值法实例化字符串对象),且该对象不会自动在字符串常量池里自动保存,要想入池就需要调用intern()这个方法手动入池,如下:

String str1 = "haha";
String str2 = new String("haha");
System.out.println(str1 == str2); //false

此时这两个字符串对象不是同一个对象(内存地址不一样),intern()手动入池后:

String str1 = "haha";
String str2 = new String("haha").intern();
System.out.println(str1 == str2); //true

结果是true,因为两者都是指向字符串池中同一个内存区域。

2、“==”和“equals()”的区别

对于int类型的变量,我们只需要“==”来比较两者大小,但是对于两个字符串类型的变量不能完全用“==”来比较大小。

“==”和“equals()”的区别:

  • “==” :用户数值的比较,但是比较字符串时比较的是两个字符串对象的内存地址是否相同;
  • “equals()”: 比较的是两个对象的内容是否相等。

3、String字符串常量池

String字符串常量池的设计主要目的是为了实现数据共享,可以分为静态常量池和运行时常量池。

  • 静态常量池: 也叫class常量池程序。*.class文件在加载时自动将此程序中保存的字符串、普通常量、类等全部进行分配的常量池。每个class文件都有一个class常量池。
  • 运行时常量池: 在运行期间也可以将新的变量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是String类的intern()方法。
String str1 = "haha";
String str2 = "nidaye.haha.ni";
String str3 = "nidaye." + str1+ ".ni";
System.out.println(str3 == str2); //false

这里str3表达式中的str1在加载时是不确定的。

4、字符串修改

String类型是由final关键字修饰的,意味着不可修改;另一个角度,String其实是一个数组,数组的最大缺点是不可改变长度,一旦定义时确定了长度,长度就不可改变,数组内存分析如下:

在看如下代码:

String str1 = "haha";
str1 + = "ni";
System.out.println(str1);

以上字符串并未改变,只是引用地址在变化,产生垃圾空间,因此不要频繁改动字符型变量,产生很多垃圾空间降低系统性能。

5、String类为什么是final修饰的?value属性为什么也是final修饰的?

String类为什么是final修饰的?

先说结论:是为了安全和效率

 为什么说是为了安全呢?

有两个方面:String类final修饰,一是不可继承,防止方法被重写,被植入恶意代码,破坏程序规则;另一方面是多线程时,string的不可修改可以做到线程安全。

首先你要理解final的用途,在分析String为什么要用final修饰,final可以修饰类,方法和变量,并且被修饰的类或方法,被final修饰的类不能被继承,即它不能拥有自己的子类,被final修饰的方法不能被重写, final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。

final修饰的String,代表了String的不可继承性,那么就不会有子类重写String类的方法, final保证了String的不可变性,String不可变很简单,由上面的内存分析可知,给一个已有字符串"abcd"第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。

也就是说,用 final 修饰,代表虚拟机栈里的这个叫 value 的引用地址不可变,不能再指向别的对象。但是 array 对象本身数据是可以改变的,常见的就是使用反射来改变;当然若是集合对象,里面的元素肯定是可以改变的,只是这个集合对象的应用不变。

现在用反证法来说明下:假设String没有用final来修饰,或者说String是可变的,那String就会有子类,且它的方法可能被重写,我们假设MyString就是String的子类,并且要重写String的length()方法:

// String原来的方法
public int length() {
   return value.length;
}
public MyString extends String {
    @Override
    public int length() {
        return 0;
    }
}

源码中value是一个char [ ],即一个char数组,length()方法是返回这个数组的长度;但我们自定义的重写的length()方法中无论什么时候都是0,这样在实际业务场景中如果用到了这个length()方法,因为计算规则不同,那就会出现不统一的情况。

那为什么说为了效率呢?

在日常的实际开发中,开发者会用到大量的字符串对象,可以说我们无时无刻不在和字符串打交道。大量的字符串被轻易的创建出来,这就涉及到一个非常严重的问题,即性能的开销,我们知道分配给Java虚拟机的内存是有限的,如果不加节制的创建字符串对象,那么弊端显而易见:内存迅速被占满,程序执行缓慢!!!于是Java的设计者采用了一种非常有效的解决办法,即:共享字符串。共享字符串对象的方法是将字符串对象存放到虚拟机中的方法区里面的常量池里,不同的类,不同的方法,甚至是不同的线程,可以使用同一个字符串对象,而不需要再在内存中开辟新的内存空间,从而极大的降低了内存的消耗,也提升了程序运行效率。字符串共享是解决内存消耗以及庞大的性能开销的必然选择。
这个字符串常量池的基础就是这个字符串不可变,要是可变的还能叫常量吗,还能组成池吗,肯定不行。实际上,解决共享变量安全性的最好的手段,就是禁止修改共享对象,于是字符串对象的这个char数组就必然要被 final 修饰了,因为 final 意味着禁止改变

只有当字符串是不可变的,字符串池才有可能实现。

其他好处:

  • 方便做hash中的key
  • String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
  • String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

六、通过反射改变 String 的值

前面说了 final 修饰的变量如果是引用只是地址不能改变,但是引用指向的对象是可以改变的,虽然没有公开的get方法,但是我们可以反射去改变对象的值。

    @Test
    public void test9() {
        String s = "abcd";
        System.out.println("s = " + s);
        Field valueField = null;
        try {
            valueField = String.class.getDeclaredField("value");
            valueField.setAccessible(true);

            char[] value = (char[]) valueField.get(s);

            value[3] = 'e';
            System.out.println("s = " + s);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
s = abcd
s = abce

7、String常见API

Java的API文档JavaDoc https://docs.oracle.com/javase/9/docs/api/overview-summary.html

上面是Java的API地址,本文主要考虑String相关较常见的API。

1、charAt( )

用于获取某一指定索引位置的字符

String str = "nidayenidaniang";
char a = str.charAt(4);
System.out.println(a);

结果:

y

2、getBytes( )

用于字符串向字节数组转换,在进行二进制的数据传输或编码转换时使用。

String str = "nidayenidaniang";
byte data[] = str.getBytes();
for(int i=0; i<data.length; i++) {
    // 小写转大写
    data[i] -= 32;
    System.out.println(data[i]+"、");
}
System.out.println(new String(data));

结果:

78、
73、
68、
65、
89、
69、
NIDAYE

3、contains ( )

用于判断某字符串是否包含某个特定的字符或者字符串。

String str = "nidayehahahaha";
System.out.println(str.contains("haha"));
System.out.println(str.contains("dddd"));

结果:

true
false

4、split( )

字符串拆分

String str = "nidaye haha haha";
// 空格拆分 ,拆成两份
String data [] = str.split(" ", 2);
for(int i=0; i<data.length; i++) {
    System.out.println(data[i]);
}

结果:

nidaye
haha haha

有时遇到特殊的字符需要合理使用转义“\\”

String str = "176.16.66.66";
// 空格拆分 ,拆成两份
String data [] = str.split(".");
for(int i=0; i<data.length; i++) {
    System.out.println(data[i]);
}

以上,直接用 “.” 来分隔会报错,需要改写成如下:

String str = "176.16.66.66";
// 空格拆分
String data [] = str.split("\\.");
for(int i=0; i<data.length; i++) {
    System.out.println(data[i]);
}

结果如下:

176
16
66
66

5、substring( )

用于字符串截取。

String str = "176.16.66.66";
System.out.println(str.substring(1));
System.out.println(str.substring(1, 6));

结果:

76.16.66.66
76.16

6、format( )

Java提供了格式化数据的处理操作,常用的有:字符串(%s)、字符(%c)、整数(%d)、小数(%f)。

String name = "你大爷";
int age  = 88;
double money = 122.6656565651;
String str = String.format("姓名:%s、钱:%5.2f", name, money);
System.out.println(str);

结果:

姓名:你大爷、钱:122.67

7、concat( )

用于字符或者字符串连接。

String str1 = "hahaha";
String str2 = "你大爷".concat(str1).concat("哈哈哈");
System.out.println(str2);
System.out.println(str2 == str1);

结果:

你大爷hahaha哈哈哈
false

8、toUpperCase( )和 toLowerCase ( )

String str1 = "hahaha";
String str2 = "JJJ";
System.out.println(str1.toUpperCase());
System.out.println(str2.toLowerCase());

结果:

HAHAHA
jjj

8、StringBuffer和StringBuilder的联系是?区别是?

1、当对字符串进行修改的时候,需要使用StringBuffer和StringBuilder类。StringBuffer是线程安全的,每次操作字符串,String会生成一个新的对象,而StringBuffer和StringBuilder不会生成新的对象。

2、和String类不同的是,StringBuilder和StringBuffer的对象是变量,对变量进行操作是直接对该对象进行更改,而不进行创建和回收的操作,不产生新的未使用对象,所以速度要比String快很多。

3、StringBuilder类和StringBuffer之间的最大不同在于StringBuilder的方法不是线程安全的,由于StringBuilder相较于StringBuffer有速度优势,所以多数情况下建议使用StringBuilder类。然而在应用程序要求线程安全的情况下,则必须使用StringBuffer类。

posted @ 2022-06-25 14:03  沙滩de流沙  阅读(54)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货