十八、字符串(1)

news2024/11/20 4:32:48

本章概要

  • 字符串的不可变
    • 的重载与 StringBuilder
  • 意外递归
  • 字符串操作

字符串操作毫无疑问是计算机程序设计中最常见的行为之一。

在 Java 大展拳脚的 Web 系统中更是如此。在本章中,我们将深入学习在 Java 语言中应用最广泛的 String 类,并研究与之相关的类及工具。

字符串的不可变

String 对象是不可变的。查看 JDK 文档你就会发现,String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象,以包含修改后的字符串内容。而最初的 String 对象则丝毫未动。

看看下面的代码:

public class Immutable {
    public static String upcase(String s) {
        return s.toUpperCase();
    }

    public static void main(String[] args) {
        String q = "howdy";
        System.out.println(q); // howdy 
        String qq = upcase(q);
        System.out.println(qq); // HOWDY 
        System.out.println(q); // howdy 
    }
} 

在这里插入图片描述

当把 q 传递给 upcase() 方法时,实际传递的是引用的一个拷贝。其实,每当把 String 对象作为方法的参数时,都会复制一份引用,而该引用所指向的对象其实一直待在单一的物理位置上,从未动过。

回到 upcase() 的定义,传入其中的引用有了名字 s,只有 upcase() 运行的时候,局部引用 s 才存在。一旦 upcase() 运行结束,s 就消失了。当然了,upcase() 的返回值,其实是最终结果的引用。这足以说明,upcase() 返回的引用已经指向了一个新的对象,而 q 仍然在原来的位置。

String 的这种行为正是我们想要的。例如:

String s = "asdf";
String x = Immutable.upcase(s);

难道你真的希望 upcase() 方法改变其参数吗?对于一个方法而言,参数是为该方法提供信息的,而不是想让该方法改变自己的。在阅读这段代码时,读者自然会有这样的感觉。这一点很重要,正是有了这种保障,才使得代码易于编写和阅读。

+ 的重载与 StringBuilder

String 对象是不可变的,你可以给一个 String 对象添加任意多的别名。因为 String 是只读的,所以指向它的任何引用都不可能修改它的值,因此,也就不会影响到其他引用。

不可变性会带来一定的效率问题。为 String 对象重载的 + 操作符就是一个例子。重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义(用于 String++= 是 Java 中仅有的两个重载过的操作符,Java 不允许程序员重载任何其他的操作符 )。

操作符 + 可以用来连接 String

public class Concatenation {
    public static void main(String[] args) {
        String mango = "mango";
        String s = "abc" + mango + "def" + 47;
        System.out.println(s);
    }
}

在这里插入图片描述

可以想象一下,这段代码是这样工作的:String 可能有一个 append() 方法,它会生成一个新的 String 对象,以包含“abc”与 mango 连接后的字符串。该对象会再创建另一个新的 String 对象,然后与“def”相连,生成另一个新的对象,依此类推。

这种方式当然是可行的,但是为了生成最终的 String 对象,会产生一大堆需要垃圾回收的中间对象。我猜想,Java 设计者一开始就是这么做的(这也是软件设计中的一个教训:除非你用代码将系统实现,并让它运行起来,否则你无法真正了解它会有什么问题),然后他们发现其性能相当糟糕。

想看看以上代码到底是如何工作的吗?可以用 JDK 自带的 javap 工具来反编译以上代码。命令如下:

javap -c Concatenation

这里的 -c 标志表示将生成 JVM 字节码。我们剔除不感兴趣的部分,然后做细微的修改,于是有了以下的字节码:

