文章目录
前言
在C语言中,要定义一个字符串需要先定义一个char类型数组,继而将此数组作为字符串处理。而Java中有String类,但不是八大基本数据类型,String类是作为一种引用数据类型使用的。
String类是Java中一种使用很多的特殊类,在面试和笔试中永远都是座上宾。
一、String类介绍
1、String类的创建和实现
Java中的String类是引用数据类型,即String和对象一样,通过引用的方式进行访问。
关于字符串的创建方式常用的有四种方式:
1、使用字符串的常量直接赋值
2、通过字符串常量的构造方法
3、通过字符数组进行构造(类似C语言)
4、通过字符串的valueOf方法
String是引用数据类型,因此String str只是保存了字符串对象的地址,而具体内存中是怎么保存的?
这张图的本质就是:Java中的字符串实际内部也是使用字符数组来存储具体的数值。事实上,所有高级语言,字符串类型本质上都是使用的字符数组,字符串就是由一个个的字符来组成的。Java中使用String类型来包装字符数组,提供了非常多的内置方法,让字符串变得更加好用。
2、String类的不可变性
字符串在设计时,被设计为当一个字符串对象产生之后,它的内容就无法修改,这称之为字符串的不可变性
首先看看String类的源码:
我们知道final关键字修饰的内容不可变,所以String类型不可变就是因为使用了final关键字修饰吗?网上有很多解答是这么说的,但其实不对!我们来看个栗子:
可以看到,我们使用了final修饰value数组,但是其内容照样被修改,final修饰引用数据类型的变量,不能改的是这个引用的指向不变至于引用的对象内部如何变化,这是可以的! 。就像我们使用final修饰了一个遥控器,这个遥控器就只能操控某个特定的空调,但是空调本身还是可以改变状态的。
因此字符串对象内容不可变和final修饰没啥关系,final只是确保了value数组的指向不变!
之所以字符串对象的内容不可变,本质在于private封装!<\font>
private修饰的内容只在类的内部可见,因此出了String类,外部根本无法直接操纵这个value数组!,在String类中,将value数组完全封装,对外没有提供任何获取和修改该value内容的方法,因此字符串对象一旦产生,内容无法修改!==》本质在于String类的外部完全无法拿到这个value数组才无法修改。
这个栗子可以很好的解释String的不可变性:
可以看到,在内存中,调用fun函数时,数组定义的内容可以直接更改,而str则是重新生成了一个字符数组。
话又说回来,既然private保证了它的不可变性,那用final修饰是出于什么目的?其实这是出于线程安全的考虑,被final修饰的字符串对象是线程安全的,这在StringBuffer(线程安全,效率低)和StringBuilder(线程不安全,效率高)中得以体现。
为什么String类设计成不可变的,主要有以下几点:
1.方便实现字符串对象池.如果String 可变,那么对象池就需要考虑写时拷贝的问题了.
2.不可变对象是线程安全的.
3.不可变对象更方便缓存 hash code,作为 key时可以更高效的保存到HashMap 中.
二、String类常见操作
1.字符串的比较
字符串比较通常有四种形式:
1、通过==比较,此时比较的是两个字符串的地址,不看内容,相等则表示指向同一个引用。
2、通过equals方法比较两个字符串的内容是否相等(大小写敏感)
3、通过equalsIgnoreCase方法比较两个字符串的内容是否相等(大小写不敏感)
4、通过compareTo方法比较两个字符串对象的大小关系
其中,两个equals方法返回的是Boolean类型,而compareTo方法返回值为int类型。
在之前的文章有介绍过compareTo方法,这里很明显String类实现了comparable接口并重写了compareTo方法。String类中的compareTo方法比较规则如下:
a.首先按照两个字符串的字典序比较大小,若出现不相等字符,直接返回这两个字符的大小差值(ASCⅡ码)
‘h’-‘H’=32
b.若前k个字符相等(k是较短的字符串长度),返回长度差
2、字符串内容的查找
2.1、在字符串中查找指定字符
char charAt(int index):返回字符串中指定索引index处的字符这个方法非常常用,所以举个栗子:
比如写一个方法判断一个字符串是否由纯数字组成(在日常生活中,注册时填手机号啊之类的地方都是这么判断的)
public static boolean isDigit(String str) {
// 1.边界条件,字符串为空null 或内容为空
if (str == null || str.length() == 0) {
return false;
}
// 2.需要将字符串拆分为一个个的字符来依次判断
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 如何知道c是一个数字型的字符?
if (c < '0' || c > '9') {
// 一定不是数字字符!,找到反例!
return false;
}
// // 判断c是否是数字字符 这里也可使用Character的内置方法进行判断
// if (!Character.isDigit(c)) {
// return false;
// }
}
// 走完整个循环,还没找到反例,则str一定是纯数字字符串!
return true;
}
2.2、其他查找方法:
写在前面:所有和索引相关的方法调用一定要注意是否越界的问题
补充:还有一个判断字符串中是否有指定子串的方法: boolean contains(“*****”),若有返回true,否则返回false。
3、字符串转化
3.1 将其他类型转化为字符串对象,使用 valueOf方法,可以接受所有参数。
3.2大小写转化
这两个方法只作用于字母字符串,只对英文字符起效果。
3.3字符串转数组
第一种,使用方法toCharArray();
第二种,转为字节数组getBytes(可以传入指定的字符编码)
拓展:String中提供了format()方法,可以将字符串格式化输出,类似于C语言中的printf,但是java中支持正则表达式
4、字符串的替换
对字符串内部分内容进行替换,使用replace方法
5、字符串的拆分
将一个字符串按照指定格式拆分为字符串数组,较为常用。使用split()方法
注意:当调用split碰到某些特殊字符时,需要用到转义字符,否则无法正确拆分
以拆分IP地址为例:
6、字符串的截取
演示示例:传入一个字符串(只包含字母,进行首字母大写的操作)
拓展:String中提供了trim()方法,可以将字符串两端的空格(制表符、换行符)去掉,保留中间的部分。
7、力扣题
通过一个力扣题将上面所使用的方法融会贯通,题目如下:
代码:
public String capitalizeTitle(String title) {
//使用StringBuffer便于修改字符串
StringBuilder sb=new StringBuilder();
//按指定格式拆分为字符串数组
String[] strings=title.split(" ");
for (int i = 0; i < strings.length; i++) {
//对每一个字符串进行处理
String s=strings[i];
if(s.length()<3){
sb.append(s.toLowerCase());
} else{
String s1=s.substring(0,1).toUpperCase();
String s2=s.substring(1).toLowerCase();
sb.append(s1).append(s2);
}
sb.append(" ");
}
return sb.toString().trim();
}
三、字符串常量池
池化思路,是程序设计中为了节省内存提高效率而引出的一种规则,字符串字面量经常会被重复使用,因此放入常量池,当再次使用该内容时,省去了创建对象,开辟内存的时间,直接复用已有对象。
先来看一段代码:
public class StringTheory {
public static void main(String[] args) {
String str = "hello";
String str1 = "hello";
String str2 = new String("hello");
String str3 = new String("hello");
System.out.println(str == str1);
System.out.println(str == str2);
System.out.println(str3 == str2);
}
}
这段代码的运行结果是什么?按照引用类型的比较规则,比较内容时应使用.equals()方法,所以这里输出应该全为false,但事实并非如此。
这是因为采用直接赋值类似String str = "字符串字面量"的方式创建字符串对象,会使用字符串的常量池,若该对象是第一次使用,就在堆中开辟新空间,产生新对象,产生之后将该对象置入字符串的常量池。当下次再次使用该对象时(还是采用直接赋值的方式),若常量池中已有该内容的字符串,直接引用常量池中的对象,不再创建字符串。
str被创建的时候,“hello”第一次出现,JVM产生该对象并置入常量池,此时常量池中已经有了值为hello的对象,定义str1时,JVM直接将池中对象的地址赋值给str1,不再创建新对象。只有直接赋值的方式会用到字符串的常量池!
字符串的intern方法
上面说了,只有直接赋值才会创建字符串常量放入常量池,那么如何将别的方法构造出的字符串放入常量池呢?答案是使用intern方法:调用intern方法,会将产生的字符串对象放入常量池(不存在的前提下),若常量池已有该对象,则该方法返回常量池中的字符串对象引用
来看下面的代码:
char[] ch = new char[] {'a','b','c'};
String s1 = new String(ch);
// s1.intern();
String s2 = "abc";
// 输出true还是false
System.out.println(s1 == s2);
在调用s1.intern前,输出false,调用之后将s1指向的字符串放入了常量池,因此s2定义时,直接指向了常量池中的“abc”输出true。
注意:每次调用intern后,都要接受一下返回值,因为不确定此时常量池中有没有该字符串对象,不接受的话有可能还在指向堆中创建的地址。
因为常量池已有该对象,则该方法返回常量池中的字符串对象引用,即0x100,但是这里没接受,所以还是指向0xf300这个地址。
不接收输出true ,false
如果改为str2=str2.intern(),则此时输出都是true。
常量池小结:
1.存储一系列的字符串对象,当字符串常量第一次出现,且常量池中没有该值的字符串对象,就会将该对象置入常量池。
⒉.若采用直接赋值的方式声明字符串引用,若常量中存在该对象,则直接返回常量池的字符串对象,不会创建新对象
3.关于intern方法,尝试将当前字符串对象置入常量池,若常量池没有该值,则将当前对象置入常量池若常量池已经包含了该值的对象,则不会将对象对象置入常量池,intern方法直接返回常量池对应的对象
总结
String类时Java开发时非常常见和使用的类,本文对String类做了详细的介绍以及其内部的各种方法的使用进行了介绍,以及字符串常量池的规则和使用以及注意细节。
总的来说,String类是开发学习中很基础也很重要的知识点,所以单独拉出来写了一篇,一定要手拿把掐!