String详细讲解

  • 目录

     

    String

    1、String字符串

    2、String字符串常用方法

    3、StringBuffer的常用方法:

    4、StringBuilder

    原因解析:

    new String("abc")创建几个对象的解释

    String,new String,StringBuffer的性能优化思考

    Object o与Object o=null的区别

    字符串比较

    字符串的判断有2种:


String

1、String字符串

  实例化String对象:

  (1)直接赋值,如:String str="hello";

  (2)使用关键字 new,如:String str=new String("hello");

 

        String详细讲解_第1张图片

   

  由图可知:使用 new 的方式在堆内存中开辟了两个空间,

  第一个 "hello",对象 str 没有指向,无用等待回收,

  第二个 "hello",被 str 指向,有用。

    所以 直接赋值(只开辟了一个空间) 的方式更常用和合理,可以节省一些空间。

  字符串的内容不可更改

     如:

   

        String详细讲解_第2张图片

   

2、String字符串常用方法

     String字符串的方法较多,可以根据API给出的方法去做测试,下面是常用方法:

     (1)字符串长度:length() 「数组中的是 length 属性」

     (2)字符串转换数组:toCharArray()

     (3)从字符串中取出指定位置的字符:charAt()

     (4)字符串与byte数组的转换:getBytes()

     (5)过滤字符串中存在的字符:indexOf()  返回指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1。(可以用在判断多位数是否包含某个数字时,先转成string再判断)

     (6)去掉字符串的前后空格:trim() 「空格易影响到对字符串的判断,需适时去掉」

     (7)从字符串中取出子字符串:subString()

     (8)大小写转换:toLowerCase() toUpperCase()

     (9)判断字符串的开头结尾字符:startWith() endsWith()

     (10)替换String字符串中的一个字符:replace()

 

3、StringBuffer的常用方法:

  append() insert() replace() indexOf()

  StringBuffer类的应用:

     为什么有的时候要用StringBuffer,而不用String:

   String是不可变的,所以要对String作改动不能直接改那个String本身,而是要创建新的String,所以如果改动次数多,用String就效率不高,而StringBuffer(如果是单线程则建议用StringBuilder,不用考虑线程安全性,效率更高点)。有朋友说1.5后StringBuffer和String的拼接性能差不多则不敢苟同,如下测试在我的机器上用JDK1.8测试:

 1 public class demo {
 2     public static void main(String[] args) {
 3         long l = System.currentTimeMillis();
 4         String s = "";
 5         for (int i = 0; i < 100000; i++) {
 6             s += "q";
 7         }
 8         System.out.println(System.currentTimeMillis() - l);
 9         System.out.println(s.substring(0, 1));
10 
11         l = System.currentTimeMillis();
12         StringBuffer sb = new StringBuffer();
13         for (int i = 0; i < 100000; i++) {
14             sb.append("q");
15         }
16         System.out.println(System.currentTimeMillis() - l);
17         System.out.println(sb.toString().substring(0, 1));
18     }
19 }
 

输出:

3870
q
3
q

 

    也就是做10万次拼接,String要3.87秒,而StringBuffer则只要0.003秒,差别还是相当大的。(JDK1.6有9s多的差距)

 

4、StringBuilder

  一个可变的字符序列,该类被设计作用StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候,建议优先考虑该类,速度比StringBuffer要快

     但如果涉及到线程安全方面,建议使用StringBuffer

    常用方法:

  append() insert() …

 

String是一个非常常用的类,应该深入的去了解String

如:

String str =new String("abc")

String str1 = "abc"

System.out.println(str == str1)

System.out.println(str.equal(str1))

结果:

false

true

 

