String

递归

(菲波那切数列)

递归:方法自己调用自己的现象就称为递归。

递归的分类:

  • 递归分为两种,直接递归和间接递归。
  • 直接递归称为方法自身调用自己。
  • 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法。

注意事项

  • 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。

  • 在递归中虽然有限定条件,但是递归深度不能太深,否则效率低下,或者也会发生栈内存溢出。

    • 能够使用循环代替的,尽量使用循环代替递归

字符串String

java.lang.String 类代表字符串。Java语言提供对象字符串的特殊支持:

  • Java程序中所有的字符串文字(例如"abc" )都可以被看作是实现此类的实例,即所有""引起来的内容都是字符串对象
  • Java语言提供对字符串串联符号("+"),即Java重载了+的功能。
  • Java语言提供了将任意类型对象转换为字符串的特殊支持(toString()方法)。

字符串的特点

1、字符串String类型本身是final声明的,意味着我们不能继承String。

2、String对象内部是用字符数组进行保存的

​ char[] value; 默认的长度 16
​ 扩容方式同 int newCapacity = (value.length << 1) + 2;

(1)JDK8的String内部是用字符数组进行保存的

private final char[] value;

"abc" 等效于 char[] data={ 'a' , 'b' , 'c' }

例如: 
String str = "abc";

相当于: 
char data[] = {'a', 'b', 'c'};     
String str = new String(data);
// String底层是靠字符数组实现的。

(2)JDK9之后String内部是用byte数组进行保存的

private final byte[] value;
private final byte coder;//新增加的字段,表示字符串采用的编码 1是UTF-16,0是LATIN1

定义的字符串中,如果没有汉字,每个字符将采用LATIN1编码表存储,如果字符串中包含汉字等非ASCII码表的字符,存储的编码就是UTF-16,相比JDK1.8节约内存

3、final class String 不能有子类

4、实现了Comparable接口 字符串对象是可以进行比较的

5、字符串数据存在字符串常量池内。。。如果存在相同的数据只会开辟一块空间

字符串的对象也是不可变对象,意味着一旦进行修改(不是重新赋值),就会产生新对象。

    @Test
    public void test01(){
        // 创建一个字符串对象
        String originalString = "Hello";
        System.out.println("Original String: " + originalString);

        // 修改字符串,实际上是创建了一个新的字符串对象
        String newString = originalString.concat(" World");
        System.out.println("New String after concatenation: " + newString);

        // 查看原始字符串是否改变
        System.out.println("Original String after modification: " + originalString);
    }

构造字符串对象

使用构造方法

  • public String() :初始化新创建的 String对象,以使其表示空字符序列。
  • String(String original): 初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
  • public String(char[] value) :通过当前参数中的字符数组来构造新的String。
  • public String(char[] value,int offset, int count) :通过字符数组的一部分来构造新的String。
  • public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String。
  • public String(byte[] bytes,String charsetName) :通过使用指定的字符集解码当前参数中的字节数组来构造新的String。
//字符串常量对象,推荐
String str = "hello";

// 无参构造,不推荐
String str1 = new String();

//创建"hello"字符串常量的副本,不推荐
String str2 = new String("hello");

//通过字符数组构造
char chars[] = {'a', 'b', 'c','d','e'};     
String str3 = new String(chars);
String str4 = new String(chars,0,3);

// 通过字节数组构造
byte bytes[] = {97, 98, 99 };     
String str5 = new String(bytes);
String str6 = new String(bytes,"GBK");

使用"+"

任意数据类型与"字符串"进行拼接,结果都是字符串

	public static void main(String[] args) {
		int num = 123456;
		String s = num + "";
		System.out.println(s);
		
		Student stu = new Student();
		String s2 = stu + "";//自动调用对象的toString(),然后与""进行拼接
		System.out.println(s2);
	}

 	字符串常量+字符串常量:  字符串常量

    字符串变量+字符串变量:
    字符串常量+字符串变量:  new StringBuilder()
    字符串变量+字符串常量:

使用字面量的方式

