【Java SE基础专题 二】String类型详解

简介: 【Java SE基础专题 二】String类型详解

本篇文章具体聊聊Java中关于String类型的数据问题,String为什么是不可变的。Java中的变量和基本类型的值存放于栈内存,而new出来的对象本身存放于堆内存,指向对象的引用还是存放在栈内存。例如如下的代码:

int  i=1;
String s =  new  String( "Hello World" );

变量i和s以及1存放在栈内存,而s指向的对象Hello World存放于堆内存

内存的常量数据共享

栈内存的一个特点是数据共享,这样设计是为了减小内存消耗,前面定义了i=1,i和1都在栈内存内,

  • 如果再定义一个j=1,此时将j放入栈内存,然后查找栈内存中是否有1,如果有则j指向1。
  • 如果给j赋值2,则在栈内存中查找是否有2,如果没有就在栈内存中放一个2,然后j指向2。
  • 如果j++,这时指向的变量并不会改变,而是在栈内寻找新的常量(比原来的常量大1),如果栈内存有则指向它,如果没有就在栈内存中加入此常量并将j指向它

也就是如果该常量也在栈内存中,就将变量指向该常量,如果没有就在该栈内存增加一个常量,并将变量指向这个增加的常量这种基本类型之间比较大小和我们逻辑上判断大小是一致的。如定义i和j是都赋值1,则i==j结果为true==用于判断两个变量指向的地址是否一样。i==j就是判断i指向的1和j指向的1是同一个吗?当然是了

同样的对于直接赋值的字符串常量(如String s=“Hello World”;中的Hello World)也是存放在栈内存中,所以也满足条件

String s="Hello World"
String w="Hello World"
s==w//为true

如果定义和,s==w吗?肯定是true,因为他们指向的是同一个Hello World,栈中的Hello World

堆内存非数据共享

堆内存没有数据共享的特点,例如通过new的方式定义两个字符串:

String s =  new  String( "Hello World" );//变量s在栈内存内,Hello World 这个String对象在堆内存内
String w = new  String( "Hello World" );//变量w存放在栈内存,w指向这个新的String对象
s==w//为false

堆内存中不同对象(指同一类型的不同对象)的比较如果用==则结果肯定都是false,s和w指向堆内存中不同的String对象。如何判断两个String对象相等呢?用equals方法。

综合分析

知道了上边的知识综合分析以下下边的问题

public class StringDemo{
  private static final String MESSAGE="taobao";
  public static void main(String [] args) {
    String a ="tao"+"bao";
    String b="tao";
    String c="bao";
    System.out.println(a==MESSAGE);  //true
    System.out.println((b+c)==MESSAGE); //false
  }
}

MESSAGE 成员变量及其指向的字符串常量肯定都是在栈内存里的

  • 变量 a 运算完也是指向一个字符串taobao 这涉及到编译器优化问题。对于字符串常量的相加,在编译时直接将字符串合并,而不是等到运行时再合并。也就是说String a = “tao” + “bao” ;和String a = “taobao” ;编译出的字节码是一样的。所以等到运行时,根据上面说的栈内存是数据共享原则,a和MESSAGE指向的是同一个字符串
  • 对于后面的(b+c),(b+c)只能等到运行时才能判定是什么字符串,编译器不会优化,运行时b+c计算出来的taobao和栈内存里已经有的taobao不是一个,b+c计算出来的"taobao"应该是放在堆内存中的String对象

那么为什么b+c计算出来的在堆内存呢?Java对String的相加是通过StringBuffer实现的,先构造一个StringBuffer里面存放”tao”,然后调用append()方法追加”bao”,然后将值为”taobao”的StringBuffer转化成String对象。StringBuffer对象在堆内存中,那转换成的String对象理所应当的也是在堆内存中。

intern改进

下面改造一下这个语句

System. out .println( (b+c).intern()== MESSAGE );//true

结果是true, intern() 方法会先检查 String 池 ( 或者说成栈内存 ) 中是否存在相同的字符串常量,如果有就返回。所以 intern()返回的就是MESSAGE指向的"taobao"。

final改进

再把变量b和c的定义改一下

final  String b =  "tao" ;
final  String c =  "bao" ;
System. out .println( (b+c)== MESSAGE );//true

现在b和c不可能再次赋值了,所以编译器将b+c编译成了taobao。因此,这时的结果是true。因为被当成常量处理了,在字符串相加中,只要有一个是非final类型的变量,编译器就不会优化,因为这样的变量可能发生改变,所以编译器不可能将这样的变量替换成常量。例如将变量b的final去掉,结果又变成了false。这也就意味着会用到StringBuffer对象,计算的结果在堆内存中。

堆中String的intern改进

如果对指向堆内存中的对象的String变量调用intern()会怎么样呢?(b+c).intern(),b+c的结果就是在堆内存中。对于指向栈内存中字符串常量的变量调用intern()返回的还是它自己,没有多大意义。它会根据堆内存中对象的值,去查找常量池中是否有相同的字符串,如果有就将变量指向这个常量池中的变量。

String a = "tao"+"bao";
String b = new String("taobao");
System.out.println(a==MESSAGE); //true
System.out.println(b==MESSAGE);  //false
b = b.intern();
System.out.println(b==MESSAGE); //true
System. out .println(a==a.intern());  //true

