Java学习笔记-《Java程序员面试宝典》-第四章基础知识-4.5字符串与数组(4.5.1-4.5.3)

4.5.1字符串创建与存储的机制是什么

在Java语言中,字符串的声明和初始化主要有一下两种情况:
1>对于String s1=new String(“abc”)语句与String s2=new String(“abc”)语句,存在两个引用对象s1,s2。即使是两个内容相同的字符串对象”abc”,它们在内存中的地址也是不同的。只要用到new,总会产生新的对象。
2>对于String s1=”abc”语句与String s2=”abc”语句,在JVM中存在着一个字符串池,其中保存着很多的String对象,并且可以被共享使用,s1、s2引用的是同一个常量池中的对象。由于String的实现使用了Flyweight的设计模式,当创建一个字符串常量时,例如String s=”abc”,会首先在字符串常量池中查找是否已经有相同的字符串被定义(其判断依据是String类euqals()方法的返回值)。若已经定义,则直接获取对其的引用,此时不需要创建新的对象;若没有定义,则首先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。由于String是不可变类,一旦创建好了就不能被修改,因此String对象可以共享而且不会导致程序的混乱。

对同一变量的不同初始化方式而言:

String a = "abc";                    //把"abc"放到常量区中,在编译时产生
String a = "ab" + "c";                //把"ab" + "c"转换为字符串常量"abc"放到常量区中
String a = new String("abc");         //在运行时把"abc"放到堆中

将同一个字符串内容赋值给不同的变量:

String s1 = "abc";          //在常量区里面存放了一个"abc"字符串对象
String s2 = "abc";          //s2引用常量区中的对象,因此不会创建新的对象
String s3 = new String("abc");   //在堆中创建新的对象
String s4 = new String("abc");   //在堆中又创建一个新的对象

为了便于理解,可以将String s = new String(“abc”)语句的执行人为的分为两个过程:第一个过程是新建对象的过程,即new String(“abc”);第二个过程是赋值的过程,即String s=。由于第二个过程只是定义了一个名为s的String类型的变量,将一个String类型对象的引用赋值给s,因此在这个过程中不会创建新的对象。第一个过程中new String(“abc”)会调用String类的构造函数:

public String(String original){
    //body
}

在调用这个构造函数时,传入了一个字符串常量,因此语句new String(“abc”)也就等价于”abc”和new String()两个操作了。若在字符串池中不存在”abc”,则会创建一个字符串常量”abc”,并将其添加到字符串池中;若存在,则不创建,然后new String()会在堆中创建一个新的对象,所以s3与s4指向的是堆中不同的String对象,地址自然也不同了。

引申:对于String类型的变量,赋值语句s=null与s=”“是否相同?
不同。对于s=null,其中s是一个字符串类型的引用,不指向任何字符串。而s=”“中的s是一个字符串类型的引用,它指向一个值为”“的字符串。

问:new String(“abc”)创建了几个对象?
答:一个或两个。如果字符串常量池中有”abc”,那么只创建一个对象;如果常量池中原来没有字符串”abc”,那么就会创建两个。

4.5.2”==”、equals和hashCode有什么区别

1>”==”运算符用来比较两个变量的值是否相等。也就是说,该运算符用于比较变量对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相等,只能使用”==”运算符。
具体而言,如果两个变量是基本数据类型,可以直接使用”==”运算符来比较其对应的值是否相等。如果一个变量指向的数据是对象(引用类型),那么,此时涉及到两块内存,对象本身占用一块内存(堆内存),变量也占用一块内存,例如,对于赋值语句String s = new String(),变量s占用一块内存,而new String()则存储在另一块存储空间里面。此时,变量s所对应的内存中存储的就是对象占用的那块内存的首地址。对于指向对象类型的变量,如果要比较两个变量是否指向同一个对象,即要看这两个变量所对应内存中的数值是否相等(这两个对象是否指向同一块内存),这时候就可以用”==”运算符进行比较。但是,如果要比较这两个对象的内容是否相等,那么用”==”运算符就无法实现了。
2>equals是Object类提供的方法之一。每一个Java类都继承自Object类,所以每一个对象都具有equals这个方法。Object类中定义的equals(Object)方法是直接使用”==”运算符比较的两个对象,所以在没有覆盖equals(Object)方法的情况下,equals(Object)与”==”运算符一样,比较的是引用。
相比”==”运算符,equals(Object)方法的特殊之处在于它可以被覆盖,所以可以通过覆盖的方法让它不是比较引用而是比较数据内容,例如String类的equals方法是用于比较两个独立对象的内容是否相同,即堆中的内容是否相同。以下面的代码为例:

String s1 = new String("Hello");
String s2 = new String("Hello");

两条new语句创建了两个对象,然后用s1、s2这两个变量分别指向一个对象,这是两个不同的对象,他们的首地址是不同的,即a和b中存储的数值是不同的,所以表达式a==b返回false,而这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。
如果一个类没有自己定义equals()方法,那么它将继承Object类的equals()方法,Object类的equals()方法的实现代码如下:

boolean equals(Object o)
{
    return this == o;
}

