这一节,我们学习第一个类:String类。String翻译成汉语就是“字符串”,是字符的序列。我们知道,在Java中,默认采用Unicode字符集,因此字符串就是Unicode字符的序列。例如字符串“Java大失叔”,就是由7个Unicode字符‘J’、‘a’、‘v’、‘a’、‘大’、‘失’、‘叔’组成。在JDK中,把字符串抽象成一个类String提供给我们使用。String类在java.lang包中。
上面我们说了,想看电视得先买一台电视,电视在出厂的时候厂家会初始化它的状态。想使用String类,得先得到一个String的对象,然后指定属性的初始状态,然后才能使用它。得到对象的过程,叫做构造对象。在Java中,我们用构造器(constructor)来构造实例,构造器其实是一种特殊的方法,用来构造并初始化对象。我们采用在构造器前面加上new关键字来实现,例如:
new 构造器();
我们查看String类的API文档(怎么查这里不再赘述),构造方法截图如下:
发现String类的构造方法有几个特点:
构造方法的名字和类名相同,这是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中有几十个方法,我们挑选一些常用的演示一下。
首先,我们回忆一下关于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类提供了一些关于代码点和代码单元相关的方法,请看下面摘抄的几个方法:
修饰和类型 | 方法 | 描述 |
---|---|---|
int | length() | 返回字符串的长度 |
int | codePointCount(int beginIndex, int endIndex) | 返回beginIndex和endIndex-1之间的代码点的数量。 |
char | charAt(int index) | 返回index索引处的char |
int | codePointAt(int index) | 返回index索引处的代码点 |
我们想获得字符的数量(即代码点的数量),需要用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
上面我们看到,创建出来一个String对象,一般我们会赋值给一个变量。那么对象和变量之间有什么关系和区别呢?我们先看几行代码:
String a;
String b;
a = "大失叔喜欢打麻将";
b = a;
这几行代码,会涉及到下面一些行为:
我们用一张图示意如下:
我们需要牢牢记住一点:在Java中,任何对象的值都是存放在堆内存中的,而对象类型的变量对应的内存中保存的是对象的内存地址,我们称之为对象引用。因此new操作符返回的结果其实是一个引用。
我们可以显式的把一个对象变量设置为null,这时候该变量的内存存放的将是空值,表明它不引用任何对象。如果我们对一个值为null的变量进行方法调用,程序在运行时则会抛出异常。
在Java中,字符串的拼接有一种很简单的方法,就是用加号(+)连接两个字符串,结果会构造出一个新的字符串对象。我们看代码:
String a = "Java大失叔";
String b = "喜欢打麻将";
String c = a + b;
System.out.println(c);// 结果将输出:"Java大失叔喜欢打麻将"
在这段代码中,堆内存中将会分配3块空间,分别对应字符串"Java大失叔"、"喜欢打麻将"、" Java大失叔喜欢打麻将"。我们用一张图来演示这个过程:
我们还可以将一个字符串和一个非字符串用+连接起来,这时候非字符串对象会被转换为字符串(具体如何转换,后续会详细探讨)。例如:
String a = "Java大失叔卡里只有";
int b = 200;
String c = "元钱了";
System.out.println(a + b + c);// 结果将输出:Java大失叔卡里只有200元钱了
String类的API中还提供了一个方法concat用来拼接字符串,方法摘抄如下:
修饰和类型 | 方法 | 描述 |
---|---|---|
String | concat(String str) | 将str拼接在本字符串后面 |
使用起来也很简单,代码如下:
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这三者的比较,笔者给出如下结论:
关于字符串还会经常使用比较和截取的方法,先列出方法如下:
修饰和类型 | 方法 | 描述 |
---|---|---|
boolean | startsWith(String prefix) | 检查字符串是否以指定的前缀prefix开始 |
boolean | endsWith(String suffix) | 检查字符串是否以指定的后缀suffix结尾 |
String | trim() | 删除字符串前后的空白,并返回一个新字符串 |
boolean | equals(Object anObject) | 检测2个字符串是否相等 |
boolean | equalsIgnoreCase(String anotherString) | 检测2个字符串在忽略大小写的情况下是否相等 |
String | substring(int beginIndex) | 截取从beginIndex到末尾的字符串并返回 |
String | substring(int beginIndex, int endIndex) | 截取从beginIndex到endIndex的字符串并返回,不包括endIndex |
我们经常会比较一个字符串是否以某个字符串开头或结尾,代码如下:
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+/自动补全,如下图: