`

JDK源码分析之String篇

    博客分类:
  • java
 
阅读更多

------------------------------String在内存中的存储情况(一下内容摘自参考资料1)-----------------------------------

前提:先了解下什么是声明,什么时候才算是产生了对象实例

其中x并未看到内存分配,变量在使用前必须先声明,再赋值,然后才可以使用。java基础数据类型会用对应的默认值进行初始化

一、首先看看Java虚拟机JVM的内存块及其变量、对象内存空间是怎么存储分配的

 

1、栈: 存放基本数据类型及对象变量的引用,对象本身不存放于栈中而是存放于堆中

1)、基础类型 byte (8位)、boolean (1位)、char (16位)、int (32位)、short (16位)、float (32位)、double (64位)、long (64位)

2)、java代码作用域中定义一个变量时,则java就在栈中为这个变量分配内存空间,当该变量退出该作用域时,java会自动释放该变量所占的空间

2、堆: new操作符的对象

1)、new创建的对象和数组

2)、在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理

3、静态域: static定义的静态成员变量

4、常量池: 存放常量

二、Java String类型

Java中String不是基本数据类型,而是一种特殊的类。String代表的是不可变的字符序列,为不可变对象,一旦被创建,就不能修改它的值,对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去

三 、String实例代码分析

 1 package terry.java.base;
 2 
 3 public class StringTest {
 4      public static void main(String[] args) {
 5       String a = "hello";
 6       String b = "hello";
 7       
 8       String newA = new String("hello");
 9       String newB = new String("hello");
10       
11       System.out.println("****** Testing Object == ******");
12       System.out.println("a==b ? :" + (a==b));
13       System.out.println("newA==newB ? :" +(newA==newB));
14       System.out.println("a==newA ? :" + (a==newA));
15       
16       System.out.println("***** Testing String Object intern method******");
17       System.out.println("a.intern()==b.intern() ? : " + (a.intern()==b.intern()));
18       System.out.println("newA.intern()==newB.intern() ? :" + (newA.intern()==newB.intern()));
19       System.out.println("a.intern()==newA.intern() ? :" + (a.intern()==newA.intern()));
20       System.out.println("a=a.intern() ? :" + (a==a.intern()));
21       System.out.println("newA==newA.intern() ? : " + (newA==newA.intern()));
22       
23       System.out.println("****** Testing String Object equals method******");
24       System.out.println("equals() method :" + a.equals(newA));
25       
26       String c = "hel";
27       String d = "lo";
28       final String finalc = "hel";
29       final String finalgetc = getc();
30       
31       System.out.println("****** Testing Object splice ******");
32       System.out.println("a==\"hel\"+\"lo\" ? :" + (a=="hel"+"lo"));
33       System.out.println("a==c+d ? : " + (a==c+d));
34       System.out.println("a==c+\"lo\" ? : " + (a==c+"lo"));
35       System.out.println("a==finalc+\"lo\" ? :" + (a==finalc+"lo"));
36       System.out.println("a==finalgetc+\"lo\" ? :" + (a==finalgetc+"lo"));
37       
38      }
39      private static String getc(){
40       return "hel";
41      } 
42 }
View Code

Run As Java Application -- 输出结果:

 1 ****** Testing Object == ******
 2 a==b ? :true
 3 newA==newB ? :false
 4 a==newA ? :false
 5 ***** Testing String Object intern method******
 6 a.intern()==b.intern() ? : true
 7 newA.intern()==newB.intern() ? :true
 8 a.intern()==newA.intern() ? :true
 9 a==a.intern() ? :true
10 newA==newA.intern() ? : false
11 ****** Testing String Object equals method******
12 equals() method :true
13 ****** Testing Object splice ******
14 a=="hel"+"lo" ? :true
15 a==c+d ? : false
16 a==c+"lo" ? : false
17 a==finalc+"lo" ? :true
18 a==finalgetc+"lo" ? :false
View Code

内存分析:

上述各个变量及引用在JVM分配的内存情况

String类型对象实例直接赋值和new操作符产生的结果在JVM内存分配过程是不同的,如下注释说明

1 String a = "hello"; //先在栈中创建一个对String类的对象引用变量a,然后通过符号引用去字符串常量池里找有没有"hello",如果没有,则将"hello"存放进字符串常量池 ,并令a指向"hello",如果已经有"hello"则直接将a指向"hello"    -->  产生1个对象及1个引用
2 String b = "hello"; //先在栈中创建一个对String类的对象引用变量b,然后通过符号引用去字符串常量池里找有没有"hello",因为之前在常量池中已经有"hello",所以直接将b指向"hello"    -->  因为不需要在常量池产生"hello",所以只是在栈中产生1个引用
3 String newA = new String("hello"); //先在栈中创建一个对String类的对象引用变量newA,然后new()操作会在heap堆中产生一个新的对象"hello",并将newA指向堆中的"hello",同时检查String pool常量池中是否有对象"hello",如果没有也产生一个对象"hello",如果有则不产生,因为这里之前已经在常量池中产生过了,所以   -->  只需要产生1个对象及1个引用
4 String newB = new String("hello");  //因为new每次都会保证在heap堆内存中产生新的对象,并将栈中的引用指向对应的堆中的地址,所以此语句同上一条的处理
View Code

String常量+的拼接 及 String常量与引用实例+的拼接 的区别

1 System.out.println("a==\"hel\"+\"lo\" ? :" + (a=="hel"+"lo"));  //JVM对于字符串常量的"+"号连接,在程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值,因此"hel" + "lo"优化后完全等同于"hello"
2 System.out.println("a==c+d ? : " + (a==c+d)); 
3 System.out.println("a==c+\"lo\" ? : " + (a==c+"lo"));  //JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,所以c+\"lo\"实际是c在栈中保存的地址+字符串"lo"于常量池中指向的地址 所指向的在堆中新分配的一块内存空间
4 System.out.println("a==finalc+\"lo\" ? :" + (a==finalc+"lo"));  //对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量 池中或嵌入到它的字节码流中,在编译期就已经确定了内存空间地址,所以此类似于2个字符串常量的+
5 System.out.println("a==finalgetc+\"lo\" ? :" + (a==finalgetc+"lo"));
View Code

关于String对象的intern()方法的说明

一个初始时为空的字符串池,它由类 String  私有地维护

当调用 intern 方法时,如果池已经包含一个等于此 String  对象的字符串(该对象由  equals(Object)  方法确定),则返回池中的字符串。否则,将此 String  对象添加到池中,并且返回此 String  对象的引用,因此a.intern(),b.intern(),newA.intern(),newB.intern()隐含的各自在栈中分配了各自的内存区域,同时都将栈中的应用全部指向了String pool常量池中的同一块区域"hello"

-----------------------------------------------Thinking in Java 读书笔记--------------------------------------------

《Java编程思想<第四版>》-第13章

本章第一小节标题为“不可变String”,第一句话为“ String对象时不可变的 ”。String类中任何一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容,原来的String对象丝毫未动。

给Java初学者举个栗子,有基础的直接跳过:

1 String s = "ABCabc";
2 System.out.println("s = " + s);
3  
4 s = "123456";
5 System.out.println("s = " + s);
View Code

打印结果是:

s = ABCabc
s = 123456
View Code

貌似s的值被改变了,改变的只是s的这个引用,本来s该引用指向了 常量池中 的“ABCabc”,后来指向了 常量池中 的“123456”。结合上面的内存模型,常量池中有两个常量“ABCabc”、“123456”,栈中只有一个引用s,本来这个s指向“ABCabc”,后来被赋值给了“123456”,图就不画了,还不明白,就别往下看了,看一点更基础的比较好。

来看下String的源代码:

public final class String
	implements java.io.Serializable, Comparable<String>, CharSequence {
	/** The value is used for character storage. */汉化一下:这个value是用来装char的.明白了吧,String其实是char数组的包装类。
	//而这个数组是final类型的,不可能指向别的对象,但是可以改,这个请耐心看下去
	private final char value[];
	/** Cache the hash code for the string */缓存hashCode
	private int hash; // Default to 0
}

源码中String的本质是一个final类型的char数组,既然是final类型,那个该数组引用value就不允许再指向其他对象了,因此只从类的设计角度讲:如果jdk源码中并没有提供对value本身的修改,那么理论上来讲String是不可变的。

StringBuilder的故事

大家都知道String的有两个关系很亲近的小伙伴:StringBuffer、StringBuilder,其中的区别大家可以看源代码,StringBuffer对每个方法(除了构造函数)都用了同步,StringBuilder就内向多了。

 

大家还知道java中是不支持运算符重载的,但是有且仅有两个例外:+、+=对String进行的重载。

+、+=被重载了,+用来连接String操作:就是说 “a" + "b" + ”c“= ”abc“;

上文书说道:

String是不可变的,即是说String a = "a" ; a + "b" 之后,a还是”a“,这个”ab“实际上又生成了一个新的String对象。将这个赋值语句剖析一下:

"a" + "b" + "c":先计算前两个  "a" + "b":这时内存中其实有四个字符串,”a“,"b","c","ab",然后再+”c“,这是内存中有五个String:”a“,"b","c","ab",”abc“。

真的是这样吗?

可以通过javap来反编译上面的赋值语句,会发现,在编译本条语句时,编译器会自作主张的 引入了StringBuilder ,并调用了StringBuilder.append方法,这样就不用再生成多余的字符串了;

因此在用StringBuilder进行append操作时候,千万不要使用append("a" + "b")这样的操作,因为酱,编译器会为你另外的创建一个StringBuilder对象来处理括号里的字符串操作。

toString的故事,无意识的递归:

toString方法里面谨慎返回this,可能因此 无限递归

this遇到+“”时候,会将this转换成String,怎么转换呢?通过调用toString方法的好了,无限递归,栈溢出。

String真的不可变吗?

从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。 那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:

//创建字符串"Hello World", 并赋给引用s
    String s = "Hello World"; 
     
    System.out.println("s = " + s); //Hello World
     
    //获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");
     
    //改变value属性的访问权限
    valueFieldOfString.setAccessible(true);
     
    //获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(s);
     
    //改变value所引用的数组中的第5个字符
    value[5] = '_';
     
    System.out.println("s = " + s);  //Hello_World
}
View Code

打印结果为: s = Hello World

s = Hello_World

在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。 

常用方法:

public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];//先判断越界,然后直接返回合法值
    }
CharAt()
public boolean isEmpty() {
        return value.length == 0;
    }
isEmpty()
public int length() {
        return value.length;
    }
length()

从源码的实现来看,length() == 0 和 isEmpty()效率是一样的。

public boolean equals(Object anObject) {
		if (this == anObject) {
			return true;
		}//重写了Object的equals方法,判断字符串内容
		if (anObject instanceof String) {
			String anotherString = (String) anObject;
			int n = value.length;
			if (n == anotherString.value.length) {
				char v1[] = value;
				char v2[] = anotherString.value;
				int i = 0;
				while (n-- != 0) {
					if (v1[i] != v2[i])
							return false;
					i++;
				}
				return true;
			}
		}
		return false;
	}
equals()
public boolean contentEquals(StringBuffer sb) {
        synchronized (sb) {
            return contentEquals((CharSequence) sb);
        }
    }
contentEquals(StringBuilder sb)
public boolean contentEquals(CharSequence cs) {
		if (value.length != cs.length())
			return false;
		// Argument is a StringBuffer, StringBuilder
		if (cs instanceof AbstractStringBuilder) {
			char v1[] = value;
			char v2[] = ((AbstractStringBuilder) cs).getValue();
			int i = 0;
			int n = value.length;
			while (n-- != 0) {
				if (v1[i] != v2[i])
					return false;
				i++;
			}
			return true;
		}
		// Argument is a String
		if (cs.equals(this))
			return true;
		// Argument is a generic CharSequence
		char v1[] = value;
		int i = 0;
		int n = value.length;
		while (n-- != 0) {
			if (v1[i] != cs.charAt(i))
				return false;
			i++;
		}
		return true;
	}
contentEquals(CharSequence cs)

contentEquals(charSequence cs)参数可以是StringBuilder、StringBuffer、String以及CharSequence,contentEquals(StringBuilder sb)只是为了保证sb的一致性,在外面加了互斥锁。equals方法也只是contentEquals的一种情况的实现,完全可以用contentEquals(cs)来取代。

public String concat(String str) {
		int otherLen = str.length();
		if (otherLen == 0) {
			return this;
		}
		int len = value.length;
		char buf[] = Arrays.copyOf(value, len + otherLen);
		str.getChars(buf, len);
		return new String(buf, true);//返回了新的string
	}
concat(String str)

concat方法可以看出,最后返回的其实是一个新new的string。并不对原value内容进行改动。

同样的:

public String substring(int beginIndex) {
		if (beginIndex < 0) {
			throw new StringIndexOutOfBoundsException(beginIndex);
		}
		int subLen = value.length - beginIndex;
		if (subLen < 0) {
			throw new StringIndexOutOfBoundsException(subLen);
		}
		return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
	}
substring

这两个方法很好用

public boolean startsWith(String prefix, int toffset) {
	char ta[] = value;
	int to = toffset;
	char pa[] = prefix.value;
	int po = 0;
	int pc = prefix.value.length;
	// Note: toffset might be near -1>>>1.
	if ((toffset < 0) || (toffset > value.length - pc)) {
	    return false;
	}
	while (--pc >= 0) {
	    if (ta[to++] != pa[po++]) {
		return false;
	    }
	}
	return true;
    }
startsWith
public boolean endsWith(String suffix) {
        return startsWith(suffix, value.length - suffix.value.length);
    }
endsWith

endsWith是用startsWith实现的,只看名字有点搞笑。

分享到:
评论

相关推荐

    JDK源码分析之String、StringBuilder和StringBuffer

    主要给大家介绍了关于JDK源码分析之String、StringBuilder和StringBuffer的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用jdk具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

    java源码剖析-JavaSourceLearn:JDK1.8源码的代码分析和学习

    JDK1.8源码分析 导入源码过程中的注意事项 源码在%JAVA_HOME%\src.zip 源码在src目录下 以下两个类手动添加的,解决编译过程中该包的丢失 sun.font.FontConfigManager sun.awt.UNIXToolkit 其中: 1.请手动添加jdk...

    jdk8-source-code:jdk源码解析

    jdk1.8-source-analysis JDK1.8源码分析引入原始过程中的注意事项JDK1.8对应JDK版本下载: 码:49wi原始码在src目录下以下两个类手动添加的,解决编译过程中该包的丢失sun.font.FontConfigManager sun.awt....

    jdk1.8-source-analysis:JDK1.8源码分析

    jdk1.8-source-analysis JDK1.8源码分析引入原始过程中的注意事项JDK1.8对应JDK版本下载: 码:49wi原始码在src目录下以下两个类手动添加的,解决编译过程中该包的丢失sun.font.FontConfigManager sun.awt....

    Collections源码java-jdk1.8-source-analysis:Java8源码分析,J.U.C、ThreadPool、Col

    JDK1.8源码分析 导入源码过程中的注意事项 JDK1.8对应JDK版本下载: 提取码:49wi 源码在src目录下 以下两个类手动添加的,解决编译过程中该包的丢失 sun.font.FontConfigManager sun.awt.UNIXToolkit 其中: 1.请...

    jdk1.8-source-analysis:Java 8源码分析,JUC,ThreadPool,Collection

    JDK1.8源码分析 引入原始过程中的注意事项 JDK1.8对应JDK版本下载: 码:49wi 原始码在src目录下 以下两个类手动添加的,解决编译过程中该包的丢失 sun.font.FontConfigManager sun.awt.UNIXToolkit 其中:1.请...

    硬核ArrayList源码分析,答应我每天看一遍好么

    本篇源码分析会进行每步分析,当所有方法分析完最后还会做个大总结;如果不爱看源码分析步骤,只要面试最终结论的读者可以看看文末的总结就行了; 知识追寻者(Inheriting the spirit of open source, Spreading ...

    javajdk1.8源码-Java-source-reading:jdk1.8源代码分析

    java jdk1.8 源码 Java-source-reading 缓慢更新一些个人学习java相关源码过程中的笔记,在这里你将不可避免地看到以下情况: ...String java.util Arrays ArrayList LinkedList HashMap HashSet LinkedHashMap

    java8集合源码分析-JUC:高并发与多线程

    集合源码分析 高并发与多线程 Stargazers over time 线程 线程的创建和启动 线程的sleep、yield、join 线程的状态 代码在 部分。 synchronized关键字(悲观锁) synchronized(Object) 不能用String常量、Integer、Long...

    动力节点Java基础301集_史上最全的Java基础教程

    4: 源码分析分析讲的特别到位,尤其是HashMap的工作原理和源码分析,真正的把jdk源码翻了一遍,要是拿着这个去面试绝对是秒杀级神器。 5:使用多线程模拟用户去ATM取钱讲的也非常不错,后续还提了一个小Timer定时任务...

    Jdk动态代理 底层

    java动态代理主要有2种,Jdk动态代理、Cglib动态代理,本文主要讲解Jdk动态代理的使用、运行机制、以及源码分析。当spring没有手动开启Cglib动态代理,即:或@EnableAspectJAutoProxy(proxyTargetClass = true),...

    最全java面试题及答案

    具体来说 JDK 其实包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要...

    两万字吐血总结,代理模式及手写实现动态代理(aop原理,基于jdk动态代理)

    jdk动态代理源码分析(通过该示例学会阅读源码的方法)2.jdk动态代理生成的代理类的源码3.总结三、手写实现jdk动态代理 一、代理模式 熟悉代理模式的可以直接点击目录第二章,jdk动态代理实现原理,本文的精髓所在,...

    生鲜配送平台源码java-bj:北京

    程序调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。 2 .== 和 equals 的区别是什么? == 解读 对于基本类型和引用类型 == 的作用效果是...

    Java-String&StringBuilder&StringBuffer的区别与分析

    文章目录一、前言二、值可变性三、线程安全性四、时间性能排名 一、前言 此博客基于JDK1.8。 我们先用一张表格来回顾一下Java中String,StringBuilder,StringBuffer...接着我们由源码可以看到,Java中String类,Stri

    生鲜配送平台源码java-test_0723:我的第一个git仓库

    程序调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。 2.== 和 equals 的区别是什么? == 解读 对于基本类型和引用类型 == 的作用效果是...

    Map

    前面源码分析中常见它的身影,在里面充当了一个什么作用? Map存储的元素为键值对,通常称为key-value 而key是不允许重复的 Set存储对象唯一 二、掌握Map的常用方法 三、基本方法的使用 掌握上述的每个方法的应用...

    java基础案例与开发详解案例源码全

    9.1 String类220 9.1.1 字符串常量221 9.1.2 字符串对象操作224 9.1.3 字符串对象修改228 9.1.4 类型转换230 9.2 StringBuffer类的使用231 9.3 StringBuilder类的使用233 9.4 日期类简介234 9.5 Java语言国际化时间...

    java范例开发大全(pdf&源码)

    第1篇 Java编程基础 第1章 Java开发环境的搭建(教学视频:9分钟) 2 1.1 理解Java 2 1.2 搭建Java所需环境 3 1.2.1 下载JDK 3 1.2.2 安装JDK 4 1.2.3 配置环境 5 1.2.4 测试JDK配置是否成功 7 实例1 开发第一个Java...

Global site tag (gtag.js) - Google Analytics