19 声明多行字符串(文本块)
在写这本书的时候,JDK12 有一个关于添加多行字符串的建议,称为 JEP326:原始字符串字面值。但这是在最后一刻掉的
从 JDK13 开始,重新考虑了这个想法,与被拒绝的原始字符串字面值不同,文本块被三个双引号包围,""",如下所示:
String text = """My high school, the Illinois Mathematics and Science Academy, showed me that anything is possible and that you're never too young to think big.""";
文本块对于编写多行 SQL 语句、使用 polyglot 语言等非常有用。更多详情见这个页面。
尽管如此,在 JDK13 之前有几种替代解决方案可以使用。这些解决方案有一个共同点,即使用行分隔符:
private static final String LS = System.lineSeparator();
从 JDK8 开始,解决方案可以依赖于String.join()
,如下所示:
String text = String.join(LS, "My high school, ", "the Illinois Mathematics and Science Academy,", "showed me that anything is possible ", "and that you're never too young to think big.");
在 JDK8 之前,一个优雅的解决方案可能依赖于StringBuilder
。本书附带的代码中提供了此解决方案。
虽然前面的解决方案适合于相对大量的字符串,但如果我们只有几个字符串,下面的两个就可以了。第一个使用+
运算符:
String text = "My high school, " + LS + "the Illinois Mathematics and Science Academy," + LS + "showed me that anything is possible " + LS + "and that you're never too young to think big.";
第二个使用String.format()
:
String text = String.format("%s" + LS + "%s" + LS + "%s" + LS + "%s", "My high school, ", "the Illinois Mathematics and Science Academy,", "showed me that anything is possible ", "and that you're never too young to think big.");
如何处理多行字符串的每一行?好吧,快速方法需要 JDK11,它与String.lines()方法一起提供。该方法通过行分隔符(支持\n、\r、\r\n对给定字符串进行拆分,并将其转换为Stream<String>。或者,也可以使用String.split()方法(从 JDK1.4 开始提供)。如果字符串的数量变得重要,建议将它们放入一个文件中,并逐个读取/处理它们(例如,通过getResourceAsStream()方法)。其他方法依赖于StringWriter或BufferedWriter.newLine()。
对于第三方库支持,请考虑 Apache Commons Lang、StringUtils.join()、Guava、Joiner和自定义注解@Multiline。
20 连接相同字符串 n 次
在 JDK11 之前,可以通过StringBuilder
快速提供解决方案,如下:
public static String concatRepeat(String str, int n) { StringBuilder sb = new StringBuilder(str.length() * n); for (int i = 1; i <= n; i++) { sb.append(str); } return sb.toString(); }
从 JDK11 开始,解决方案依赖于String.repeat(int count)方法。此方法返回一个字符串,该字符串通过将此字符串count连接几次而得到。在幕后,这个方法使用了System.arraycopy(),这使得这个速度非常快:
String result = "hello".repeat(5);
其他适合不同场景的解决方案如下:
- 以下是基于
String.join()
的解决方案:
String result = String.join("", Collections.nCopies(5, TEXT));
- 以下是基于
Stream.generate()
的解决方案:
String result = Stream.generate(() -> TEXT) .limit(5) .collect(joining());
- 以下是基于
String.format()
的解决方案:
String result = String.format("%0" + 5 + "d", 0) .replace("0", TEXT);
- 以下是基于
char[]
的解决方案:
String result = new String(new char[5]).replace("\0", TEXT);
对于第三方库支持,请考虑 ApacheCommonsLang,StringUtils.repeat()
和 Guava,Strings.repeat()
。
要检查字符串是否是相同子字符串的序列,请使用以下方法:
public static boolean hasOnlySubstrings(String str) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length() / 2; i++) { sb.append(str.charAt(i)); String resultStr = str.replaceAll(sb.toString(), ""); if (resultStr.length() == 0) { return true; } } return false; }
该解决方案循环给定字符串的一半,并通过逐字符将原始字符串追加到StringBuilder
中,逐步用""
(子字符串构建)替换它。如果这些替换结果是空字符串,则表示给定的字符串是相同子字符串的序列。
21 删除前导空格和尾随空格
这个问题的最快解决方法可能依赖于String.trim()方法。此方法能够删除所有前导和尾随空格,即代码点小于或等于 U+0020 或 32 的任何字符(空格字符):
String text = "\n \n\n hello \t \n \r"; String trimmed = text.trim();
前面的代码片段将按预期工作。修剪后的字符串将为hello。这只适用于所有正在使用的空格小于 U+0020 或 32(空格字符)。有 25 个字符定义为空格,trim()只覆盖其中的一部分(简而言之,trim()不知道 Unicode)。让我们考虑以下字符串:
char space = '\u2002'; String text = space + "\n \n\n hello \t \n \r" + space;
\u2002是trim()无法识别的另一种类型的空白(\u2002在\u0020之上)。这意味着,在这种情况下,trim()将无法按预期工作。从 JDK11 开始,这个问题有一个名为strip()的解决方案。此方法将trim()的功能扩展到 Unicode 领域:
String stripped = text.strip();
这一次,所有的前导和尾随空格都将被删除。
此外,JDK11 还提供了两种类型的strip()
,用于仅删除前导(stripLeading()
)或尾部(stripTrailing()
)空格。trim()
方法没有这些味道。
22 查找最长的公共前缀
让我们考虑以下字符串数组:
String[] texts = {"abc", "abcd", "abcde", "ab", "abcd", "abcdef"};
现在,让我们把这些线一根接一根,如下所示:
abc abcd abcde ab abcd abcdef
通过对这些字符串的简单比较可以看出,ab是最长的公共前缀。现在,让我们深入研究解决此问题的解决方案。我们在这里提出的解决方案依赖于一个简单的比较。此解决方案从数组中获取第一个字符串,并将其每个字符与其余字符串进行比较。如果发生以下任一情况,算法将停止:
第一个字符串的长度大于任何其他字符串的长度
第一个字符串的当前字符与任何其他字符串的当前字符不同
如果由于上述情况之一而强制停止算法,则最长的公共前缀是从 0 到第一个字符串的当前字符索引的子字符串。否则,最长的公共前缀是数组中的第一个字符串。此解决方案的代码如下:
public static String longestCommonPrefix(String[] strs) { if (strs.length == 1) { return strs[0]; } int firstLen = strs[0].length(); for (int prefixLen = 0; prefixLen < firstLen; prefixLen++) { char ch = strs[0].charAt(prefixLen); for (int i = 1; i < strs.length; i++) { if (prefixLen >= strs[i].length() || strs[i].charAt(prefixLen) != ch) { return strs[i].substring(0, prefixLen); } } } return strs[0]; }
这个问题的其他解决方案使用众所周知的算法,例如二分搜索或 Trie。在本书附带的源代码中,还有一个基于二分搜索的解决方案。
23 应用缩进
从 JDK12 开始,我们可以通过String.indent(int n)
方法缩进文本。
假设我们有以下String
值:
String days = "Sunday\n" + "Monday\n" + "Tuesday\n" + "Wednesday\n" + "Thursday\n" + "Friday\n" + "Saturday";
用 10 个空格的缩进打印这个String
值可以如下所示:
System.out.print(days.indent(10));
输出如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ziozvzzr-1657077108924)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/3c34fd48-6ec4-427d-a0cd-3037407f6bc8.png)]
现在,让我们试试层叠缩进:
List<String> days = Arrays.asList("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"); for (int i = 0; i < days.size(); i++) { System.out.print(days.get(i).indent(i)); }
输出如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fwmBXszC-1657077108924)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/e9d00aa3-22ae-4c73-b34d-d3f2cf66e702.png)]
现在,让我们根据String值的长度缩进:
days.stream() .forEachOrdered(d -> System.out.print(d.indent(d.length())));
输出如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yvtiz6zI-1657077108925)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/342a0e70-c60d-4f4d-95c1-2de21e6a0d8e.png)]
缩进一段 HTML 代码怎么样?让我们看看:
String html = "<html>"; String body = "<body>"; String h2 = "<h2>"; String text = "Hello world!"; String closeH2 = "</h2>"; String closeBody = "</body>"; String closeHtml = "</html>"; System.out.println(html.indent(0) + body.indent(4) + h2.indent(8) + text.indent(12) + closeH2.indent(8) + closeBody.indent(4) + closeHtml.indent(0));
输出如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9T42XLeQ-1657077108926)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/db341df1-83f8-438d-992e-8a62408fc196.png)]
24 转换字符串
假设我们有一个字符串,我们想把它转换成另一个字符串(例如,把它转换成大写)。我们可以通过应用像Function<? super String, ? extends R>
这样的函数来实现这一点。
在 JDK8 中,我们可以通过map()
来实现,如下两个简单的例子所示:
// hello world String resultMap = Stream.of("hello") .map(s -> s + " world") .findFirst() .get(); // GOOOOOOOOOOOOOOOOL! GOOOOOOOOOOOOOOOOL! String resultMap = Stream.of("gooool! ") .map(String::toUpperCase) .map(s -> s.repeat(2)) .map(s -> s.replaceAll("O", "OOOO")) .findFirst() .get();
从 JDK12 开始,我们可以依赖一个名为transform(Function<? super String, ? extends R> f)
的新方法。让我们通过transform()
重写前面的代码片段:
// hello world String result = "hello".transform(s -> s + " world"); // GOOOOOOOOOOOOOOOOL! GOOOOOOOOOOOOOOOOL! String result = "gooool! ".transform(String::toUpperCase) .transform(s -> s.repeat(2)) .transform(s -> s.replaceAll("O", "OOOO"));
虽然map()
更一般,但transform()
专用于将函数应用于字符串并返回结果字符串。
25 计算两个数的最小值和最大值
在 JDK8 之前,一个可能的解决方案是依赖于Math.min()
和Math.max()
方法,如下所示:
int i1 = -45; int i2 = -15; int min = Math.min(i1, i2); int max = Math.max(i1, i2);
Math类为每个原始数字类型(int、long、float和double提供了min()和max()方法。
从 JDK8 开始,每个原始数字类型的包装器类(Integer、Long、Float和Double都有专用的min()和max()方法,在这些方法后面,还有来自Math类的对应调用。请参见下面的示例(这是一个更具表现力的示例):
double d1 = 0.023844D; double d2 = 0.35468856D; double min = Double.min(d1, d2); double max = Double.max(d1, d2);
在函数式风格的上下文中,潜在的解决方案将依赖于BinaryOperator
函数式接口。此接口有两种方式,minBy()
和maxBy()
:
float f1 = 33.34F; final float f2 = 33.213F; float min = BinaryOperator.minBy(Float::compare).apply(f1, f2); float max = BinaryOperator.maxBy(Float::compare).apply(f1, f2);
这两种方法能够根据指定的比较器返回两个元素的最小值(分别是最大值)。
26 两个大的int/long
值求和并导致操作溢出
让我们从+
操作符开始深入研究解决方案,如下例所示:
int x = 2; int y = 7; int z = x + y; // 9
这是一种非常简单的方法,适用于大多数涉及int
、long
、float
和double
的计算。
现在,让我们将此运算符应用于以下两个大数(与其自身相加为 2147483647):
int x = Integer.MAX_VALUE; int y = Integer.MAX_VALUE; int z = x + y; // -2
此时,z
将等于 -2,这不是预期的结果,即 4294967294。仅将z
类型从int
更改为long
将无济于事。但是,将x
和y
的类型从int
改为long
将有所帮助:
long x = Integer.MAX_VALUE; long y = Integer.MAX_VALUE; long z = x + y; // 4294967294
但如果不是Integer.MAX_VALUE
,而是Long.MAX_VALUE
,问题就会再次出现:
long x = Long.MAX_VALUE; long y = Long.MAX_VALUE; long z = x + y; // -2
从 JDK8 开始,+
操作符被一个原始类型数字类型的包装器以一种更具表现力的方式包装。因此,Integer
、Long
、Float
和Double
类具有sum()
方法:
long z = Long.sum(); // -2
在幕后,sum()方法也使用+操作符,因此它们只产生相同的结果。
但同样从 JDK8 开始,Math类用两种addExact()方法进行了丰富。一个addExact()用于两个int变量的求和,一个用于两个long变量的求和。如果结果容易溢出int或long,这些方法非常有用,如前面的例子所示。在这种情况下,这些方法抛出ArithmeticException,而不是返回误导性的结果,如下例所示:
int z = Math.addExact(x, y); // throw ArithmeticException
代码将抛出一个异常,例如java.lang.ArithmeticException: integer overflow。这是很有用的,因为它允许我们避免在进一步的计算中引入误导性的结果(例如,早期,-2 可以悄悄地进入进一步的计算)。
在函数式上下文中,潜在的解决方案将依赖于BinaryOperator函数式接口,如下所示(只需定义相同类型的两个操作数的操作):
BinaryOperator<Integer> operator = Math::addExact; int z = operator.apply(x, y);
除addExact()外,Math还有multiplyExact()、substractExact()、negateExact()。此外,众所周知的增量和减量表达式i++和i--可以通过incrementExact()和decrementExact()方法(例如Math.incrementExact(i))来控制溢出它们的域。请注意,这些方法仅适用于int和long。
在处理大量数字时,还要关注BigInteger(不可变任意精度整数)和BigDecimal(不可变任意精度带符号十进制数)类。
27 字符串按照基数转换为无符号数
对无符号算术的支持从版本 8 开始添加到 Java 中。Byte
、Short
、Integer
和Long
类受此影响最大。
在 Java 中,表示正数的字符串可以通过parseUnsignedInt()
和parseUnsignedLong()
JDK8 方法解析为无符号的int
和long
类型。例如,我们将以下整数视为字符串:
String nri = "255500";
将其解析为以 36 为基数(最大可接受基数)的无符号int
值的解决方案如下:
int result = Integer.parseUnsignedInt(nri, Character.MAX_RADIX);
第一个参数是数字,第二个参数是基数。基数应在[2, 36]或[Character.MIN_RADIX, Character.MAX_RADIX]范围内。
使用基数 10 可以很容易地完成如下操作(此方法默认应用基数 10):
int result = Integer.parseUnsignedInt(nri);
从 JDK9 开始,parseUnsignedInt()有了新的味道。除了字符串和基数之外,这个方法还接受一个范围的[beginIndex, endIndex]类型。这一次,解析是在这个范围内完成的。例如,可以按如下方式指定范围[1, 3]:
int result = Integer.parseUnsignedInt(nri, 1, 4, Character.MAX_RADIX);
parseUnsignedInt()
方法可以解析表示大于Integer.MAX_VALUE
的数字的字符串(尝试通过Integer.parseInt()
完成此操作将引发java.lang.NumberFormatException
异常):
// Integer.MAX_VALUE + 1 = 2147483647 + 1 = 2147483648 int maxValuePlus1 = Integer.parseUnsignedInt("2147483648");
对于Long
类中的长数存在相同的方法集(例如,parseUnsignedLong()
。