综合分析2

这里的str1指的是方法区中的字符串常量池中的“hello”,编译时期就知道的; String str2 = "he" + new String("llo");这里的str2必须在运行时才知道str2是什么,所以它是指向的是堆里定义的字符串“hello”,所以这两个引用是不一样的。

String str1="hello";
String str2="he"+ new String("llo");
System.out.println(str1==str2);

基于此有如下判断:

  • 编译器没那么智能,它不知道"he" + new String("llo")的内容是什么,所以才不敢贸然把"hello"这个对象的引用赋给str2. 如果语句改为:"he"+"llo"这样就是true了
  • 如果用str1.equal(str2),那么返回的是true;因为String类重写了equals()方法

new String(“llo”)实际上创建了2个String对象,一个是使用“llo”通过双引号创建的(在字符串常量池)字面量在常量池中,另一个是通过new创建的(在堆里)。只不过他们的创建的时期不同,一个是编译期,一个是运行期。

对于 String s = "a"+"b"+"c";语句中,“a”,"b", "c"都是常量,编译时就直接存储他们的字面值,而不是他们的引用,在编译时就直接将它们连接的结果提取出来变成"abc"了,也就是不存在符号引用。

String的不可变性

简单的来说:String 类中使⽤ final 关键字修饰字符数组来保存字符串, private final char value[] ,所以 String 对象是不可变的,⽽ StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,在AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[]value 但是没有⽤ final 关键字修饰,所以这两种对象都是可变的

相关文章
|
7天前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
16天前
|
数据安全/隐私保护
【Azure Function App】PowerShell Function 执行 Get-AzAccessToken 的返回值类型问题:System.String 与 System.Security.SecureString
将PowerShell Function部署到Azure Function App后,Get-AzAccessToken返回值类型在不同环境中有差异。正常为SecureString类型,但部分情况下为System.String类型,导致后续处理出错。解决方法是在profile.ps1中设置环境变量$env:AZUREPS_OUTPUT_PLAINTEXT_AZACCESSTOKEN=false,以禁用明文输出。
|
16天前
|
安全 Java API
Java SE 与 Java EE 区别解析及应用场景对比
在Java编程世界中,Java SE(Java Standard Edition)和Java EE(Java Enterprise Edition)是两个重要的平台版本,它们各自有着独特的定位和应用场景。理解它们之间的差异,对于开发者选择合适的技术栈进行项目开发至关重要。
72 1
|
2月前
|
安全 算法 Java
Java泛型编程:类型安全与擦除机制
Java泛型详解:从基础语法到类型擦除机制,深入解析通配符与PECS原则,探讨运行时类型获取技巧及最佳实践,助你掌握泛型精髓,写出更安全、灵活的代码。
|
2月前
|
安全 IDE Java
Java记录类型(Record):简化数据载体类
Java记录类型(Record):简化数据载体类
299 120
|
2月前
|
Java 测试技术
Java浮点类型详解:使用与区别
Java中的浮点类型主要包括float和double,它们在内存占用、精度范围和使用场景上有显著差异。float占用4字节,提供约6-7位有效数字;double占用8字节,提供约15-16位有效数字。float适合内存敏感或精度要求不高的场景,而double精度更高,是Java默认的浮点类型,推荐在大多数情况下使用。两者都存在精度限制,不能用于需要精确计算的金融领域。比较浮点数时应使用误差范围或BigDecimal类。科学计算和工程计算通常使用double,而金融计算应使用BigDecimal。
876 102
|
2月前
|
安全 Java 编译器
Java类型提升与类型转换详解
本文详解Java中的类型提升与类型转换机制,涵盖类型提升规则、自动类型转换(隐式转换)和强制类型转换(显式转换)的使用场景与注意事项。内容包括类型提升在表达式运算中的作用、自动转换的类型兼容性规则,以及强制转换可能引发的数据丢失和运行时错误。同时提供多个代码示例,帮助理解byte、short、char等类型在运算时的自动提升行为,以及浮点数和整型之间的转换技巧。最后总结了类型转换的最佳实践,如避免不必要的转换、使用显式转换提高可读性、金融计算中使用BigDecimal等,帮助开发者写出更安全、高效的Java代码。
135 0
|
3月前
|
自然语言处理 Java Apache
在Java中将String字符串转换为算术表达式并计算
具体的实现逻辑需要填写在 `Tokenizer`和 `ExpressionParser`类中,这里只提供了大概的框架。在实际实现时 `Tokenizer`应该提供分词逻辑,把输入的字符串转换成Token序列。而 `ExpressionParser`应当通过递归下降的方式依次解析
222 14
|
4月前
|
存储 JSON JavaScript
[go]byte类型, string 类型, json 类型
本文介绍了Go语言中byte类型的基本概念、特点及用法。byte是8位无符号整数,取值范围为0-255,常用于二进制数据操作,如网络通信和文件读写。文章还详细说明了byte与字符串的转换、遍历byte数据以及与其他类型间的转换。此外,探讨了Go中json.Marshal和json.Unmarshal函数实现[]byte与JSON间的转换,并对比了[]byte与JSON的区别,帮助开发者更好地理解其应用场景与差异。
147 2

热门文章

最新文章