str1 = 'Hello, World!'  
str2 = "Hello, World!"  
str3 = '''This is a  

可以使用方法

words = ['Hello', 'World']  
combined_str = ' '.join(words)  # 'Hello World'  

字符串的常用方法

基础操作

(1)boolean isEmpty():字符串是否为空

(2)int length():返回字符串的长度

(3)String concat(xx):拼接,等价于+

(4)boolean equals(Object obj):比较字符串是否相等,区分大小写

(5)boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写

(6)int compareTo(String other):比较字符串大小,区分大小写,按照Unicode编码值比较大小

(7)int compareToIgnoreCase(String other):比较字符串大小,不区分大小写

(8)String toLowerCase():将字符串中大写字母转为小写

(9)String toUpperCase():将字符串中小写字母转为大写

(10)String trim():去掉字符串前后空白符

查找

(11)boolean contains(xx):是否包含xx

(12)int indexOf(xx):从前往后找当前字符串中xx,即如果有返回第一次出现的下标,要是没有返回-1

(13)int lastIndexOf(xx):从后往前找当前字符串中xx,即如果有返回最后一次出现的下标,要是没有返回-1

@Test
	public void test01(){
		String str = "尚硅谷是一家靠谱的培训机构,尚硅谷可以说是IT培训的小清华,JavaEE是尚硅谷的当家学科,尚硅谷的大数据培训是行业独角兽。尚硅谷的前端和UI专业一样独领风骚。";
		System.out.println("是否包含清华:" + str.contains("清华")); //true
		System.out.println("培训出现的第一次下标:" + str.indexOf("培训"));  //9
		System.out.println("培训出现的最后一次下标:" + str.lastIndexOf("培训"));
}

字符串截取

(14)String substring(int beginIndex) :返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。

(15)String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。

@Test
	public void test01(){
		String str = "helloworldjavaatguigu";
		String sub1 = str.substring(5);
		String sub2 = str.substring(5,10);
		System.out.println(sub1);
		System.out.println(sub2);
	}

@Test
	public void test02(){
		String fileName = "快速学习Java的秘诀.dat";
		//截取文件名
		System.out.println("文件名:" + fileName.substring(0,fileName.lastIndexOf(".")));
		//截取后缀名
		System.out.println("后缀名:" + fileName.substring(fileName.lastIndexOf(".")));
	}

和字符相关

(16)char charAt(index):返回[index]位置的字符

(17)char[] toCharArray(): 将此字符串转换为一个新的字符数组返回

(18)String(char[] value):返回指定数组中表示该字符序列的 String。

(19)String(char[] value, int offset, int count):返回指定数组中表示该字符序列的 String。

(20)static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String

(21)static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String

(22)static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String

(23)static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String

@Test
	public void test01(){
		//将字符串中的字符按照大小顺序排列
		String str = "helloworldjavaatguigu";
		char[] array = str.toCharArray();
		Arrays.sort(array);
		str = new String(array);
		System.out.println(str);
	}
	
	@Test
	public void test02(){
		//将首字母转为大写
		String str = "jack";
		str = Character.toUpperCase(str.charAt(0))+str.substring(1);
		System.out.println(str);
	}

编码与解码

(24)byte[] getBytes():编码,把字符串变为字节数组,按照平台默认的字符编码进行编码

​ byte[] getBytes(字符编码方式):按照指定的编码方式进行编码

(25)new String(byte[] ) 或 new String(byte[], int, int):解码,按照平台默认的字符编码进行解码

​ new String(byte[],字符编码方式 ) 或 new String(byte[], int, int,字符编码方式):解码,按照指定的编码方式进行解码

@Test
	public void test01(){
		//将字符串中的字符按照大小顺序排列
		String str = "helloworldjavaatguigu";
		char[] array = str.toCharArray();
		Arrays.sort(array);
		str = new String(array);
		System.out.println(str);
	}
	
	@Test
	public void test02(){
		//将首字母转为大写
		String str = "jack";
		str = Character.toUpperCase(str.charAt(0))+str.substring(1);
		System.out.println(str);
	}

开头与结尾

(26)boolean startsWith(xx):是否以xx开头

(27)boolean endsWith(xx):是否以xx结尾

	@Test
	public void test2(){
		String name = "张三";
		System.out.println(name.startsWith("张"));
	}
	
	@Test
	public void test(){
		String file = "Hello.txt";
		if(file.endsWith(".java")){
			System.out.println("Java源文件");
		}else if(file.endsWith(".class")){
			System.out.println("Java字节码文件");
		}else{
			System.out.println("其他文件");
		}
	}

替换

(29)String replace(xx,xx):不支持正则

(30)String replaceFirst(正则,value):替换第一个匹配部分

(31)String repalceAll(正则, value):替换所有匹配部分

	@Test
	public void test4(){
		String str = "hello244world.java;887";
		//把其中的非字母去掉
		str = str.replaceAll("[\\d]", "");
		System.out.println(str);
	}

拆分(分割)

(32)String[] split(正则):按照某种规则进行拆分

	@Test
	public void test4(){
		String str = "张三.23|李四.24|王五.25";
		//|在正则中是有特殊意义,我这里要把它当做普通的|
		String[] all = str.split("\\|");
		
		//转成一个一个学生对象
		Student[] students = new Student[all.length];
		for (int i = 0; i < students.length; i++) {
			//.在正则中是特殊意义,我这里想要表示普通的.
			String[] strings = all[i].split("\\.");//张三,  23
			String name = strings[0];
			int age = Integer.parseInt(strings[1]);
			students[i] = new Student(name,age);
		}
		
		for (int i = 0; i < students.length; i++) {
			System.out.println(students[i]);
		}
		
	}
	
	@Test
	public void test3(){
		String str = "1Hello2World3java4atguigu5";
		str = str.replaceAll("^\\d|\\d$", "");
		String[] all = str.split("\\d");
		for (int i = 0; i < all.length; i++) {
			System.out.println(all[i]);
		}
	}
	
	@Test
	public void test2(){
		String str = "1Hello2World3java4atguigu";
		str = str.replaceFirst("\\d", "");
		System.out.println(str);
		String[] all = str.split("\\d");
		for (int i = 0; i < all.length; i++) {
			System.out.println(all[i]);
		}
	}
	
	
	@Test
	public void test1(){
		String str = "Hello World java atguigu";
		String[] all = str.split(" ");
		for (int i = 0; i < all.length; i++) {
			System.out.println(all[i]);
		}
	}

字符串常量池

字符串常量对象可以共享的原因和好处

字符串常量对象可以共享的原因:字符串对象不可变

字符串常量对象共享的好处:节省内存

String s1 = "atguigu";
String s2 = "atguigu";
System.out.println(s1 == s2);//这里只创建了一个字符串对象"atguigu"。

s2 = s2.replace("a","o");
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);

这里s1指向的"atguigu"和s2指向的"atguigu"是同一个。
如果无法保证"atguigu"对象不可变,那么当s2将"a"替换为"o"之后,那么s1就会受到影响,这样是不安全的。
但是,现在我们发现s1并未受到影响,也就是说,s1指向的"atguigu"对象并未被修改,而是基于"atguigu"重新复制了一个新对象"atguigu",然后替换成"otguigu"。

hashCode方法

Object类有一个int hashCode()方法,该方法用于计算对象的哈希值。哈希值的作用就好比生活中的身份证号,用一串数字代表一个对象。哈希值的计算是有讲究的,按照常规协定hashCode方法和equals方法要一起重写,要求两个“相等”的对象hashCode必须相同,如果两个对象的哈希值不同,它俩调用equals方法也必须是false,但是如果两个对象的哈希值相同,它俩调用equals方法却不一定true。

    public int hashCode() {
        int h = hash;
        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;
    }
在计算哈希码时选择31作为乘数,主要是基于以下几个原因:
1. 质数特性
减少哈希冲突:31是一个质数(素数),在哈希计算中选择质数作为乘数可以减少哈希冲突的可能性。质数在乘法运算中产生的结果更分散,使得不同的输入值更有可能产生不同的哈希码,从而减少相同值产生相同哈希码的概率。
2. 合适的范围
避免溢出和过小范围:如果选择一个较小的质数(如2),那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值可能会超出int的最大范围(在Java等语言中),导致溢出。31作为一个“不大不小”的质数,能够较好地平衡这两个问题。
3. JVM优化
位运算优化:JVM里最有效的计算方式就是进行位运算。31乘以一个整数i可以优化为(i << 5) - i,即先将i左移5位(相当于乘以32),然后再减去i本身。这种优化减少了乘法运算,提高了计算效率。
4. 实验验证
低冲突率:通过大量实验验证,使用31作为乘数,在处理大量数据时(如超过50,000个英文单词),哈希值的冲突数相对较低。这进一步证明了31作为乘数的有效性。

字符串常量池

字符串常量池是一个哈希表,它里面记录了可以共享使用的字符串常量对象的地址。采用哈希表结构的目的是为了提高性能,用空间换时间。字符串对象的地址散列存储在哈希表中,虽然是散列存储,但是因为可以使用字符串对象的hashCode值快速的计算存储位置的下标,所以效率还是很高的。

image-20240718113615995

String s1 = "hello";
String s2 = "hello";

当给s1赋值"hello"时,根据"hello"的hashCode()值,计算出来index=[2],如果table[2]=null,那就把"hello"对象的字符串的地址0x7534放到table[2]中。
    
当给s2赋值"hello"时,根据"hello"的hashCode()值,计算出来index=[2],此时table[2]!=null,那就直接把"hello"的内存地址0x7534赋值给s2。
    
String s3 = "Aa";
String s4 = "BB";
当给s3赋值"Aa"时,根据"Aa"的hashCode()值,计算出来index=[6],如果table[6]=null,那就把"Aa"对象的字符串的地址0x8989放到table[6]中。
    
当给s4赋值"BB"时,根据"BB"的hashCode()值,计算出来index=[6],此时table[index]=table[6]!=null,但是"BB"和"Aa"不一样,那就直接把"BB"的内存地址0x6666也放到table[6]中,相当于table[6]中记录了两个字符串对象的地址,它们使用链表连接起来。    

不同方法创建字符串的存储位置

直接赋值字符串:存储到字符串常量池中

//s指向常量池中的引用
String s ="lizhi";  //只会存储到字符串常量池中
//创建字符串时jvm会判断字符串常量池中是否有“该字符串”如果有直接返回对象的引用。否则会创建一个新的字符串常量

image-20240801100828288

使用new 关键字的字符串

//str指向内存中的对象引用
String str =new String("lizhi");
/*
通过new关键字创建的字符串引用,字符串常量池和堆内存都会有这个对象,没有就创建,最后返回的是堆内存中的对象引用(堆中地址)。
因为有1izhi这个字面量,先去检查《字符串常量池中》是否存在该字符串
如果不存在,就直接先在字符串常量池中创建一个字符串对象,然后再去堆内存中创建一个字符串对象1izi;
如果存在,就直接去堆内存创建一个字符串对象(在堆中开辟空间),内容为1izhi(指向常量池中对象);
最后将堆内存的字符串引用返回(堆中地址)。
*/

//<<这种方式会存在两个对象>>

image-20240801100718454

使用"+"连接字符串

//s指向常量池中的引用
String s="a"+"b"+"c"; //"abc"

字符串常量+字符串常量:  字符串常量
    
字符串变量+字符串变量:
字符串常量+字符串变量:  new StringBuilder()
字符串变量+字符串常量:

字符串的intern()方法:

  • 当调用 intern 方法时,首先会在字符串常量池中判断是否有该对象引用,如果有直接** **
  • 否则,将此 String 对象添加到字符串常量池中,并返回此 String 对象的引用
  • 注意:添加到字符串常量池中,是指把堆中对象的引用添加到常量池中。
String s1 = new String("lizhi");   
s1.intern();
String s3 = "s";
System.out.println(s3 == s1); //false

image-20240801100944322

String s1 = new String("li");   
String s2 = s1 +"zhi"
String s3 = s2.intern();
System.out.println(s2 == s3); //true

image-20240801102635985

String s1 = "s".intern(); //首先将s添加到字符串常量池中,并返回引用
String s="s";
System.out.println(s==s1);  //true 

String s1 = new String("s")+new String("b"); //
s1.intern();
String s3 = "sb";
System.out.println(s3==s1); //true 

public class TestStringIntern {
    @Test
    public void test1(){
        String s1 = new String("hello");
        String s2 = s1.intern();
        System.out.println(s1 == s2);
        /*
        JDK8:false
         */
    }

    @Test
    public void test2(){
        String s1 = "he".concat("llo");
        String s2 = s1.intern();
        System.out.println(s1 == s2);
        /*
        JDK8:true
         */
    }
}

哪些字符串对象地址放入字符串常量池?

需要共享的字符串地址记录到字符串常量池的table表中

需要共享的字符串地址记录到字符串常量池的table表中,不需要共享的字符串对象其地址值不需要记录到字符串常量池的table表中。除了以下2种,其他的都不放入字符串常量池:
(1)""直接的字符串 (备注:两个""的字符串直接+,编译器处理成一个""字符串)
(2)字符串对象.intern()结果
 
其他:
(1)直接new
(2)valueOf,copyValueOf等
(3)字符串对象拼接:concat拼接 以及 字符串变量 + 拼接
(4)toUpperCase,toLowerCase,substring,repalce等各种String方法得到的字符串
这些方式,本质都是新new的。

字符串对象和字符串常量池在哪里?

字符串常量池表:

  • JDK1.6:在方法区的永久代
  • JDK1.7之后:

字符串对象:

  • JDK1.7之前:需要共享的字符串对象存储在方法区的永久代,然后把对象地址记录到字符串常量池的table表中,不需要共享的字符串对象存储在堆中,其地址值不需要记录到字符串常量池的table表中。
  • JDK1.7之后:所有字符串对象都存储在堆中。同样需要共享的字符串地址记录到字符串常量池的table表中,不需要共享的字符串对象其地址值不需要记录到字符串常量池的table表中。

StringBuilder&StringBuffer

因为String对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低。因此,JDK又在java.lang包提供了可变字符序列StringBuilder和StringBuffer类型。

StringBuilder,StringBuffer区别

StringBuffer:老的,线程安全的(因为它的方法有synchronized修饰),效率低

StringBuilder:线程不安全的,效率高

相同点: 可变的字符串对象 char[] value; 默认的长度 16

扩容方式同 int newCapacity = (value.length << 1) + 2;

常用API

常用的API,StringBuilder、StringBuffer的API是完全一致的

(1)StringBuffer append(xx):拼接,追加

(2)StringBuffer insert(int index, xx):在[index]位置插入xx

(3)StringBuffer delete(int start, int end):删除[start,end)之间字符

​ StringBuffer deleteCharAt(int index):删除[index]位置字符

(4)void setCharAt(int index, 值):替换[index]位置字符值;

(5)StringBuffer reverse():反转

(6)void setLength(int newLength) :设置当前字符序列长度为newLength

(7)StringBuffer replace(int start, int end, String str):替换[start,end)范围的字符序列为str

(8)int indexOf(String str):在当前字符序列中查询str的第一次出现下标

​ int indexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的第一次出现下标

​ int lastIndexOf(String str):在当前字符序列中查询str的最后一次出现下标

​ int lastIndexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的最后一次出现下标

(9)String substring(int start):截取当前字符序列[start,最后]

(10)String substring(int start, int end):截取当前字符序列[start,end)

(11)String toString():返回此序列中数据的字符串表示形式

String 和 StringBuffer的区别

​ 1.可变与不可变
​ String
​ StringBuffer
​ 2.内存区域
​ String 堆中 堆中的常量池
​ StringBffer 堆中
​ 3.拼接效率
​ String 低
​ StringBuffer 高

posted @ 2024-08-18 13:55  CH_song  阅读(8)  评论(0编辑  收藏  举报