Java String 字符串类细节探秘

Java String详解
本文深入剖析Java中的String类型,包括字符串初始化、字符串常量池的概念、字符串连接操作的底层原理及性能考量。

一. 字符串基本知识要点

  字符串类型String是Java中最常用的引用类型。我们在使用Java字符串的时候,通常会采用两种初始化的方式:1. String str = "Hello World"; 2. String str = new String("Hello World"); 这两种方式都可以将变量初始化为java字符串类型,通过第一种方式创建的字符串又被称为字符串常量。需要注意的是,Java中的String类是一个final类,str指向的字符串对象存储于堆中,而str本身则是存储在栈中的一个引用罢了。字符串对象一旦被初始化,则不允许再次被修改。从如下String的定义中我们可以验证以上所述:

1 public final class String implements java.io.Serializable, Comparable<String>, CharSequence{
2     
3     /** The value is used for character storage. */
4     private final char value[];
5     
6     /** Cache the hash code for the string */
7     private int hash; // Default to 0
8     
9 }

从代码中我们发现,String前有final修饰,表示是final类;而其中存储的字符数组value[],也是由final修饰,表明一旦被赋值,则不允许再次修改。

  那么,使用如上两种字符串初始化的方式有什么不同呢?我们可以通过如下代码体会:

public class EqualTest {
	public static void main(String[] args) {
		String s1 = "Hello";
		String s2 = new String("Hello");
		System.out.println(s1 == s2);
		System.out.println(s1.equals(s2));
	}
}

  程序输出结果为false和true。从== 和equals的区别上,我们一般这样来总结:==比较的是两个对象的引用,对象必须一模一样;equals则比较的是对象的内容,字符串内容一致便返回true。这说明,两种初始化的方式所构造的字符串对象,内容是一致的(可以理解为values数组一致),但是却是两个不同的对象,分别存储在内存的不同位置。其实,这两种初始化方式的最大不同在于,s1被初始化在字符串常量池中,而s2则存储在堆中。那么,什么是字符串常量池呢?

  字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中,就返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串常量并放到池中。Java能够进行这样的优化是因为字符串是不可变的final类型,共享的时候不用担心数据冲突(读写不冲突,因为不能写,相当于数据库中的S锁,即共享锁)。在常量池中,任何字符串至多维护一个对象。字符串常量总是指向常量池中的一个对象。通过new操作符创建的字符串对象不指向池中的任何对象,但是可以通过使用字符串的intern()方法来指向其中的某一个。java.lang.String.intern()返回一个池字符串,就是一个在全局常量池中有了一个入口。如果该字符串以前没有在全局常量池中,那么它就会被添加到里面。

  Java String类中有很多基本的方法。主要分成以下两个部分:

  1)和value[]相关的方法:

  • int length(); //返回String长度,即value[]数组长度;
  • char charAt(int index); //返回指定位置字符;
  • int indexOf(int ch, int fromIndex); //从fromIndex位置开始,查找ch字符在字符串中首次出现的位置;
  • char[] toCharArray();   //将字符串转换成一个新的字符数组

  2)和其他字符串相关的方法:

  • int indexOf(String str, int fromIndex); //从fromIndex位置开始,查找str字符串在字符串中首次出现的位置;
  • int lastIndexOf(String str, int fromIndex); //从fromIndex位置开始,反向查找str字符串在字符串中首次出现的位置;
  • boolean contains(String str); //contains内部实现也是调用的indexOf,找不到则返回-1
  • boolean startsWith(String str); //判断字符串是否以str开头
  • boolean endsWith(String str); //判断字符串是否以str结尾
  • String replace(CharSequence target, CharSequence replacement);  //使用replacement替换target
  • String substring(int beginIndex,  int endIndex);  //字符串截取,不传第二个参数则表示直接截取到字符串末尾
  • String[] split(String regex);  // 以regex作为分割点进行字符串分割

   另外一个值得注意的细节是,String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变),StringBuffer线程安全,StringBuilder非线程安全。关于String的append操作,会在下面结合具体的例子进行解释。

二. 几个关于String的程序分析

2.1 intern的程序示例

  参看如下程序:

