键盘敲烂,月薪过万作业不做,等于没学
当前系列: J&C 所有题目 添加题目 修改

字符串(String/string)是非常特别的,值得单独一讲。

首先我们要知道,它是由class定义的引用类型。(演示转到定义&复习:值类型和引用类型

但是,字符串的一系列行为表现得就像值类型一样!


比较

一般来说,如果是(演示)

  • 引用类型,==运算符会比较两个对象的堆地址;但
  • 值类型,==运算符直接比较两个对象的值

我们看看string的比较:

String fg = "飞哥";
String dfg = "飞哥";
//String dfg = new String("飞哥");

System.out.println(fg == dfg);

如果没有new String()捣乱,我们可以简单(但实际上错误)的理解为:String就像值类型一样……

但真正正确的答案是:fg和dfg就真的指向了同一对象!

eclipse演示:fg和dfg具有同一/不同的id(C#查看地址更方便)

字符串池

编译(注意:是编译的时候,是运行的时候)的时候,编译器会设置一个字符串池(pool)

每次要实例化一个新字符串的时候,首先在池中进行检查:

  • 如果池中已经有完全相同的字符串,直接将这个字符串的堆地址赋值给新变量;否则
  • 实例化这个字符串,然后放到字符串池里

这样,就可以节省很多的堆空间,尤其是当相同的字符串非常多的时候。

PPT动画演示:

“池”这种优化技术,我们以后还会多次接触到。^_^

C#演示

C#中“堵漏”堵得更严:

  • new String()被优化到编译时
  • == 被运算符重载,就只比较值

所以需要通过

String dfg = string.Join("", "飞", "哥");
配合使用指针查看地址确认:


imutable:不可更改

@想一想@:既然使用了池,多个变量可以指向同一个对象,那么如果修改一个变量(所对应这个对象),岂不是要影响所有其他(指向这个对象的)变量?

但实际上,这是不可能的:

没法修改

你没有办法修改一个字符串,因为它:

  1. 所有字段都是私有的
  2. 所有属性(如果有的话)都是只读的
  3. 没有任何可以修改String对象本身的方法
  4. 最后连String类是不可继承的(final/sealed)(@想一想@:这又是为什么?)

查看源图像

PS:所有包装类也都这样,这种类被称之为不可更改类

改变String的方法

  • 替换:replace
    String newGreet = greet.replace('一', '1');
    String newGreet = greet.replace("一起帮", "17bang");
  • 截取:subString
    String newGreet = greet.substring(4);
    String newGreet = greet.substring(0, 3);
  • 大小写转换:toUpper/toLower(C#中没有Case后缀)
    String newGreet = greet.toUpperCase();
    String newGreet = greet.toLowerCase();
  • 修剪前后空白字符:trim
    String newGreet = greet.trim();

注意:所有的方法都不会改变字符串slagan本身,而是返回一个新的字符串。(@想一想@:为什么?)

有关+=

有些爱思考的同学会说:“等等,好像+=可以修改String?”
String slagon = "大神小班,拎包入住";
slagon += "!";
System.out.println(slagon);    //大神小班,拎包入住!

@想一想@:是这样的吗?

,这是一个重新赋值的过程,等于:

slagon = slagon + "!";
slagon完完全全的指向了另外一个对象。

如果不明白这一点,你就会对下面这个方法感到迷惑:

static void change(String slagon) {
	slagon += "!";
}
方法change明明改变了传入参数slagon:调用方法:
change(slagon);
System.out.println(slagon);

运行的结果,slagon没变,“感觉”就像是传递了一个slagon的副本给方法say()一样——当然其实并一样。

上述代码类似于:

static void change(Person slagon) {
	slagon = new Person();
}

这样的代码会影响传入参数Person么?

无论如何String在太多方面表现得和值类型类似,以至于JavaScript中字符串就是值类型,而C#中将string归为基本类型


其他方法

获取字符串长度(字符个数):length()方法(Java)/ 只读属性Length(C#)
System.out.println(slagon.length());    //Java
Console.WriteLine(slagon.Length);    //C#
字符串转字符数组(char[]):
System.out.println(slagon.toCharArray().length); 
System.out.println(slagon.toCharArray()[0]); 

这样转换之后,就可以对字符串的每个字符进行过滤筛选。

返回值为boolean值的判断,是否:

  • 为空字符串(Empty):
    String slagon = "";
    System.out.println(slagon.isEmpty());     //true
    注意和null值的区分,null值调用empty会直接报空引用异常
    String slagon = null;
    System.out.println(slagon == null);    //正确写法
    
  • 包含(Contains):
    System.out.println(slagon.contains("小"));
  • 以……开始(Starts)/ 结束(Ends)
    System.out.println(slagon.startsWith("大"));
    System.out.println(slagon.endsWith("住"));
查找,返回int类型下标的IndexOf()和LastIndexOf():
System.out.println(slagon.indexOf("小"));
System.out.println(slagon.lastIndexOf("小"));

此外,还有连接和拆分:

  • Contact:直接将字符串拼接,等价于“+”
    System.out.println(slagon.concat("!"));
  • Join:静态方法,将多个字符串用指定字符(分隔符)连接起来
    System.out.println(String.join("-", "atai", "fg", "bo"));
    //结果为:atai-fg-bo
  • Split:Join的逆操作,将字符串按分隔符拆分
    String[] names = slagon.split("-");
    for (int i = 0; i < names.length; i++) {
    	System.out.println(names[i]);
    }
  • Format:内插的拼接,在一个字符串模板的基础上,插入若干字符串
    //%标记需要插入的位置
    //s表示字符串
    //f表示浮点数(小数),.2表示显示两位小数,不指定的话默认6为
    String slagon = "大神(%s)小班,拎包入住(住宿费%.2f)";
    
    //format
    System.out.println(String.format(slagon, "飞哥", 98.6));
    //如果是98.6F的话且没有指定小数位数的话,会出现精度损失
    PS:C#的模板格式有所区别,分部内容细讲


StringBuilder

你或许看到有这样的建议:不要使用加号(+)或concat进行字符串拼接,而总是应该使用StringBuilder

#常见面试题:真的是这样么?#

同学们注意一定要警惕这样的绝对的简单化的论断。

你可以反过来想一想:StringBuilder和string是同时推出的,如果“用加号(+)进行字符串拼接”真的不行,为什么微软要搞这么一个语法出来?

拼接的问题

虽说字符串被称之为“串”,但其实它不是像链表那样把字符一个一个串起来的,而是由一个字符数组(char[])予以存放。

演示:查看源代码

所以,(如果没有编译器优化)两个字符串a和b的拼接的过程应该是这样的:

  1. 计算出a和b的长度,然后相加达到总长度
  2. 按总长度新生成一个新的char[]数组
  3. 将a和b的内容依次复制到新的char[]数组
  4. 将新的char[]数组合成字符串

这样的话,多个字符串的拼接就是一场灾难,比如这样的for循环代码:

String[] students = { "王新", "陈元", "彭志强" };
for (int i = 1; i < students.length; i++)
{ 
    students[0] += students[i];
}
比如说100次的循环,就会产生99次没有必要的字符串长度计算,99次没有必要的内存划分(需要new一个新的char[]),99(?)次没有必要的字符串复制……

可变长度

为了节省掉这些不必要的性能损耗,StringBuilder被引入,其工作原理是:

  1. 在StringBuilder实例化的时候,生成一个长度(capacity)或者由构造函数参数指定,或者默认为16的char[]数组
  2. 将a、b、c、d……等字符串依次往char[]数组里装,如果
  3. char[]数组的长度不够了,StringBuilder自动扩充其capacity,生成一个双倍长度的新数组继续装
  4. 直到调用ToString(),将char[]数组转换成字符串

对比如下图所示:
String slagon = "源栈" + "," + "欢迎您!";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < students.length; i++) {
    sb.append(students[i]);
    sb.append('-');
}
System.out.println(sb.toString());

转到定义演示:

  • StringBuilder重载的构造函数。可以使用默认的容积(capacity),也可以自己指定:
    public StringBuilder(int capacity);
  • append()中进行了“扩容”

选择

所以,如果是大规模的字符串拼接,使用StringBuilder确实有性能上的优势。

那么为什么加号拼接仍然这么常见呢?

  • 拼接的次数不多,StringBuilder也没有太大的性能提升;
  • 图方便啦!^_^,代码可读性/开发效率也是要考虑的——好吧,我承认,就是懒,哈哈。

但是,你也要明白一点:懒,并不一定是坏事。尤其是对于程序员而言。


凡是涉及性能的问题,我们这已经是老生常谈了,一定要牢记几个原则:

  • 天下没有免费的午餐
  • 首先找到瓶颈
  • no profile no improvement


正则

复习:正则表达式

操作字符串最强大的工具!

用字符串存储正则表达式,可以进行:

  • 匹配(match)
  • 替换(replace)
捕获组(group


作业

  1. 确保文章(Article)的标题不能为null值,也不能为一个或多个空字符组成的字符串,而且如果标题前后有空格,也予以删除
  2. 设计一个适用的机制,能确保用户(User)的昵称(Name)不能含有admin、17bang、管理员等敏感词。
  3. 确保用户(User)的密码(Password):
    1. 长度不低于6
    2. 只能由大小写英语单词、数字和特殊符号(~!@#$%^&*()_+)三种字符组成,且每种字符至少包含一个
  4. 通过控制台读取用户输入,比如:3月,12周,100天,利用之前作业的GetDate()方法,输出指定时间段后的日期
  5. 不使用string自带的Join()方法,定义一个mimicJoin()方法,能将若干字符串用指定的分隔符连接起来,比如:mimicJoin("-","a","b","c","d"),其运行结果为:a-b-c-d
  6. 实现GetCount(string container, string target)方法,可以统计出container中有多少个target
  7. 之前正则表达式作业-3的基础上,完成:
    1. 检查有无a标签(<a ...>...</a>)的存在
    2. 如果有,找出其中的href属性(href='...'中...的值) 
    3. 控制台逐行输出:相邻>和<之间的内容
    4. 将原字符串替换为只有7.2中的内容
字符串 String

试一试:章节测试

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 J&C 中继续学习:

我们的 特色

  • 先学习,后付费
  • 线上/线下,自由组合

更多了解

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