目录
字符char型
char变量保存的数据占两个字节,被解释为特定字符的Unicode值,支持多国语言字符。
字符类型的包装类型Character
char对应着一个Character包装类型,封装了与字符操作相关的一些功能。
下面这个例子中包括了大部分常用的方法
import java.util.Scanner;
public class UseCharacter {
public static void main(String[] args)
{
//characterBasic();
testGetCharInfo();
}
static void testGetCharInfo() {
var scanner = new Scanner(System.in);
System.out.print("输入一个字符,之后敲回车键:");
var userInput = scanner.nextLine();
//移除前后的空格
userInput = userInput.strip();
//取出第一个字符
String result = getCharInfo(userInput.charAt(0));
System.out.println(result);
}
static String getCharInfo(char c) {
String info =
"是否在Unicode标准字符集中: " + Character.isDefined(c) +
"\n是否数字: " + Character.isDigit(c) +
"\n是否可作为Java标识符的第一个字符:" +
Character.isJavaIdentifierStart(c) +
"\n是否可作为Java标识符的其他字符: " +
Character.isJavaIdentifierPart(c) +
"\n是不是字母: " + Character.isLetter(c) +
"\n是字母或者数字: " +
Character.isLetterOrDigit(c) +
"\n是小写字母: " + Character.isLowerCase(c) +
"\n是大写字母: " + Character.isUpperCase(c) +
"\n转为大写: " + Character.toUpperCase(c) +
"\n转为小写: " + Character.toLowerCase(c);
return info;
}
}
字符串String
事实上,String类内部是使用字节数据byte[](而不是char[])来保存字符信息的,这么做的原因主要是为了尽可能少地占用内存。
内存模型
按照官方的说法:Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。
JVM主要管理两种类型内存:堆和非堆,堆内存(Heap Memory)是在 Java 虚拟机启动时创建,非堆内存(Non-heap Memory)是在JVM堆之外的内存。
简单来说,非堆包含方法区、JVM内部处理或优化所需的内存(即时编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码。
Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、 anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。
虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集合,包括直接常量(string,integer和 floating point常量)和对其他类型,字段和方法的符号引用。
对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的, 对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。在程序执行的时候,常量池会储存在Method Area,而不是堆中。常量池中保存着很多String对象; 并且可以被共享使用,因此它提高了效率。
当然,如果直接使用new关键字创建字符串对象时,虽然值一致(都是“Hello”),但仍然是两个独立的对象。
static void stringPool() {
String s0 = "Hello";
String s1 = "Hello";
String s2 = "He" + "llo";
System.out.println(s0 == s1);//true
System.out.println(s0 == s2);//true
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(s3 == s4); // false
}
再来一个栗子
static void stringAssign() {
String s1 = "a";
String s2 = s1;
System.out.println(s1 == s2); //true
s1 += "b";
System.out.println(s1 == s2); //false
System.out.println(s1 == "ab"); //false
System.out.println(s1.equals("ab")); //true
}
“==”运算符用于比较两个字符串变量是否引用同一个字符串对象。
示例中用s1给字符串变量s2赋值意味着:两个变量(s1,s2)现在引用同一个字符串对象“a”,所以,s1 == s2 返回 true。
String对象的内容是只读的,示例中使用“+=”修改s1变量的值,实际上是得到了一个新的字符串对象,其内容为“ab”,而s2引用的还是原来的“a”,所以,这时,s1 == s2 返回 false。
另外,代码中的“ab”字符串是一个常量,它所代表的字符串对象与使用“+=”生成的并由s1所引用的“ab”对象无关,是两个独立的对象。
String.equals()方法可以比较两个字符串的内容(区分大小写),类似地,有一个equalsIgnoreCase()方法,比较时不理会大小写。
字符串常量池的一些理解
为什么要有字符串常量池?
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,大量频繁的创建字符串,极大程度地影响程序的性能
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化,所以为字符串开辟一个字符串常量池,类似于缓存区
创建字符串常量时,首先检查字符串常量池是否存在该字符串
字符串常量池中是不会存储相同内容的字符串的
常量池就类似一个JAVA系统级别提供的缓存
字符串的拼接操作
因为String的不可变性,那么字符串的拼接操作之后的字符串是在字符串常量池呢,还是是个对象存储在堆上呢?先给出结论
常量与常量的拼接结果在常量池,原理是编译期优化
只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
举个栗子
下面操作一共创建了几个对象
String str1 = new String("1"); //2个(不包括引用类型)
String str2 = new String("1") + new String("2"); //6个(不包括引用类型)
String str1 = new String("1");
这一行代码创建了两个字符串对象:
- 一个是
"1"
的字面量,在字符串池中(常量池)存在。- 另一个是通过
new String()
创建的String
对象,这个对象在堆内存中。因此,总共有 2个字符串对象(不包括引用类型)。
String str2 = new String("1") + new String("2");
这一行代码的情况更复杂,涉及多个字符串对象的创建:
new String("1")
生成一个在堆内存中的字符串对象。new String("2")
生成另一个在堆内存中的字符串对象。new String("1") + new String("2")
的结果是一个新的字符串对象,这个对象也是在堆内存中生成的。因此,这里我们有以下几个对象:
"1"
字面量(常量池)— 1个new String("1")
(堆内存)— 1个"2"
字面量(常量池)— 1个new String("2")
(堆内存)— 1个new String("1") + new String("2")
的结果(堆内存)— 1个总共为 6个对象(不包括引用类型)。这里的计数包括常量池和堆中的字符串对象。
字符串的构造方法
//展示字符串的构造方法
static void stringConstructors() {
char charArray[] = {'b', 'i', 'r', 't', 'h', ' ',
'd', 'a', 'y'};
byte byteArray[] = {(byte) 'n', (byte) 'e', (byte) 'w',
(byte) ' ', (byte) 'y', (byte) 'e',
(byte) 'a', (byte) 'r'};
String s, s1, s2, s3, s4, s5, s6, s7, s8, s9, output;
s = new String("hello");
//分配缓冲区
var buffer = new StringBuffer("Welcome to Java Programming!");
// 字符串对象的多种构造方法
s1 = new String(); //缺省构造方法,可被简化为直接赋值空串
s2 = new String(s); //拷贝构造方法
s3 = new String(charArray); //以字符数组初始化字串
//从字符数组第6个元素起取3个字符
s4 = new String(charArray, 6, 3);
s5 = new String(byteArray); //以字节数组初始化字串
//从字节数组第4个元素起取4个字节
s6 = new String(byteArray, 4, 4);
//从StringBuffer对象中提取字符数据创建字串(注意,是复制)
s7 = new String(buffer);
//使用valueOf()系列方法,可以很方便地将各种类型的数据直接转换为字符串
//将字符数组转换为字符串
s8 = String.valueOf(charArray);
//将double数值转换为字符串
s9 = String.valueOf(100.0d);
output = "s1 = " + s1 +
"\ns2 = " + s2 +
"\ns3 = " + s3 +
"\ns4 = " + s4 +
"\ns5 = " + s5 +
"\ns6 = " + s6 +
"\ns7 = " + s7 +
"\ns8 = " + s8 +
"\ns9 = " + s9;
System.out.println(output);
}
字符串转换为字符数组
//从字符串中提取字符数组
static void stringAndArray() {
String str = "你好,中国!";
//将整个字符串中的所有字符都提取出来
char[] charArray = str.toCharArray();
//输出字符数组的内容
printStringArray(charArray);
char[] chars = new char[2];
//从字符串中提取字符,构建字符数组
//四个参数的含义
//1.被拷贝字符在字串中的起始位置
//2.被拷贝的最后一个字符在字串中的下标再加1
//3.目标字符数组
//4.拷贝的字符放在字符数组中的起始下标
str.getChars(3, 5, chars, 0);
printStringArray(chars);
}
//打印字符数组的内容
static void printStringArray(char[] chars) {
String output = "";
for (int i = 0; i < chars.length; i++)
output += "[" + i + "] " + chars[i] + "\n";
System.out.println(output);
}
字符串的拼接
//重复拼接子串,生成新字符串
static void stringRepeat() {
String a5 = "a".repeat(5);
//生成:aaaaa
System.out.println(a5);
}
//字符串的拼接
static void stringConcat() {
String s1 = "Happy ", s2 = "Birthday";
//使用concat方法将两个字符串连接为一个新的字符串
System.out.println(s1.concat(s2)); //Happy Birthday
//也可以使用“+”达到相同的目的
System.out.println(s1 + s2); //Happy Birthday
//"+"运算符可以将字符串与任意一个对象相拼接
System.out.println("99+1=" + (99 + 1)); //99+1=100
//当前时间:2020-10-01
System.out.println("当前时间:" + LocalDate.now());
}
拼接之后,得到的是一个新的字符串,并不是修改原有的字符串实现的。
填充字符串
使用String.format方法,可以定义一个字符串模板,在运行时,将特定变量的值“填入”到模板中,动态地构建出一个字符串。
//字符串的“填充”
static void stringFill() {
var item = "Shirt";
var size = "M";
var price = 14.99;
var color = "Red";
var template = "Clothing item: %s, size %s, color %s, $%.2f";
var itemString = String.format(template,
item, size, color, price);
//Clothing item: Shirt, size M, color Red, $14.99
System.out.println(itemString);
}
String.format()方法的第一个参数是字符串模板,其中%打头的是可以被“填入”值的“空”,%s表示这里需要一个字符串,%.2f表示这里需要一个保留两位小数的float数值。
移除空白字符
//移除首尾的空白字符(JDK11)
static void stringStrip() {
String example = " 前后都有空白字符的字符串 ";
System.out.println("|" + example.strip() + "|");
System.out.println("|" + example.stripLeading() + "|");
System.out.println("|" + example.stripTrailing() + "|");
}
在实际开发中,遇到需要用户从键盘输入的场景时,建议调用strip()方法移除字符前后的空白字符。
多行文本块——TextBlock
多行文本块特别适合于输出大段文字。
空串与空白串的判断
//判断字符串是否为空
static void stringIsBlankOrEmpty() {
var str = ""; //空串,其长度为0
//isBlank: JDK11引入,用于检测字符串是否仅由
// white space(即空白字符)构成
System.out.println(str.isBlank());//true
//isEmpty(),JDK 6引入,仅用于检测空串
System.out.println(str.isEmpty());//true
str = " "; //全部由空格组成的字符串
System.out.println(str.isBlank());//true
System.out.println(str.isEmpty());//false
//Tab,也算作空白字符
str = " ";
System.out.println(str.isBlank());//true
System.out.println(str.isEmpty());//false
str = "\n"; //用于换行的格式控制字符,也算是空白符
System.out.println(str.isBlank());//true
System.out.println(str.isEmpty());//false
}
hashCode方法
只要两对象的“内容”相等,其hashCode值也应该相等
static void stringHashCode() {
String s1 = "hello",
s2 = "Hello",
s3 = new String(s2);
//hello的hashCode=99162322
System.out.println(s1 + "的hashCode=" + s1.hashCode());
//s2与s3引用不同的字符串对象
System.out.println(s2 == s3); //false
//s2与s3所引用字符串对象的值相等
System.out.println(s2.equals(s3)); //true
//Hello的hashCode=69609650
System.out.println(s2 + "的hashCode=" + s2.hashCode());
//Hello的hashCode=69609650
System.out.println(s3 + "的hashCode=" + s3.hashCode());
}
字符串比较-1:compareTo
compareTo:使用字典法进行比较,返回0表两字串相等,小于返回负值,大于返回正值。
static void compareTest(){
var s1 = new String("hello");
var s2 = new String("hello1");
var output = "s1.compareTo(s2) = " + s1.compareTo(s2) +
"\ns1.compareTo(s2) = " + s2.compareTo(s1);
System.out.println(output);
}
示例中从字典序来看:s1<s2,所以第一个输出的是负数,第二个是正数。
字串的比较-2:regionMatches
比较两字符串中的某一部分是否相等。
static void compareTest2(){
var s1 = new String("hello2");
var s2 = new String("hello1");
//比较两字串中的某一部分是否相等
//regionMatches方法参数的含义如下:
//1.调用这个方法的字串中的起始下标
//2.要比较的字串
//3.要比较的字串的起始下标
//4.要比较的字串比较的字符长度
if(s1.regionMatches(0, s2,0, 5)){
System.out.println("s3 和 s4 的前5个字符匹配\n");
}
else{
System.out.println("s1 和 s1 的前5个字符不匹配");
}
}
字串查找-1:indexOf
在字串查找字符或子串,调用 indexOf 和 lastIndexOf方法即可。
indexOf是从参数的位置开始正向搜索
lastIndexOf是从参数的位置开始反向搜索
static void stringIndex() {
String letters = "abcdefghijklmabcdefghijklm";
System.out.println(letters.indexOf('c')); //2
//第2个参数是开始查找的起始下标
System.out.println(letters.indexOf('c', 1));//2
System.out.println(letters.indexOf('$'));//-1
System.out.println(letters.lastIndexOf("lmabc"));//15
System.out.println(letters.lastIndexOf('m', 5));//-1
}
字串查找-2:
查询字串是否以某字串开头和结尾:startsWith 和 endWith方法。
static void stringStartEnd() {
String[] strings = {"started", "starting",
"ended", "ending"};
for (String string : strings) {
if (string.startsWith("st"))
System.out.println(string + "以st开头");
}
for (String string : strings) {
if (string.endsWith("ed"))
System.out.println(string + "以ed结尾");
}
}
提取子串
substring() 方法返回字符串的子字符串。
语法
public String substring(int beginIndex) 这种情况是从起始索引到最后 或 public String substring(int beginIndex, int endIndex)
参数
-
beginIndex -- 起始索引(包括), 索引从 0 开始。
-
endIndex -- 结束索引(不包括)。
//提取子串
static void stringSlice() {
String letters = "abcdefghijklmabcdefghijklm";
//输出:hijklm
System.out.println(letters.substring(20));
//输出:bcdef
//(起始下标,要拷贝子串的最后下标加1但不包括该下标)
System.out.println(letters.substring(1, 6) );
}
可修改字符串(StringBuffer)
前面我们己经知道,String对象创建后,它的内容是不可改的,如果确实需要一个可以修改内容的字符串,可以使用StringBuffer。
StringBuffer对象其内容可以修改,其占用的空间能自动增长
可以调用StringBuffer.append()方法不断地向StringBuffer对象“追加”新的内容,追加完毕之后,再调用它的toString()方法得到最终的字符串。在整个字符串构建过程中,都是在同一块内存区域中完成的。
在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,所以如果需要对字符串进行修改推荐使用 StringBuffer。
StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。
StringBuffer对象的创建
static void stringBufferConstructors() {
//常用的三种StringBuffer构造方法
var buf1 = new StringBuffer();
var buf2 = new StringBuffer(10);
var buf3 = new StringBuffer("hello");
//使用toString()方法将字符串缓冲区中的字符提取出来
System.out.println(buf1.toString());
System.out.println(buf2.toString());
System.out.println(buf3.toString());
}
向StringBuffer中追加数据
static void stringBufferAppend() {
var buf = new StringBuffer();
//append()方法有多种重载形式,可接收多种类型的数据
buf.append("hello\n");
char[] charArray = {'a', 'b', 'c', 'd', 'e', 'f'};
buf.append(charArray);
buf.append("\n");
buf.append(charArray, 0, 3);
//append()方法支持级联调用
buf.append("\n").append(true).append("\n")
.append('Z').append("\n").append(100);
//输出最终结果
System.out.println(buf.toString());
}
当你查看append方法的源码时,你会看到它返回值是this,即这个对象本身,所以可以使用级联调用。
操作缓冲区中的字符
setCharAt() , getChars() , reverse()
static void stringBufferChars() {
var buf = new StringBuffer("hello there");
System.out.println("\nCharacter at 0: " + buf.charAt(0));
buf.setCharAt(0, 'H');
buf.setCharAt(6, 'T');
System.out.println(buf.toString());
//提取字符串缓冲区中的字符到字符数组中
var charArray = new char[buf.length()];
//(起始下标,长度,字符数组名,起始下标)
buf.getChars(0, buf.length(), charArray, 0);
for (int i = 0; i < charArray.length; ++i)
System.out.print(charArray[i]+",");
System.out.println();
//reverse:倒转字串
buf.reverse();
System.out.println(buf.toString());
}
StringBuffer的容量与长度
• length:当前字符个数
• capacity:不另外分配内存可以存放的字符个数
static void stringBufferCapLen() {
var buf = new StringBuffer("Hello,");
//length = 6
System.out.println( "length = " + buf.length());
//capacity = 22
System.out.println("capacity = " + buf.capacity());
//追加一个新的字符串
buf.append("how are you?");
//输出:Hello,how are you?
System.out.println(buf.toString());
//length = 18
System.out.println( "length = " + buf.length());
//capacity = 22
System.out.println("capacity = " + buf.capacity());
//ensureCapacity:保证StringBuffer的最小容量
buf.ensureCapacity(75);
//New capacity = 75
System.out.println("New capacity = " + buf.capacity());
//setLength:增大或减小StringBuffer的长度
buf.setLength(10);
System.out.println("New length = " + buf.length());
//原有的内容被截断,输出:Hello,how
System.out.println(buf.toString());
}
StringTokenizer类:定界符分割
StringTokenizer 是Java的一个实用工具类,属于 java.util 包,主要用于将字符串分割成一系列的标记(tokens)。它可以使用默认的分隔符(如空格、制表符、换行符等)或者指定的分隔符来分割字符串。此外,StringTokenizer还允许选择是否返回分隔符作为标记。
StringTokenizer的构造方法
StringTokenizer类提供了三种构造方法,以适应不同的使用场景:
// 使用默认分隔符(空格、制表符、换行符等)构造StringTokenizer对象
StringTokenizer st1 = new StringTokenizer("Hello Runoob How are you");
// 使用指定的分隔符构造StringTokenizer对象
StringTokenizer st2 = new StringTokenizer("JAVA : Code : String", ":");
// 使用指定的分隔符构造StringTokenizer对象,并决定是否返回分隔符
StringTokenizer st3 = new StringTokenizer("JAVA : Code : String", ":", true);
StringTokenizer的常用方法
StringTokenizer类提供了一系列方法来操作分割后的字符串,包括:
// 计算剩余标记的数量
int count = st.countTokens();
// 检查是否还有更多的标记
boolean hasMore = st.hasMoreTokens();
// 获取下一个标记
String token = st.nextToken();
// 使用指定的分隔符获取下一个标记
String tokenWithDelim = st.nextToken(":");
实际应用示例
以下是使用StringTokenizer类的两个示例,展示了如何使用不同的构造方法和操作方法来处理字符串:
// 示例1: 使用逗号作为分隔符
String str = "runoob,google,taobao,facebook,zhihu";
StringTokenizer st = new StringTokenizer(str, ",");
while(st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
// 示例2: 使用空格和冒号作为分隔符,并返回分隔符
StringTokenizer st = new StringTokenizer("JAVA : Code : String", " :", true);
while(st.hasMoreTokens()) {
System.out.println(st.nextToken());
}