public static void main(java.lang.String[]); 
 Code:
  Stack=2, Locals=3, Args_size=1
  0: ldc #2; //String mango 
  2: astore_1 
  3: new #3; //class StringBuilder 
  6: dup 
  7: invokespecial #4; //StringBuilder."<init>":() 
  10: ldc #5; //String abc 
  12: invokevirtual #6; //StringBuilder.append:(String) 
  15: aload_1 
  16: invokevirtual #6; //StringBuilder.append:(String) 
  19: ldc #7; //String def 
  21: invokevirtual #6; //StringBuilder.append:(String) 
  24: bipush 47 
  26: invokevirtual #8; //StringBuilder.append:(I) 
  29: invokevirtual #9; //StringBuilder.toString:() 
  32: astore_2 
  33: getstatic #10; //Field System.out:PrintStream;
  36: aload_2 
  37: invokevirtual #11; //PrintStream.println:(String) 
  40: return

如果你有汇编语言的经验,以上代码应该很眼熟(其中的 dupinvokevirtual 语句相当于Java虚拟机上的汇编语句。即使你完全不了解汇编语言也无需担心)。需要重点注意的是:编译器自动引入了 java.lang.StringBuilder 类。虽然源代码中并没有使用 StringBuilder 类,但是编译器却自作主张地使用了它,就因为它更高效。

在这里,编译器创建了一个 StringBuilder 对象,用于构建最终的 String,并对每个字符串调用了一次 append() 方法,共计 4 次。最后调用 toString() 生成结果,并存为 s (使用的命令为 astore_2)。

现在,也许你会觉得可以随意使用 String 对象,反正编译器会自动为你做性能优化。可是在这之前,让我们更深入地看看编译器能为我们优化到什么程度。下面的例子采用两种方式生成一个 String:方法一使用了多个 String 对象;方法二在代码中使用了 StringBuilder

public class WhitherStringBuilder {
    public String implicit(String[] fields) {
        String result = "";
        for (String field : fields) {
            result += field;
        }
        return result;
    }

    public String explicit(String[] fields) {
        StringBuilder result = new StringBuilder();
        for (String field : fields) {
            result.append(field);
        }
        return result.toString();
    }
}

现在运行 javap -c WhitherStringBuilder,可以看到两种不同方法(我已经去掉不相关的细节)对应的字节码。首先是 implicit() 方法:

public java.lang.String implicit(java.lang.String[]); 
0: ldc #2 // String 
2: astore_2
3: aload_1 
4: astore_3 
5: aload_3 
6: arraylength 
7: istore 4 
9: iconst_0 
10: istore 5 
12: iload 5 
14: iload 4 
16: if_icmpge 51 
19: aload_3 
20: iload 5 
22: aaload 
23: astore 6 
25: new #3 // StringBuilder 
28: dup 
29: invokespecial #4 // StringBuilder."<init>"
32: aload_2 
33: invokevirtual #5 // StringBuilder.append:(String) 
36: aload 6 
38: invokevirtual #5 // StringBuilder.append:(String;) 
41: invokevirtual #6 // StringBuilder.toString:() 
44: astore_2 
45: iinc 5, 1 
48: goto 12 
51: aload_2 
52: areturn

注意从第 16 行到第 48 行构成了一个循环体。第 16 行:对堆栈中的操作数进行“大于或等于的整数比较运算”,循环结束时跳转到第 51 行。第 48 行:重新回到循环体的起始位置(第 12 行)。注意:StringBuilder 是在循环内构造的,这意味着每进行一次循环,会创建一个新的 StringBuilder 对象。

下面是 explicit() 方法对应的字节码:

public java.lang.String explicit(java.lang.String[]); 
0: new #3 // StringBuilder 
3: dup
4: invokespecial #4 // StringBuilder."<init>" 
7: astore_2 
8: aload_1 
9: astore_3 
10: aload_3 
11: arraylength 
12: istore 4 
14: iconst_0 
15: istore 5 
17: iload 5 
19: iload 4 
21: if_icmpge 43 
24: aload_3 
25: iload 5 
27: aaload 
28: astore 6 
30: aload_2 
31: aload 6 
33: invokevirtual #5 // StringBuilder.append:(String) 
36: pop
37: iinc 5, 1 
40: goto 17 
43: aload_2 
44: invokevirtual #6 // StringBuilder.toString:() 
47: areturn

