4.2String类

       这一节,我们学习第一个类:String类。String翻译成汉语就是“字符串”,是字符的序列。我们知道,在Java中,默认采用Unicode字符集,因此字符串就是Unicode字符的序列。例如字符串“Java大失叔”,就是由7个Unicode字符‘J’、‘a’、‘v’、‘a’、‘大’、‘失’、‘叔’组成。在JDK中,把字符串抽象成一个类String提供给我们使用。String类在java.lang包中。

4.2.1构造String对象

  上面我们说了,想看电视得先买一台电视,电视在出厂的时候厂家会初始化它的状态。想使用String类,得先得到一个String的对象,然后指定属性的初始状态,然后才能使用它。得到对象的过程,叫做构造对象。在Java中,我们用构造器(constructor)来构造实例,构造器其实是一种特殊的方法,用来构造并初始化对象。我们采用在构造器前面加上new关键字来实现,例如:

new 构造器();

我们查看String类的API文档(怎么查这里不再赘述),构造方法截图如下:

 《Java从入门到失业》第四章:类和对象(4.2):String类-LMLPHP

发现String类的构造方法有几个特点:

  • 足足有15个构造方法
  • 有的方法上标有Deprecated,这个标签的含义是不推荐使用,将来在新版本中可能会移除
  • 构造方法的名字和类名相同

构造方法的名字和类名相同,这是Java构造器的特点,也是规定。我们挑选其中一个构造方法:String(char[] value)

我们看到,这其实就是用一个char数组来构造一个字符串,那么首先我们得有一个char数组才行,例如我们想要得到一个字符串“Java大失叔,你真棒”。那么代码如下:

char[] a = { 'J', 'a', 'v', 'a', '大', '失', '叔', ',', '你', '真', '棒' };
String s = new String(a);
System.out.println(s);// 结果输出:Java大失叔,你真棒 

事实上,由于String太常用了,Java给我们提供了更加简便的构造方法,直接用双引号将一段字符序列包起来,就得到了一个String的实例:

String s = "Java大失叔,你真棒";  

OK,我们得到了一个String对象了,下面我们来使用这个对象。我们可以看到,API中有几十个方法,我们挑选一些常用的演示一下。

4.2.2代码点和代码单元

       首先,我们回忆一下关于char和Unicode的知识。Unicode定义了U+0000到U+10FFFF一共1114112个码位(code point),英文直译为代码点。一个代码点表示一个字符。char是用来存放UTF-16编码中的一个代码单元(code unit),即2个字节。平面0的代码点用一个代码单元即一个char就可以表示,其余的代码点需要用2个代码单元即2个char才能表示。

       我们知道Stirng是Unicode字符的序列,但是底层的实现实际上是用char构成的。String类提供了一些关于代码点和代码单元相关的方法,请看下面摘抄的几个方法:

我们想获得字符的数量(即代码点的数量),需要用codePointCount方法,而length方法返回的是char的数量(即代码单元的数量)。调用对象的方法很简单,用如下形式:

对象.方法();

代码示例如下:

String s = "大失叔喜欢打麻将🀀🀁🀂🀃🀄🀅";//
System.out.println("字符串s的代码单元数量为:" + s.length());
System.out.println("字符串s的代码点数量为" + s.codePointCount(0, s.length())); 

输出结果:

字符串s的代码单元数量为:20
字符串s的代码点数量为:14 

我们可以看到,对于🀀🀁🀂🀃🀄🀅,这6个字符,每个字符占用2个代码单元,所以length方法的结果是20,而codePointCount方法的结果是14。

       我们再看看后面2个方法,这应该就相对简单了,一个是返回index处的代码单元,一个是返回index处的代码点。我们直接看代码:

String s = "大失叔喜欢打麻将🀀🀁🀂🀃🀄🀅";//
int c = s.charAt(8);// 把char赋值给一个int,对应这个代码单元对应的十进制,结果是55356,十六进制为0xD83C
int d = s.codePointAt(8);// 结果是126976,十六进制为0x1F000 
 

4.2.3对象与变量

       上面我们看到,创建出来一个String对象,一般我们会赋值给一个变量。那么对象和变量之间有什么关系和区别呢?我们先看几行代码:

String a;
String b;
a = "大失叔喜欢打麻将";
b = a;
 

这几行代码,会涉及到下面一些行为:

  • 第1、2行,我们定义了2个String类型的变量a和b。这时候Java会在内存中分别分配一块空间给a和b,但是这时候这2块内存空间中没有存放任何值。
  • 第3行,我们把一个字符串赋值给变量a。Java会在内存中分配一块空间,存放这个字符串,然后把这块空间的地址存放到变量a的内存空间中。
  • 第4行,把变量a赋值给b,相当于把变量a内存空间中的地址存放到变量b的内存空间中,这时候a和b同时指向字符串“大失叔喜欢打麻将”对应的内存空间。

我们用一张图示意如下:

 《Java从入门到失业》第四章:类和对象(4.2):String类-LMLPHP

我们需要牢牢记住一点:在Java中,任何对象的值都是存放在堆内存中的,而对象类型的变量对应的内存中保存的是对象的内存地址,我们称之为对象引用。因此new操作符返回的结果其实是一个引用。

       我们可以显式的把一个对象变量设置为null,这时候该变量的内存存放的将是空值,表明它不引用任何对象。如果我们对一个值为null的变量进行方法调用,程序在运行时则会抛出异常。

