java难点之字符串

字符操作

下面的程序将打印出什么呢?
<span style="font-size:14px;">public class LastLaugh{
public static void main(String[] args){
System.out.print("H"+"a");
System.out.print('H'+'a');
}
}</span>


程序可能会打印出HaHa,但是实际打印出来的是Ha169。问题在于'H'和'a'是字符型字面常量,因为这两个操作数都不是字符串类型的,所以 + 操作符执行的是加法而不是字符串连接。编译器在计算常量表达式'H'+'a'时,是通过我们熟知的拓宽原始类型转换将两个具有字符型数值的操作数('H'和'a')提升为 int 数值而实现的。从 char 到int 的拓宽原始类型转换是将 16 位的 char 数值零扩展到 32 位的 int。对于'H',char 数值是 72,而对于'a',char 数值是 97,因此表达式'H'+'a'等价于 int常量 72 + 97,或 169。
字符连接可以使用如下的方法:
StringBuffer sb = new StringBuffer();
sb.append('H');
sb.append('a');
System.out.println(sb);
也可以通过确保至少有一个操作数为字符串类型,来强制 + 操作符去执行一个字符串连接操作,而不是一个加法操作。这种常见的惯用法用一个空字符串("")作为一个连接序列的开始,如下所示:System.out.println("" + 'H' + 'a');

ABC:

下面的程序将打印什么呢?
<span style="font-size:14px;">public class ABC{
public static void main(String[] args){
String letters = "ABC";
char[] numbers = {'1', '2', '3'};
System.out.println(letters + " easy as " + numbers);
}
}</span>