public class StringTest1 {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String s1 = "hello world";
		String s2 = new String("hello world");
		String s3 = s2.intern();
		System.out.println(s1 == s2);
		System.out.println(s1 == s3); 
	}
}

  程序的输出是false,true。关于==和equals的区别在上面已做了详细的解释,由于s1是分配在字符串常量池中,s2则存储在堆中,因此两个对象并不是同一个对象,==操作返回false。而intern在JDK 1.7及以下,都是返回一个池字符串,该池字符串和原来的String对象的内容一致。若池中无该常量则添加,若有,则直接返回该常量的引用。因此,s1和s3是一个对象。说白了,在JVM的字符串常量池中,对于每一个字符串,只有一个共享的对象。

2.2 通过字节码进行深入分析

  当情况变得复杂的时候,参看如下程序:

public class StringTest2 {
	public static void main(String[] args){
		String baseStr = "base";
		final String baseFinalStr = "base";
		//extend
		String s1 = "baseext";
		String s2 = "base" + "ext";
		String s3 = baseStr + "ext";
		String s4 = baseFinalStr + "ext";
		String s5 = new String("baseext").intern();
		System.out.println(s1 == s2);
		System.out.println(s1 == s3);
		System.out.println(s1 == s4);
		System.out.println(s1 == s5);
	}
}

  这段程序乍一看非常复杂,里面有final String(final是限制在String对象的引用上,即该引用不能再更改所指向的String对象,String对象本身便是final类型的),还有字符串常量,以及字符串对象,和各个对象之间的“+”操作(“+”操作在下面的程序中详细解释)。那么我们不禁会问,在“+”操作的过程中,JVM到底是如何进行对象转换和操作呢?要想搞清楚这个问题,我们需要深入Bytecode一探究竟。使用javap -v XXX.class命令,可以打印出字节码文件中的符号表和指令等信息,该段程序的字节码输出如下:

  这里的constant pool指的是JVM内存结构中的运行时常量池,是方法区的一部分(参见周志明 《深入理解Java虚拟机》),我们上文提到的字符串常量池只是constant pool的一部分,除此之外,它还主要用来存储编译期生成的各种字面量和符号引用。javap -v的输出主要分为constant pool和方法体指令两部分,而指令中的操作数则是常量池中的序号。为了方便接下来的描述,我们先对常用的JVM字节码指令做一下说明:

LDC        将int, float或String型常量值从常量池中推送至栈顶;
ASTORE_<N>    Store reference into local variable,将栈顶的引用赋值给第N个局部变量;
ALOAD       将指定的引用类型本地变量推送至栈顶
INVOKE VIRTUAL  调用实例方法
INVOKE SPECIAL  调用超类构造方法等初始化方法
INVOKE STATIC   调用静态方法
NEW         创建一个对象,并将其引用值压入栈顶
DUP         复制栈顶数值并将复制值压入栈顶

  以上指令是需要仔细理解的。使用javap -v进行字节码的查看和理解可能比较困难,因为你要将 #序号 和 constant pool中的字面量不断照应已方便理解。Eclipse中提供了Bytecode Outline的插件可以很方便的查看和理解bytecode。插件的安装请自行百度,这里不再赘述。这里贴出本段代码的outline:

 // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0  //
    LINENUMBER 5 L0
    LDC "base"  //将"base"从常量池推送至栈顶
    ASTORE 1    //赋值给baseStr变量
   L1
    LINENUMBER 6 L1
    LDC "base"
    ASTORE 2    //赋值给baseFinalStr变量
   L2
    LINENUMBER 8 L2
    LDC "baseext"
    ASTORE 3    
   L3
    LINENUMBER 9 L3
    LDC "baseext"  //注意,这里直接将"baseext"赋值给了s2,而没有进行"+"操作!!!
    ASTORE 4
   L4
    LINENUMBER 10 L4
    NEW java/lang/StringBuilder  //创建StringBuilder对象
    DUP
    ALOAD 1 //将baseStr推送至栈顶
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String; //获取baseStr的value
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V  //将创建的StringBuilder对象初始化为上一步获得的value
    LDC "ext"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;  //调用StringBuilder对象的append实例方法
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;  //调用StringBuilder对象的toString实例方法
    ASTORE 5  //将toString的结果赋值给s3
   L5
    LINENUMBER 11 L5
    LDC "baseext"  //s4也是直接赋值
    ASTORE 6
   L6
    LINENUMBER 12 L6
    NEW java/lang/String
    DUP
    LDC "baseext"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/String.intern ()Ljava/lang/String; //调用intern()方法
    ASTORE 7