可以看到,不仅循环部分的代码更简短、更简单,而且它只生成了一个 StringBuilder 对象。显式地创建 StringBuilder 还允许你预先为其指定大小。如果你已经知道最终字符串的大概长度,那预先指定 StringBuilder 的大小可以避免频繁地重新分配缓冲。

因此,当你为一个类编写 toString() 方法时,如果字符串操作比较简单,那就可以信赖编译器,它会为你合理地构造最终的字符串结果。但是,如果你要在 toString() 方法中使用循环,且可能有性能问题,那么最好自己创建一个 StringBuilder 对象,用它来构建最终结果。请参考以下示例:

import java.util.*;
import java.util.stream.*;

public class UsingStringBuilder {
    public static String string1() {
        Random rand = new Random(47);
        StringBuilder result = new StringBuilder("[");
        for (int i = 0; i < 25; i++) {
            result.append(rand.nextInt(100));
            result.append(", ");
        }
        result.delete(result.length() - 2, result.length());
        result.append("]");
        return result.toString();
    }

    public static String string2() {
        String result = new Random(47)
                .ints(25, 0, 100)
                .mapToObj(Integer::toString)
                .collect(Collectors.joining(", "));
        return "[" + result + "]";
    }

    public static void main(String[] args) {
        System.out.println(string1());
        System.out.println(string2());
    }
} 

在这里插入图片描述

在方法 string1() 中,最终结果是用 append() 语句拼接起来的。如果你想走捷径,例如:append(a + ": " + c),编译器就会掉入陷阱,从而为你另外创建一个 StringBuilder 对象处理括号内的字符串操作。如果拿不准该用哪种方式,随时可以用 javap 来分析你的程序。

StringBuilder 提供了丰富而全面的方法,包括 insert()replace()substring(),甚至还有reverse(),但是最常用的还是 append()toString()。还有 delete(),上面的例子中我们用它删除最后一个逗号和空格,以便添加右括号。

string2() 使用了 Stream,这样代码更加简洁美观。可以证明,Collectors.joining() 内部也是使用的 StringBuilder,这种写法不会影响性能!

StringBuilder是 Java SE5 引入的,在这之前用的是 StringBuffer。后者是线程安全的,因此开销也会大些。使用 StringBuilder 进行字符串操作更快一点。

意外递归

Java 中的每个类从根本上都是继承自 Object,标准集合类也是如此,它们都有 toString() 方法,并且覆盖了该方法,使得它生成的 String 结果能够表达集合自身,以及集合包含的对象。例如 ArrayList.toString(),它会遍历 ArrayList 中包含的所有对象,调用每个元素上的 toString() 方法:

import java.util.*;
import java.util.stream.*;

public class ArrayListDisplay {
    public static void main(String[] args) {
        List<Coffee> coffees =
                Stream.generate(new CoffeeSupplier())
                        .limit(10)
                        .collect(Collectors.toList());
        System.out.println(coffees);
    }
}

在这里插入图片描述

如果你希望 toString() 打印出类的内存地址,也许你会考虑使用 this 关键字:

import java.util.stream.*;

public class InfiniteRecursion {
    @Override
    public String toString() {
        return " InfiniteRecursion address: " + this + "\n";
    }

    public static void main(String[] args) {
        Stream.generate(InfiniteRecursion::new)
                .limit(10)
                .forEach(System.out::println);
    }
}

在这里插入图片描述

当你创建了 InfiniteRecursion 对象,并将其打印出来的时候,你会得到一串很长的异常信息。如果你将该 InfiniteRecursion 对象存入一个 ArrayList 中,然后打印该 ArrayList,同样也会抛出异常。其实,当运行到如下代码时:

"InfiniteRecursion address: " + this

这里发生了自动类型转换,由 InfiniteRecursion 类型转换为 String 类型。因为编译器发现一个 String 对象后面跟着一个 “+”,而 “+” 后面的对象不是 String,于是编译器试着将 this 转换成一个 String。它怎么转换呢?正是通过调用 this 上的 toString() 方法,于是就发生了递归调用。