这段代码可能打印的结果是ABC easy as 123,但是实际上它的打印结果为:ABC easy as [C@1ce84763,
解决方案:
可以在调用字符串连接操作之前,显式地将一个数组转换成一个字符串:
System.out.println(letters + " easy as " +
String.valueOf(numbers));
也可以将 System.out.println 调用分解为两个调用,以利用 println 的
char[]重载版本:
System.out.print(letters + " easy as ");
System.out.println(numbers);

畜牧场

<span style="font-size:14px;">public class AnimalFarm{
public static void main(String[] args){
final String pig = "length: 10";
final String dog = "length: " + pig.length();
System.out. println("Animals are equal: "
+ pig == dog);
}
}</span>


它将打印出什么,表面分析可能会认为它应该打印出 Animal are equal: true。String 类型的编译期常量是内存限定的。换句话说,任何两个 String类型的常量表达式,如果标明的是相同的字符序列,那么它们就用相同的对象引用来表示。如果用常量表达式来初始化 pig 和 dog,那么它们确实会指向相同的对象,但是 dog 并不是用常量表达式初始化的。它打印的只是 false,+ 操作符,不论是用作加法还是字符串连接操作,它都比 == 操作符的优先级高。因此,println 方法的参数是按照下面的方式计算的:
System.out.println(("Animals are equal: " + pig) == dog);
这个布尔表达式的值当然是 false,它正是该程序的所打印的输出。
正确的是System.out.println("Animals are equal: " + pig.equals(dog));
总结:字符串连接的优先级不应该和加法一样。这意味着重载 + 操作符来执行字符串连接是有问题的,就像在谜题 11 中提到的一样。还有就是,对于不可修改的类型,例如 String,其引用的等价性比值的等价性更加让人感到迷惑。 也许 == 操作符在被应用于不可修改的类型时应该执行值比较。要实现这一点,一种方法是将 == 操作符作为 equals方法的简便写法,并提供一个单独的类似于 System.identityHashCode的方法来执行引用标识的比较。

转义字符的溃败

<span style="font-size:14px;">public class EscapeRout{
public static void main(String[] args){
// \u0022 是双引号的 Unicode 转义字符
System.out.println("a\u0022.length()
+\u0022b".length());
}</span>


对该程序的一种很肤浅的分析会认为它应该打印出 26,因为在由两个双引号"a\u0022.length()+\u0022b"标识的字符串之间总共有 26 个字符。稍微深入一点的分析会认为该程序应该打印 16,因为两个 Unicode 转义字符每一个在源文件中都需要用 6 个字符来表示,但是它们只表示字符串中的一个字符。因此这个字符串应该比它的外表看其来要短 10 个字符。 运行这个程序,就会发现它打印的既不是 26 也不是 16,而是 2。Java 对在字符串字面常量中的 Unicode 转义字符没有提供任何特殊处理。编译器在将程序解析成各种符号之前,先将 Unicode转义字符转换成为它们所表示的字符[JLS 3.2]。因此,程序中的第一个 Unicode转义字符将作为一个单字符字符串字面常量("a")的结束引号,而第二个Unicode 转义字符将作为另一个单字符字符串字面常量("b")的开始引号。程序打印的是表达式"a".length()+"b".length(),即 2。
正确的写法可以这样写:
ystem.out.println("a".length()+"b".length());
总结:在字符串和字符字面常量中要优先选择的是转义字符序列,而不是Unicode 转义字符。Unicode 转义字符可能会因为它们在编译序列中被处理得过早而引起混乱。不要使用 Unicode 转义字符来表示 ASCII 字符。在字符串和字符字面常量中,应该使用转义字符序列;对于除这些字面常量之外的情况,应该直接将 ASCII 字符插入到源文件中。

Hello

<span style="font-size:14px;">/**
* Generated by the IBM IDL-to-Java compiler, version 1.0
* from F:\TestRoot\apps\a1\units\include\PolicyHome.idl
* Wednesday, June 17, 1998 6:44:40 o’clock AM GMT+00:00
*/
public class Test{
public static void main(String[] args){
System.out.print("Hell");
System.out.println("o world");
}
}</span>


上面的程序会打印出什么?会是Hello world吗,如果你运行以后你就会发现不会跟你预想的一样。
实际上,它根本就通不过编译问题在于注释的第三行,它包含了字符\units。这些字符以反斜杠(\)以及紧跟着的字母 u 开头的,而它(\u)表示的是一个 Unicode 转义字符的开始。遗憾的是,这些字符后面没有紧跟四个十六进制的数字,因此,这个 Unicode 转义字符是病构的,而编译器则被要求拒绝该程序。Unicode 转义字符必须是良构的,即使是出现在注释中也是如此。
总结:要确保字符\u 不出现在一个合法的 Unicode 转义字符上下文之外,即使是在注释中也是如此。在机器生成的代码中要特别注意此问题。

行打印程序

<span style="font-size:14px;">public class LinePrinter{
public static void main(String[] args){
// Note: \u000A is Unicode representation of linefeed (LF)
char c = 0x000A;
System.out.println(c);
}
}</span>


这个程序的行为是平台无关的:它在任何平台上都不能通过编译。如果你尝试着去编译它,就会得到类似下面的出错信息:
LinePrinter.java:3: ';' expected
// Note: \u000A is Unicode representation of linefeed (LF)
^
1 error
关键就是程序第三行的注释。与最好的注释一样,这条注释也是一种准确的表达,遗憾的是,它有一点准确得过头了。编译器不仅会在将程序解析成为符号之前把 Unicode 转义字符转换成它们所表示的字符(谜题 14),而且它是在丢弃注释和空格之前做这些事的[JLS 3.2]它位于程序唯一的注释行中。就像注释所陈述的,这个转义字符表示换行符,编译器将在丢弃注释之前适时地转换它。遗憾的是,这个换行符是表示注释开始的两个斜杠符之后的第一个行终结符(line terminator),因此它将终结该注释[JLS 3.4]。所以,该转义字符之后的字(is Unicode representation of linefeed (LF))就不是注释的一部分了,而它们在语法上也不是有效的
订正该程序的最简单的方式就是在注释中移除 Unicode 转义字符,但是更好的方式是用一个转义字符序列而不是一个十六进制整型字面常量来初始化 c,从而消除使用注释的必要:
public class LinePrinter{
public static void main(String[] args){
char c = '\n';
System.out.println(c);
}
}
总结::Unicode 转义字符绝对会产生混乱。教训很简单:除非确实是必需的,否则就不要使用 Unicode 转义字符。它们很少是必需的

问题:

下面的是一个合法的 Java 程序吗?如果是,它会打印出什么呢?
\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020
\u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079
\u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020
\u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063
\u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028
\u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020
\u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b
\u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074
\u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020
\u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b
\u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d
这当然是一个合法的 Java 程序!每当你没必要地使用了一个 Unicode 转义字符时,都会使你的程序的可理解性更缺失一点,而该程序将这种做法发挥到了极致。下面给出了将其进行格式化整理之后的样子:
public class Ugly {
public static void main(String[] args){
System.out.println("Hello w"+"orld");
}
}
Unicode 转义字符只有在你要向程序中插入用其他任何方式都无法表示的字符时才是必需的,除此之外的任何情况都不应该避免使用它们。Unicode 转义字符降低了程序的清晰度,并且增加了产生 bug 的可能性。

字符串

下面的程序从一个字节序列创建了一个字符串,然后迭代遍历字符串中的字符,并将它们作为数字打印。请描述一下程序打印出来的数字序列:
public class StringCheese {
public static void main(String[] args) {
byte bytes[] = new byte[256];
for (int i = 0; i < 256; i++)
bytes[i] = (byte)i;
String str = new String(bytes);
for (int i = 0, n = str.length(); i < n; i++)
System.out.println((int)str.charAt(i) + " ");
}
}


如果你运行该程序,可能会看到这样的序列。但是在运行一次,可能看到的就不是这个序列了。我们在四台机器上运行它,会看到四个不同的序列,包括前面描述的那个序列。这个程序甚至都不能保证会正常终止,比打印其他任何特定字符串都要缺乏这种保证。它的行为完全是不确定的。
在通过解码使用平台缺省字符集的指定 byte 数组来构造一个新的 String 时,该新String 的长度是字符集的一个函数,因此,它可能不等于 byte 数组的长度。当给定的所有字节在缺省字符集中并非全部有效时,这个构造器的行为是不确定
的。
总结:每当你要将一个 byte 序列转换成一个 String 时,你都在使用某一个字符集,不管你是否显式地指定了它。如果你想让你的程序的行为是可预知的,那么就请你在每次使用字符集时都明确地指定。对 API 的设计者来说,提供这么一个依赖于缺省字符集的 String(byte[])构造器可能并非是一个好主意

漂亮的火花

下面的程序用一个方法对字符进行了分类。这个程序会打印出什么呢?
public class Classifier {
public static void main(String[] args) {
System.out.println(
classify('n') + classify('+') + classify('2'));
}
static String classify(char ch) {
if ("0123456789".indexOf(ch) >= 0)
return "NUMERAL ";
if ("abcdefghijklmnopqrstuvwxyz".indexOf(ch) >= 0)
return "LETTER ";
/* (Operators not supported yet)
if ("+-*/&|!=" >= 0)
return "OPERATOR ";
*/
return "UNKNOWN";
}
}


如果你猜想该程序将打印 LETTER UNKNOWN NUMERAL,那么你就掉进陷阱里面了。这个程序连编译都通不过。注释在包含了字符*/的字符串内部就结束了,结果使得程序在语法上变成非法的了。我们将程序中的一部分注释出来的尝试之所以失败了,是因
为字符串字面常量在注释中没有被特殊处理。注释掉一个代码段的最好的方式是使用单行的注释序列
总结:块注释不能可靠地注释掉代码段,应该用单行的注释序列来代替。对语言设计者来说,应该注意到可嵌套的块注释并不是一个好主意。他们强制编译器去解析块注释内部的文本,而由此引发的问题比它能够解决的问题还要多。

我的类

这个程序会打印出什么呢?
package com.javapuzzlers;
public class Me {
public static void main(String[] args){
System.out.println(
Me.class.getName().
replaceAll(".","/") + ".class");
}
}


看起来会获得它的类名(“com.javapuzzlers.Me”),然后用“/”替换掉所有出现的字符串“.”,并在末尾追加字符串“.class”。你可能会认为该程序将打印 com/javapuzzlers/Me.class,该程序正式从这个类文件中被加载的。如果你运行这个程序,就会发现它实际上打印的是///.class。
问题在于 String.replaceAll 接受了一个正则表达式作为它的第一个参数,而并非接受了一个字符序列字面常量。正则表达式“.”可以匹配任何单个的字符,因此,类名中的每一个字符都被替换成了一个斜杠,进而产生了我们看到的输出。在正则表达式中的句点必须在其前面添加一个反斜杠(\)进行转义。因为反斜杠字符在字面含义的字符串中具有特殊的含义——它标识转义字符序列的开始——因此反斜杠自身必须用另一个反斜杠来转义,这样就可以产生一个转义字符序列,它可以在字面含义的字符串中生成一个反斜杠。

我的类二

这个程序会打印出其正确的、平台相关的类文件名吗?
<span style="font-size:14px;">package com.javapuzzlers;
import java.io.File;
public class MeToo {
public static void main(String[] args){
System.out.println(MeToo.class.getName().
replaceAll("\\.", File.separator) + ".class");
}
}</span>


该程序将打印像下面这样的内容:
Exception in thread "main"
java.lang.StringIndexOutOfBoundsException: String index out of range: 1
at java.lang.String.charAt(String.java:558)
at java.util.regex.Matcher.appendReplacement(Mather.
java:696)
at java.util.regex.Matcher.replaceAll(Mather.java:806)
at java.lang.String.replaceAll(String.java:2000)
at com.javapuzzlers.MeToo.main(MeToo.java:6)
String.replaceAll 的第二个参数不是一个普通的字符串,而是一个替代字符串,就像在 java.util.regex 规范中所定义的那样。在替代字符串中出现的反斜杠会把紧随其后的字符进行转义,从而导致其被按字面含义而处理了。
在使用不熟悉的类库方法时一定要格外小心。当你心存疑虑时,就要求助于 Javadoc。还有就是正则表达式是很棘手的:它所引发的问题趋向于在运行时刻而不是在编译时刻暴露出来。

URL 的愚弄

考虑下面的程序将会做些什么?
<span style="font-size:14px;">public class BrowserTest {
public static void main(String[] args) {
System.out.print("iexplore:");
http://www.google.com;
System.out.println(":maximize");
}
}</span>


在程序中间出现的 URL 是一个语句标号后面跟着一行行尾注释(end-of-line comment)它的价值所在,就是提醒你,如果你真的想要使用标号,那么应该用一种更合理的方式来格式化程序:
public class BrowserTest {
public static void main(String[] args) {
System.out.print("iexplore:");
http: //www.google.com;
System.out.println(":maximize");
}
}
令人误解的注释和无关的代码会引起混乱。要仔细地写注释,并让它们跟上时代;要切除那些已遭废弃的代码。还有就是如果某些东西看起来过于奇怪,以至于不像对的,那么它极有可能就是错的。

不劳无获

下面的程序将打印一个单词,其第一个字母是由一个随机数生成器来选择的。请描述该程序的行为:
import java.util.Random;
public class Rhymes {
private static Random rnd = new Random();
public static void main(String[] args) {
StringBuffer word = null;
switch(rnd.nextInt(2)) {
case 1: word = new StringBuffer('P');
case 2: word = new StringBuffer('G');
default: word = new StringBuffer('M');
}
word.append('a');
word.append('i');
word.append('n');
System.out.println(word);
}
}


实际上既不会打印 Pain, 也不会打印 Gain。 也许更令人吃惊的是,它也不会打印 Main,并且它的行为不会在一次又一次的运行中发生变化,它总是在打印 ain。为了避免这类问题,不管在什么时候,都要尽可能使用熟悉的惯用法和 API。如果你必须使用不熟悉的 API,那么请仔细阅读其文档。在本例中,程序应该使用常用的接受一个 String 的 StringBuffer 构造器。
正确的写法如下:
它将以均等的概率打印 Pain、Gain 和 Main:
import java.util.Random;
public class Rhymes1 {
private static Random rnd = new Random();
public static void main(String[] args) {
StringBuffer word = null;
switch(rnd.nextInt(3)) {
case 1:
word = new StringBuffer("P");
break;
case 2:
word = new StringBuffer("G");
break;
default:
word = new StringBuffer("M");
break;
}
word.append('a');
word.append('i');
word.append('n');
System.out.println(word);
}
}


或者更简单的写法:
import java.util.Random;
public class Rhymes2 {
private static Random rnd = new Random();
public static void main(String[] args) {
System.out.println("PGM".charAt(rnd.nextInt(3)) + "ain");
}
}


或者
import java.util.Random;
public class Rhymes3 {
public static void main(String[] args) {
String a[] = {"Main","Pain","Gain"};
System.out.println(randomElement(a));
}
private static Random rnd = new Random();
private static String randomElement(String[] a){
return a[rnd.nextInt(a.length)];
}
}


总结一下:首先,要当心栅栏柱错误(这个名字来源于对下面这个问题最常见的但却是错误的答案,如果你要建造一个100 英尺长的栅栏,其栅栏柱间隔为 10 英尺,那么你需要多少根栅栏柱呢?11根或 9 根都是正确答案,这取决于是否要在栅栏的两端树立栅栏柱,但是 10 根却是错误的。要当心栅栏柱错误,每当你在处理长度、范围或模数的时候,都要仔细确定其端点是否应该被包括在内,并且要确保你的代码的行为要与其相对应。)。其次,牢记在 switch 语句的每一个 case中都放置一条 break 语句。第三,要使用常用的惯用法和 API,并且当你在离开老路子的时候,一定要参考相关的文档。第四,一个 char 不是一个 String,而是更像一个 int。最后,要提防各种诡异的谜题。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值