通过以上例子可以说明,如果一个类没有自己定义equals()方法,它默认的equals()方法就是使用”==”运算符,若比较的是两个独立的对象,则总会返回false。如果希望能够比较编写的类创建的两个实例对象的内容是否相等,那就必须覆盖equals()方法,由开发人员来决定什么情况下即可认为两个对象的内容是相同的。
关于String类的equals()方法的源码,参见博客 http://blog.sina.com.cn/s/blog_79333b2c0100xd34.html
3>hashCode()方法是从Object类中继承过来的,它也用来鉴定两个对象是否相等。Object类中的hashCode()方法返回对象在内存中地址转换成的一个int值,所以如果没有重写hashCode()方法,任何对象的hashCode()方法都是不相等的。
虽然equals()方法也是用来判断两个对象是否相等,但是它与hashCode()方法是有区别的。一般来讲,equals()方法是给用户调用的,如果需要判断两个对象是否相等,可以重写equals()方法,然后在代码中调用。hashCode()方法相当于是一个对象的编码,就像是文件中的md5(一种编码方式),它与equals()方法不用之处就在于它返回的是int型,比较起来不直观。
一般在覆盖equals()方法的同时也要覆盖hashCode()方法,否则,就会违反Object.hashCode的通用约定,从而导致该类无法与所有的基于散列值(hash)的集合类(HashMap、HashSet、Hashtable)结合在一起正常运行。

hashCode()方法的返回值和equals()方法的关系如下:
如果x.equals(y)返回true,即两个对象根据equals()方法比较是相等的,那么调用这两个对象中的任意一个对象的hashCode()方法都必须产生相同的整数结果。如果x.equals(y)返回false,即两个对象根据equals()方法比较是不相等的,那么x和y的hashCode()方法的返回值有可能相等,也有可能不相等。反之,hashCode()方法的返回值不相等,equals()的返回值一定不相等。而hashCode()方法的返回值相等,equals()方法的返回值不一定相等。

4.5.3String、StringBuffer、StringBuilder和StringTokenizer有什么区别

Java有4个类可以对字符或字符串进行操作,他们分别是Character、String、StringBuffer、StringTokenizer,其中Character用于单个字符操作,String用于字符串操作,属于不可变类,而StringBuffer也是用于字符串操作,不同之处是StringBuffer属于可变类。
String是不可变类,也就是说,String对象一旦被创建,其值将不能被改变,而StringBuffer是可变类,当对象被创建后依然可以对其值进行改变。由于String是不可变类,因此适合在需要被共享的场合中使用,而当一个字符串需要经常被修改时,最好使用StringBuffer来实现。如果要用String来保存一个经常被修改的字符串时,在字符串被修改时会比StringBuffer多很多附加的操作,同时生成很多无用的对象,由于这些无用的对象会被垃圾回收器来回收,因此会影响程序的性能。
String与StringBuffer的另外一个区别在于初始化。实例化String时,可以利用构造函数(String s1 = new String(“world”))的方式来对其进行初始化,也可以用赋值(String s = “Hello”)的方式来初始化;而StringBuffer只能使用构造函数(StringBufer s = new StringBuffer(Hello))的方式来初始化。
String字符串修改实现的原理如下:当用String类型来对字符串进行修改时,其实现方法是首先创建一个StringBuffer,其次调用StringBuffer的append()方法,最后调用StringBuffer的toString()方法把结果返回,示例如下:

String s = "Hello";
s += "World";

以上代码等价于下述代码:

StringBuffer sb = new StringBuffer(s);
sb.append("World");
s = sb.toString();

可以看出,如果对一个String经常修改,比StringBuffer多了一些附加操作,同时也生成了一些临时的对象,导致程序的执行效率降低。下面通过一个实例分析:

public class TestString {

    public static void modifyString(){

        String s = "Hello";
        String s1 = "world";
        long start = System.currentTimeMillis();
        for(int i=0;i<10000;i++){
            s += s1;
        }
        long end = System.currentTimeMillis();
        long runTime = end - start;
        System.out.println("modifyString runTime:"+runTime);

    }

    public static void modifyStringBuffer(){

        StringBuffer sb = new StringBuffer("Hello");
        String s1 = "world";
        long start = System.currentTimeMillis();
        for(int i=0;i<10000;i++){
            sb.append(s1);
        }
        long end = System.currentTimeMillis();
        long runTime = end - start;
        System.out.println("modifyStringBuffer runTime:"+runTime);


    }


    public static void main(String[] args){

        modifyString();
        modifyStringBuffer();

    }
}

运行结果:
modifyString runTime:899
modifyStringBuffer runTime:2

从程序的运行结果可以看出,当一个字符串需要经常被修改时,使用StringBuffer比使用String要好很多。
StringBuilder也可以被修饰的字符串,它与StringBuffer类似,都是字符串缓冲区,但StringBuilder不是线程安全的,如果只是在单线程中使用字符串缓冲区,那么StringBuilder的效率会更高一些。因此,在只有单线程访问时可以使用StringBuilder,当有多个线程访问时,最好使用线程安全的StringBuffer。因为StringBuffer必要时可以对这些方法进行同步,所以任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
在执行效率方面,StringBuilder最高,StringBuffer次之,String最低,鉴于这一情况,一般而言,如果要操作的数据量比较小,应优先使用String类;如果是在单线程下操作大量数据,应该优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先考虑StringBuffer类。
StringTokenizer是用来分割字符串的工具类,示例如下:


import java.util.StringTokenizer;

public class Test {

    public static void main(String[] args){

        /**
         * StringTokenizer
         * 
         * Constructs a string tokenizer for the specified string. 
         * The tokenizer uses the default delimiter set, which is " \t\n\r\f": 
         * the space character, the tab character, the newline character, the carriage-
         * return character, and the form-feed character. 
         * 
         */
        StringTokenizer st = new StringTokenizer("Welcome to our country");

        while(st.hasMoreTokens()){
            System.out.println(st.nextToken());;
        }
    }
}


运行结果:
Welcome
to
our
country
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值