如果你真的想要打印对象的内存地址,应该调用 Object.toString() 方法,这才是负责此任务的方法。所以,不要使用 this,而是应该调用 super.toString() 方法。

字符串操作

以下是 String 对象具备的一些基本方法。重载的方法归纳在同一行中:

方法参数,重载版本作用
构造方法默认版本,StringStringBuilderStringBufferchar数组,byte数组创建String对象
length()String中字符的个数
charAt()int索引获取String中索引位置上的char
getChars()getBytes()待复制部分的开始和结束索引,复制的目标数组,目标数组的开始索引复制charbyte到一个目标数组中
toCharArray()生成一个char[],包含String中的所有字符
equals()equalsIgnoreCase()与之进行比较的String比较两个String的内容是否相同。如果相同,结果为true
compareTo()compareToIgnoreCase()与之进行比较的String按词典顺序比较String的内容,比较结果为负数、零或正数。注意,大小写不等价
contains()要搜索的CharSequence如果该String对象包含参数的内容,则返回true
contentEquals()与之进行比较的CharSequenceStringBuffer如果该String对象与参数的内容完全一致,则返回true
isEmpty()返回boolean结果,以表明String对象的长度是否为0
regionMatches()String的索引偏移量,另一个String及其索引偏移量,要比较的长度。重载版本增加了“忽略大小写”功能返回boolean结果,以表明所比较区域是否相等
startsWith()可能的起始String。重载版本在参数中增加了偏移量返回boolean结果,以表明该String是否以传入参数开始
endsWith()String可能的后缀String返回boolean结果,以表明此参数是否是该字符串的后缀
indexOf()lastIndexOf()重载版本包括:charchar与起始索引,StringString与起始索引如果该String并不包含此参数,就返回-1;否则返回此参数在String中的起始索引。lastIndexOf()是从后往前搜索
matches()一个正则表达式返回boolean结果,以表明该String和给出的正则表达式是否匹配
split()一个正则表达式。可选参数为需要拆分的最大数量按照正则表达式拆分String,返回一个结果数组
join()(Java8引入的)分隔符,待拼字符序列。用分隔符将字符序列拼接成一个新的String用分隔符拼接字符片段,产生一个新的String
substring()(即subSequence()重载版本:起始索引;起始索引+终止索引返回一个新的String对象,以包含参数指定的子串
concat()要连接的String返回一个新的String对象,内容为原始String连接上参数String
replace()要替换的字符,用来进行替换的新字符。也可以用一个CharSequence替换另一个CharSequence返回替换字符后的新String对象。如果没有替换发生,则返回原始的String对象
replaceFirst()要替换的正则表达式,用来进行替换的String返回替换首个目标字符串后的String对象
replaceAll()要替换的正则表达式,用来进行替换的String返回替换所有目标字符串后的String对象
toLowerCase()toUpperCase()将字符的大小写改变后,返回一个新的String对象。如果没有任何改变,则返回原始的String对象
trim()String两端的空白符删除后,返回一个新的String对象。如果没有任何改变,则返回原始的String对象
valueOf()static重载版本:Objectchar[]char[],偏移量,与字符个数;booleancharintlongfloatdouble返回一个表示参数内容的String
intern()为每个唯一的字符序列生成一个且仅生成一个String引用
format()要格式化的字符串,要替换到格式化字符串的参数返回格式化结果String

从这个表可以看出,当需要改变字符串的内容时,String 类的方法都会返回一个新的 String 对象。同时,如果内容不改变,String 方法只是返回原始对象的一个引用而已。这可以节约存储空间以及避免额外的开销。

本章稍后还将介绍正则表达式在 String 方法中的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1112161.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

功率放大器在材料测试中的应用有哪些

功率放大器在材料测试中有广泛的应用&#xff0c;尤其在材料的物理、电子和热学性质等方面的研究中起到了重要的作用。下面Aigtek安泰将详细介绍功率放大器在材料测试中的一些主要应用。 电学特性测试&#xff1a;功率放大器用于材料的电学特性测试&#xff0c;如电导率、介电常…

划词搜索IP插件

插件背景 浏览器插件可以让用户根据个人工作及日常需求来定制浏览器的功能和界面。当用户在网页上看到一些IP地址时&#xff0c;或许会好奇它们的来源和归属。传统的做法是&#xff0c;用户需要复制这个IP地址&#xff0c;然后跳转到埃文科技旗下的http://IPUU.net网站进行查询…

分类预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost多输入分类预测

分类预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost多输入分类预测 目录 分类预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost多输入分类预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 1.MATLAB实现基于BiLSTM-…

MMoE: 基于多门专家混合的多任务学习任务关系建模

文章链接&#xff1a;Modeling Task Relationships in Multi-task Learning with Multi-gate Mixture-of-Experts 发表会议: KKD 2018 &#xff08;Knowledge Discovery and Data Mining&#xff0c;数据挖掘领域顶会&#xff09; 目录 1.背景介绍Recommendation SystemMulti-…

wps excel js编程

定义全局变量 const a "dota" function test() {Debug.Print(a) }获取表格中单元格内容 function test() {Debug.Print("第一行第二列",Cells(1,2).Text)Debug.Print("A1:",Range("A1").Text) }写单元格 Range("C1").Val…

【UE】两步实现“从UI中拖出Actor放置到场景中”

效果 步骤 1. 创建两个actor蓝图 在两个蓝图中分别添加立方体和球体形状的静态网格体组件&#xff0c;注意移动性设置为“可移动” 设置碰撞预设为“NoCollsion” 2. 先创建一个控件蓝图 打开控件蓝图&#xff0c;在画布面板中添加两个按钮 为按钮添加“按压时”和“松开时”的…

手工测试的迷茫:除了重复劳动,到底还有什么?

我是在2008年毕业的&#xff0c;三本的学校&#xff0c;不上不下的专业水平&#xff0c;毕业的时候&#xff0c;恰好遇到了金融危机。校园招聘里阴差阳错的巧合&#xff0c;让我走上了软件测试工程师的道路。 入职第一天&#xff0c;来了个高大上的讲师&#xff0c;记得他是这…

easyphoto 妙鸭相机

AIGC专栏7——EasyPhoto 人像训练与生成原理详解-CSDN博客如何训练一个高品质的人像Lora与应用高品质Lora的链路对于写真生成而言非常重要。由《LoRA: Low-Rank Adaptation of Large Language Models》 提出的一种基于低秩矩阵的对大参数模型进行少量参数微调训练的方法&#x…

【牛客网】HJ91.走方格的方案数

题目 思路 考虑特殊情况,假设行数为m1,列数为n 则最短路径为mn 假设行数为m,列数n1,则最短路径为mn 考虑普遍情况 假设行数为m,列数为n 则总路经数为行数为m-1列数为n和行数为m列数为n-1的两个的和 根据上述条件,可以考虑使用递归的方式进行解决 代码 import java.util.Scan…

springmvc视图格式——模板引擎freemarker输出HTML文本

目录 1. freemarker 介绍创建测试工程2.2.2) 配置文件2.2.3) 创建模型类2.2.4) 创建模板2.2.5) 创建controller2.2.6) 创建启动类2.2.7) 测试 2.3) freemarker基础2.3.1) 基础语法种类2.3.2) 集合指令&#xff08;List和Map&#xff09;2.3.3) if指令2.3.4) 运算符2.3.5) 空值处…

Java设计模式 | 基于订单批量支付场景,对策略模式和简单工厂模式进行简单实现

基于订单批量支付场景&#xff0c;对策略模式和简单工厂模式进行简单实现 文章目录 策略模式介绍实现抽象策略具体策略1.AliPayStrategy2.WeChatPayStrategy 环境 使用简单工厂来获取具体策略对象支付方式枚举策略工厂接口策略工厂实现 测试使用订单实体类对订单进行批量支付结…

VS code中使用code Runner插件直接运行Typescript

使用VS code运行ts 运行问题 我们知道&#xff0c;在VS code中运行.ts文件&#xff0c;是不能直接运行的&#xff0c;需要在修改代码之后&#xff0c;都重复执行两个命令&#xff0c;才能运行ts代码 tsc 文件名.ts &#xff08;tsc 文件名.ts -w 可以监视ts文件(监视模…

【数字人】5、RAD-NeRF | 通过解耦 audio-spatial 编码来实现基于 NeRF 的高效数字人合成

文章目录 一、背景二、方法2.1 问题定义2.2 Decomposed audio-spatial encoding module2.3 Pseudo-3D Deformable Module 用于控制 torso2.4 训练细节 三、效果3.1 实验设置3.2 对比 论文&#xff1a;Real-time Neural Radiance Talking Portrait Synthesis via Audio-spatial …

