Java基础知识(四)——Java字符串与数组

 

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的设计模式,当创建一个字符串常量时,例如Strings="abc",会首先在字符串常量池中查找是否已经有相同的字符串被定义,其判断依据是String类equals(Object obj)方法的返回值。若已经定义,则直接获取对其的引用,此时不需要创建新的对象;若没有定义,则首先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。由于String是不可变类,一旦创建好了就不能被修改,因此String对象可以被共享而且不会导致程序的混乱。


Java基础知识(四)——Java字符串与数组

为了便于理解,可以把Strings=new String("abc")语句的执行人为地分解成两个过程:第一个过程是新建对象的过程,即new String("abc”)第二个过程是赋值的过程,即String s=。由于第二个过程只是定义了一个名为s的String类型的变量,将一个String类型对象的引用赋值给s,因此在这个过程中不会创建新的对象。第一个过程中new String("abc")会调用String类的构造函数:
Java基础知识(四)——Java字符串与数组

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

引申:对于String类型的变量s,赋值语句s=null与s=“”是否相同?

对于赋值语句s=null,其中s是一个字符串类型的引用,它不指向任何一个字符串。而赋值语句s=“”中的s是一个字符串类型的引用,它指向另外一个字符串(这个字符串的值为“”,即空字符串),因此,这两者是不同的。
 

常见笔试题:

new String(“abc”)创建了几个对象?

答案:一个或两个。如果常量池中原来有“abc”,那么只创建一个对象;如果常量池中原来没有字符串“abc”,那么就会创建两个对象。
 

2、“==”、equals和hashCode有什么区别

1)“==”运算符用来比较两个变量的是否相等。

也就是说,该运算符用于比较变量对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相等,只能使用“==”运算符。

具体而言,如果两个变量是基本数据类型,可以直接使用“==”运算符来比较其对应的值是否相等。如果一个变量指向的数据是对象(引用类型),那么,此时涉及了两块内存,对象本身占用一块内存(堆内存),变量也占用一块内存,例如,对于赋值语句Strings=new String(),变量s占用一块存储空间,而new String()则存储在另外一块存储空间里,此时,变量s所对应内存中存储的数值就是对象占用的那块内存的首地址。对于指向对象类型的变量,如果要比较两个变量是否指向同一个对象,即要看这两个变量所对应内存中的数值是否相等(这两个对象是否指向同一块存储空间),这时候就可以用“==”运算符进行比较。但是,如果要比较这两个对象的内容是否相等,那么用“==”运算符就无法实现了。

2)equals是Object类提供的方法之一。

每一个Java类都继承自Object类,所以每一个对象都具有equals这个方法。Object类中定义的equals(Object)方法是直接使用“==”运算符比较的两个对象,所以在没有覆盖equals(Object)方法的情况下,equals(Object)与“==”
运算符一样,比较的是引用。

相比“==”运算符,equals(Object)方法的特殊之处就在于它可以被覆盖,所以可以通过覆盖的方法让它不是比较引用而是比较数据内容,例如String类的equals方法是用于比较两个独立对象的内容是否相同,即堆中的内容是否相同,以下面的代码为例:
Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

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

通过以上例子可以说明,如果一个类没有自己定义equals()方法,它默认的equals()方法(从Object类继承的)就是使用“==”运算符,也是在比较两个变量指向的对象是否是同一对象,此时使用equals()方法和使用“==”运算符会得到同样的结果。若比较的是两个独立的对象,则总返回false。如果编写的类希望能够比较该类创建的两个实例对象的内容是否相同,那么必须覆盖equals()方法,由开发人员自己编写代码来决定在什么情况下即可认为两个对象的内容是相同的

3)hashCode()方法是从Object类中继承过来的,它也用来鉴定两个对象是否相等。

Object类中的hashCode()方法返回对象在内存中地址转换成的一个int值所以如果没有重写hashCode()方法,任何对象的hashCode()方法都是不相等的。

虽然equals()方法也是用来判断两个对象是否相等的,但是它与hashCode()方法是有区别的。一般来讲,equals()方法是给用户调用的,如果需要判断两个对象是否相等,可以重写equals()方法,然后在代码中调用,这样就可以判断它们是否相等了。对于hashCode()方法,用户一般不会去调用它,例如在hashmap中,由于key是不可以重复的,它在判断key是否重复时就判断了hashCode()这个方法,而且也用到了equals()方法。此处“不可以重复”指的是equals()和hashCode()只要有一个不等就可以了。所以,hashCode()方法相当于是一个对象的编码,就好像文件中的md5,它与equals()方法的不同之处就在于它返回的是int型,比较起来不直观。

一般在覆盖equals()方法的同时也要覆盖hashCode()方法,否则,就会违反Object.hashCode的通用约定,从而导致该类无法与所有基于散列值(hash)的集合类(Hash-Map、HashSet和Hashtable)结合在一起正常运行。hashCode()方法的返回值和equals()方法的关系如下:如果xequals(y)返回true,即两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode()方法都必须产生同样的整数结果。如果x.equals(y)返回false,即两个对象根据equals()方法比较是不相等的,那么x和y的hashCode()方法的返回值有可能相等,也有可能不相等。反之,hashCode()方法的返回值不相等,一定能推出equals()方法的返回值也不相等,而 hashCode()方法的返回值相等,equals方法的返回值则可能相等,也可能不相等。
Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

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