//===================================以下为 输出部分,可以忽略========================================= .......
  ....... L19 LINENUMBER 17 L19 RETURN L20 //类似于符号表,对应于local variable和变量编号 LOCALVARIABLE args [Ljava/lang/String; L0 L20 0 LOCALVARIABLE baseStr Ljava/lang/String; L1 L20 1 LOCALVARIABLE baseFinalStr Ljava/lang/String; L2 L20 2 LOCALVARIABLE s1 Ljava/lang/String; L3 L20 3 LOCALVARIABLE s2 Ljava/lang/String; L4 L20 4 LOCALVARIABLE s3 Ljava/lang/String; L5 L20 5 LOCALVARIABLE s4 Ljava/lang/String; L6 L20 6 LOCALVARIABLE s5 Ljava/lang/String; L7 L20 7 MAXSTACK = 3 MAXLOCALS = 8

  如果想深入理解,请逐行理解以上字节码程序。根据程序的分析,我们不难得出输出结果:true false true true。

  s1和s2,s4,s5都是指向字符串常量池中的同一个字符串常量。s2和s4中的“+”并没有起任何作用。String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接。而s3中的第一个操作数是String对象类型,因此会首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象。

  这里要注意的一点是,对于final字段修饰的字符串常量,编译期直接进行了常量替换。如果final修饰的不是字符串常量,而是字符串对象,如final String a = new String("baseStr"); 则和没有final修饰的情况是一样的,同样需要用StringBuilder进行append并toString才可以。

  我们再通过一个程序来更深入的理解字符串常量和“+”操作符。程序如下:

public class AppendTest {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String a = "aa";
		String b = "bb";
		String c = "xx" + "yy " + a + "zz" + "mm" + b;
		System.out.println(c);
	}
}

  程序输出自然不用赘述,我们通过同样的方法查看Bytecode的outline,输出如下:

// access flags 0x21
public class com/yelbosh/java/str/AppendTest {

  // compiled from: AppendTest.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/yelbosh/java/str/AppendTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 6 L0
    LDC "aa"
    ASTORE 1
   L1
    LINENUMBER 7 L1
    LDC "bb"
    ASTORE 2
   L2
    LINENUMBER 8 L2
    NEW java/lang/StringBuilder
    DUP
    LDC "xxyy " //直接load的是字符串常量“xxyy ”
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; //之后都是在调用StringBuilder对象的append实例方法
    LDC "zz"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "mm"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3
   L3
    LINENUMBER 9 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 3
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 10 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE a Ljava/lang/String; L1 L5 1
    LOCALVARIABLE b Ljava/lang/String; L2 L5 2
    LOCALVARIABLE c Ljava/lang/String; L3 L5 3
    MAXSTACK = 3
    MAXLOCALS = 4
}

  通过这个程序,更印证了我们如上的结论。

  通过这个深入分析,我们在写代码的时候,也要注意使用StringBuilder对象。如果直接在for循环中使用“+”操作符进行字符串对象(常量无所谓)的拼接,那么实际上在每次循环的时候,都要创建StringBuilder,然后append,再toString出来,因此性能是十分低下的。这个时候,就需要在循环外声明StringBuilder对象,然后在循环内调用append方法进行拼接。另外要注意的是,StringBuilder是线程不安全的,如果涉及到多个线程同时对StringBuilder的append操作,请使用synchronized或lock确保并发访问的安全性,或者转而使用线程安全的StringBuffer。

 

总结:Java String是非常灵活的一个对象,但是只要把细节搞清楚,问题还是很简单的。在实际编码的过程中,一定要考虑字符串操作的性能和线程安全问题,这样才能更好的运用字符串完成自己的业务逻辑。希望这篇博文能对您的学习有些帮助,如果错误,请不吝赐教。

转载于:https://www.cnblogs.com/yelbosh/p/5857739.html