【LittleXi】【MIT6.S081-2022Fall】Lab: syscall

【LittleXi】【MIT6.S081-2022Fall】Lab: syscall 文章目录 lab2实验1&#xff1a;Process counting实验思路实验过程 实验2&#xff1a;Free Memory Cou实验思路实验过程 实验3&#xff1a;System call tracin实验思路实验过程 实验4&#xff1a;流程概述1.请概述用户从发出系…

嵌入式养成计划-44----QT--消息对话框(QMessageBox)--字体对话框--颜色对话框--文件对话框

一百一十三、消息对话框 &#xff08;QMessageBox&#xff09; 消息对话框给用户提供一个交互式的弹窗&#xff0c;该类提供两种实现版本&#xff0c; 基于属性版本基于静态成员函数版本 基于属性版本 需要用消息对话框这样的类 实例化对象 用该对象调用类里的相关成员函数进…

web:[MRCTF2020]Ez_bypass

题目 点进题目 调整一下 进行代码审计&#xff0c;先看第一段 if(isset($_GET[gg])&&isset($_GET[id])) {$id$_GET[id];$gg$_GET[gg];if (md5($id) md5($gg) && $id ! $gg) {echo You got the first step; get参数传参&#xff0c;后判断md5后的值是否相等&…

2023前端面试题总结

给大家推荐一个实用面试题库 1、前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;web前端面试题库 Html5和CSS3 常见的水平垂直居中实现方案 最简单的方案当然是flex布局 .father {display: flex;justify-content…

手部关键点检测4:Android实现手部关键点检测(手部姿势估计)含源码 可实时检测

目录 1. 前言 2.手部关键点检测(手部姿势估计)方法 (1)Top-Down(自上而下)方法 (2)Bottom-Up(自下而上)方法&#xff1a; 3.手部关键点检测模型训练 4.手部关键点检测模型Android部署 &#xff08;1&#xff09; 将Pytorch模型转换ONNX模型 &#xff08;2&#xff09; …

嘉立创使用技巧

立创社区&#xff1a;电子工程师交流社区_电子发烧友论坛_嘉立创&立创商城旗下专业电子论坛【立创社区】 (szlcsc.com) 嘉立创官网使用教程&#xff1a;立创EDA使用教程 (lceda.cn) 嘉立创是国产软件对新手友好&#xff0c;中国人更懂中国人。下面介绍我在使用中用到的技巧…

【unity小技巧】适用于任何 2d 游戏的钥匙门系统和buff系统——UnityEvent的使用

文章目录 每篇一句前言开启配置门的开启动画代码调用&#xff0c;控制开启门动画 新增CollisionDetector 脚本&#xff0c;使用UnityEvent &#xff0c;控制钥匙和门的绑定多把钥匙控制多个门一把钥匙控制多个门 BUFF系统扩展参考源码完结 每篇一句 人总是害怕去追求自己最重要…