4.2.4字符串拼接

       在Java中,字符串的拼接有一种很简单的方法,就是用加号(+)连接两个字符串,结果会构造出一个新的字符串对象。我们看代码:

String a = "Java大失叔";
String b = "喜欢打麻将";
String c = a + b;
System.out.println(c);// 结果将输出:"Java大失叔喜欢打麻将"

在这段代码中,堆内存中将会分配3块空间,分别对应字符串"Java大失叔"、"喜欢打麻将"、" Java大失叔喜欢打麻将"。我们用一张图来演示这个过程:

 《Java从入门到失业》第四章:类和对象(4.2):String类-LMLPHP

我们还可以将一个字符串和一个非字符串用+连接起来,这时候非字符串对象会被转换为字符串(具体如何转换,后续会详细探讨)。例如:

String a = "Java大失叔卡里只有";
int b = 200;
String c = "元钱了";
System.out.println(a + b + c);// 结果将输出:Java大失叔卡里只有200元钱了

String类的API中还提供了一个方法concat用来拼接字符串,方法摘抄如下:

使用起来也很简单,代码如下:

String a = "Java大失叔";
String b = "喜欢打麻将";
String c = a.concat(b);
System.out.println(c);// 结果将输出:Java大失叔喜欢打麻将

  有的时候,需要将很多个字符串拼接成一个大字符串,这时,如果用+的方式,不是很合适了。因为用+的方式,每次都会构建一个新的对象,比较耗时,还占内存,效率比较低。好在Java提供了另外一种方式,就是采用StringBuilder类和StringBuffer类。一般情况下我们都会采用StringBuilder类,因为它的效率略高。而Stringbuffer类是线程安全的,关于线程会在后面专门讨论。这2个类的API几乎完全一样。用StringBuilder非常简单,代码演示如下:

StringBuilder sb = new StringBuilder();// 首先构建StringBuilder对象  
sb.append("Java");// 然后用append方法添加小字符串  
sb.append("大失叔");
sb.append("太帅了");
String s = sb.toString();// 最后调用toString()方法,返回一个字符串对象  
System.out.println(s);// 结果将输出:Java大失叔太帅了

其实append方法返回的依然是StringBuilder对象,因此还可以采用一种更为简洁的方式:

String s = new StringBuilder().append("Java").append("大失叔").append("太帅了").toString();
System.out.println(s);// 结果将输出:Java大失叔太帅了

关于加号、concat、StringBuilder这三者的比较,笔者给出如下结论:

  1. 对于拼接少量的字符串,用哪种方式都差不多,加号书写起来更加方便。笔者几乎没用过concat方法。
  2. 加号和StringBuilder都可以拼接非字符串类型(可以查看API,有很多个append方法)。
  3. 对于需要拼接多个字符串的时候,强烈建议使用StringBuilder。(笔者在早年编写一个网络程序的时候,吃过亏)

4.2.5字符串截取和比较

       关于字符串还会经常使用比较和截取的方法,先列出方法如下:

我们经常会比较一个字符串是否以某个字符串开头或结尾,代码如下:

String a = "Java大失叔";
boolean b1 = a.startsWith("Java");// 结果为true  
boolean b2 = a.startsWith("java");// 结果为false  
boolean b3 = a.endsWith("叔");// 结果为true  

  有时候,经过网络传输后的字符串经常前后会带一些空白,眼睛又看不见,很不利于比较,会用trim方法去掉前后的空白:

String a = "   Java大失叔       ";
String b = a.trim();
System.out.println(b);// 结果将输出:Java大失叔 

需要注意,这里的空白指的是Unicode编码小于或等于”\u0020”的字符。

       对于字符串的截取,用subString方法将非常方便:

String a = "Java大失叔 ";
String b = a.substring(4);// 结果是:大失叔  
String c = a.substring(2, 6);// 结果是:va大失

这里要注意的是,返回的结果字符串是包括beginIndex位置的代码单元,但是不包括endIndex位置的代码单元。

       比较2个字符串是否相等,用equals方法,如果相等返回ture,否则返回false。如果想不区分大小写比较是否相等,则可以使用equalsIgnoreCase方法。表达式为:

a.equals(b)

其中,a和b即可以是变量,也可以是字符串常量。

String a = "Java大失叔";
String b = "java大失叔";
System.out.println(a.equals(b));// 结果为false  
System.out.println(a.equalsIgnoreCase(b));// 结果为true
System.out.println("JAVA大失叔".equalsIgnoreCase(b));// 结果为true

这里需要特别注意,千万不能用==运算符来比较2个字符串是否相等。因为==运算符比较的是2个字符串是否存放在同一个内存位置上。但是事实上,对于2个字符内容完全一样的字符串,是很有可能存放在不同的内存空间的,因此用==比较结果将为false。这个问题Java新手经常会犯。

  最后我们很容易发现,String的API中没有提供修改字符串内容的方法。这其实是因为String类被定义为final的(关于final后面也会介绍),我们看一下String的源代码(在Eclipse中,可以很轻松的查看源代码,鼠标移动的任意一个String字符上,按住Ctrl键后,点击鼠标左键):

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence 

用final修饰一个类后,这个类的对象将不能被修改。

       String类提供了50个多个方法,这些方法都很有用,但是我们不可能记住所有的方法名和参数要求,这里还有一个Eclipse的小技巧,当我们敲完变量名加“点”后,Eclipse会自动弹出提示,或者还可以用Ctrl+/自动补全,如下图:

 《Java从入门到失业》第四章:类和对象(4.2):String类-LMLPHP

09-15 04:53