标题SpringBoot智能在线预约挂号系统研究AI更换标题第1章引言介绍智能在线预约挂号系统的研究背景、意义、国内外研究现状及论文创新点。1.1研究背景与意义阐述智能在线预约挂号系统对提升医疗服务效率的重要性。1.2国内外研究现状分析国内外智能在线预约挂号系统的研究与应用情况。1.3研究方法及创新点概述本文采用的技术路线、研究方法及主要创新点。第2章相关理论总结智能在线预约挂号系统相关理论,包括系统架构、开发技术等。2.1系统架构设计理论介绍系统架构设计的基本原则和常用方法。2.2SpringBoot开发框架理论阐述SpringBoot框架的特点、优势及其在系统开发中的应用。2.3数据库设计与管理理论介绍数据库设计原则、数据模型及数据库管理系统。2.4网络安全与数据保护理论讨论网络安全威胁、数据保护技术及其在系统中的应用。第3章SpringBoot智能在线预约挂号系统设计详细介绍系统的设计方案,包括功能模块划分、数据库设计等。3.1系统功能模块设计划分系统功能模块,如用户管理、挂号管理、医生排班等。3.2数据库设计与实现设计数据库表结构,确定字段类型、主键及外键关系。3.3用户界面设计设计用户友好的界面,提升用户体验。3.4系统安全设计阐述系统安全策略,包括用户认证、数据加密等。第4章系统实现与测试介绍系统的实现过程,包括编码、测试及优化等。4.1系统编码实现采用SpringBoot框架进行系统编码实现。4.2系统测试方法介绍系统测试的方法、步骤及测试用例设计。4.3系统性能测试与分析对系统进行性能测试,分析测试结果并提出优化建议。4.4系统优化与改进根据测试结果对系统进行优化和改进,提升系统性能。第5章研究结果呈现系统实现后的效果,包括功能实现、性能提升等。5.1系统功能实现效果展示系统各功能模块的实现效果,如挂号成功界面等。5.2系统性能提升效果对比优化前后的系统性能
在金融行业中,对信用风险的判断是核心环节之一,其结果对机构的信贷政策和风险控制策略有直接影响。本文将围绕如何借助机器学习方法,尤其是Sklearn工具包,建立用于判断信用状况的预测系统。文中将涵盖逻辑回归、支持向量机等常见方法,并通过实际操作流程进行说明。 一、机器学习基本概念 机器学习属于人工智能的子领域,其基本理念是通过数据自动学习规律,而非依赖人工设定规则。在信贷分析中,该技术可用于挖掘历史数据中的潜在规律,进而对未来的信用表现进行预测。 二、Sklearn工具包概述 Sklearn(Scikit-learn)是Python语言中广泛使用的机器学习模块,提供多种数据处理和建模功能。它简化了数据清洗、特征提取、模型构建、验证与优化等流程,是数据科学项目中的常用工具。 三、逻辑回归模型 逻辑回归是一种常用于分类任务的线性模型,特别适用于二类问题。在信用评估中,该模型可用于判断借款人是否可能违约。其通过逻辑函数将输出映射为0到1之间的概率值,从而表示违约的可能性。 四、支持向量机模型 支持向量机是一种用于监督学习的算法,适用于数据维度高、样本量小的情况。在信用分析中,该方法能够通过寻找最佳分割面,区分违约与非违约客户。通过选用不同核函数,可应对复杂的非线性关系,提升预测精度。 五、数据预处理步骤 在建模前,需对原始数据进行清理与转换,包括处理缺失值、识别异常点、标准化数值、筛选有效特征等。对于信用评分,常见的输入变量包括收入水平、负债比例、信用历史记录、职业稳定性等。预处理有助于减少噪声干扰,增强模型的适应性。 六、模型构建与验证 借助Sklearn,可以将数据集划分为训练集和测试集,并通过交叉验证调整参数以提升模型性能。常用评估指标包括准确率、召回率、F1值以及AUC-ROC曲线。在处理不平衡数据时,更应关注模型的召回率与特异性。 七、集成学习方法 为提升模型预测能力,可采用集成策略,如结合多个模型的预测结果。这有助于降低单一模型的偏差与方差,增强整体预测的稳定性与准确性。 综上,基于机器学习的信用评估系统可通过Sklearn中的多种算法,结合合理的数据处理与模型优化,实现对借款人信用状况的精准判断。在实际应用中,需持续调整模型以适应市场变化,保障预测结果的长期有效性。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值