素材来自互联网,因为太多,不一一列举了。
Java把内存分成两种,一种叫做栈内存,一种叫做堆内存。
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。这就意味着随着代码块的结束,内存得到释放。堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。栈中的变量指向堆内存中的变量。
String并不是基本数据类型(八种基本数据类型:Byte,Short,Integer,Long,Float,Double,Character,Boolean),而是一个对象,并且是不可变的对象(String类为final型的(当然也不可被继承),所以存放在堆内存中。几乎每一个修改String对象的操作,实际上都是创建了一个全新的String对象)。String 作为一种对象,表现的更加特殊一些,有基础变量的一些特性,也有对象的特点(Java将所有类型均定义为对象)。
一、初始化:
1.字符串为对象,那么在初始化之前,它的值为null,
String a=null;//对象的引用还没有创建,也没有分配内存空间
String a="";//已经new了,只不过内部为空,但是它创建了对象的引用,是需要分配内存空间的。
String a=new String();//同上
2.在java中有一个“字符数据池”的内存管理机制。在字符串直接赋值时,会触发。当然字符数据池也在堆内存中。
String s1=new String("abc");//直接在堆中生成新的“abc”和字符数据池无关
String s2=new String("abc");//直接在堆中生成新的“abc”和字符数据池无关
String s3="abc";//先去“字符数据池”搜索时候有“abc”这个字符串,如果有,则将字符串的首地址赋值给s3,如果没有,则在“字符数据池”中生成一个新的字符串“abc”并且将首地址赋值给s3;
这个机制是非常有用的,因为可以提高效率,减少了内存空间的占用。由于这种机制的存在,使用字符串的过程中,推荐使用直接赋值(即String s=”abc”),除非有必要才会新建一个String对象(即String s = new String(”aa”))。
二、字符串的存储(字符串池在哪?)
String s = new String("abc");中的abc存储在堆空间中,而s则是在操作数栈中。这里"abc"本身就是常量池中的一个对象,而在运行时执行new String()时,将常量池中的对象复制一份放到堆中,并且把堆中的这个对象的引用交给s持有。所以这条语句就创建了2个String对象。String s = "abc";直接对于字符串池进行操作。
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。 常量池属于类型信息的一部分,类型信息也就是每一个被转载的类型,这个类型反映到JVM内存模型中是对应存在于JVM内存模型的方法区中,也就是这个类型信息中的常量池概念是存在于在方法区中,而方法区是在JVM内存模型中的堆中由JVM来分配的。所以,abc的值是应该是存在堆空间中的。
三、字符串比较
Java各个对象的equals(),都会有自己的实现。String的是:判断内容是否相同。== :判断内容与地址是否相同。
String a = "abc";
String b = "abc";
String c = new String("abc");
String d = new String("abc");
a == b//true,a,b指向的地址相同
c == d//false,new方法不使用字符数据池,所以地址不同
a == c//false
a.equals(b)//true
a.equals(c)//true
从这个结果来看,首先应该了解Java中引用的概念。
Object * oop = new Object(); //c++中的定义
Java中没有了传统的指针类型,那么对象指针变量前面的*也就可以不要了。对象指针变量也就可以简称为对象变量了,反正也不会和其它概念混淆!所有的对象变量都是指针,没有非指针的对象变量。所以,a == b,意味着比较a和b的地址!由于字符数据池的存在,String b = "abc"时,和a指向了相同的区域,所以a == b为真。
四、不可变性
String可以进行+操作,但是结果并不是在原有字符串基础上操作,而是创建了新的字符串,所有在字符串上的操作几乎都是这样,比如subString(),这就是不可变性。在这里+操作有一个优化,JVM对于字符串常量的"+"号连接,将程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值。
五、常用API(为了面试,其他的用到了再查我觉得完全没问题)
charAt(int index) ------返回指定索引index位置上的字符,索引范围从0开始。
indexOf(String str)------从字符串开始检索str,并返回第一次出现的位置,未出现返回-1。
subString(int beginIndex,int endIndex)------返回的字符串是从beginIndex开始到endIndex-1的串。
trim()-------
六、StringBuffer 和StringBuilder
上面了解了String对象的特性,可以说它的特点有利有弊吧,所以Java也提供了其他对象供选择。
StringBuffer和String一样都是用来存储字符串的,只不过由于他们内部的实现方式不同,导致他们所使用的范围不同,对于StringBuffer而言,他在处理字符串时,若是对其进行修改操作,它并不会产生一个新的字符串对象,所以说在内存使用方面它是优于String的。
其实在使用方法,StringBuffer的许多方法和String类都差不多,所表示的功能几乎一模一样,只不过在修改时StringBuffer都是修改自身,而String类则是产生一个新的对象,这是他们之间最大的区别。
同时StringBuffer是不能使用=进行初始化的,它必须要产生StringBuffer实例,也就是说你必须通过它的构造方法进行初始化。
在StringBuffer的使用方面,它更加侧重于对字符串的变化,例如追加、修改、删除,相对应的方法:
1、append():追加指定内容到当前StringBuffer对象的末尾,类似于字符串的连接,这里StringBuffer对象的内容会发生改变。
2、insert:该类方法主要是在StringBuffer对象中插入内容。
3、delete:该类方法主要用于移除StringBuffer对象中的内容。
StringBuilder也是一个可变的字符串对象,他与StringBuffer不同之处就在于它是线程不安全的,基于这点,它的速度一般都比StringBuffer快。与StringBuffer一样,StringBuider的主要操作也是append与insert方法。这两个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符添加或插入到字符串生成器中。
1、String:在字符串不经常变化的场景中可以使用String类,如:常量的声明、少量的变量运算等。
2、StringBuffer:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等。
3、StringBuilder:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,如SQL语句的拼装、JSON封装等(貌似这两个我也是使用|StringBuffer)。
七、如何优化?
String中最需要优化的恐怕就是频繁的拼接操作了,你可以根据上面的条件来选择使用,那么他们具体是如何做到的呢?
对于字符串而言我们经常是要对其进行拼装处理的,在java中提高了三种拼装的方法:+、concat()以及append()方法。
append()速度最快,concat()次之,+最慢。原因请看下面分解:
(一)+方式拼接字符串
在前面我们知道编译器对+进行了优化,它是使用StringBuilder的append()方法来进行处理的,我们知道StringBuilder的速度比StringBuffer的速度更加快,但是为何运行速度还是那样呢?主要是因为编译器使用append()方法追加后要同toString()转换成String字符串,也就说 str +=”b”等同于
str = new StringBuilder(str).append("b").toString();
它变慢的关键原因就在于new StringBuilder()和toString(),这里可是创建了10W个StringBuilder对象,而且每次还需要将其转换成String,速度能不慢么?
(二)concat()方法拼接字符串
public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } char buf[] = new char[count + otherLen]; getChars(0, count, buf, 0); str.getChars(0, otherLen, buf, count); return new String(0, count + otherLen, buf); }
这是concat()的源码,它看上去就是一个数字拷贝形式,我们知道数组的处理速度是非常快的,但是由于该方法最后是这样的:return new String(0, count + otherLen, buf);这同样也创建了10W个字符串对象,这是它变慢的根本原因。
(三)append()方法拼接字符串
public synchronized StringBuffer append(String str) { super.append(str); return this; }
StringBuffer的append()方法是直接使用父类AbstractStringBuilder的append()方法,该方法的源码如下:
public AbstractStringBuilder append(String str) { if (str == null) str = "null"; int len = str.length(); if (len == 0) return this; int newCount = count + len; if (newCount > value.length) expandCapacity(newCount); str.getChars(0, len, value, count); count = newCount; return this; }
与concat()方法相似,它也是进行字符数组处理的,加长,然后拷贝,但是请注意它最后是返回并没有返回一个新串,而是返回本身,也就说这这个10W次的循环过程中,它并没有产生新的字符串对象。
通过上面的分析,我们需要在合适的场所选择合适的字符串拼接方式,但是并不一定就要选择append()和concat()方法,原因在于+根据符合我们的编程习惯,只有到了使用append()和concat()方法确实是可以对我们系统的效率起到比较大的帮助,才会考虑,同时鄙人也真的没有怎么用过concat()方法。