根据视频 尚硅谷Java零基础全套视频教程(宋红康2023版,java入门自学必备) 整理,视频对应资料:
百度网盘:https://pan.baidu.com/s/1bLXVIeh61RFuu5uToCJmeQ?pwd=yyds 提取码: yyds
阿里云盘:https://www.aliyundrive.com/s/tAHuEK8vmmM(教程配套资料请从百度网盘下载)
Java SE 相关文章总结整理归纳于:https://www.yuque.com/u27599042/cda39w
String 类的声明
public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc { ...... }
- final:String 是不可被继承的
- Serializable:可序列化的接口,凡是实现此接口的类的对象,表示支持序列化,可以通过网络或本地流进行数据的传输。
- Comparable:实现此接口的类,其对象都可以比较大小。
- Constable 与 ConstantDesc 在 JDK 12 中新增,位于 JDK 12 新增的 java.lang.constant 包中
- Constable:实现此接口的类,其对象是可以在常量池中表示的常量
- ConstantDesc:在此接口中,定义了对常量的名义描述符
String 内部声明的属性
value
JDK 8 中
private final char value[];
- 在 JDK 8 中,字符串采用的是 char 数组进行存储
- 用于存储字符串的 char 数组为 final,所以字符串一旦初始化,字符串指向的用于存储的 char 数组就不能再进行修改,因此字符串具有不可变性
- 但是,字符串类型的变量对于字符串的指向是可以修改,不能修改的是字符串的值,即真正存储字符串的 value 数组的值
JDK 9 开始
private final byte[] value;
- 从 JDK 9 开始,字符串中真正用于存储字符串的 value 数组修改为了 byte 类型的数组
- 将 char 类型数组修改为 byte 类型数组,是为了节省内存空间,优化所占用的内存空间
- 因为经过统计,在 value 数组中存储字符的值大部分为使用 ASCII 编码就可以表示字符,即大部分情况下存储的是使用一个字节(byte)空间就可以存储的字符,而一个 char 类型的数组会占用两个字节的空间,所以将 char 类型修改 byte 类型,可以节省一半的内存空间。
- 如果对于 value 数组中存储的字符为中文字符,则其仍然使用两个字节的内存空间继续存储表示
字符串常量的存储位置
- 字符串常量都存储在字符串常量池(String Table)中
- 在字符串常量池中不允许存放两个相同的字符串常量。
- 字符串常量池,在不同的 jdk 版本中,存放位置不同。
- jdk7 之前:字符串常量池存放在方法区
- jdk7 及之后:字符串常量池存放在堆空间。
- 将字符串常量池的存放位置从方法区中移动到堆中,主要是为了 GC 可以更好的进行垃圾回收。
- 由于在方法区中存放者加载到虚拟机内存中的类,而类基本上都被引用,很少进行垃圾回收,GC 执行较少,所以如果将字符串常量池放在方法区中,会导致字符串常量越来越多,占用内存空间越来越大。为了使无用的字符串常量能够尽可能被 GC 回收,所以将字符串常量池移动到了堆中
- 在堆空间中,存放我们创建的对象,GC 执行垃圾回收较频繁
- 在 JDK 8 中,将方法区命名修改为元空间。因为方法区中的类等资源基本上不被 GC 回收,几乎一直占用内存空间,所以在 JDK 8 中让方法去直接使用宿主机的内存空间了,因此命名修改为元空间
String 的不可变性
使用相同字面量为不同变量赋值
- 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串存储在字符串常量池当中,且字符串常量池当中不会存储两个相同的字符串,即如果给两个字符串变量赋值相同的字符串字面量,都是将字符串常量池当中同一个字符串常量的首地址赋值给字符串变量。
使用字面量修改字符串变量的值
- 如果将一个新的字符串字面量赋值给原先已有字符串字面量的字符串变量,即为字符串变量重新以字符串字面量的方式赋值,则会在字符串常量池当中新创建一个字符串字面量,并将新的字符串字面量的首地址赋值给字符串变量,即重新指定内存区域赋值。
- 因为真正用于存储字符串字符的数组为 final,字符串具有不可变性,所以要修改字符串只能重新创建新的字符串字面量(如果字符串字面量在字符串常量池中不存在)
- 以字符串字面量为字符串变量赋值不能在原先的字符串字面量上进行修改,体现了字符串的不可变性。
使用字符串连接操作修改字符串
- 如果对现有的字符串进行连接操作,一样不能在原有的字符串字面量上进行修改(字符串的不可变性),需要重新指定内存区域赋值
- 当对现有的字符串进行拼接操作时,需要重新开辟空间保存拼接以后的字符串,不能在原有的位置修改
- 新的字符串存储在堆空间中,是由字符串的拼接操作(字符串变量与字符串常量进行拼接),底层会调用 StringBuilder 的 append 方法在原有字符串的后面追加需要拼接的字符串,拼接完成后,会调用 StringBuilder 的 toString 方法生成新的字符串,而该方法是通过 new 生成新的字符串
- 通过 new 创建的对象都会存放在堆空间中
public String toString() { // Create a copy, don't share the array return isLatin1() ? StringLatin1.newString(value, 0, count) : StringUTF16.newString(value, 0, count); }
- 实际上,真正存储字符串字符的数组仍然存放在方法区中
使用 replace 方法修改字符串的值
- 如果修改了原先字符串的某个字符,一样不会在原有的字符串字面量上进行修改(字符串的不可变性),而是会新创建一个字符串,新字符串的值为修改后的字符串,然后将新字符串的地址赋值给字符串变量。
public class Test { public static void main(String[] args) { String s1 = "abc"; String s2 = s1.replace("a", "m"); System.out.println(s1==s2); } }
- 当调用字符串的replace()替换现有的某个字符时,需要重新开辟空间保存修改以后的字符串,不能在原有的位置修改
- 新创建的字符串位于堆空间中,是由于 replace 方法返回的新字符串是通过 new 出来的,通过 new 创建的对象都会存放在堆空间中
- 实际上,真正存储字符串字符的数组仍然存放在方法区中
String 实例化的两种方式
String 实例化的两种方式
- String 实例化有两种方式:
- 第 1 种方式:String s1 = “hello”;
- 第 2 种方式:String s2 = new String(“hello”);
- 通过字符串字面量的方式,实例化一个字符串对象,会在常量池中创建一个字符串字面量,该字符串字面量中 value 属性会指向在常量池中创建的真正用于存储字符串字面量字符的数组
- 通过使用 new+构造器 的方式为字符串变量赋值,是先在堆中创建 String 的对象,然后将堆中 String 对象的地址赋值给字符串变量,堆中 String 对象的 value 属性会指向字符串常量池当中对应的真正用于存储字符串字面量字符的数组
String s = new String(“abc”) 内存中创建了几个对象
- 使用 String s = new String(“abc”) 创建对象,在内存中会创建两个对象,一个对象是在堆中通过new+构造器创建的String对象,另一个是堆中 String 对象的 value 属性指向的字符串常量池当中对应的真正用于存储字符串字面量字符的数组:“abc”。
练习
System.out.println(s1 == s2); // true // 由于s1和s2都是通过字符串字面量进行赋值, // 且s1和s2赋值的字符串字面量都是同一个, // 所以s1和s2都是指向方法区字符串常量池当中的“JavaEE”的首地址 // 所以 s1 == s2 为 true System.out.println(s1 == s3); // false System.out.println(s1 == s4); // false // 由于s3和s4都是通过new+构造器的方式进行赋值的, // 所以s3和s4都是指向堆中相应的String对象的地址, // 不为字符串常量池当中“JavaEE”的首地址 // 所以 s1 == s3 s1 == s4 为 false System.out.println(s3 == s4); // false // 由于s3和s4都是通过new+构造器的方式进行赋值的, // 所以s3和s4都是指向堆中相应的String对象的地址, // 由于每次new都会创建一个新的对象, // 所以s3和s4指向堆中String对象的地址不同 // 所以 s3 == s4 为 false
public class Test { public static void main(String[] args) { Person p1 = new Person("Tom", 12); Person p2 = new Person("Tom", 12); System.out.println(p1.name == p2.name); // true // p1和p2都是通过new+构造器的方式创建的对象, // 会先在堆中创建p1和p2对象, // 由于p1.name p2.name的赋值是使用字面量的方式进行赋值的,且赋值为同一个字符串字面量 // 所以p1.name p2.name都是指向字符串常量池中同一个字符串字面量 // 因此p1.name == p2.name 为 true System.out.println(p1.hashCode()); System.out.println(p2.hashCode()); System.out.println(p1.name.hashCode()); System.out.println(p2.name.hashCode()); } } class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } }
String 的连接操作 +
常量 + 常量
- 此时的常量可能是字面量,也可能是 final 修饰的常量
- 使用 final 修饰的字符串常量,因为是常量所以确定其未来不会进行修改,编译时会进行优化,字符串常量就相当于常量池中的字符串字面量
- 常量 + 常量 结果仍然存储在字符串常量池中,返回此字面量的地址。
- 常量 + 常量 是在字符串常量池中创建两个字符串常量拼接结果对应的字符串字面量,然后将新创建的字符串字面量地址返回
@Test public void test4(){ // 使用 final 修饰的字符串常量, // 因为是常量所以确定其未来不会进行修改, // 编译时会进行优化, // 字符串常量就相当于常量池中的字符串字面量 final String s1 = "hello"; final String s2 = "world"; String s3 = "helloworld"; String s4 = "hello" + "world"; String s5 = s1 + "world"; String s6 = "hello" + s2; String s7 = s1 + s2; System.out.println(s3 == s5);//true System.out.println(s3 == s6);//true System.out.println(s3 == s7);//true }
常量 + 变量 或 变量 + 变量
- 常量 + 变量 或 变量 + 变量 都会通过 new 的方式创建一个新的字符串,返回堆空间中此字符串对象的地址
- 底层会调用 StringBuilder 的 append 方法在原有字符串的后面追加需要拼接的字符串,拼接完成后,会调用 StringBuilder 的 toString 方法生成新的字符串,而该方法是通过 new 生成新的字符串
- 通过 new 创建的对象都会存放在堆空间中
调用字符串的intern()
- 调用字符串的 intern() 返回的是字符串常量池中字面量的地址
@Test public void test3(){ String s1 = "hello"; String s2 = "world"; String s3 = "helloworld"; String s4 = "hello" + "world"; //通过查看字节码文件发现调用了StringBuilder的toString()---> new String() String s5 = s1 + "world"; String s6 = "hello" + s2; String s7 = s1 + s2; System.out.println(s3 == s4);//true System.out.println(s3 == s5);//false System.out.println(s3 == s6);//false System.out.println(s3 == s7);//false System.out.println(s5 == s6);//false System.out.println(s5 == s7);//false System.out.println(); String s8 = s5.intern(); //intern():返回的是字符串常量池中字面量的地址 System.out.println(s3 == s8);//true }
concat(xxx)
- 不管是常量调用此方法,还是变量调用,同样不管参数是常量还是变量,总之,调用完concat()方法都返回一个新new的对象。
- 通过 new 创建的对象都会存放在堆空间中
@Test public void test5(){ String s1 = "hello"; String s2 = "world"; String s3 = s1.concat(s2); String s4 = "hello".concat("world"); String s5 = s1.concat("world"); System.out.println(s3 == s4);//false System.out.println(s3 == s5);//false System.out.println(s4 == s5);//false }
// concat 源码 public String concat(String str) { if (str.isEmpty()) { return this; } return StringConcatHelper.simpleConcat(this, str); } static String simpleConcat(Object first, Object second) { ... return newString(buf, indexCoder); } static String newString(byte[] buf, long indexCoder) { // Use the private, non-copying constructor (unsafe!) if (indexCoder == LATIN1) { return new String(buf, String.LATIN1); } else if (indexCoder == UTF16) { return new String(buf, String.UTF16); } else { throw new InternalError("Storage is not completely initialized, " + (int)indexCoder + " bytes left"); } }
String 的构造器和常用方法
构造器
public String()
:初始化新创建的 String对象,以使其表示空字符序列。public 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。
常用方法
🍅 int length()
- int length():返回字符串的长度
- return value.length
String s = "abc"; System.out.println(s.length());
🍅 char charAt(int index)
- char charAt(int index): 返回某索引处的字符
- return value[index]
String s = "abc"; System.out.println(s.charAt(0)); System.out.println(s.charAt(2)); // 索引不能越界 // java.lang.StringIndexOutOfBoundsException: String index out of range: 3 // System.out.println(s.charAt(3));
🍅 boolean isEmpty()
- boolean isEmpty():判断是否是空字符串
- return value.length == 0
- 通过String的length是否为0进行判断
String s = "abc"; System.out.println(s.isEmpty()); String s1 = ""; System.out.println(s1.isEmpty());
🍅 String toLowerCase()
- String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
String s = "AbC"; // 由于String的不可变性,不会修改原来的字符串,会生成一个新字符串进行返回 System.out.println(s.toLowerCase());
🍅 String toUpperCase()
- String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
String s = "abc"; // 由于String的不可变性,不会修改原来的字符串,会生成一个新字符串进行返回 System.out.println(s.toUpperCase());
🍅 String trim()
- String trim():返回字符串的副本,忽略前导空白和尾部空白
String s = " A b C "; System.out.println(s); // 由于String的不可变性,不会修改原来的字符串,会生成一个新字符串进行返回 System.out.println(s.trim());
🍅 boolean equals(Object obj)
- boolean equals(Object obj):比较字符串的内容是否相同
String s = "AbC"; System.out.println(s.equals(new String("AbC"))); System.out.println(s.equals(new String("Abc")));
🍅 boolean equalsIgnoreCase(String anotherString)
- boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
String s = "AbC"; System.out.println(s.equalsIgnoreCase(new String("abC"))); System.out.println(s.equalsIgnoreCase(new String("Abc")));
🍅 String concat(String str)
- String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
String s = "AbC"; System.out.println(s); System.out.println(s.concat("def"));
🍅 int compareTo(String anotherString)
- int compareTo(String anotherString):比较两个字符串的大小
String s = "abc"; System.out.println(s.compareTo("abc")); System.out.println(s.compareTo("abe")); System.out.println(s.compareTo("abb")); // 从左向右一个一个进行比较 // 遇见不相等的字符时,返回当前字符串的当前字符ASCII码-指定字符串的当前字符ASCII码 // 返回值大于0,当前字符串大;返回值小于0,当前字符串小;返回值等于0,相等 // 如果字符一样,则采用字符串的长度进行比较 // 源码 // int k = 0; // while (k < lim) { // char c1 = v1[k]; // char c2 = v2[k]; // if (c1 != c2) { // return c1 - c2; // } // k++; // } // return len1 - len2;
🍅 String substring(int beginIndex)
- String substring(int beginIndex):返回一个新的字符串,它是此字符串的从- beginIndex开始截取到最后的一个子字符串。
String s = "abcdefg"; System.out.println(s.substring(3));
🍅 String substring(int beginIndex, int endIndex)
- String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
String s = "abcdefg"; System.out.println(s.substring(3, 5));
🍅 boolean endsWith(String suffix)
- boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
String s = "abcdefg"; System.out.println(s.endsWith("fg"));
🍅 boolean startsWith(String prefix)
- boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
String s = "abcdefg"; System.out.println(s.startsWith("fg")); System.out.println(s.startsWith("ab"));
🍅 boolean startsWith(String prefix, int toffset)
- boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
String s = "abcdefg"; System.out.println(s.startsWith("cd", 2));
🍅 boolean contains(CharSequence s)
- boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
String s = "abcdefg"; System.out.println(s.contains("a")); System.out.println(s.contains("abc")); System.out.println(s.contains("aaaa"));
🍅 int indexOf(String str)
- int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
- 指定的子字符串存在字符串内,返回第一次出现的索引;否则返回-1
String s = "abcdefg"; System.out.println(s.indexOf("cde")); System.out.println(s.indexOf("cda"));
🍅 int indexOf(String str, int fromIndex)
- int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
- 未找到返回-1
String s = "abcdefg"; System.out.println(s.indexOf("cde", 4));
🍅 int lastIndexOf(String str)
- int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引(子字符串首个字符在字符串中的索引,从右向左找)
- 未找到返回-1
String s = "abcdefgcde"; System.out.println(s.lastIndexOf("cde"));
🍅 int lastIndexOf(String str, int fromIndex)
- int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
- 未找到返回-1
String s = "abcdefgcde"; System.out.println(s.lastIndexOf("cde")); System.out.println(s.lastIndexOf("cde", 4));
- 什么情况下,indexOf(str)和lastIndexOf(str)返回值相同?
- 情况一:存在唯一的一个str。
- 情况二:不存在str
🍅 String replace(char oldChar, char newChar)
- String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
String s1 = "h-e-l-l-o w-o-r-l-d"; String s2 = s1.replace('-', '='); System.out.println(s1); System.out.println(s2);
🍅 String replace(CharSequence target, CharSequence replacement)
- String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列,替换此字符串所有匹配字面值目标序列的子字符串。
String s1 = "h-e-l-l-o w-o-r-l-d hi-hi"; String s2 = s1.replace("hi", "hello"); System.out.println(s1); System.out.println(s2);
🍅 String replaceAll(String regex, String replacement)
- String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
String s1 = "12h-12e-12l-12l-12o 12w-12o-12r-12l-12d hi-hi"; // 将所有的数字使用 - 进行替换 String s2 = s1.replaceAll("\\d+", "-"); System.out.println(s1); System.out.println(s2);
🍅 String replaceFirst(String regex, String replacement)
- String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
String s1 = "12h-12e-12l-12l-12o 12w-12o-12r-12l-12d hi-hi"; // 将所有的数字使用 - 进行替换 String s2 = s1.replaceFirst("\\d+", "==="); System.out.println(s1); System.out.println(s2);
🍅 boolean matches(String regex)
- boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。
String s1 = "12-123456"; String s2 = "123456789"; // 字符串的格式是否满足 12- 开头,且后面有6位数字 boolean matches1 = s1.matches("12-\\d{6}"); boolean matches2 = s2.matches("12-\\d{6}"); System.out.println(s1); System.out.println(matches1); System.out.println(s2); System.out.println(matches2);
🍅 String[] split(String regex)
- String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
String s1 = "123-456-789"; // 根据 - 对字符串进行拆分 String[] strings = s1.split("-"); System.out.println(Arrays.toString(strings));
🍅 String[] split(String regex, int limit)
- String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
String s1 = "123-456-789"; // 根据 - 对字符串进行拆分 // 最多拆分成 2 个子字符串 String[] strings = s1.split("-", 2); System.out.println(Arrays.toString(strings));
String 与基本数据类型的转换
String转换成基本数据类型
- 调用包装类的静态方法:parseXxxx(str)
String s1 = "123"; int i = Integer.parseInt(s1); System.out.println(i);
基本数据类型转换成String
- 调用String重载的valueOf(xxx)
int i = 10000; String s = String.valueOf(i); System.out.println(s);
- 通过字符串拼接将基本数据类型隐式转换为字符串
String s = 10000 + ""; System.out.println(s);
字符串转换为字符数组char[]
- 字符串转换为字符数组:调用String的toCharArray()
String s = "abc123123"; char[] chars = s.toCharArray(); System.out.println(Arrays.toString(chars));
字符数组char[]转换为字符串
- 字符数组转换为字符串:调用String的构造方法
String s = "abc123123"; char[] chars = s.toCharArray(); System.out.println(Arrays.toString(chars)); String s1 = new String(chars); System.out.println(s1);
字符串转换为字节数组byte[]
- 字符串转换为字节数组:调用String的getBytes()方法
String s = "abc123123你好"; // 使用默认的字符集编码 // UTF-8中一个中文占三个字节 byte[] bytes = s.getBytes(); System.out.println(Arrays.toString(bytes)); // 指定字符集编码 // GBK中一个中文占2个字节 byte[] bytes1 = s.getBytes("gbk"); System.out.println(Arrays.toString(bytes1));
字节数组byte[]转换为字符串
- 字节数组转换为字符串:调用String的构造器
- 注意:解码与编码选择的字符集必须一致,否则会由于编码与解码字符集不一致导致乱码
String s = "abc123123你好"; // 使用默认的字符集编码 // UTF-8中一个中文占三个字节 byte[] bytes = s.getBytes(); System.out.println(Arrays.toString(bytes)); // 指定字符集编码 // GBK中一个中文占2个字节 byte[] bytes1 = s.getBytes("gbk"); System.out.println(Arrays.toString(bytes1)); // 没有指定字符集编码,使用默认的字符集编码 String s1 = new String(bytes); System.out.println(s1); // 指定字符集编码 String s2 = new String(bytes1, "gbk"); System.out.println(s2);