Java语言有4个类可以对字符或字符串进行操作,它们分别是Character、String、StringBuffer 和StringTokenizer,其中Character用于单个字符操作,String用于字符串操作,属于不可变类,而StringBuffer也是用于字符串操作,不同之处是StringBuffer属于可变类

String是不可变类,也就是说,String对象一旦被创建,其值将不能被改变,而StringBuffer是可变类,当对象被创建后仍然可以对其值进行修改。由于String是不可变类,因此适合在需要被共享的场合中使用,而当一个字符串经常需要被修改时,最好使用StringBuffer来实现。

如果用String来保存一个经常被修改的字符串时,在字符串被修改时会比StringBuffer 多很多附加的操作,同时生成很多无用的对象,由于这些无用的对象会被垃圾回收器来回收,因此会影响程序的性能。在规模小的项目里面这个影响很小,但是在一个规模大的项目里面,这会对程序的运行效率带来很大的影响。

String与StringBuffer的另外一个区别在于当实例化String时,可以利用构造函数(String sl=new String("world"))的方式来对其进行初始化,也可以用赋值(Strings="Hello")的方式来初始化,而StringBuffer 只能使用构造函数(StringBuffer s=new StringBuffer("Hello"))的方式来初始化。

String字符串修改实现的原理如下:当用String类型来对字符串进行修改时,其实现方法是首先创建一个StringBuffer,其次调用StringBuffer的append()方法,最后调用StringBuffer的toString()方法把结果返回,示例如下:


Java基础知识(四)——Java字符串与数组

由此可以看出,上述过程比使用StringBuffer多了一些附加的操作,同时也生成了一些临时的对象,从而导致程序的执行效率降低。为了更好地说明这一问题,下面分析一个示例:
Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

从程序的运行结果可以看出,当一个字符串需要经常被修改时,使用StringBufer比使用String要好很多。StringBuilder也可以被修改的字符串,它与StringBuffer类似,都是字符串缓冲区,但StringBuilder不是线程安全的,如果只是在单线程中使用字符串缓冲区,那么StringBuilder的效率会更高些。因此在只有单线程访问时可以使用StringBuilder,当有多个线程访问时,最好使用线程安全的StringBuffer。因为StringBuffer必要时可以对这些方法进行同步,所以任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。

在执行效率方面,StringBuilder最高,StringBuffer次之,String最低,鉴于这一情况,一般而言,如果要操作的数据量比较小,应优先使用String类;如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先考虑StringBuffer类。

StringTokenizer是用来分割字符串的工具类,示例如下:
Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

4、Java中数组是不是对象

数组是指具有相同类型的数据的集合,它们一般具有固定的长度,并且在内存中占据连续的空间。在C/C++语言中,数组名只是一个指针,这个指针指向了数组的首元素,既没有属性也没有方法可以调用,而在Java语言中,数组不仅有其自己的属性(例如length属性),也有一些方法可以被调用(例如clone方法)。由于对象的特点是封装了一些数据,同时提供了一些属性和方法,从这个角度来讲,数组是对象。每个数组类型都有其对应的类型,可以通过instanceof来判断数据的类型,示例如下:
Java基础知识(四)——Java字符串与数组

 

5、数组的初始化方式有哪几种

在Java语言中,一维数组的声明方式为type arrayName[]或type[]arrayName 其中,type既可以是基本的数据类型,也可以是类,arrayName表示数组的名字,[]用来表示这个变量的类型为一维数组。与C/C++语言不同的是,在Java语言中,数组被创建后会根据数组存放的数据类型初始化成对应的初始值(例如,int类型会初始化为0,对象会初始化为null)。另外一个不同之处是Java数组在定义时,并不会给数组元素分配存储空间,因此[]中不需要指定数组的长度,对于使用上面方式定义的数组在使用时还必须为之分配空间,分配方法为:

Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

以上主要介绍了一维数组的声明与初始化的方式,下面介绍二维数组的声明与初始化的方式,二维数组有3种声明的方法:
Java基础知识(四)——Java字符串与数组

需要注意的是,在声明二维数组时,其中[]必须为空。二维数组也可以用初始化列表的方式来进行初始化,其一般形式为
Java基础知识(四)——Java字符串与数组

除了以上介绍的方法以外,也可以通过new关键字来给数组申请存储空间,形式如下:
Java基础知识(四)——Java字符串与数组

与C/C++语言不同的是,在Java语言中,二维数组的第二维的长度可以不同。假如要定义一个有两行的二维数组,第一行有两列,第二行有三列,定义方法如下:
Java基础知识(四)——Java字符串与数组

对二维数组的访问也是通过下标来完成,一般形式为arryName[行号][列号],下例介绍二维数组的遍历方法:
Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

 

6、length 属性与length()方法有什么区别

在C/C++语言中,每当调用一个方法需要传递数组时,就必须同时传递数组的长度,因为在方法调用时传递的参数为数组的首地址,而对数组的实际长度却无法获知,这样会导致在对数组进行访问时可能产生越界。而在Java语言中,数组提供了length属性来获取数组的长度

在Java语言中,length()方法是针对字符串而言的,String提供了length()方法来计算字符串的长度,示例如下例:
Java基础知识(四)——Java字符串与数组

Java基础知识(四)——Java字符串与数组

除了length属性与length()方法外,Java中还有一个计算对象大小的方法—size()方法,该方法是针对泛型集合而言的,用于查看泛型中有多少个元素。(备注:泛型是对Java语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类,可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样)