原因解析:

  • Java运行环境有一个字符串池,由String类维护。

 1. 执行语句String str="abc";时。首先查看字符串池中是否存在字符串"abc",如果存在则直接将“abc”赋给str,如果不存在则先在  字 符串池中新建一个字符串"abc",然后再将其赋给str.

 2. 执行语句String str = new String("abc");时。不管字符串池中是否存在字符串“abc”,直接新建一个字符串“abc”,(注意,新建的字符串“abc”不是在字符串池中), 然后将其赋给str。由此可见 1.的效率要高于2的效率。


 3. String str1="java";//指向字符串常量池
     String str2="blog";//指向字符串常量池
       String   s = str1+str2;

  +运算符会在中建立起两个String对象(注意是堆中不是字符串常量池,这两个对象的值分别是“java”,"blog",也就是说从字符串常量池中复制这两个值,然后再堆中创建两个对象。然后再建立对象s,然后将“javablog”的堆地址赋给s.  这句话共创建了3个String对象


       System.out.println(s=="javablog");//结果是false;

       JVM确实对形如String str="javablog";的对象放在常量池中,但是它是在编译时name做的。而String s=str1+str2;是在运行时候才能知道的,也就是说str1+str2是在堆里创建的,所以结果为false了

1     String s="java"+"blog";//直接将javablog对象放入字符串池中。        
2     System.out.println(s=="javablog");//结果是true;

3     String s=str1+"blog";//不放在字符串池中,而是在堆中分分配。       
4     System.out.println(s=="javablog");//结果是false;


      总之,创建字符串有两种方式:两种内存区域(pool,heap)
      1.""创建的字符串在字符串池中。
      2.new 创建字符串时,首先查看池中是否有相同的字符串,如果有则拷贝一份放到堆中,然后返回堆中的地址;如果池中没有则在堆中创建一分,然后返回堆中的地址,
      3.在对字符串赋值时,如果右操作数含有一个或一个以上的字符串引用时,则在堆中再建立一个字符串对象,返回引用如:String s= str1+"blog";
 

之间的区别

第1种:

  String a="abc";
  String b="abc";
  System.out.print(a==b);

  结果:true

  原因:编译时,这两个"abc"被认为是同一个对象保存到了常量池中;运行时JVM则认为这两个变量赋的是同一个对象,所以返回true。

---------------------
第2种:

  String a=new String("abc");
  String b=new String("abc");
  System.out.print(a==b);

  结果:false

  原因:用构造器创建的对象,是不会被放入常量池中的,也很明显这完全是两个对象,只是内容相同罢了,结果当然为false了。用equals()或者System.out.print(a.intern()==b.intern());就返回true了。

------------------------------
第3种

  String a="abc";
  String b=new String("abc");
  System.out.print(a==b);

  结果:false

  原因:同上。此外,a的类加载时就完成了初始化,而b要在执行引擎执行到那一行代码时才完成初始化。

---------------------------
第4种

  String a="abcdef";
  System.out.print(a=="abcdef");

  结果:true

  原因:运行出现的字符串常量,若是在常量池中出现过,则JVM会认为同一个对象,以节省内存开销,所以这两个字符串会被认为是同一个对象。

-------------------------------------------
第5种

  String a="abcdef";
  String b="";
  String c=a+b;
  System.out.print(c=="abcdef");

  结果:false

  原因:编译时,先将"abcedf"放在常量池中,而c的值则是在运行时在堆里创建的。所以为false。

new String("abc")创建几个对象的解释

String str=new String("abc");创建了几个String对象呢?答案也是众所周知的,2个。接下来我们就从这道题展开,一起回顾一下与创建String对象相关的一些JAVA知识。

      我们可以把上面这行代码分成String str="abc"new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器:

public String(String original) {
//other code ...
}

大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:

使用new创建对象。
调用Class类的newInstance方法,利用反射机制创建对象。

我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本。

这种方式是String特有的,并且它与new的方式存在很大区别。

String str="abc";

毫无疑问,这行代码创建了一个String对象。

String a="abc";
String b="abc";

那这里呢?答案还是一个。

String a="ab"+"cd";

再看看这里呢?答案是三个。有点奇怪吗?说到这里,我们就需要引入对字符串池相关知识的回顾了。

在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。

我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。

对于第三个例子:

String a="ab"+"cd";

"ab"和"cd"分别创建了一个对象,它们经过“+”连接后又创建了一个对象"abcd",因此一共三个,并且它们都被保存在字符串池里了。

现在问题又来了,是不是所有经过“+”连接后得到的字符串都会被添加到字符串池中呢?我们都知道“==”可以用来比较两个变量,它有以下两种情况:

      如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),则是判断它们的值是否相等。
如果表较的是两个对象变量,则是判断它们的引用是否指向同一个对象。


下面我们就用“==”来做几个测试。为了便于说明,我们把指向字符串池中已经存在的对象也视为该对象被加入了字符串池:

