递归
(菲波那切数列)
递归:方法自己调用自己的现象就称为递归。
递归的分类:
- 递归分为两种,直接递归和间接递归。
- 直接递归称为方法自身调用自己。
- 间接递归可以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值快速的计算存储位置的下标,所以效率还是很高的。
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会判断字符串常量池中是否有“该字符串”如果有直接返回对象的引用。否则会创建一个新的字符串常量
使用new 关键字的字符串
//str指向内存中的对象引用
String str =new String("lizhi");
/*
通过new关键字创建的字符串引用,字符串常量池和堆内存都会有这个对象,没有就创建,最后返回的是堆内存中的对象引用(堆中地址)。
因为有1izhi这个字面量,先去检查《字符串常量池中》是否存在该字符串
如果不存在,就直接先在字符串常量池中创建一个字符串对象,然后再去堆内存中创建一个字符串对象1izi;
如果存在,就直接去堆内存创建一个字符串对象(在堆中开辟空间),内容为1izhi(指向常量池中对象);
最后将堆内存的字符串引用返回(堆中地址)。
*/
//<<这种方式会存在两个对象>>
使用"+"连接字符串
//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
String s1 = new String("li");
String s2 = s1 +"zhi"
String s3 = s2.intern();
System.out.println(s2 == s3); //true
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 高