public class StringTest {
public static void main(String[] args) {
    String a = "ab";// 创建了一个对象,并加入字符串池中
    System.out.println("String a = \"ab\";");
    String b = "cd";// 创建了一个对象,并加入字符串池中
    System.out.println("String b = \"cd\";");
    String c = "abcd";// 创建了一个对象,并加入字符串池中

    String d = "ab" + "cd";
    // 如果d和c指向了同一个对象,则说明d也被加入了字符串池
    if (d == c) {
        System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中");
    }
    // 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池
    else {
        System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中");
    }

    String e = a + "cd";
    // 如果e和c指向了同一个对象,则说明e也被加入了字符串池
    if (e == c) {
        System.out.println(" a  +\"cd\" 创建的对象 \"加入了\" 字符串池中");
    }
    // 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池
    else {
        System.out.println(" a  +\"cd\" 创建的对象 \"没加入\" 字符串池中");
    }

    String f = "ab" + b;
    // 如果f和c指向了同一个对象,则说明f也被加入了字符串池
    if (f == c) {
        System.out.println("\"ab\"+ b   创建的对象 \"加入了\" 字符串池中");
    }
    // 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池
    else {
        System.out.println("\"ab\"+ b   创建的对象 \"没加入\" 字符串池中");
    }

    String g = a + b;
    // 如果g和c指向了同一个对象,则说明g也被加入了字符串池
    if (g == c) {
        System.out.println(" a  + b   创建的对象 \"加入了\" 字符串池中");
    }
    // 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池
    else {
        System.out.println(" a  + b   创建的对象 \"没加入\" 字符串池中");
    }
    if (g == f) {
        System.out.println(" g==f");
    }
    // 如果g和f没有指向了同一个堆地址,则说明g没有被加入字符串池
    else {
        System.out.println(" g!=f");
    }
  }
}


运行结果如下:

String a = "ab";
String b = "cd";
"ab"+"cd" 创建的对象 "加入了" 字符串池中
a  +"cd" 创建的对象 "没加入" 字符串池中
"ab"+ b   创建的对象 "没加入" 字符串池中
a  + b   创建的对象 "没加入" 字符串池中
 g!=f


从上面的结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。因此我们提倡大家用引号包含文本的方式来创建String对象以提高效率,实际上这也是我们在编程中常采用的。

接下来我们再来看看intern()方法,它的定义如下:

public native String intern();

这是一个本地方法。在调用这个方法时,JAVA虚拟机首先检查字符串池中是否已经存在与该对象值相等对象存在,如果有则返回字符串池中对象的引用;如果没有,则先在字符串池中创建一个相同值的String对象,然后再将它的引用返回。

我们来看这段代码:

public class StringInternTest {
public static void main(String[] args) {
// 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
String a = new String(new char[] { 'a', 'b', 'c', 'd' });
String b = a.intern();
if (b == a) {
System.out.println("b被加入了字符串池中,没有新建对象");
} else {
System.out.println("b没被加入字符串池中,新建了对象");
}
}
}

运行结果:

b没被加入字符串池中,新建了对象

如果String类的intern()方法在没有找到相同值的对象时,是把当前对象加入字符串池中,然后返回它的引用的话,那么b和a指向的就是同一个对象;否则b指向的对象就是JAVA虚拟机在字符串池中新建的,只是它的值与a相同罢了。上面这段代码的运行结果恰恰印证了这一点。

最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别:

栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。
堆(heap):用于存储对象。

我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。

当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。

 

你好,
public class StringInternTest {
public static void main(String[] args) {
// 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
String a = new String(new char[] { 'a', 'b', 'c', 'd' });
String b = a.intern();
if (b == a) {
System.out.println("b被加入了字符串池中,没有新建对象");
} else {
System.out.println("b没被加入字符串池中,新建了对象");
}
}
}
请三思代码。方法体改为
String c = "abcd";  // 或者 String c = "abc"; 
String a = new String(new char[] { 'a', 'b', 'c', 'd' });
b = a.intern();
if (b == c) {
System.out.println("b被加入了字符串池中,没有新建对象");
} else {
System.out.println("b没被加入字符串池中,新建了对象");
}
才能说明问题。

 

String,new String,StringBuffer的性能优化思考

       java.lang.String类对大家来说最熟悉不过了,内容主要涉及初始化、串联和比较等操作。
      首先我们必须清楚的一点是String类是final类型的,因此你不可以继承这个类、不能修改这个类。我们使用String的时候非常简单,通常都是 String s = "hello",但是Java API中同时提供了一个构造函数为String(String s),因此你也可以这样使用String s = new String("hello",对于后面这样初始化一个String的方式是不推荐的,因为new操作符意味着将会在heap上生成一个新的对象,如果这样的操作发生在一个循环中,那么代价是惨重的。比如
for(int i = 0;i<1000;i++)
{
    String s = new String("hello";
}
这将会创建1000个String类型的对象,由于String类是final的,因此这样的操作事实上是每次都生成了一个新的String对象的。如果你使用String s = "hello";那么就可以实现复用了。
   看这两条语句:
   String   password="ok";
   String   password=new   String("ok";  
   不同的是,第一条现在内存中创建了"ok"这个String,然后将reference赋给password,如果这个时候还有一条语句String pwd = "ok";那么JVM将不再创建"ok",而是直接将第一个"ok"的reference赋给pwd,也就是说,password和pwd是使用同一块内存,而如果加上String   pwd   =   new   String("ok";那JVM将在内存中再创建一块区域放上“ok”这个字符串。
    通常对String的比较有两种情况,一个是使用==,另一个是使用equals()方法,注意==是对对象的地址进行比较的,而String中的 equals()方法是覆盖了Object类的方法,并且实现为对String对象的内容的比较。所以String s1 = new String("hello";String s2 = new String("hello",我们对s1和s2进行上述比较的时候,前者应该返回false,因为使用new生成的是两个不同的对象。后者应该返回 true因为他们的内容是一样的,都是"hello"。那么如果我们还有一个String s3 = "hello";他和s1的比较应该是什么样子的呢,答案是s1==s3为false,equals的比较位true。事实上String类是维持着一个 String池的,这个池初始化为空的,当我们String x = "hello"的时候,hello就会被放入这个池中,当我们再次String y = "hello"的时候,他首先去检查池中是否存在一个和hello内容一样的对象,如果存在的话就会把这个引用返回给y,如果不存在的话,就会创建一个并放入到池中。这样就实现了复用。在String有一个方法intern()他可以把String的对象放入到池冲并返回池中的对象。如果我们对s1 (String s1 = new String("hello")调用intern,s1 = s1.intern()这时候,我们再把s1和s3进行“==”的判断,你会发现结果返回true!

   String是一个final Class,StringBuffer不是。所以对于 String a = "yacht" ,String b = "yacht1" String c = a + b ; 存在一个对象拷贝构造和解析的消耗问题;对于一个StringBuffer来说,StringBuffer sb = new StringBuffer();sb.append("yacht" ; sb.append("yacht1"; 因为StringBuffer是一个可以实例化的类,而且它的内建机制是维护了一个capacity大小的字符数组,所以它的append操作不存在对象的消耗问题,所以我觉得如果存在String连接这种事情,StringBuffer来做会好很多。
    但事情并不是这么简单,看下面代码
    String a = "yacht1" + "yacht2" + "yacht3" + "yacht4";
    StringBuffer sb = new StringBuffer();
    sb.append("yacht1";
    sb.append("yacht2";
    sb.append("yacht3";
    sb.append("yacht4";
    String a = sb.toString();
    如果按照我先前说的看法,第一个效率肯定比第二个低,但经过测试不是这样,为什么?这里,我们需要理解程序过程的两个时期,一个是编译时,一个是运行时,在编译时,编译器会对你的程序做出优化,所以第一个的String a会被优化成yacht1yacht2yacht3yacht4,而第二个的StringBuffer只会在运行时才处理。所以效率是不一样的。
    如果代码是这样的:
    String a ;
    for(int i = 0; i< 100000;i++)
      a += String.valueOf(i);
     StringBuffer sb = new StringBuffer();
    for(int i = 0; i< 100000;i++)
     sb.append(i) ;
    String a = sb.toString();
    如果是这种情况的话,第一个的效率就大大不如蓝色,区别在哪里,就在于运行时和编译时的优化问题上!


效率:String 与 StringBuffer

    情景1:
    (1) String result = "hello" + " world";
    (2) StringBuffer result = new String().append("hello".append(" world";

        (1) 的效率好于 (2),不要奇怪,这是因为JVM会做如下处理
        编译前   String result = "hello" + " world";
        编译后   String result = "hello world";

    情景2:
    (1) public String getString(String s1, String s2) {
            return s1 + s2;
        }
    (2) public String getString(String s1, String s2) {
            return new StringBuffer().append(s1).append(s2);
        }

        (1) 的效率与 (2) 一样,这是因为JVM会做如下处理
        编译前   return s1 + s2;
        编译后   return new StringBuffer().append(s1).append(s2);

    情景3:
    (1) String s = "s1";
              s += "s2";
              s += "s3";
    (2) StringBuffer s = new StringBuffer().append("s1".append("s2".append("s3";

        (2) 的效率好于(1),因为String是不可变对象,每次"+="操作都会造成构造新的String对象

    情景4:
    (1) StringBuffer s = new StringBuffer();
        for (int i = 0; i < 50000; i ++) {
            s.append("hello";
        }
    (2) StringBuffer s = new StringBuffer(250000);
        for (int i = 0; i < 50000; i ++) {
            s.append("hello";
        }  
        (2) 的效率好于 (1),因为StringBuffer内部实现是char数组,默认初始化长度为16,每当字符串长度大于char
        数组长度的时候,JVM会构造更大的新数组,并将原先的数组内容复制到新数组,(2)避免了复制数组的

 

 

  • Object o与Object o=null的区别

具体差别如下:

class Test {

  public static void main(String[] args) {
  Object o1;
  o1.toString(); /*这里编译不能通过,编译器只认定o1是个引用,没指向任何对象,所以不能调用方法。*/
  Object o2 = null;
  o2.toString(); /*这里编译可以过,但是有空指针异常,编译器认定o2是一个对象,虽然是一个空对象。*/
  }
}


null对象是一个特殊的对象,他可以是任何类型。他只是作为一个标记而已,只是为了标记不存在而存在的。也没必要去追究他在内存是什么样。null就是一个标记而已。容器可以接受一个空对象,但是一个空引用则是不接受的。
Object o; //这种写法只是分配一个引用,没有分配内存。
Object o = null; //这么写则是给引用指向了一个空对象。分配了内存(空对象),所以编译不会报错,运行时报空指针异常。

转载于:https://www.cnblogs.com/onlymate/p/5433410.html

字符串比较

字符串的判断有2种:

        1、判断地址是否相等  用:==

        2、判断值是否相等  用:equals方法

Object类作为所有类的超类,而Object类的equals方法是直接比较地址的,源码如下:

public boolean equals(Object obj)  
   {  
       return this == obj;  
   }

所以,在String类中的equals方法,是重写了Object类的equals方法的,String类的equals方法源码如下:

public boolean equals(Object anObject){

    if(this==anObject){        //比较地址,如果地址相等,直接返回true
        return true;
    }
    if(anObject instanceof String){        //如果地址不相等,那就比较类型,如果类型不相等,直接返回false
        String anotherString =(String)anObject;    //由于是String类的比较,所以要把对象强转为String类
        int n=value.length;        //记录当前对象的字符长度
        if(n==anotherString.value.length){    //判断当前对象的字符串长度和传入的字符串长度是否一样,不一样,直接返回false,如果一样,接着往下比较
            char v1[]=value;    
            char v2[]=anotherString .value;
            int i=0;
            while(n-- !=0){        //将2个字符数组的循环对比
                if(v1[i] !=v2[i])    //将字符数组的字符进行对比
                    return false;    //如果出现一个字符不同,马上返回false
                i++
            }
            return true;
        }
    }
    return false;
}

 

String类的equals比较内容相同思路如下:

  1、先判断地址是否相等,相等返回true

  2、比较类型是否一样,不一样,返回false

  3、将传入的对象强转为String,比较长度,长度不相等,返回false

  4、长度相等,循环比较字符数组的元素,当有一元素比较内容不相同,马上返回false

  5、2个字符数组循环比较所有元素,没有内容不相同的元素,返回true

 

测试代码如下:

public class TestString {
    public static void main(String[] args) {
        String str="abc";
        String str2=new String("abc");   
        System.out.println("两个对象的内存地址是否相等:"+(str==str2));//false
        /**比较对应位置上的字符是否相等,如果所有的对象位置上的字符均相等,true,否则返回false*/
        System.out.println("两个对象的内容是否相等:"+(str.equals(str2)));
        //以下结果为 false  
        /**因为Person对象 instanceof String  的结果为false ,直接返回false*/
        System.out.println("String对象与Person对象的内容是否相等:"+(str.equals(new Person())));
    }
}
class Person{
    
}

结果:

两个对象的内存地址是否相等:false
两个对象的内容是否相等:true
String对象与Person对象的内容是否相等:false
 

 

JVM相关知识

下面这张图是JVM的体系结构图:

下面我们了解下Java栈、Java堆、方法区和常量池:

Java栈(线程私有数据区):

        每个Java虚拟机线程都有自己的Java虚拟机栈,Java虚拟机栈用来存放栈帧,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

 

Java堆(线程共享数据区):

       在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配。

 

方法区(线程共享数据区):

       方法区在虚拟机启动的时候被创建,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。在JDK8之前永久代是方法区的一种实现,而JDK8元空间替代了永久代,永久代被移除,也可以理解为元空间是方法区的一种实现。

 

常量池(线程共享数据区):

        常量池常被分为两大类:静态常量池和运行时常量池。

        静态常量池也就是Class文件中的常量池,存在于Class文件中。

        运行时常量池(Runtime Constant Pool)是方法区的一部分,存放一些运行时常量数据。

下面重点了解的是字符串常量池:

        字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。

        字符串常量池的存在使JVM提高了性能和减少了内存开销。

        使用字符串常量池,每当我们使用字面量(String s=”1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。

 

        使用字符串常量池,每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s。

下图是API说明:

        翻译为:“初始化一个新创建的字符串对象,以便它表示与参数相同的字符序列;换句话说,新创建的字符串是参数字符串的副本。除非需要显式的原始副本,否则使用此构造函数是不必要的,因为字符串是不可变的。”

        由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。

        鉴于String.intern()在API上的说明和new String(“a”)创建字符串(创建了两个对象,如果字符串常量池存在则是一个对象)在官方API上的说明,我个人认为字符串常量池存的是字符串对象,当然在JKD7之后,常量池中存储的可能是堆对象的引用,后面会讲到。(可用javap -c反编译即可得到JVM执行的字节码内容,javap -verbose 反编译查看常量池内容)

        关于常量池,我会在后面的一篇相关文章中进行解析。。。。

 

String源码分析

下面是String类的部分源码:

public final class String
 
    implements java.io.Serializable, Comparable, CharSequence
 
{
 
    /** The value is used for character storage. */
 
    private final char value[];
 
 
 
    /** The offset is the first index of the storage that is used. */
 
    private final int offset;
 
 
 
    /** The count is the number of characters in the String. */
 
    private final int count;
 
 
 
    /** Cache the hash code for the string */
 
    private int hash; // Default to 0
 
 
 
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
 
    private static final long serialVersionUID = -6849794470754667710L;
 
 
 
    ........
 
}


首先我们来看看String类,String类是用final修饰的,这意味着String不能被继承,而且所有的成员方法都默认为final方法。

 

接下来看看String类实现的接口:

         java.io.Serializable:这个序列化接口仅用于标识序列化的语意。

        Comparable:这个compareTo(T 0)接口用于对两个实例化对象比较大小。

       CharSequence:这个接口是一个只读的字符序列。包括length(), charAt(int index), subSequence(int start, int end)这几个API接口,值得一提的是,StringBuffer和StringBuild也是实现了改接口。

 

最后看看String的成员属性:

        value[] :char数组用于储存String的内容。

        offset :存储的第一个索引。

        count :字符串中的字符数。

        hash :String实例化的hashcode的一个缓存,String的哈希码被频繁使用,将其缓存起来,每次使用就没必要再次去计算,这也是一种性能优化的手段。这也是String被设计为不可变的原因之一,后面会讲到。

 

下面是一个String类的一个方法实现:

public String substring(int beginIndex, int endIndex) {
 
    if (beginIndex < 0) {
 
        throw new StringIndexOutOfBoundsException(beginIndex);
 
    }
 
    if (endIndex > count) {
 
        throw new StringIndexOutOfBoundsException(endIndex);
 
    }
 
    if (beginIndex > endIndex) {
 
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
 
    }
 
    return ((beginIndex == 0) && (endIndex == count)) ? this :
 
        new String(offset + beginIndex, endIndex - beginIndex, value);
 
}


 
        可以发现,最初传入的String并没有改变,其返回的是一个new String(),即新创建的String对象。其实String类的其他方法也是如此,并不会改变原字符串。这也是String的不可变性,后面会讲到。

 

Srtring在JVM层解析

创建字符串形式

        首先形如声明为S ss是一个类S的引用变量ss(我们常常称之为句柄,后面JVM相关内容会讲到),而对象一般通过new创建。所以这里的ss仅仅是引用变量,并不是对象。

        创建字符串的两种基本形式:

String s1=”1”;
String s2=new String(“1”);

        从图中可以看出,s1使用””引号(也是平时所说的字面量)创建字符串,在编译期的时候就对常量池进行判断是否存在该字符串,如果存在则不创建直接返回对象的引用;如果不存在,则先在常量池中创建该字符串实例再返回实例的引用给s1。注意:编译期的常量池是静态常量池,以后和会讲。。。。

        再来看看s2,s2使用关键词new创建字符串,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s2,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s2。注意:此时是运行期,那么字符串常量池是在运行时常量池中的。。。。

 

“+”连接形式创建字符串(更多可以查看API):

        (1)String s1=”1”+”2”+”3”;

 

    使用包含常量的字符串连接创建是也是常量,编译期就能确定了,直接入字符串常量池,当然同样需要判断是否已经存在该字符串。

 

        (2)String s2=”1”+”3”+new String(“1”)+”4”;

 

        当使用“+”连接字符串中含有变量时,也是在运行期才能确定的。首先连接操作最开始时如果都是字符串常量,编译后将尽可能多的字符串常量连接在一起,形成新的字符串常量参与后续的连接(可通过反编译工具jd-gui进行查看)。

        接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象(可变字符串对象),然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。

        实际上的实现过程为:String s2=new StringBuilder(“13”).append(new String(“1”)).append(“4”).toString();

        当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。

 

        (3)String s3=new String(“1”)+new String(“1”);

 

        这个过程跟(2)类似。。。。。。

 

String.intern()解析

        String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

下面我们来看个案例:

public class StringTest {
public static void main(String[] args) {
 
       // TODO 自动生成的方法存根
       String s3 = new String("1") + new String("1");
       System.out.println(s3 == s3.intern());
   }
}

 

JDK6的执行结果为:false
JDK7和JDK8的执行结果为:true

 

JDK6的内存模型如下:

 

        我们都知道JDK6中的常量池是放在永久代的,永久代和Java堆是两个完全分开的区域。而存在变量使用“+”连接而来的的对象存在Java堆中,且并未将对象存于常量池中,当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。所以结果为false。

 

JDK7JDK8的内存模型如下:

 

        JDK7中,字符串常量池已经被转移至Java堆中,开发人员也对intern 方法做了一些修改。因为字符串常量池和new的对象都存于Java堆中,为了优化性能和减少内存开销,当调用 intern 方法时,如果常量池中已经存在该字符串,则返回池中字符串;否则直接存储堆中的引用,也就是字符串常量池中存储的是指向堆里的对象。所以结果为true。

 

String典型案例

关于equals和== :

(1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。

(2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等。

(3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。

public class StringTest {
 
 
 
public static void main(String[] args) {
 
// TODO 自动生成的方法存根
 
        /**
         * 情景一:字符串池
          * JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象;
         * 并且可以被共享使用,因此它提高了效率。
         * 由于String类是final的,它的值一经创建就不可改变。
         * 字符串池由String类维护,我们可以调用intern()方法来访问字符串池。  
         */  
 
        String s1 = "abc";     
 
        //↑ 在字符串池创建了一个对象  
 
        String s2 = "abc";     
 
        //↑ 字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象  
 
        System.out.println("s1 == s2 : "+(s1==s2));    
 
        //↑ true 指向同一个对象,  
 
        System.out.println("s1.equals(s2) : " + (s1.equals(s2)));    
 
        //↑ true  值相等  
 
        //↑------------------------------------------------------over  
 
        /**
         * 情景二:关于new String("")
         *  
         */  
 
        String s3 = new String("abc");  
 
        //↑ 创建了两个对象,一个存放在字符串池中,一个存在与堆区中;  
 
        //↑ 还有一个对象引用s3存放在栈中  
 
        String s4 = new String("abc");  
 
        //↑ 字符串池中已经存在“abc”对象,所以只在堆中创建了一个对象  
 
        System.out.println("s3 == s4 : "+(s3==s4));  
 
        //↑false   s3和s4栈区的地址不同,指向堆区的不同地址;  
 
        System.out.println("s3.equals(s4) : "+(s3.equals(s4)));  
 
        //↑true  s3和s4的值相同  
 
        System.out.println("s1 == s3 : "+(s1==s3));  
 
        //↑false 存放的地区多不同,一个栈区,一个堆区  
 
        System.out.println("s1.equals(s3) : "+(s1.equals(s3)));  
 
        //↑true  值相同  
 
        //↑------------------------------------------------------over  
 
        /**
         * 情景三:  
         * 由于常量的值在编译的时候就被确定(优化)了。
         * 在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。
         * 这行代码编译后的效果等同于: String str3 = "abcd";
         */  
 
        String str1 = "ab" + "cd";  //1个对象  
 
        String str11 = "abcd";   
 
        System.out.println("str1 = str11 : "+ (str1 == str11));  
 
        //↑------------------------------------------------------over  
 
        /**
         * 情景四:  
         * 局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。
         *  
         * 第三行代码原理(str2+str3):
         * 运行期JVM首先会在堆中创建一个StringBuilder类,
         * 同时用str2指向的拘留字符串对象完成初始化,
         * 然后调用append方法完成对str3所指向的拘留字符串的合并,
         * 接着调用StringBuilder的toString()方法在堆中创建一个String对象,
         * 最后将刚生成的String对象的堆地址存放在局部变量str4中。
         *  
         * 而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。
         * str4与str5地址当然不一样了。
         *  
         * 内存中实际上有五个字符串对象:
         *       三个拘留字符串对象、一个String对象和一个StringBuilder对象。
         */  
 
        String str2 = "ab";  //1个对象  
 
        String str3 = "cd";  //1个对象                                         
 
        String str4 = str2+str3;                                        
 
        String str5 = "abcd";    
 
        System.out.println("str4 = str5 : " + (str4==str5)); // false  
 
        //↑------------------------------------------------------over  
 
        /**
         * 情景五:
         *  JAVA编译器对string + 基本类型/常量 是当成常量表达式直接求值来优化的。
         *  运行期的两个string相加,会产生新的对象的,存储在堆(heap)中
         */  
 
        String str6 = "b";  
 
        String str7 = "a" + str6;  
 
        String str67 = "ab";  
 
        System.out.println("str7 = str67 : "+ (str7 == str67));  
 
        //↑str6为变量,在运行期才会被解析。  
 
        final String str8 = "b";  
 
        String str9 = "a" + str8;  
 
        String str89 = "ab";  
 
        System.out.println("str9 = str89 : "+ (str9 == str89));  
 
        //↑str8为常量变量,编译期会被优化  
 
        //↑------------------------------------------------------over  
 
}
 
}


 

运行结果:

s1 == s2 : true

s1.equals(s2) : true

s3 == s4 : false

s3.equals(s4) : true

s1 == s3 : false

s1.equals(s3) : true

str1 = str11 : true

str4 = str5 : false

str7 = str67 : false

str9 = str89 : true

 

String被设计成不可变和不能被继承的原因

        String是不可变和不能被继承的(final修饰),这样设计的原因主要是为了设计考虑、效率和安全性。

 

字符串常量池的需要:

        只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段。

 

String对象缓存HashCode:

        上面解析String类的源码的时候已经提到了HashCode。Java中的String对象的哈希码被频繁地使用,字符串的不可变性保证了hash码的唯一性。

 

安全性

        首先String被许多Java类用来当参数,如果字符串可变,那么会引起各种严重错误和安全漏洞。

        再者String作为核心类,很多的内部方法的实现都是本地调用的,即调用操作系统本地API,其和操作系统交流频繁,假如这个类被继承重写的话,难免会是操作系统造成巨大的隐患。

        最后字符串的不可变性使得同一字符串实例被多个线程共享,所以保障了多线程的安全性。而且类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。

import java.io.*;
class test  
{
	public static void main (String[] args) throws java.lang.Exception
	{
	    String a="a";
	    String c=new String();
	    c=a+"";
	    String d=a;
	    System.out.println(c==d);
	    System.out.println(c.equals(d));
		
	}
}
  

运行结果

false

ture

 

 

你可能感兴趣的:(面试知识点)